Tendo o Django servindo arquivos para download

245

Desejo que os usuários no site possam baixar arquivos cujos caminhos estejam ocultos para que não possam ser baixados diretamente.

Por exemplo, eu gostaria que o URL fosse algo assim: http://example.com/download/?f=somefile.txt

E no servidor, eu sei que todos os arquivos para download residem na pasta /home/user/files/.

Existe uma maneira de fazer o Django servir esse arquivo para download, em vez de tentar encontrar uma URL e exibir para exibi-la?

Damon
fonte
2
Por que você não está simplesmente usando o Apache para fazer isso? O Apache serve conteúdo estático de maneira mais rápida e simples do que o Django jamais pôde.
315/09 S.Lott
22
Não estou usando o Apache porque não quero que os arquivos sejam acessíveis sem permissões baseadas no Django.
21909 damon
3
Se você quiser levar em conta as permissões de usuário você tem que servir de arquivos através da visão de Django
Łukasz
127
Exatamente, é por isso que estou fazendo essa pergunta.
Damon

Respostas:

189

Para o "melhor dos dois mundos", você pode combinar a solução da S.Lott com o módulo xsendfile : o django gera o caminho para o arquivo (ou o próprio arquivo), mas o serviço de arquivo real é tratado pelo Apache / Lighttpd. Depois de configurar o mod_xsendfile, a integração com sua visualização exige algumas linhas de código:

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

Obviamente, isso só funcionará se você tiver controle sobre seu servidor ou se sua empresa de hospedagem já tiver configurado o mod_xsendfile.

EDITAR:

mimetype é substituído por content_type para django 1.7

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

EDIT: Para nginxverificar isso , ele usa em X-Accel-Redirectvez do apachecabeçalho X-Sendfile.

elo80ka
fonte
6
Se o seu nome de arquivo ou path_to_file incluir caracteres não-ascii como "ä" ou "ö", smart_strele não funcionará conforme o esperado, pois o módulo apache X-Sendfile não pode decodificar a cadeia codificada por smart_str. Assim, por exemplo, o arquivo "Örinää.mp3" não pode ser exibido. E se alguém omitir o smart_str, o próprio Django gera um erro de codificação ascii porque todos os cabeçalhos são codificados no formato ascii antes do envio. A única maneira que conheço para contornar esse problema é reduzir os nomes de arquivos X-sendfile para aqueles que consistem apenas em ascii.
Ciantic
3
Para ser mais claro: S.Lott tem um exemplo simples, servindo arquivos diretamente do django, nenhuma outra configuração é necessária. O elo80ka tem o exemplo mais eficiente, em que o servidor da web lida com arquivos estáticos e o django não precisa. O último tem melhor desempenho, mas pode exigir mais configurações. Ambos têm seus lugares.
Rocketmonkeys
1
@Ciantic, veja a resposta do btimby para o que parece ser uma solução para o problema de codificação.
mlissner
Esta solução funciona com a seguinte configuração de servidor web? Back-end: 2 ou mais servidores Apache + mod_wsgi individuais (VPS) configurados para se replicar. Front-end: 1 servidor proxy nginx (VPS) usando balanceamento de carga upstream, fazendo rodízio.
Daniel
12
tipo MIME é substituído por content_type para django 1,7
ismailsunni
88

Um "download" é ​​simplesmente uma alteração no cabeçalho HTTP.

Consulte http://docs.djangoproject.com/en/dev/ref/request-response/#telling-the-browser-to-treat-the-response-as-a-file-attachment para saber como responder com um download .

Você só precisa de uma definição de URL "/download".

A solicitação GETou POSTdicionário terá as "f=somefile.txt"informações.

Sua função de visualização simplesmente mesclará o caminho base com o fvalor " ", abrir o arquivo, criar e retornar um objeto de resposta. Deve ter menos de 12 linhas de código.

S.Lott
fonte
49
Esta é essencialmente a resposta correta (simples), mas um cuidado - passar o nome do arquivo como parâmetro significa que o usuário pode potencialmente baixar qualquer arquivo (por exemplo, e se você passar "f = / etc / passwd"?) de coisas que ajudam a evitar isso (permissões de usuário, etc.), mas esteja ciente desse risco de segurança óbvio, mas comum. É basicamente apenas um subconjunto de entrada de validação: se você passar um nome de arquivo para uma visualização, verifique o nome de arquivo nessa visualização!
Rocketmonkeys
9
Uma correção muito simples para essa preocupação de segurança:filepath = filepath.replace('..', '').replace('/', '')
duality__
7
Se você usar uma tabela para armazenar informações do arquivo, incluindo quais usuários devem poder fazer o download, tudo o que você precisa enviar é a chave primária, não o nome do arquivo, e o aplicativo decide o que fazer.
Edward Newell
30

