使用Django提供可下载文件

273

我希望网站上的用户能够下载文件,但文件路径被模糊处理,以防止其直接下载。

例如,我希望URL看起来像这样:http://example.com/download/?f=somefile.txt

在服务器上,我知道所有可下载的文件都存储在文件夹/home/user/files/中。

是否有一种方法可以让Django提供该文件以供下载,而不是试图查找URL和View以将其显示出来?


2
为什么不直接使用Apache来完成这个任务呢?Apache比Django更快、更简单地提供静态内容服务。 - S.Lott
29
我不使用Apache,因为我不希望文件在没有Django权限的情况下可以访问。 - damon
3
如果你想考虑用户权限,你需要通过Django的视图来提供文件。 - Łukasz
139
没问题,这就是为什么我问这个问题的原因。 - damon
15个回答

202

为了实现"两全其美",你可以将S.Lott的解决方案与xsendfile模块结合起来:Django生成文件的路径(或文件本身),但实际的文件传输由Apache / Lighttpd处理。一旦你配置好mod_xsendfile,集成到你的视图中只需要几行代码:

from django.utils.encoding import smart_str

response = HttpResponse(mimetype='application/force-download') # mimetype is replaced by content_type for django 1.7
response['Content-Disposition'] = 'attachment; filename=%s' % smart_str(file_name)
response['X-Sendfile'] = smart_str(path_to_file)
# It's usually a good idea to set the 'Content-Length' header too.
# You can also set any other required headers: Cache-Control, etc.
return response

当然,只有在您控制服务器,或者您的托管公司已经设置了mod_xsendfile时,这将起作用。

编辑:

对于Django 1.7,mimetype已被content_type替换。

response = HttpResponse(content_type='application/force-download')  

编辑:对于nginx,请查看此链接,它使用X-Accel-Redirect代替apache的X-Sendfile头。


6
如果您的文件名或文件路径包含非ASCII字符,如“ä”或“ö”,则smart_str无法正常工作,因为Apache模块X-Sendfile无法解码smart_str编码的字符串。因此,例如“Örinää.mp3”文件无法提供服务。如果省略smart_str,Django本身会抛出ASCII编码错误,因为在发送之前,所有都会被编码为ASCII格式。我所知道避免这个问题的唯一方法是将X-sendfile文件名减少到只包含ASCII字符。 - Ciantic
3
更明确地说,S.Lott提供了一个简单的例子,只需直接从Django服务文件,无需其他设置。elo80ka提供了更高效的示例,其中Web服务器处理静态文件,而Django则不需要处理。后者具有更好的性能,但可能需要更多的设置。两者都有各自的用途。 - rocketmonkeys
1
@Ciantic,看看btimby的答案,那似乎是解决编码问题的方法。 - mlissner
12
Django 1.7中,mimetype被content_type所取代。 - ismailsunni
1
太好了。在2022年这里工作得非常出色。 - partofthething
显示剩余3条评论

89

51
这基本上是正确(简单)的答案,但有一个警告 - 将文件名作为参数传递意味着用户可能会下载任何文件(如果传递"f=/etc/passwd"怎么办?)有很多方法可以防止这种情况发生(用户权限等),但请注意这个明显但普遍的安全风险。这基本上只是验证输入的子集:如果您向视图传递文件名,请在该视图中检查文件名! - rocketmonkeys
9
这个安全问题的解决方法非常简单: filepath = filepath.replace('..', '').replace('/', '') 这行代码会将所有的'..'和'/'字符替换为空字符串,从而避免任何可能的路径穿越攻击。 - duality_
8
如果您使用表格来存储文件信息,包括哪些用户可以下载它,则只需要发送主键而不是文件名,应用程序会决定如何处理。 - Edward Newell

37

对于一个非常简单但不高效或可扩展的解决方案,您可以只使用内置的Django serve视图。这非常适合快速原型或一次性工作,但正如本问题中已经提到的那样,您应该在生产环境中使用类似Apache或Nginx的东西。

from django.views.static import serve
filepath = '/some/path/to/local/file.txt'
return serve(request, os.path.basename(filepath), os.path.dirname(filepath))

还非常有用,可为在Windows上进行测试提供备选方案。 - Amir Ali Akbari
2
为什么它不高效? - zinking
2
@zinking,通常文件应该通过诸如apache之类的东西提供,而不是通过Django进程提供。 - Cory
1
这里我们在谈论什么样的性能劣化?如果通过django提供服务,文件是否会加载到内存或类似的东西中?为什么Django不能像nginx一样高效地提供服务? - Gershom Maes
1
@GershomMaes 并没有真正的解释,但是官方文档表示这种方法“极其低效且可能不安全”,我想他们知道在说什么 https://docs.djangoproject.com/en/1.8/howto/static-files/ - Mark
显示剩余2条评论

27

S.Lott提供了“好”/简单的解决方案,elo80ka提供了“最佳”/高效的解决方案。这里是一个“更好”的/中间的解决方案-无需服务器设置,但对于大文件比朴素的修复方法更有效:

http://djangosnippets.org/snippets/365/

基本上,Django仍然处理文件的服务,但不会一次性将整个文件加载到内存中。这使得您的服务器可以(慢慢地)为大文件提供服务而不会增加内存使用情况。

同样,S.Lott的X-SendFile仍然适用于较大的文件。但如果您不能或不想费心去做那个,则此中间解决方案将使您获得更好的效率而无需麻烦。


