Hacer que Django sirva archivos descargables

245

Quiero que los usuarios del sitio puedan descargar archivos cuyas rutas están oscurecidas para que no se puedan descargar directamente.

Por ejemplo, me gustaría que la URL sea algo así: http://example.com/download/?f=somefile.txt

Y en el servidor, sé que todos los archivos descargables residen en la carpeta /home/user/files/.

¿Hay alguna manera de hacer que Django sirva ese archivo para descargar en lugar de intentar encontrar una URL y Ver para mostrarlo?

damon
fuente
2
¿Por qué no estás simplemente usando Apache para hacer esto? Apache sirve contenido estático más rápido y más simple que Django.
S.Lott
22
No estoy usando Apache porque no quiero que se pueda acceder a los archivos sin permisos basados ​​en Django.
damon
3
Si desea tener en cuenta los permisos de usuario, debe entregar el archivo a través de la vista de Django
Łukasz el
127
Exactamente, por eso estoy haciendo esta pregunta.
damon

Respuestas:

189

Para "lo mejor de ambos mundos", podría combinar la solución de S.Lott con el módulo xsendfile : django genera la ruta al archivo (o el archivo en sí), pero Apache / Lighttpd maneja el servicio del archivo real. Una vez que haya configurado mod_xsendfile, la integración con su vista requiere algunas líneas 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

Por supuesto, esto solo funcionará si tiene control sobre su servidor, o si su empresa de alojamiento ya ha configurado mod_xsendfile.

EDITAR:

mimetype se reemplaza por content_type para django 1.7

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

EDITAR: Para nginxverificar esto , usa en X-Accel-Redirectlugar del apacheencabezado X-Sendfile.

elo80ka
fuente
66
Si su nombre de archivo, o path_to_file incluye caracteres que no son ascii como "ä" o "ö", smart_strno funciona según lo previsto ya que el módulo apache X-Sendfile no puede decodificar la cadena codificada smart_str. Así, por ejemplo, el archivo "Örinää.mp3" no se puede servir. Y si uno omite smart_str, el Django arroja un error de codificación ASCII porque todos los encabezados están codificados en formato ASCII antes de enviarlos. La única forma que conozco para sortear este problema es reducir los nombres de archivos X-sendfile a los que consisten solo en ascii.
Ciantic
3
Para ser más claro: S.Lott tiene el ejemplo simple, solo sirve archivos directamente desde django, no se necesita otra configuración. elo80ka tiene el ejemplo más eficiente, donde el servidor web maneja archivos estáticos y django no tiene que hacerlo. Este último tiene un mejor rendimiento, pero puede requerir más configuración. Ambos tienen sus lugares.
rocketmonkeys
1
@Ciantic, vea la respuesta de btimby para lo que parece una solución al problema de codificación.
mlissner
¿Esta solución funciona con la siguiente configuración del servidor web? Back-end: 2 o más servidores Apache + mod_wsgi individuales (VPS) configurados para replicarse entre sí. Front-end: 1 servidor proxy nginx (VPS) que utiliza el equilibrio de carga ascendente, haciendo round-robin.
Daniel
12
mimetype se reemplaza por content_type para django 1.7
ismailsunni
88

Una "descarga" es simplemente un cambio de encabezado HTTP.

Ver http://docs.djangoproject.com/en/dev/ref/request-response/#telling-the-browser-to-treat-the-response-as-a-file-attachment para saber cómo responder con una descarga .

Solo necesita una definición de URL para "/download".

La solicitud GETo el POSTdiccionario tendrán la "f=somefile.txt"información.

Su función de vista simplemente fusionará la ruta base con el fvalor " ", abrirá el archivo, creará y devolverá un objeto de respuesta. Debe tener menos de 12 líneas de código.

