Memiliki Django melayani file yang dapat diunduh

245

Saya ingin pengguna di situs dapat mengunduh file yang jalurnya dikaburkan sehingga mereka tidak dapat langsung diunduh.

Misalnya, saya ingin URLnya seperti ini: http://example.com/download/?f=somefile.txt

Dan di server, saya tahu bahwa semua file yang dapat diunduh berada di folder /home/user/files/.

Apakah ada cara untuk membuat Django melayani file itu untuk diunduh sebagai lawan mencoba menemukan URL dan Tampilan untuk menampilkannya?

Damon
sumber
2
Mengapa Anda tidak hanya menggunakan Apache untuk melakukan ini? Apache menyajikan konten statis lebih cepat dan lebih sederhana daripada yang bisa dilakukan Django.
S.Lott
22
Saya tidak menggunakan Apache karena saya tidak ingin file dapat diakses tanpa izin yang berbasis di Django.
Damon
3
Jika Anda ingin mempertimbangkan izin pengguna akun Anda harus melayani file melalui tampilan Django
Łukasz
127
Tepat, itulah sebabnya saya menanyakan pertanyaan ini.
Damon

Jawaban:

189

Untuk "terbaik dari kedua dunia" Anda dapat menggabungkan solusi S.Lott dengan modul xsendfile : django menghasilkan path ke file (atau file itu sendiri), tetapi penyajian file aktual ditangani oleh Apache / Lighttpd. Setelah Anda mengatur mod_xsendfile, mengintegrasikan dengan tampilan Anda membutuhkan beberapa baris kode:

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

Tentu saja, ini hanya akan berfungsi jika Anda memiliki kendali atas server Anda, atau perusahaan hosting Anda telah menyiapkan mod_xsendfile.

EDIT:

mimetype digantikan oleh content_type untuk Django 1.7

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

EDIT: Untuk nginxmemeriksa ini , ia menggunakan X-Accel-Redirectbukan apacheheader X-Sendfile.

elo80ka
sumber
6
Jika nama file Anda, atau path_to_file menyertakan karakter non-ascii seperti "ä" atau "ö", smart_stritu tidak berfungsi sebagaimana mestinya karena modul apache X-Sendfile tidak dapat mendekodekan string smart_str yang dikodekan. Jadi misalnya file "Örinää.mp3" tidak dapat dilayani. Dan jika seseorang menghilangkan smart_str, Django itu sendiri melempar kesalahan pengkodean ascii karena semua header dikodekan ke format ascii sebelum mengirim. Satu-satunya cara yang saya tahu untuk menghindari masalah ini adalah dengan mengurangi nama file X-sendfile menjadi yang hanya terdiri dari ascii.
Ciantic
3
Agar lebih jelas: S.Lott memiliki contoh sederhana, hanya menyajikan file langsung dari Django, tidak diperlukan pengaturan lain. elo80ka memiliki contoh yang lebih efisien, di mana web-server menangani file statis dan Django tidak harus melakukannya. Yang terakhir ini memiliki kinerja yang lebih baik, tetapi mungkin memerlukan lebih banyak pengaturan. Keduanya memiliki tempat masing-masing.
rocketmonkeys
1
@ Ciantic, lihat jawaban btimby untuk apa yang tampak seperti solusi untuk masalah pengkodean.
mlissner
Apakah solusi ini berfungsi dengan konfigurasi server web berikut? Back-end: 2 atau lebih server Apache + mod_wsgi individual (VPS) diatur untuk saling meniru. Front-end: server 1 nginx proxy (VPS) menggunakan penyeimbangan beban hulu, melakukan round-robin.
Daniel
12
mimetype digantikan oleh content_type untuk django 1.7
ismailsunni
88

"Unduhan" hanyalah perubahan tajuk HTTP.

Lihat http://docs.djangoproject.com/en/dev/ref/request-response/#telling-the-browser-to-treat-the-response-as-a-file-attachment untuk cara merespons dengan unduhan .

Anda hanya perlu satu definisi URL untuk "/download".

Permintaan GETatau POSTkamus akan memiliki "f=somefile.txt"informasi.

Fungsi tampilan Anda hanya akan menggabungkan jalur dasar dengan nilai " f", membuka file, membuat dan mengembalikan objek respons. Seharusnya kurang dari 12 baris kode.

