Avere Django che serve file scaricabili


245

Voglio che gli utenti sul sito siano in grado di scaricare file i cui percorsi sono oscurati in modo che non possano essere scaricati direttamente.

Ad esempio, vorrei che l'URL fosse qualcosa del genere: http://example.com/download/?f=somefile.txt

E sul server, so che tutti i file scaricabili risiedono nella cartella /home/user/files/.

C'è un modo per far sì che Django serva quel file per il download invece di provare a trovare un URL e Visualizza per visualizzarlo?


2
Perché non stai semplicemente usando Apache per farlo? Apache offre contenuti statici più velocemente e più semplicemente di quanto Django possa mai fare.
S. Lott,

22
Non sto usando Apache perché non voglio che i file siano accessibili senza permessi basati su Django.
Damon,

3
Se vuoi prendere in considerazione le autorizzazioni degli utenti devi servire il file attraverso la vista di Django
Łukasz,

127
Esatto, ecco perché sto ponendo questa domanda.
Damon,

Risposte:


189

Per il "meglio di entrambi i mondi" è possibile combinare la soluzione di S.Lott con il modulo xsendfile : django genera il percorso del file (o del file stesso), ma l'effettivo servizio di file è gestito da Apache / Lighttpd. Dopo aver impostato mod_xsendfile, l'integrazione con la vista richiede alcune righe di codice:

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

Ovviamente, questo funzionerà solo se hai il controllo sul tuo server o se la tua società di hosting ha già mod_xsendfile impostato.

MODIFICARE:

mimetype è sostituito da content_type per django 1.7

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

EDIT: per nginxcontrollare questo , usa X-Accel-Redirectinvece l' apacheintestazione X-Sendfile.


6
Se il tuo nome file o path_to_file include caratteri non ascii come "ä" o "ö", smart_strnon funziona come previsto poiché il modulo X-Sendfile di Apache non può decodificare la stringa codificata smart_str. Pertanto, ad esempio, il file "Örinää.mp3" non può essere pubblicato. E se si omette smart_str, lo stesso Django genera un errore di codifica ASCII perché tutte le intestazioni sono codificate in formato ASCII prima dell'invio. L'unico modo che conosco per aggirare questo problema è ridurre i nomi dei file X-sendfile a quelli che sono solo ascii.
Ciantic,

3
Per essere più chiari: S.Lott ha il semplice esempio, serve solo file direttamente da Django, non è necessaria alcuna altra installazione. elo80ka ha l'esempio più efficiente, in cui il web server gestisce i file statici e django non deve farlo. Quest'ultimo ha prestazioni migliori, ma potrebbe richiedere una maggiore configurazione. Entrambi hanno i loro posti.
rocketmonkeys,

1
@Ciantic, vedi la risposta di btimby per quella che sembra una soluzione al problema della codifica.
mlissner,

Questa soluzione funziona con la seguente configurazione del server Web? Back-end: 2 o più server Apache + mod_wsgi individual (VPS) impostati per replicarsi a vicenda. Front-end: 1 server proxy nginx (VPS) che utilizza il bilanciamento del carico a monte, eseguendo round-robin.
Daniel,

12
mimetype è sostituito da content_type per django 1.7
ismailsunni

88

Un "download" è semplicemente una modifica dell'intestazione HTTP.

Vedi http://docs.djangoproject.com/en/dev/ref/request-response/#telling-the-browser-to-treat-the-response-as-a-file-attachment per come rispondere con un download .

Hai solo bisogno di una definizione URL per "/download".

La richiesta GETo il POSTdizionario avranno le "f=somefile.txt"informazioni.

La funzione di visualizzazione unirà semplicemente il percorso di base con il fvalore " ", aprirà il file, creerà e restituirà un oggetto risposta. Dovrebbe essere inferiore a 12 righe di codice.