S.Lott
fuente
49
Esta es esencialmente la respuesta correcta (simple), pero una advertencia: pasar el nombre del archivo como parámetro significa que el usuario puede descargar cualquier archivo (es decir, ¿qué pasa si pasa "f = / etc / passwd"?) Hay muchos de cosas que ayudan a prevenir esto (permisos de usuario, etc.), pero solo tenga en cuenta este riesgo de seguridad obvio pero común. Básicamente es solo un subconjunto de entrada de validación: si pasa un nombre de archivo a una vista, ¡verifique el nombre de archivo en esa vista!
rocketmonkeys
99
Una solución muy simple para este problema de seguridad:filepath = filepath.replace('..', '').replace('/', '')
dualidad_
77
Si usa una tabla para almacenar información de archivos, incluidos los usuarios que deberían poder descargarla, entonces todo lo que necesita enviar es la clave principal, no el nombre de archivo, y la aplicación decide qué hacer.
Edward Newell
30

Para una solución muy simple pero no eficiente o escalable , puede usar la servevista integrada de django . Esto es excelente para prototipos rápidos o trabajos únicos, pero como se ha mencionado a lo largo de esta pregunta, debe usar algo como apache o nginx en la producción.

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
fuente
También es muy útil para proporcionar un respaldo para realizar pruebas en Windows.
Amir Ali Akbari
Estoy haciendo un proyecto independiente de django, destinado a funcionar como un cliente de escritorio, y esto funcionó perfectamente. ¡Gracias!
daigorocub
1
¿Por qué no es eficiente?
Zinking
2
@zinking porque los archivos generalmente deben servirse a través de algo como apache en lugar de a través del proceso django
Cory
1
¿De qué tipo de inconvenientes de rendimiento estamos hablando aquí? ¿Los archivos se cargan en la RAM o algo por el estilo si se sirven a través de django? ¿Por qué django no es capaz de servir con la misma eficiencia que nginx?
Gershom
27

S.Lott tiene la solución "buena" / simple, y elo80ka tiene la "mejor" / solución eficiente. Aquí hay una solución "mejor" / intermedia: sin configuración del servidor, pero más eficiente para archivos grandes que la solución ingenua:

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

Básicamente, Django todavía maneja el servicio del archivo pero no carga todo en la memoria de una vez. Esto permite que su servidor sirva (lentamente) un archivo grande sin aumentar el uso de memoria.

Una vez más, X-SendFile de S.Lott es aún mejor para archivos más grandes. Pero si no puede o no quiere molestarse con eso, entonces esta solución intermedia le brindará una mayor eficiencia sin la molestia.

rocketmonkeys
fuente
44
Ese fragmento no es bueno. Ese recorte se basa en el django.core.servers.httpbasemódulo privado indocumentado, que tiene una gran señal de advertencia en la parte superior del código " ¡NO USAR PARA USO DE PRODUCCIÓN! ", Que ha estado en el archivo desde que se creó por primera vez . En cualquier caso, la FileWrapperfuncionalidad en la que se basa este fragmento se ha eliminado en django 1.9.
eykanal
16

Intenté la solución @Rocketmonkeys pero los archivos descargados se almacenaron como * .bin y se les dio nombres aleatorios. Eso no está bien, por supuesto. Agregar otra línea de @ elo80ka resolvió el problema.
Aquí está el código que estoy usando ahora:

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

Ahora puede almacenar archivos en un directorio privado (no dentro de / media ni / public_html) y exponerlos a través de django a ciertos usuarios o bajo ciertas circunstancias.
Espero eso ayude.

Gracias a @ elo80ka, @ S.Lott y @Rocketmonkeys por las respuestas, obtuve la solución perfecta combinando todas ellas =)

Salvatorelab
fuente
1
¡Gracias, esto era exactamente lo que estaba buscando!
ihatecache
1
Agregue comillas dobles alrededor del nombre del archivo filename="%s"en el encabezado Content-Disposition para evitar problemas con espacios en los nombres de archivo. Referencias: los nombres de archivo con espacios se truncan al descargarlos . ¿Cómo codificar el parámetro de nombre de archivo del encabezado Content-Disposition en HTTP?
Christian Long
1
Tus soluciones me funcionan. Pero tuve un error de "byte de inicio no válido ..." para mi archivo. Lo FileWrapper(open(path.abspath(file_name), 'rb'))
resolvió
FileWrapperha sido eliminado desde Django 1.9
freethebees
Es posible usarfrom wsgiref.util import FileWrapper
Kriss
15

Solo menciono el objeto FileResponse disponible en Django 1.10