Para uma solução muito simples, mas não eficiente ou escalonável , você pode simplesmente usar a serveexibição django embutida . Isso é excelente para protótipos rápidos ou trabalhos pontuais, mas, como foi mencionado ao longo desta pergunta, você deve usar algo como apache ou nginx na produção.

from django.views.static import serve
filepath = '/some/path/to/local/file.txt'
return serve(request, os.path.basename(filepath), os.path.dirname(filepath))
Cory
fonte
Também é muito útil para fornecer um substituto para testes no Windows.
Amir Ali Akbari
Estou fazendo um projeto de django independente, destinado a funcionar como um cliente de desktop, e isso funcionou perfeitamente. Obrigado!
daigorocub 23/01
1
por que não é eficiente?
zinking
2
@zinking porque os arquivos geralmente deve ser servido através de algo como apache em vez de através do processo de Django
Cory
1
De que tipo de desvantagens de desempenho estamos falando aqui? Os arquivos são carregados na RAM ou algo do tipo se forem servidos pelo django? Por que o django não é capaz de servir com a mesma eficiência que o nginx?
Gershom
27

S.Lott tem a solução "boa" / simples e elo80ka tem a "melhor" / solução eficiente. Aqui está uma solução "melhor" / média - sem configuração do servidor, mas mais eficiente para arquivos grandes do que a correção ingênua:

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

Basicamente, o Django ainda lida com a veiculação do arquivo, mas não carrega tudo na memória de uma só vez. Isso permite que o servidor (lentamente) sirva um arquivo grande sem aumentar o uso de memória.

Novamente, o X-SendFile da S.Lott ainda é melhor para arquivos maiores. Mas se você não pode ou não quer se preocupar com isso, essa solução intermediária aumentará sua eficiência sem problemas.

rocketmonkeys
fonte
4
Esse trecho não é bom. Esse recorte depende do django.core.servers.httpbasemódulo privado não documentado, que possui um grande sinal de alerta na parte superior do código " NÃO USE PARA USO DE PRODUÇÃO !!! ", que está no arquivo desde que foi criado . De qualquer forma, a FileWrapperfuncionalidade em que esse trecho se baseia foi removida no django 1.9.
eykanal
16

Tentei a solução @Rocketmonkeys, mas os arquivos baixados estavam sendo armazenados como * .bin e recebiam nomes aleatórios. Isso não está bem, é claro. Adicionar outra linha de @ elo80ka resolveu o problema.
Aqui está o código que estou usando agora:

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

Agora você pode armazenar arquivos em um diretório privado (não dentro de / media nem / public_html) e expô-los via django a certos usuários ou sob certas circunstâncias.
Espero que ajude.

Graças a @ elo80ka, @ S.Lott e @Rocketmonkeys pelas respostas, obtive a solução perfeita combinando todas elas =)

Salvatorelab
fonte
1
Obrigado, era exatamente isso que eu estava procurando!
Ihatecache
1
Adicione aspas duplas ao redor do nome do arquivo filename="%s"no cabeçalho Disposição de Conteúdo, para evitar problemas com espaços nos nomes dos arquivos. Referências: nomes de arquivos com espaços são truncados durante o download . Como codificar o parâmetro filename do cabeçalho Content-Disposition no HTTP?
Christian Long
1
Suas soluções funcionam para mim. Mas eu tive o erro "byte inicial inválido ..." no meu arquivo. Resolvido com #FileWrapper(open(path.abspath(file_name), 'rb'))
Mark Mishyn
FileWrapperfoi removido desde Django 1.9
freethebees
É possível usarfrom wsgiref.util import FileWrapper
Kriss
15

Apenas mencionando o objeto FileResponse disponível no Django 1.10

Edit: Acabei de encontrar minha própria resposta enquanto procurava uma maneira fácil de transmitir arquivos via Django, então aqui está um exemplo mais completo (para o futuro). Parte do princípio que o nome 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'),
...
shadi
fonte
1
Muito obrigado! É a solução mais simples para baixar arquivos binários e funciona.
Julia Zhao
13

Foi mencionado acima que o método mod_xsendfile não permite caracteres não ASCII nos nomes de arquivos.

