问题出现
你的Web应用可能会提供下载文件的功能。下载个几百KB乃至几个MB的文件直接用open读取文件返回response就可以了。内存它也不在乎你占用它那么点空间。但是,如果下载个几百M乃至几个G的文件直接用open读取文件返回response那就悲剧了,内存可不乐意一下子被你占用这么多空间。
问题解决
解决方法网上相关文章很多,比如:http://djangosnippets.org/snippets/365/
简单说就是用FileWrapper类来迭代器化一下文件对象,实例化出一个经过更适合大文件下载场景的文件对象,具体实现可以看源代码:django/core/servers/basehttp.py中的FileWrapper类的实现。
实现原理相当与把内容一点点从文件中读取,放到内存,下载下来,直到完成整个下载过程。这样内存就不会担心你一下子占用它那么多空间了。
新的问题
也许你有又会碰到一个新的问题,下载下来的文件竟然是个空文件。
如果你单步跟踪调试会发现文件内容返回的response在通过各个中间件过程中会被提前使用,最后发现是gzip这个中间件的一段代码对response取了下len,导致提前使用了这个迭代器化的文件对象,从而response返回内容没有了,表现为下载了一个空文件。
继续处理
这个时候你有很多选择:
- 继续用FileWrapper,不用gzip中间件
- 继续用gzip中间件,直接重写FileWrapper
- 重写gzip中间件,继续用FileWrapper
看了下Django源码,会发现重写gzip中间件会比较靠谱,只要简单修改一段代码逻辑即可。
但要有效,必须在下载的views代码的response返回值中设置个header。
遗留问题
其实更治本的办法应该是重写FileWapper,因为保证不了其它中间件或其它组建进行了类似gzip中间件的处理。
Django官网也有相关讨论,里面还有各种思路:https://code.djangoproject.com/ticket/2131
问题发现和处理使用的Django版本是1.3.0,也许未来Django应该会提供更好的官方解决方案吧。
代码片段
下载的views代码片段如下:
def tarball(request, release):
file_name = 'dj-download-%s.tar.gz' % release
file_path = os.path.join(FILE_FOLDER, file_name)
try:
tarball_file = open(file_path)
except IOError:
raise Http404
wrapper = FileWrapper(tarball_file)
response = HttpResponse(wrapper, content_type='application/zip')
response['Content-Encoding'] = 'utf-8' # 设置该值gzip中间件就会直接返回而不进行后续操作
response['Content-Disposition'] = 'attachment; filename=%s' % file_name
return response
修改Django的gzip中间件代码片段如下:
def process_response(self, request, response):
# Avoid gzipping if we've already got a content-encoding.
if response.has_header('Content-Encoding'):
return response# It's not worth compressing non-OK or really short responses.
if response.status_code != 200:
return response
if len(response.content) < 200:
return responsepatch_vary_headers(response, ('Accept-Encoding',))
# ... 省略的代码
相应对比Django的gzip中间件代码片段如下:
def process_response(self, request, response):
# It's not worth compressing non-OK or really short responses.
if response.status_code != 200 or len(response.content) < 200:
return responsepatch_vary_headers(response, ('Accept-Encoding',))
# Avoid gzipping if we've already got a content-encoding.
if response.has_header('Content-Encoding'):
return response
# ... 省略的代码
代码实例下载:https://github.com/akun/dj-download/archives/master