6
这段代码不太好。这段代码依赖于未记录的私有模块django.core.servers.httpbase,在代码开头有个大警示标志 “DON'T USE FOR PRODUCTION USE!!!”,而且自从这个文件创建以来,就一直有这个警示。无论如何,这段代码依赖的FileWrapper功能已在Django 1.9中删除。 - eykanal

22

在 Django 1.10 中可以使用 FileResponse 对象来处理文件响应。

编辑:当我在搜索通过 Django 流式传输文件的简单方法时,发现了我的答案,所以这里提供一个更完整的示例(给未来的自己)。假设 FileField 的名称为 imported_file

views.py

from django.views.generic.detail import DetailView   
from django.http import FileResponse
class BaseFileDownloadView(DetailView):
  def get(self, request, *args, **kwargs):
    filename=self.kwargs.get('filename', None)
    if filename is None:
      raise ValueError("Found empty filename")
    some_file = self.model.objects.get(imported_file=filename)
    response = FileResponse(some_file.imported_file, content_type="text/csv")
    # https://docs.djangoproject.com/en/1.11/howto/outputting-csv/#streaming-large-csv-files
    response['Content-Disposition'] = 'attachment; filename="%s"'%filename
    return response

class SomeFileDownloadView(BaseFileDownloadView):
    model = SomeModel

urls.py

...
url(r'^somefile/(?P<filename>[-\w_\\-\\.]+)$', views.SomeFileDownloadView.as_view(), name='somefile-download'),
...

18

尝试了@Rocketmonkeys的解决方案,但下载的文件被存储为*.bin并赋予随机名称。这当然是不好的。添加来自@elo80ka的另一行代码解决了问题。
这是我现在正在使用的代码:

from wsgiref.util import FileWrapper
from django.http import HttpResponse

filename = "/home/stackoverflow-addict/private-folder(not-porn)/image.jpg"
wrapper = FileWrapper(file(filename))
response = HttpResponse(wrapper, content_type='text/plain')
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(filename)
response['Content-Length'] = os.path.getsize(filename)
return response

现在您可以将文件存储在私有目录中(而不是/media或/public_html内),并通过Django将其公开给特定用户或在特定情况下使用。
希望这能帮到您。

感谢 @elo80ka、@S.Lott和@Rocketmonkeys 的答案,我结合了所有答案得到了完美的解决方案=)


1
谢谢,这正是我在寻找的! - ihatecache
1
在Content-Disposition头中的文件名filename="%s"周围添加双引号,以避免文件名中的空格问题。参考资料:下载文件名带有空格时被截断如何在HTTP中编码Content-Disposition头的文件名参数? - Christian Long
1
你的解决方案对我有用。但是我的文件出现了“无效的起始字节…”错误。使用FileWrapper(open(path.abspath(file_name), 'rb'))解决了这个问题。 - Mark Mishyn
自Django 1.9版本起,FileWrapper已被移除。 - freethebees
1
可以使用 from wsgiref.util import FileWrapper - Kriss

13

如上面提到的,mod_xsendfile方法不允许在文件名中使用非ASCII字符。

因此,我有一个针对mod_xsendfile的补丁,可以允许发送任何文件,只要名称是URL编码,并且附加了额外的头信息:

X-SendFile-Encoding: url

同样也被发送。

http://ben.timby.com/?p=149


补丁现在已经并入核心库。 - mlissner

7

2
在一年前,我的个人分支还没有包含在原始代码库中的非Apache文件服务回退功能。 - Roberto Rosario
你为什么删除了链接? - kiok46
@kiok46 与Github政策冲突。已编辑以指向规范地址。 - Roberto Rosario

6
您应该在生产中使用像apachenginx这样的流行服务器提供的sendfile api来保护文件。多年来,我一直在使用这些服务器的sendfile api来保护文件。然后创建了一个简单的基于django的中间件应用程序,适用于开发和生产目的。您可以在此处访问源代码。
更新:在新版本中,python提供程序如果可用,将使用django FileResponse,并添加对从lighthttp、caddy到hiawatha的许多服务器实现的支持。 用法
pip install django-fileprovider
  • fileprovider应用添加到INSTALLED_APPS设置中,
  • fileprovider.middleware.FileProviderMiddleware添加到MIDDLEWARE_CLASSES设置中
  • 在生产环境中将FILEPROVIDER_NAME设置为nginxapache,默认情况下,它是python用于开发目的。

在您的基于类或函数的视图中,将响应头X-File值设置为文件的绝对路径。例如:

def hello(request):
   # code to check or protect the file from unauthorized access
   response = HttpResponse()  
   response['X-File'] = '/absolute/path/to/file'  
   return response

django-fileprovider 实现了一种方式,使得您的代码只需要进行最少的修改。

Nginx 配置

为了保护文件不被直接访问,您可以设置以下配置:

location /files/ {
  internal;
  root   /home/sideffect0/secret_files/;
}

这里nginx设置了一个位置网址/files/,仅在内部访问。如果您使用上述配置,可以将X-File设置为:
response['X-File'] = '/files/filename.extension'

通过使用nginx配置来实现此操作,既可以保护文件,也可以从django视图中控制该文件。

3
def qrcodesave(request): 
    import urllib2;   
    url ="http://chart.apis.google.com/chart?cht=qr&chs=300x300&chl=s&chld=H|0"; 
    opener = urllib2.urlopen(url);  
    content_type = "application/octet-stream"
    response = HttpResponse(opener.read(), content_type=content_type)
    response["Content-Disposition"]= "attachment; filename=aktel.png"
    return response 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接