7
4

More than 3 years have passed since last update.

【Django】メモリを節約しつつ大容量ファイルをダウンロードする。

Last updated at Posted at 2021-01-20

Django で大容量ファイルをダウンロードする際にメモリエラーを回避する方法。
StreamingHttpResponsewsgiref.util.FileWrapperを使う。

※関数ベースのviewの実装例を記載

HttpResponseを使う場合

from django.http import HttpResponse

def download_view(request, *args, **kwargs):
    with open('path/to/dir/small.csv', 'rb') as f:
        response = HttpResponse(f.read(), content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename=small.csv'
    return response

f.read()の部分でファイルの全容量をメモリに読み込んでしまう。

StreamingHttpResponseFileWrapperを使う場合

import os
from wsgiref.util import FileWrapper

from django.http import StreamingHttpResponse

# 8MB ずつ読み込む
chunksize = 8 * (1024 ** 2)

def streaming_download_view(request, *args, **kwargs):
    path = os.path.join('path/to/dir', 'large.csv')
    response = StreamingHttpResponse(
        FileWrapper(open(path, 'rb'), chunksize),
        content_type='text/csv'
    )
    response['Content-Length'] = os.path.getsize(path)
    response['Content-Disposition'] = 'attachment; filename=large.csv'
    return response

なお FileWrapper の第一引数には"filelike-object"を指定するが、コンテキストマネージャを使うと(なぜか)エラーが発生する。

あんまり調査していないが、ドキュメントの使用例と同じようにopen()を使うのが無難である。

失敗する例(コンテキストマネージャを使用)
...
def streaming_download_view(request, *args, **kwargs):
    path = os.path.join('path/to/dir', 'large.csv')
    with open(path, 'rb') as f:
        response = StreamingHttpResponse(
            FileWrapper(f, chunksize),
            content_type='text/csv'
        )
        ...
        return response
エラー出力
Traceback (most recent call last):
  File "/home/ec2-user/.pyenv/versions/3.7.7/lib/python3.7/wsgiref/handlers.py", line 138, in run
    self.finish_response()
  File "/home/ec2-user/.pyenv/versions/3.7.7/lib/python3.7/wsgiref/handlers.py", line 183, in finish_response
    for data in self.result:
  File "/home/ec2-user/.pyenv/versions/3.7.7/lib/python3.7/wsgiref/util.py", line 30, in __next__
    data = self.filelike.read(self.blksize)
ValueError: read of closed file

応用: ダウンロード後にファイルを削除する

ダウンロードするファイルを一時ファイル的に扱いたい場合にはTemporaryDirectoryと組み合わせることで実現できる。

import os
from tempfile import TemporaryDirectory
from wsgiref.util import FileWrapper

from django.http import StreamingHttpResponse

# 8MB ずつ読み込む
chunksize = 8 * (1024 ** 2)


def generate_return_file(target_dir):
    # ダウンロード対象のファイルを指定ディレクトリ配下に作成する何らかの処理
    pass


def streaming_download_view_with_tempdir(request, *args, **kwargs):
    with TemporaryDirectory() as tempdir:
        path = os.path.join(tempdir, 'large.csv')
        # tempdir 配下に対象ファイルを作成する
        generate_return_file(tempdir)

        response = StreamingHttpResponse(
            FileWrapper(open(path, 'rb'), chunksize),
            content_type='text/csv'
        )
        response['Content-Length'] = os.path.getsize(path)
        response['Content-Disposition'] = 'attachment; filename=large.csv'
        return response
7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4