Faire en sorte que Django serve des fichiers téléchargeables

245

Je souhaite que les utilisateurs du site puissent télécharger des fichiers dont les chemins sont masqués afin qu'ils ne puissent pas être téléchargés directement.

Par exemple, j'aimerais que l'URL soit quelque chose comme ceci: http://example.com/download/?f=somefile.txt

Et sur le serveur, je sais que tous les fichiers téléchargeables résident dans le dossier /home/user/files/.

Existe-t-il un moyen de faire en sorte que Django serve ce fichier à télécharger au lieu d'essayer de trouver une URL et de la visualiser pour l'afficher?

damon
la source
2
Pourquoi n'utilisez-vous pas simplement Apache pour cela? Apache sert du contenu statique plus rapidement et plus simplement que Django n'aurait jamais pu.
S.Lott
22
Je n'utilise pas Apache parce que je ne veux pas que les fichiers soient accessibles sans autorisations basées sur Django.
damon
3
Si vous souhaitez prendre en compte les autorisations des utilisateurs, vous devez servir le fichier via la vue de Django
Łukasz
127
Exactement, c'est pourquoi je pose cette question.
damon

Réponses:

189

Pour le "meilleur des deux mondes", vous pouvez combiner la solution de S.Lott avec le module xsendfile : django génère le chemin vers le fichier (ou le fichier lui-même), mais le service de fichier réel est géré par Apache / Lighttpd. Une fois que vous avez configuré mod_xsendfile, l'intégration à votre vue prend quelques lignes de code:

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

Bien sûr, cela ne fonctionnera que si vous avez le contrôle sur votre serveur, ou si votre hébergeur a déjà configuré mod_xsendfile.

ÉDITER:

mimetype est remplacé par content_type pour django 1.7

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

EDIT: Pour nginxvérifier cela , il utilise à la X-Accel-Redirectplace de l'en apache-tête X-Sendfile.

elo80ka
la source
6
Si votre nom de fichier ou path_to_file contient des caractères non ascii tels que "ä" ou "ö", le smart_strne fonctionne pas comme prévu car le module apache X-Sendfile ne peut pas décoder la chaîne codée smart_str. Ainsi, par exemple, le fichier "Örinää.mp3" ne peut pas être servi. Et si l'on omet le smart_str, le Django lui-même génère une erreur de codage ascii car tous les en- têtes sont codés au format ascii avant l'envoi. La seule façon que je connaisse pour contourner ce problème est de réduire les noms de fichiers X-sendfile à ceux qui ne comprennent que ascii.
Ciantic
3
Pour être plus clair: S.Lott a l'exemple simple, servant uniquement des fichiers directement à partir de django, aucune autre configuration n'est nécessaire. elo80ka a l'exemple le plus efficace, où le serveur web gère les fichiers statiques et que django n'a pas à le faire. Ce dernier a de meilleures performances, mais peut nécessiter plus de configuration. Les deux ont leur place.
rocketmonkeys
1
@Ciantic, voir la réponse de btimby pour ce qui ressemble à une solution au problème d'encodage.
mlissner
Cette solution fonctionne-t-elle avec la configuration de serveur Web suivante? Back-end: 2 serveurs Apache + mod_wsgi individuels (VPS) ou plus configurés pour se répliquer. Front-end: 1 serveur proxy nginx (VPS) utilisant l'équilibrage de charge en amont, effectuant un round robin.
Daniel
12
mimetype est remplacé par content_type pour django 1.7
ismailsunni
88

Un "téléchargement" est simplement un changement d'en-tête HTTP.

Voir http://docs.djangoproject.com/en/dev/ref/request-response/#telling-the-browser-to-treat-the-response-as-a-file-attachment pour savoir comment répondre avec un téléchargement .

Vous n'avez besoin que d'une définition d'URL pour "/download".

La demande GETou le POSTdictionnaire contiendra les "f=somefile.txt"informations.

Votre fonction d'affichage fusionnera simplement le chemin de base avec la fvaleur " ", ouvrira le fichier, créera et renverra un objet de réponse. Il doit contenir moins de 12 lignes de code.

