12

So I want to serve a couple of mp3s from a folder in /home/username/music. I didn't think this would be such a big deal but I am a bit confused on how to do it using generic views and my own url.

urls.py

url(r'^song/(?P<song_id>\d+)/download/$', song_download, name='song_download'),

The example I am following is found in the generic view section of the Django documentations: http://docs.djangoproject.com/en/dev/topics/generic-views/ (It's all the way at the bottom)

I am not 100% sure on how to tailor this to my needs. Here is my views.py

def song_download(request, song_id):
    song = Song.objects.get(id=song_id)

    response = object_detail(
        request,
        object_id = song_id,
        mimetype = "audio/mpeg",
    )
    response['Content-Disposition'= "attachment; filename=%s - %s.mp3" % (song.artist, song.title)

    return response

I am actually at a loss of how to convey that I want it to spit out my mp3 instead of what it does now which is to output a .mp3 with all of the current pages html contained. Should my template be my mp3? Do I need to setup apache to serve the files or is Django able to retrieve the mp3 from the filesystem(proper permissions of course) and serve that? If it do need to configure Apache how do I tell Django that?

Thanks in advance. These files are all on the HD so I don't need to "generate" anything on the spot and I'd like to prevent revealing the location of these files if at all possible. A simple /song/1234/download would be fantastic.

Juergen
  • 12,378
  • 7
  • 39
  • 55
TheLizardKing
  • 2,014
  • 3
  • 20
  • 27

4 Answers4

19

Why do you want to do this with a generic view? It's very easy to do this without generic views:

from django.http import HttpResponse


def song_download(request, song_id):
    song = Song.objects.get(id=song_id)
    fsock = open('/path/to/file.mp3', 'rb')
    response = HttpResponse(fsock, content_type='audio/mpeg')
    response['Content-Disposition'] = "attachment; filename=%s - %s.mp3" % \
                                     (song.artist, song.title)
    return response

I'm not sure if it's possible to make this work somehow with a generic view. But either way, using one is redundant here. With no template to render, the context that is automatically provided by the generic view is useless.

Barney Szabolcs
  • 11,846
  • 12
  • 66
  • 91
Ben James
  • 121,135
  • 26
  • 193
  • 155
9

To wrap my comment to Tomasz Zielinski into a real answer:

For several reasons it is indeed better to let apache/nginx/etc do the work of sending files. Most servers have mechanisms to help in that usecase: Apache and lighttpd have xsendfile, nginx has X-Accel-Redirect.

The idea is that you can use all the features of django like nice urls, authentification methods, etc, but let the server do the work of serving files. What your django view has to do, is to return a response with a special header. The server will then replace the response with the actual file.

Example for apache:

def song_download(request):
    path = '/path/to/file.mp3'
    response = HttpResponse()
    response['X-Sendfile'] = smart_str(path)
    response['Content-Type'] = "audio/mpeg"
    response['Content-Length'] = os.stat(path).st_size
    return response
  • install mode_xsendfile
  • add XSendFileOn on and (depending on the version) XSendFileAllowAbove on or XSendFilePath the/path/to/serve/from to your apache configuration.

This way you don't reveale the file location, and keep all the url management in django.

xubuntix
  • 2,333
  • 18
  • 19
  • you could use os.path.getsize(pathToFile) instead of os.stat(path).st_size – iMath Jun 18 '16 at 04:19
  • This is the real deal, thanks @xubuntix! I love that with your solution django does not get bogged down reading the file if it is a large file. – Barney Szabolcs Feb 14 '21 at 22:54
  • What's the role of `smart_str` here? -- update: I see now that the answer is from the old python 2 days. I think with python3 we don't need smart_str anymore. Is that correct? – Barney Szabolcs Feb 14 '21 at 22:57
1

Serving static files with Django is a bad idea, use Apache, nginx etc.

https://docs.djangoproject.com/en/dev/howto/static-files/deployment/

Saransh Singh
  • 730
  • 4
  • 11
Tomasz Zieliński
  • 16,136
  • 7
  • 59
  • 83
  • I hear ya, I am all for using apache but how can I accomplish the accepted answer using Apache as my web server? I want to use my own urls and not reveal my download url. – TheLizardKing Apr 21 '10 at 09:18
  • I'm not a static-file-serving-expert, but I bet you can rewrite static media URL as well – Tomasz Zieliński Apr 21 '10 at 17:46
  • My contention with the above link are lines like "We recommend using a separate Web server -- i.e., one that's not also running Django -- for serving media. Here are some good choices:" I hear them loud and clear but let's say I have a full path and/or a url to the file, how do I spit that out using my above mentioned url styling? I'm trying to find open source django projects to see how they do it. – TheLizardKing Apr 21 '10 at 23:56
  • You should be rather looking at Apache or ngixs configuration and/or modules - it has nothing to do with Django which works behind http server. – Tomasz Zieliński Apr 22 '10 at 07:14
  • 2
    That would be me. If have had the exact same problem and came to this page to find the answer. Your answer, however, solves no problem at all. It's just a link to site which does also not solve this problem. Something like this might have helped: use apache, mod_xsendfile, add "XSendFile on" "XSendFileAllowAbove on" to your apache conf, use like this: `def song_download(request, song_id): response = HttpResponse() response['X-Sendfile'] = smart_str(filename) response['Content-Type'] = 'audio/mpeg' response['Content-Length'] = os.stat(filename).st_size return response` – xubuntix Sep 04 '11 at 15:36
1

To answer the original question how to use a generic view, you could do the following:

from django.views.generic import DetailView
from django.http.response import FileResponse

class DownloadSong(DetailView):

    model = Song

    def get(self, request, *args, **kwargs):
        super().get(request, *args, **kwargs)
        song = self.object
        return FileResponse(open(song, 'rb'), 
                            as_attachment=True, 
                            filename=f'{song.artist} - {song.title}.mp3')

Docs:

If your Django version does not have the FileResponse object, use the HttpResponse as shown in the other answers.

xystum
  • 939
  • 6
  • 8