S.Lott
sumber
49
Ini pada dasarnya adalah jawaban yang benar (sederhana), tetapi satu peringatan - melewatkan nama file sebagai parameter berarti bahwa pengguna berpotensi mengunduh file apa pun (mis., Bagaimana jika Anda meneruskan "f = / etc / passwd"?) Ada banyak hal-hal yang membantu mencegah hal ini (izin pengguna, dll), tetapi hanya waspada terhadap risiko keamanan yang jelas namun umum ini. Ini pada dasarnya hanya subset dari memvalidasi input: Jika Anda memasukkan nama file ke tampilan, periksa nama file dalam tampilan itu!
rocketmonkeys
9
Sebuah sangat sederhana untuk memperbaiki masalah keamanan ini:filepath = filepath.replace('..', '').replace('/', '')
duality_
7
Jika Anda menggunakan tabel untuk menyimpan informasi file, termasuk pengguna mana yang dapat mengunduhnya, maka yang perlu Anda kirim adalah kunci utama, bukan nama file, dan aplikasi memutuskan apa yang harus dilakukan.
Edward Newell
30

Untuk solusi yang sangat sederhana namun tidak efisien atau scalable , Anda bisa menggunakan servetampilan Django bawaan . Ini bagus untuk prototipe cepat atau pekerjaan sekali saja, tetapi seperti yang telah disebutkan di seluruh pertanyaan ini, Anda harus menggunakan sesuatu seperti apache atau nginx dalam produksi.

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
sumber
Juga sangat berguna untuk memberikan cadangan untuk pengujian pada Windows.
Amir Ali Akbari
Saya sedang melakukan proyek Django mandiri, yang dimaksudkan untuk bekerja seperti klien desktop, dan ini bekerja dengan sempurna. Terima kasih!
daigorocub
1
mengapa tidak efisien?
zinking
2
@inkink karena file umumnya harus dilayani melalui sesuatu seperti apache daripada melalui proses Django
Cory
1
Kelemahan kinerja seperti apa yang kita bicarakan di sini? Apakah file dimuat ke dalam RAM atau semacamnya jika dilayani melalui Django? Mengapa django tidak mampu melayani dengan efisiensi yang sama dengan nginx?
Gershom
27

S.Lott memiliki solusi "baik" / sederhana, dan elo80ka memiliki solusi "terbaik" / efisien. Ini adalah solusi "lebih baik" / menengah - tidak ada pengaturan server, tetapi lebih efisien untuk file besar daripada perbaikan naif:

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

Pada dasarnya, Django masih menangani melayani file tetapi tidak memuat semuanya ke dalam memori sekaligus. Ini memungkinkan server Anda untuk (perlahan-lahan) menyajikan file besar tanpa meningkatkan penggunaan memori.

Sekali lagi, X-SendFile S.Lott masih lebih baik untuk file yang lebih besar. Tetapi jika Anda tidak dapat atau tidak ingin repot dengan itu, maka solusi tengah ini akan memberi Anda efisiensi yang lebih baik tanpa kesulitan.

rocketmonkeys
sumber
4
Cuplikan itu tidak baik. Potongan itu bergantung pada django.core.servers.httpbasemodul pribadi tidak berdokumen, yang memiliki tanda peringatan besar di bagian atas kode " JANGAN GUNAKAN UNTUK PENGGUNAAN PRODUKSI !!! ", yang telah ada dalam file sejak pertama kali dibuat . Bagaimanapun, FileWrapperfungsi yang diandalkan oleh cuplikan ini telah dihapus di Django 1.9.
eykanal
16

Mencoba solusi @Rocketmonkeys tetapi file yang diunduh disimpan sebagai * .bin dan diberi nama acak. Tentu saja itu tidak baik. Menambahkan baris lain dari @ elo80ka memecahkan masalah.
Ini kode yang saya gunakan sekarang:

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

Anda sekarang dapat menyimpan file dalam direktori pribadi (bukan di dalam / media atau / public_html) dan mengeksposnya melalui Django ke pengguna tertentu atau dalam keadaan tertentu.
Semoga ini bisa membantu.

Terima kasih kepada @ elo80ka, @ S.Lott dan @Rocketmonkeys untuk jawabannya, dapatkan solusi sempurna yang menggabungkan semuanya =)

Salvatorelab
sumber
1
Terima kasih, ini persis apa yang saya cari!
ihatecache
1
Tambahkan tanda kutip ganda di sekitar nama file filename="%s"di header Content-Disposition, untuk menghindari masalah dengan spasi pada nama file. Referensi: Nama file dengan spasi terpotong saat diunduh , Bagaimana cara menyandikan parameter nama file header Content-Disposition di HTTP?
Christian Long
1
Solusi Anda bekerja untuk saya. Tetapi saya memiliki kesalahan "byte awal tidak valid ..." untuk file saya. FileWrapper(open(path.abspath(file_name), 'rb'))
Selesaikan
FileWrappertelah dihapus sejak Django 1.9
freethebees
Dimungkinkan untuk menggunakanfrom wsgiref.util import FileWrapper
Kriss
15

Hanya menyebutkan objek FileResponse yang tersedia di Django 1.10