S.Lott
la source
49
C'est essentiellement la bonne réponse (simple), mais une mise en garde - le fait de passer le nom de fichier comme paramètre signifie que l'utilisateur peut potentiellement télécharger n'importe quel fichier (c'est-à-dire si vous passez "f = / etc / passwd"?) des choses qui aident à empêcher cela (autorisations des utilisateurs, etc.), mais soyez conscient de ce risque de sécurité évident mais commun. Il s'agit essentiellement d'un sous-ensemble de validation d'entrée: si vous passez un nom de fichier à une vue, vérifiez le nom de fichier dans cette vue!
rocketmonkeys
9
Une solution très simple pour ce problème de sécurité:filepath = filepath.replace('..', '').replace('/', '')
duality_
7
Si vous utilisez un tableau pour stocker des informations sur les fichiers, y compris les utilisateurs qui devraient pouvoir les télécharger, alors tout ce que vous devez envoyer est la clé primaire, pas le nom de fichier, et l'application décide quoi faire.
Edward Newell
30

Pour une solution très simple mais pas efficace ou évolutive , vous pouvez simplement utiliser la servevue django intégrée . C'est excellent pour les prototypes rapides ou les travaux ponctuels, mais comme cela a été mentionné tout au long de cette question, vous devez utiliser quelque chose comme apache ou nginx en production.

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
la source
Également très utile pour fournir une solution de rechange pour les tests sous Windows.
Amir Ali Akbari
Je fais un projet django autonome, destiné à fonctionner un peu comme un client de bureau, et cela a parfaitement fonctionné. Merci!
daigorocub
1
pourquoi ce n'est pas efficace?
zinking
2
@zinking car les fichiers doivent généralement être servis via quelque chose comme apache plutôt que via le processus django
Cory
1
De quel type d'inconvénients de performances parlons-nous ici? Les fichiers sont-ils chargés dans la RAM ou quelque chose du genre s'ils sont servis via Django? Pourquoi Django n'est-il pas capable de servir avec la même efficacité que Nginx?
Gershom
27

S.Lott a la "bonne" / solution simple, et elo80ka a la "meilleure" / solution efficace. Voici une "meilleure" solution / intermédiaire - pas de configuration de serveur, mais plus efficace pour les gros fichiers que le correctif naïf:

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

Fondamentalement, Django gère toujours le service du fichier mais ne charge pas le tout en mémoire à la fois. Cela permet à votre serveur de servir (lentement) un gros fichier sans augmenter l'utilisation de la mémoire.

Encore une fois, le fichier X-SendFile de S.Lott est encore meilleur pour les fichiers plus volumineux. Mais si vous ne pouvez pas ou ne voulez pas vous en préoccuper, cette solution intermédiaire vous permettra de gagner en efficacité sans les tracas.

rocketmonkeys
la source
4
Cet extrait n'est pas bon. Cet extrait repose sur le django.core.servers.httpbasemodule privé non documenté, qui a un grand panneau d'avertissement en haut du code " NE PAS UTILISER POUR L'UTILISATION DE LA PRODUCTION !!! ", qui se trouve dans le fichier depuis sa création . Dans tous les cas, la FileWrapperfonctionnalité sur laquelle repose cet extrait a été supprimée dans django 1.9.
eykanal
16

J'ai essayé la solution @Rocketmonkeys mais les fichiers téléchargés étaient stockés sous * .bin et donnaient des noms aléatoires. Ce n'est pas très bien sûr. L'ajout d'une autre ligne à partir de @ elo80ka a résolu le problème.
Voici le code que j'utilise maintenant:

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

Vous pouvez désormais stocker des fichiers dans un répertoire privé (pas à l'intérieur de / media ni / public_html) et les exposer via django à certains utilisateurs ou dans certaines circonstances.
J'espère que ça aide.

Merci à @ elo80ka, @ S.Lott et @Rocketmonkeys pour les réponses, a obtenu la solution parfaite en les combinant tous =)

Salvatorelab
la source
1
Merci, c'est exactement ce que je cherchais!
ihatecache
1
Ajoutez des guillemets doubles autour du nom de fichier filename="%s"dans l'en-tête Content-Disposition, pour éviter les problèmes avec les espaces dans les noms de fichiers. Références: Les noms de fichiers avec des espaces sont tronqués lors du téléchargement , comment coder le paramètre de nom de fichier de l'en-tête Content-Disposition en HTTP?
Christian Long
1
Vos solutions fonctionnent pour moi. Mais j'avais une erreur "octet de départ invalide ..." pour mon fichier. Résolu avecFileWrapper(open(path.abspath(file_name), 'rb'))
Mark Mishyn
FileWrappera été supprimé depuis Django 1.9
freethebees
Il est possible d'utiliserfrom wsgiref.util import FileWrapper
Kriss
15

Il suffit de mentionner l' objet FileResponse disponible dans Django 1.10