49
Questa è essenzialmente la risposta corretta (semplice), ma un avvertimento: passare il nome file come parametro significa che l'utente può potenzialmente scaricare qualsiasi file (es. Cosa succede se si passa "f = / etc / passwd"?) Ci sono molte delle cose che aiutano a prevenirlo (autorizzazioni utente, ecc.), ma sii consapevole di questo ovvio ma comune rischio per la sicurezza. Fondamentalmente è solo un sottoinsieme di input di convalida: se passi un nome file a una vista, controlla il nome file in quella vista!
rocketmonkeys,

9
UN molto semplice per questo problema di sicurezza:filepath = filepath.replace('..', '').replace('/', '')
dualità_

7
Se usi una tabella per archiviare le informazioni sui file, inclusi gli utenti che dovrebbero essere in grado di scaricarlo, tutto ciò che devi inviare è la chiave primaria, non il nome file e l'app decide cosa fare.
Edward Newell,

30

Per una soluzione molto semplice ma non efficiente o scalabile , puoi semplicemente usare la servevista django integrata . Questo è eccellente per prototipi rapidi o lavori una tantum, ma come è stato menzionato in questa domanda, dovresti usare qualcosa come apache o nginx in produzione.

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

Molto utile anche per fornire un fallback per i test su Windows.
Amir Ali Akbari,

Sto facendo un progetto django autonomo, destinato a funzionare come un client desktop, e questo ha funzionato perfettamente. Grazie!
daigorocub,

1
perché non è efficiente?
tintinnio l'

2
@zinking perché i file dovrebbero generalmente essere serviti tramite qualcosa come apache piuttosto che attraverso il processo di django
Cory

1
Di che tipo di svantaggi prestazionali stiamo parlando qui? I file vengono caricati nella RAM o qualcosa del genere se vengono serviti tramite django? Perché django non è in grado di servire con la stessa efficienza di nginx?
Gershom,

27

S.Lott ha la soluzione "buona" / semplice e elo80ka ha la soluzione "migliore" / efficiente. Ecco una soluzione "migliore" / media: nessuna configurazione del server, ma più efficiente per file di grandi dimensioni rispetto alla soluzione ingenua:

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

Fondamentalmente, Django gestisce ancora il servizio del file ma non carica tutto in memoria contemporaneamente. Ciò consente al server di (lentamente) servire un file di grandi dimensioni senza aumentare l'utilizzo della memoria.

Ancora una volta, X-SendFile di S.Lott è ancora migliore per file più grandi. Ma se non puoi o non vuoi preoccuparti di questo, allora questa soluzione intermedia ti garantirà una migliore efficienza senza problemi.


4
Quel frammento non è buono. Questo frammento si basa sul django.core.servers.httpbasemodulo privato non documentato, che ha un grande segnale di avvertimento nella parte superiore del codice " NON UTILIZZARE PER L'USO DI PRODUZIONE !!! ", che è stato nel file da quando è stato creato per la prima volta . In ogni caso, la FileWrapperfunzionalità su cui si basa questo snippet è stata rimossa in django 1.9.
eykanal,

16

Ho provato la soluzione @Rocketmonkeys ma i file scaricati sono stati archiviati come * .bin e hanno dato nomi casuali. Non va bene ovviamente. L'aggiunta di un'altra riga da @ elo80ka ha risolto il problema.
Ecco il codice che sto usando ora:

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

Ora puoi archiviare i file in una directory privata (non all'interno di / media né / public_html) ed esporli via django a determinati utenti o in determinate circostanze.
Spero che sia d'aiuto.

Grazie a @ elo80ka, @ S.Lott e @Rocketmonkeys per le risposte, ho ottenuto la soluzione perfetta combinandoli tutti =)


1
Grazie, questo era esattamente quello che stavo cercando!
ihatecache,

1
Aggiungi virgolette attorno al nome del file filename="%s"nell'intestazione Content-Disposition, per evitare problemi con spazi nei nomi dei file. Riferimenti: I nomi di file con spazi vengono troncati al momento del download , Come codificare il parametro nome file dell'intestazione Content-Disposition in HTTP?
Christian Long,

1
Le tue soluzioni funzionano per me. Ma ho avuto l'errore "byte iniziale non valido ..." per il mio file. Risolto conFileWrapper(open(path.abspath(file_name), 'rb'))
Mark Mishyn il