Sunting: Hanya berlari ke jawaban saya sendiri sambil mencari cara mudah untuk melakukan streaming file melalui Django, jadi di sini adalah contoh yang lebih lengkap (untuk masa depan saya). Diasumsikan bahwa nama FileField adalahimported_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
sumber
1
Terima kasih banyak! Ini adalah solusi paling sederhana untuk mengunduh file biner dan berfungsi.
Julia Zhao
13

Disebutkan di atas bahwa metode mod_xsendfile tidak memungkinkan untuk karakter non-ASCII dalam nama file.

Untuk alasan ini, saya memiliki tambalan yang tersedia untuk mod_xsendfile yang akan memungkinkan file apa pun untuk dikirim, asalkan namanya dikodekan url, dan header tambahan:

X-SendFile-Encoding: url

Dikirim juga.

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

btimby
sumber
Patch sekarang dilipat ke perpustakaan corer.
mlissner
7

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

"Abstraksi untuk melepas file unggahan ke server-web (mis. Apache dengan mod_xsendfile) begitu Django telah memeriksa izin, dll."

Roberto Rosario
sumber
2
Pada saat itu (1 tahun yang lalu) garpu pribadi saya memiliki file non Apache yang melayani fallback, repositori asli belum termasuk.
Roberto Rosario
Mengapa Anda menghapus tautannya?
kiok46
@ kiok46 Konflik dengan kebijakan Github. Diedit untuk menunjuk ke alamat kanonik.
Roberto Rosario
6

Anda harus menggunakan apis sendfile yang diberikan oleh server populer seperti apacheatau nginx dalam produksi. Bertahun-tahun saya menggunakan api sendfile dari server ini untuk melindungi file. Kemudian dibuat aplikasi django berbasis middleware sederhana untuk tujuan ini cocok untuk tujuan pengembangan & produksi. Anda dapat mengakses kode sumber di sini .
UPDATE: di pythonpenyedia versi baru menggunakan Django FileResponsejika tersedia dan juga menambahkan dukungan untuk banyak implementasi server dari lighthttp, caddy ke hiawatha

Pemakaian

pip install django-fileprovider
  • tambahkan fileprovideraplikasi keINSTALLED_APPS pengaturan,
  • tambahkan fileprovider.middleware.FileProviderMiddlewarekeMIDDLEWARE_CLASSES pengaturan
  • mengatur FILEPROVIDER_NAMEpengaturan ke nginxatau apachedalam produksi, secara default itu pythonuntuk tujuan pengembangan.

di tampilan berbasis fungsi atau class Anda mengatur X-Filenilai header respons ke path absolut ke file. Sebagai contoh,

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 diterapkan sedemikian rupa sehingga kode Anda hanya perlu modifikasi minimum.

Konfigurasi nginx

Untuk melindungi file dari akses langsung, Anda dapat mengatur konfigurasi sebagai

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

Di sini nginxmenetapkan url lokasi/files/ hanya akses internal, jika Anda menggunakan konfigurasi di atas, Anda dapat mengatur X-File sebagai,

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

Dengan melakukan ini dengan konfigurasi nginx, file akan dilindungi & Anda juga dapat mengontrol file dari Django views

Renjith Thankachan
sumber
2

Django menyarankan Anda menggunakan server lain untuk melayani media statis (server lain yang berjalan pada mesin yang sama baik-baik saja.) Mereka merekomendasikan penggunaan server seperti lighttp .

Ini sangat sederhana untuk diatur. Namun. jika 'somefile.txt' dihasilkan berdasarkan permintaan (konten bersifat dinamis) maka Anda mungkin ingin django menyajikannya.

Django Docs - File Statis

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

Proyek lain untuk melihat: http://readthedocs.org/docs/django-private-files/en/latest/usage.html Tampak menjanjikan, belum mengujinya sendiri belum.

Pada dasarnya proyek abstrak konfigurasi mod_xsendfile dan memungkinkan Anda untuk melakukan hal-hal seperti:

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
sumber
1
request.user.is_authenticated adalah metode, bukan atribut. (bukan request.user.is_anonymous ()) sama persis dengan request.user.is_authenticated () karena is_authenticated adalah kebalikan dari is_anonymous.
Meledak
@explodes Bahkan yang terburuk, kode itu benar dari dokumen django-private-files...
Armando Pérez Marqués
0

Saya melakukan proyek tentang ini. Anda dapat melihat repo github saya:

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

Modul ini menyediakan cara sederhana untuk menyajikan file untuk diunduh dalam kerangka istirahat Django menggunakan modul Apache Xsendfile. Ini juga memiliki fitur tambahan untuk melayani unduhan hanya untuk pengguna yang termasuk dalam kelompok tertentu

nicks_4317
sumber