Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Byte Ranges in Django

Is there an existing implementation of HTTP byte ranges in Django? I.e. when the client sends a Range: header, I want my Django app to accept that and reply with a HTTP "206 Partial Content" response.

I could write something from scratch, but surely someone has already done this?

There are various parts to this:

  1. Parsing and sanity checking the Range header
  2. Actually generating the ranges
  3. Support for returning a single range in the response
  4. Support for returning multiple ranges in a single response, appropriately MIME encoded

Surely at least (1) and (4) have library support somewhere?

like image 468
user9876 Avatar asked Jan 14 '13 18:01

user9876


3 Answers

Here's some basic middleware code that should work for Django 1.11+. It only handles a single range, but that's all I personally need.

import os

from django.utils.deprecation import MiddlewareMixin


class RangesMiddleware(MiddlewareMixin):
    def process_response(self, request, response):
        if response.status_code != 200 or not hasattr(response, 'file_to_stream'):
            return response
        http_range = request.META.get('HTTP_RANGE')
        if not (http_range and http_range.startswith('bytes=') and http_range.count('-') == 1):
            return response
        if_range = request.META.get('HTTP_IF_RANGE')
        if if_range and if_range != response.get('Last-Modified') and if_range != response.get('ETag'):
            return response
        f = response.file_to_stream
        statobj = os.fstat(f.fileno())
        start, end = http_range.split('=')[1].split('-')
        if not start:  # requesting the last N bytes
            start = max(0, statobj.st_size - int(end))
            end = ''
        start, end = int(start or 0), int(end or statobj.st_size - 1)
        assert 0 <= start < statobj.st_size, (start, statobj.st_size)
        end = min(end, statobj.st_size - 1)
        f.seek(start)
        old_read = f.read
        f.read = lambda n: old_read(min(n, end + 1 - f.tell()))
        response.status_code = 206
        response['Content-Length'] = end + 1 - start
        response['Content-Range'] = 'bytes %d-%d/%d' % (start, end, statobj.st_size)
        return response

Install it in settings.py like so:

MIDDLEWARE_CLASSES = [
    'path.to.RangesMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
]
like image 84
Collin Anderson Avatar answered Oct 22 '22 04:10

Collin Anderson


The are two relevant feature requests (one is open, another is a duplicate of the first):

  • Ticket #22479 - Support byte range requests in django.views.static.serve
  • Ticket #23382 - Support byte range requests in django.views.static.serve

Both of the issues are based on the Google Group discussion.

The ticket is in a "hanging" state due to architectural concerns and since there is no consensus on whether this is really something that Django should support. Mostly because web-servers are capable of byte serving.

If you are still interested in the implementation, there is a not-yet-reviewed patch sent implementing a special RangedFileReader class for returning the response in chunks using StreamingHttpResponse, there is parsing and sanity checking for HTTP_RANGE header:

  • Add support from the HTTP range header

You can try out the fork or use the solution as a base of your own.

FYI, there was an another attempt made here - it was not finished, but can be useful to review.


To parse Range header, see:

  • werkzeug's parse_range_header()
  • parse_range_header() from the mentioned above PR
  • httpheader's parse_range_header() (see also Using HTTP range requests)
like image 34
alecxe Avatar answered Oct 22 '22 03:10

alecxe


Three approches about content range

Choose proper one by your situation.

1. Support all dynamic views and static files

The WhiteNoise provides a middleware to support global settings.

2. Support static files only

Nginx can support static and media files.

See more details on Setting up Django and your web server with uWSGI and nginx. If some views need redirect to static or media files, see the stackoverflow answer.

3. Support specific dynamic views only

Django Ranged Response supports responses with content range. Use this response for every views you wanted. The views are mostly designed for media API, like speech synthesis, video generators.


declare: I am a contributer of Django Ranged Response. But I think using WhiteNoise is easiest to maintain.

like image 3
sih4sing5hog5 Avatar answered Oct 22 '22 04:10

sih4sing5hog5