Editar: acabo de encontrar mi propia respuesta mientras buscaba una manera fácil de transmitir archivos a través de Django, así que aquí hay un ejemplo más completo (para mí en el futuro). Se supone que el nombre de FileField esimported_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
fuente
1
¡Muchas gracias! Es la solución más simple para descargar archivos binarios y funciona.
Julia Zhao
13

Se mencionó anteriormente que el método mod_xsendfile no permite caracteres no ASCII en los nombres de archivo.

Por esta razón, tengo un parche disponible para mod_xsendfile que permitirá enviar cualquier archivo, siempre que el nombre esté codificado en url y el encabezado adicional:

X-SendFile-Encoding: url

También se envía.

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

btimby
fuente
El parche ahora está doblado en la biblioteca de corer.
mlissner
7

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

"Abstracción para descargar archivos subidos al servidor web (por ejemplo, Apache con mod_xsendfile) una vez que Django haya verificado los permisos, etc."

Roberto Rosario
fuente
2
En ese momento (hace 1 año), mi bifurcación personal tenía el archivo que no era de Apache, y el depósito original aún no se ha incluido.
Roberto Rosario
¿Por qué eliminaste el enlace?
kiok46
@ kiok46 Conflicto con las políticas de Github. Editado para apuntar a la dirección canónica.
Roberto Rosario
6

Debe usar las apis sendfile proporcionadas por servidores populares como apacheo nginx en producción. Muchos años estuve usando sendfile api de estos servidores para proteger archivos. Luego creó una aplicación django basada en middleware simple para este propósito, adecuada tanto para el desarrollo como para la producción. Puede acceder al código fuente aquí .
ACTUALIZACIÓN: en la nueva versión, el pythonproveedor usa django FileResponsesi está disponible y también agrega soporte para muchas implementaciones de servidor desde lighthttp, caddy hasta hiawatha

Uso

pip install django-fileprovider
  • agregar fileprovideraplicación a la INSTALLED_APPSconfiguración,
  • agregar fileprovider.middleware.FileProviderMiddlewarea la MIDDLEWARE_CLASSESconfiguración
  • establecer FILEPROVIDER_NAMEconfiguraciones en nginxo apacheen producción, por defecto es pythonpara fines de desarrollo.

en sus vistas de clase o función establezca el X-Filevalor del encabezado de respuesta en la ruta absoluta al archivo. Por ejemplo,

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 Se ha implementado de manera tal que su código solo necesitará una modificación mínima.

Configuración de Nginx

Para proteger el archivo del acceso directo, puede establecer la configuración como

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

Aquí nginxestablece una dirección URL de /files/acceso solo internamente, si está utilizando la configuración anterior, puede configurar X-File como,

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

Al hacer esto con la configuración nginx, el archivo estará protegido y también puede controlar el archivo desde django views

Renjith Thankachan
fuente
2

Django recomienda que use otro servidor para servir medios estáticos (otro servidor que se ejecuta en la misma máquina está bien). Recomienda el uso de servidores como lighttp .

Esto es muy simple de configurar. Sin embargo. si se genera 'somefile.txt' a pedido (el contenido es dinámico), entonces es posible que desee que django lo sirva.

Django Docs - Archivos estáticos

kjfletch
fuente
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
fuente
0

Otro proyecto para echar un vistazo: http://readthedocs.org/docs/django-private-files/en/latest/usage.html Parece prometedor, aunque todavía no lo he probado.

Básicamente, el proyecto abstrae la configuración mod_xsendfile y le permite hacer cosas 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
fuente
1
request.user.is_authenticated es un método, no un atributo. (no request.user.is_anonymous ()) es exactamente lo mismo que request.user.is_authenticated () porque is_authenticated es el inverso de is_anonymous.
explota el
@explodes Aún peor, ese código proviene de los documentos de django-private-files...
Armando Pérez Marqués
0

Hice un proyecto sobre esto. Puedes mirar mi repositorio github:

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

Este módulo proporciona una manera simple de servir archivos para descargar en django rest framework usando el módulo Apache Xsendfile. También tiene una función adicional de servir descargas solo a usuarios que pertenecen a un grupo en particular

nicks_4317
fuente