Edit: Je suis juste tombé sur ma propre réponse lors de la recherche d'un moyen simple de diffuser des fichiers via Django, voici donc un exemple plus complet (pour moi à l'avenir). Il suppose que le nom FileField estimported_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
la source
1
Merci beaucoup! C'est la solution la plus simple pour télécharger des fichiers binaires et ça marche.
Julia Zhao
13

Il a été mentionné ci-dessus que la méthode mod_xsendfile ne permet pas les caractères non ASCII dans les noms de fichiers.

Pour cette raison, j'ai un correctif disponible pour mod_xsendfile qui permettra d'envoyer n'importe quel fichier, tant que le nom est encodé en url, et l'en-tête supplémentaire:

X-SendFile-Encoding: url

Est également envoyé.

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

btimby
la source
Le patch est maintenant plié dans la bibliothèque de carottiers.
mlissner
7

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

"Abstraction pour décharger les téléchargements de fichiers sur le serveur Web (par exemple Apache avec mod_xsendfile) une fois que Django a vérifié les autorisations, etc."

Roberto Rosario
la source
2
À l'époque (il y a 1 an), mon fork personnel avait le fichier non Apache servant de secours, le référentiel d'origine n'a pas encore été inclus.
Roberto Rosario
Pourquoi avez-vous supprimé le lien?
kiok46
@ kiok46 Conflit avec les politiques Github. Modifié pour pointer vers l'adresse canonique.
Roberto Rosario
6

Vous devez utiliser des API sendfile fournies par des serveurs populaires comme apacheou nginx en production. De nombreuses années, j'utilisais l'API Sendfile de ces serveurs pour protéger les fichiers. Vous avez ensuite créé une application Django basée sur un middleware simple à cet effet, adaptée à la fois au développement et à la production.Vous pouvez accéder au code source ici .
MISE À JOUR: dans la nouvelle version, le pythonfournisseur utilise django FileResponses'il est disponible et ajoute également la prise en charge de nombreuses implémentations de serveur de lighthttp, caddy à hiawatha

Usage

pip install django-fileprovider
  • ajouter une fileproviderapplication aux INSTALLED_APPSparamètres,
  • ajouter fileprovider.middleware.FileProviderMiddlewareaux MIDDLEWARE_CLASSESparamètres
  • définissez les FILEPROVIDER_NAMEparamètres sur nginxou apacheen production, par défaut, c'est pythonà des fins de développement.

dans vos vues de classe ou de fonction, définissez la X-Filevaleur de l'en- tête de réponse sur le chemin absolu du fichier. Par exemple,

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 mis en œuvre de manière à ce que votre code ne nécessite qu'une modification minimale.

Configuration de Nginx

Pour protéger le fichier d'un accès direct, vous pouvez définir la configuration comme

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

Définit ici nginxune URL d'emplacement /files/uniquement pour l'accès interne, si vous utilisez la configuration ci-dessus, vous pouvez définir X-File comme,

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

En faisant cela avec la configuration nginx, le fichier sera protégé et vous pouvez également contrôler le fichier à partir de django views

Renjith Thankachan
la source
2

Django vous recommande d'utiliser un autre serveur pour servir des supports statiques (un autre serveur fonctionnant sur la même machine est très bien.) Ils recommandent l'utilisation de tels serveurs comme lighttp .

C'est très simple à mettre en place. Toutefois. si 'somefile.txt' est généré sur demande (le contenu est dynamique) alors vous voudrez peut-être que django le serve.

Django Docs - Fichiers statiques

kjfletch
la source
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
la source
0

Un autre projet à voir: http://readthedocs.org/docs/django-private-files/en/latest/usage.html Il semble prometteur, je ne l'ai pas encore testé moi-même.

Fondamentalement, le projet résume la configuration mod_xsendfile et vous permet de faire des choses comme:

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
la source
1
request.user.is_authenticated est une méthode, pas un attribut. (pas request.user.is_anonymous ()) est exactement le même que request.user.is_authenticated () car is_authenticated est l'inverse de is_anonymous.
explose
@explodes Pire encore, ce code provient des documents de django-private-files...
Armando Pérez Marqués
0

J'ai fait un projet là-dessus. Vous pouvez regarder mon repo github:

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

Ce module fournit un moyen simple de servir des fichiers à télécharger dans le framework de repos django en utilisant le module Apache Xsendfile. Il a également une fonctionnalité supplémentaire de servir les téléchargements uniquement aux utilisateurs appartenant à un groupe particulier

nicks_4317
la source