FileWrapperè stato rimosso da Django 1.9
freethebees,

È possibile utilizzarefrom wsgiref.util import FileWrapper
Kriss il

15

Solo menzionando l' oggetto FileResponse disponibile in Django 1.10

Modifica: ho appena incontrato la mia risposta durante la ricerca di un modo semplice per lo streaming di file tramite Django, quindi ecco un esempio più completo (per il futuro me). Presuppone che il nome FileField siaimported_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'),
...

1
Grazie mille! È la soluzione più semplice per il download di file binari e funziona.
Julia Zhao,

13

È stato menzionato sopra che il metodo mod_xsendfile non consente caratteri non ASCII nei nomi di file.

Per questo motivo, ho una patch disponibile per mod_xsendfile che consentirà l'invio di qualsiasi file, purché il nome sia codificato nell'URL e l'intestazione aggiuntiva:

X-SendFile-Encoding: url

Viene inviato anche.

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


La patch ora è piegata nella libreria del corer.
mlissner,

7

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

"Astrazione per scaricare i file caricati sul web-server (es. Apache con mod_xsendfile) dopo che Django ha verificato i permessi, ecc."


2
All'epoca (1 anno fa) il mio fork personale aveva il file non Apache che serviva il fallback che il repository originale non ha ancora incluso.
Roberto Rosario,

Perché hai rimosso il link?
kiok46,

@ kiok46 Conflitto con le politiche di Github. Modificato per indicare l'indirizzo canonico.
Roberto Rosario,

6

Dovresti usare le API di sendfile fornite da server popolari come apacheo nginx in produzione. Per molti anni ho usato l'API sendfile di questi server per proteggere i file. Quindi ha creato una semplice app django basata su middleware per questo scopo adatta sia allo scopo di sviluppo che di produzione . Qui puoi accedere al codice sorgente .
AGGIORNAMENTO: nella nuova versione il pythonprovider utilizza django FileResponsese disponibile e aggiunge anche il supporto per molte implementazioni di server da lighthttp, caddy a hiawatha

uso

pip install django-fileprovider
  • aggiungi fileproviderapp alle INSTALLED_APPSimpostazioni,
  • aggiungi fileprovider.middleware.FileProviderMiddlewarealle MIDDLEWARE_CLASSESimpostazioni
  • impostare le FILEPROVIDER_NAMEimpostazioni su nginxo apachein produzione, per impostazione predefinita è pythona scopo di sviluppo.

nelle viste classbased o function imposta il X-Filevalore dell'intestazione della risposta sul percorso assoluto del file. Per esempio,

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 implementato in modo tale che il codice richieda solo modifiche minime.

Configurazione di Nginx

Per proteggere il file dall'accesso diretto è possibile impostare la configurazione come

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

Qui nginximposta un indirizzo URL /files/accessibile solo internamente, se stai utilizzando la configurazione sopra puoi impostare X-File come,

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

In questo modo con la configurazione di nginx, il file sarà protetto e inoltre potrai controllare il file da django views


2

Django consiglia di utilizzare un altro server per servire supporti statici (un altro server in esecuzione sullo stesso computer va bene.) Raccomandano l'uso di server come lighttp .

Questo è molto semplice da configurare. Però. se 'somefile.txt' viene generato su richiesta (il contenuto è dinamico), potresti voler django per servirlo.

Django Docs - File statici


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 

0

Un altro progetto da dare un'occhiata a: http://readthedocs.org/docs/django-private-files/en/latest/usage.html Sembra promettente, non l'ho ancora provato da solo.

Fondamentalmente il progetto estrae la configurazione mod_xsendfile e ti permette di fare cose come:

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)

1
request.user.is_authenticated è un metodo, non un attributo. (not request.user.is_anonymous ()) è uguale a request.user.is_authenticated () perché is_authenticated è l'inverso di is_anonymous.
esplode il

@explodes Ancora peggio, quel codice deriva proprio dai documenti di django-private-files...
Armando Pérez Marqués,



Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.