Django で大容量ファイルをダウンロードする際にメモリエラーを回避する方法。
StreamingHttpResponse
とwsgiref.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()
の部分でファイルの全容量をメモリに読み込んでしまう。
StreamingHttpResponse
とFileWrapper
を使う場合
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