Django 大文件下載問題處理

問題出現

你的Web應用可能會提供下載文件的功能。下載個幾百KB乃至幾個MB的文件直接用open讀取文件返回response就能夠了。內存它也不在意你佔用它那麼點空間。可是,若是下載個幾百M乃至幾個G的文件直接用open讀取文件返回response那就悲劇了,內存可不樂意一會兒被你佔用這麼多空間。git

問題解決

解決方法網上相關文章不少,好比:http://djangosnippets.org/snippets/365/github

簡單說就是用FileWrapper類來迭代器化一下文件對象,實例化出一個通過更適合大文件下載場景的文件對象,具體實現能夠看源代碼:django/core/servers/basehttp.py中的FileWrapper類的實現。django

實現原理至關與把內容一點點從文件中讀取,放到內存,下載下來,直到完成整個下載過程。這樣內存就不會擔憂你一會兒佔用它那麼多空間了。app

新的問題

也許你有又會碰到一個新的問題,下載下來的文件居然是個空文件。spa

若是你單步跟蹤調試會發現文件內容返回的response在經過各個中間件過程當中會被提早使用,最後發現是gzip這個中間件的一段代碼對response取了下len,致使提早使用了這個迭代器化的文件對象,從而response返回內容沒有了,表現爲下載了一個空文件。調試

繼續處理

這個時候你有不少選擇:code

  • 繼續用FileWrapper,不用gzip中間件
  • 繼續用gzip中間件,直接重寫FileWrapper
  • 重寫gzip中間件,繼續用FileWrapper

看了下Django源碼,會發現重寫gzip中間件會比較靠譜,只要簡單修改一段代碼邏輯便可。orm

但要有效,必須在下載的views代碼的response返回值中設置個header。server

遺留問題

其實更治本的辦法應該是重寫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 response

    patch_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 response

    patch_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

相關文章
相關標籤/搜索