django提供文件下载时,若果文件较小,解决办法是先将要传送的内容全生成在内存中,然后再一次性传入Response对象中:
|
1
2
3
4
|
def simple_file_download(request):
# do something...
content = open("simplefile", "rb").read()
return HttpResponse(content)
|
如果文件非常大时,最简单的办法就是使用静态文件服务器,比如Apache或者Nginx服务器来处理下载。不过有时候,我们需要对用户的权限做一下限定,或者不想向用户暴露文件的真实地址,或者这个大内容是临时生成的(比如临时将多个文件合并而成的),这时就不能使用静态文件服务器了。
django文档中提到,可以向HttpResponse传递一个迭代器,流式的向客户端传递数据。
要自己写迭代器的话,可以用yield:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
def read_file(filename, buf_size=8192):
with open(filename, "rb") as f:
while True:
content = f.read(buf_size)
if content:
yield content
else:
break
def big_file_download(request):
filename = "filename"
response = HttpResponse(read_file(filename))
return response
|
或者使用生成器表达式,下面是django文档中提供csv大文件下载的例子:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import csv
from django.utils.six.moves import range
from django.http import StreamingHttpResponse
class Echo(object):
"""An object that implements just the write method of the file-like
interface.
"""
def write(self, value):
"""Write the value by returning it, instead of storing in a buffer."""
return value
def some_streaming_csv_view(request):
"""A view that streams a large CSV file."""
# Generate a sequence of rows. The range is based on the maximum number of
# rows that can be handled by a single sheet in most spreadsheet
# applications.
rows = (["Row {0}".format(idx), str(idx)] for idx in range(65536))
pseudo_buffer = Echo()
writer = csv.writer(pseudo_buffer)
response = StreamingHttpResponse((writer.writerow(row) for row in rows),
content_type="text/csv")
response[\'Content-Disposition\'] = \'attachment; filename="somefilename.csv"\'
return response
|
python也提供一个文件包装器,将类文件对象包装成一个迭代器:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class FileWrapper:
"""Wrapper to convert file-like objects to iterables"""
def __init__(self, filelike, blksize=8192):
self.filelike = filelike
self.blksize = blksize
if hasattr(filelike,\'close\'):
self.close = filelike.close
def __getitem__(self,key):
data = self.filelike.read(self.blksize)
if data:
return data
raise IndexError
def __iter__(self):
return self
def next(self):
data = self.filelike.read(self.blksize)
if data:
return data
raise StopIteration
|
使用时:
|
1
2
3
4
5
6
7
8
9
10
|
from django.core.servers.basehttp import FileWrapper
from django.http import HttpResponse
import os
def file_download(request,filename):
wrapper = FileWrapper(open(filename, \'rb\'))
response = HttpResponse(wrapper, content_type=\'application/octet-stream\')
response[\'Content-Length\'] = os.path.getsize(path)
response[\'Content-Disposition\'] = \'attachment; filename=%s\' % filename
return response
|
django也提供了StreamingHttpResponse类来代替HttpResponse对流数据进行处理。
压缩为zip文件下载:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import os, tempfile, zipfile
from django.http import HttpResponse
from django.core.servers.basehttp import FileWrapper
def send_zipfile(request):
"""
Create a ZIP file on disk and transmit it in chunks of 8KB,
without loading the whole file into memory. A similar approach can
be used for large dynamic PDF files.
""" temp = tempfile.TemporaryFile()
archive = zipfile.ZipFile(temp, \'w\', zipfile.ZIP_DEFLATED)
for index in range(10):
filename = __file__ # Select your files here.
archive.write(filename, \'file%d.txt\' % index)
archive.close()
wrapper = FileWrapper(temp)
response = HttpResponse(wrapper, content_type=\'application/zip\')
response[\'Content-Disposition\'] = \'attachment; filename=test.zip\' response[\'Content-Length\'] = temp.tell()
temp.seek(0)
return response
|
不过不管怎么样,使用django来处理大文件下载都不是一个很好的注意,最好的办法是django做权限判断,然后让静态服务器处理下载。
这需要使用sendfile的机制:"传统的Web服务器在处理文件下载的时候,总是先读入文件内容到应用程序内存,然后再把内存当中的内容发送给客户端浏览器。这种方式在应付当今大负载网站会消耗更多的服务器资源。sendfile是现代操作系统支持的一种高性能网络IO方式,操作系统内核的sendfile调用可以将文件内容直接推送到网卡的buffer当中,从而避免了Web服务器读写文件的开销,实现了“零拷贝”模式。 "
Apache服务器里需要mod_xsendfile模块来实现,而Nginx是通过称为X-Accel-Redirect的特性来实现。
nginx配置文件:
|
1
2
3
4
5
6
|
# Will serve /var/www/files/myfile.tar.gz# When passed URI /protected_files/myfile.tar.gzlocation /protected_files {
internal;
alias /var/www/files;
} |
或者
|
1
2
3
4
5
6
|
# Will serve /var/www/protected_files/myfile.tar.gz# When passed URI /protected_files/myfile.tar.gzlocation /protected_files {
internal;
root /var/www;
} |
注意alias和root的区别。
django中:
|
1
|
response[\'X-Accel-Redirect\']=\'/protected_files/%s\'%filename
|
这样当向django view函数发起request时,django负责对用户权限进行判断或者做些其它事情,然后向nginx转发url为/protected_files/filename的请求,nginx服务器负责文件/var/www/protected_files/filename的下载:
|
1
2
3
4
5
6
7
8
9
10
11
|
@login_requireddef document_view(request, document_id):
book = Book.objects.get(id=document_id)
response = HttpResponse()
name=book.myBook.name.split(\'/\')[-1]
response[\'Content_Type\']=\'application/octet-stream\'
response["Content-Disposition"] = "attachment; filename={0}".format(
name.encode(\'utf-8\'))
response[\'Content-Length\'] = os.path.getsize(book.myBook.path)
response[\'X-Accel-Redirect\'] = "/protected/{0}".format(book.myBook.name)
return response
|