Por esse motivo, tenho um patch disponível para mod_xsendfile que permitirá que qualquer arquivo seja enviado, desde que o nome seja codificado por URL e o cabeçalho adicional:

X-SendFile-Encoding: url

É enviado também.

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

btimby
fonte
O patch agora está dobrado na biblioteca de descargas.
mlissner
7

Tente: https://pypi.python.org/pypi/django-sendfile/

"Abstração para descarregar uploads de arquivos para o servidor web (por exemplo, Apache com mod_xsendfile) uma vez que o Django verifique as permissões, etc."

Roberto Rosario
fonte
2
Na época (1 ano atrás), minha bifurcação pessoal tinha o arquivo não Apache servindo fallback que o repositório original ainda não incluiu.
Roberto Rosario
Por que você removeu o link?
kiok46
@ kiok46 Conflito com as políticas do Github. Editado para apontar para o endereço canônico.
Roberto Rosario
6

Você deve usar apis sendfile fornecidas por servidores populares como apacheou nginx em produção. Muitos anos eu estava usando a API sendfile desses servidores para proteger arquivos. Em seguida, criou um aplicativo django baseado em middleware simples para esse fim, adequado tanto para fins de desenvolvimento quanto de produção. Você pode acessar o código-fonte aqui .
UPDATE: na nova versão, o pythonprovedor usa django, FileResponsese disponível, e também adiciona suporte para muitas implementações de servidor, desde lighthttp, caddy a hiawatha

Uso

pip install django-fileprovider
  • adicionar fileprovideraplicativo às INSTALLED_APPSconfigurações,
  • adicionar fileprovider.middleware.FileProviderMiddlewareàs MIDDLEWARE_CLASSESconfigurações
  • definir FILEPROVIDER_NAMEconfigurações para nginxou apacheem produção; por padrão, é pythonpara fins de desenvolvimento.

nas visualizações de função ou baseada em classe, defina o X-Filevalor do cabeçalho de resposta como caminho absoluto para o arquivo. Por exemplo,

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 incrementado de uma maneira que seu código precisará apenas de modificações mínimas.

Configuração Nginx

Para proteger o arquivo do acesso direto, você pode definir a configuração como

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

Aqui nginxdefine um URL de localização que /files/somente acessa internamente. Se você estiver usando a configuração acima, poderá definir o arquivo X como,

response['X-File'] = '/files/filename.extension' 

Ao fazer isso com a configuração nginx, o arquivo será protegido e você também poderá controlar o arquivo do django views

Renjith Thankachan
fonte
2

O Django recomenda que você use outro servidor para servir mídia estática (outro servidor rodando na mesma máquina está bom.) Eles recomendam o uso de servidores como o lighttp .

Isso é muito simples de configurar. Contudo. se 'somefile.txt' for gerado sob solicitação (o conteúdo é dinâmico), convém que o django o atenda.

Django Docs - Arquivos estáticos

kjfletch
fonte
2
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 
Saurabh Chandra Patel
fonte
0

Outro projeto para dar uma olhada: http://readthedocs.org/docs/django-private-files/en/latest/usage.html Parece promissor, mas ainda não testei.

Basicamente, o projeto abstrai a configuração mod_xsendfile e permite que você faça coisas como:

from django.db import models
from django.contrib.auth.models import User
from private_files import PrivateFileField

def is_owner(request, instance):
    return (not request.user.is_anonymous()) and request.user.is_authenticated and
                   instance.owner.pk = request.user.pk

class FileSubmission(models.Model):
    description = models.CharField("description", max_length = 200)
        owner = models.ForeignKey(User)
    uploaded_file = PrivateFileField("file", upload_to = 'uploads', condition = is_owner)
avlnx
fonte
1
request.user.is_authenticated é um método, não um atributo. (not request.user.is_anonymous ()) é exatamente o mesmo que request.user.is_authenticated () porque is_authenticated é o inverso de is_anonymous.
explode
@explodes Mesmo piores, esse código é desde os docs de django-private-files...
Armando Pérez Marqués
0

Eu fiz um projeto sobre isso. Você pode ver o meu repositório no github:

https://github.com/nishant-boro/django-rest-framework-download-expert

Este módulo fornece uma maneira simples de servir arquivos para download no framework django rest usando o módulo Apache Xsendfile. Ele também possui um recurso adicional de veicular downloads apenas para usuários pertencentes a um grupo específico

nicks_4317
fonte