@@ -61,11 +61,32 @@ def serve(request, path, document_root=None, show_indexes=False):
61
61
return HttpResponseNotModified ()
62
62
content_type , encoding = mimetypes .guess_type (fullpath )
63
63
content_type = content_type or 'application/octet-stream'
64
- response = StreamingHttpResponse (open (fullpath , 'rb' ),
64
+ ranged_file = RangedFileReader (open (fullpath , 'rb' ))
65
+ response = StreamingHttpResponse (ranged_file ,
65
66
content_type = content_type )
66
67
response ["Last-Modified" ] = http_date (statobj .st_mtime )
67
68
if stat .S_ISREG (statobj .st_mode ):
68
- response ["Content-Length" ] = statobj .st_size
69
+ size = statobj .st_size
70
+ response ["Content-Length" ] = size
71
+ response ["Accept-Ranges" ] = "bytes"
72
+ # Respect the Range header.
73
+ if "HTTP_RANGE" in request .META :
74
+ try :
75
+ ranges = parse_range_header (request .META ['HTTP_RANGE' ], size )
76
+ except ValueError :
77
+ ranges = None
78
+ # only handle syntactically valid headers, that are simple (no
79
+ # multipart byteranges)
80
+ if ranges is not None and len (ranges ) == 1 :
81
+ start , stop = ranges [0 ]
82
+ if stop > size :
83
+ # requested range not satisfiable
84
+ return HttpResponse (status = 416 )
85
+ ranged_file .start = start
86
+ ranged_file .stop = stop
87
+ response ["Content-Range" ] = "bytes %d-%d/%d" % (start , stop - 1 , size )
88
+ response ["Content-Length" ] = stop - start
89
+ response .status_code = 206
69
90
if encoding :
70
91
response ["Content-Encoding" ] = encoding
71
92
return response
@@ -144,3 +165,73 @@ def was_modified_since(header=None, mtime=0, size=0):
144
165
except (AttributeError , ValueError , OverflowError ):
145
166
return True
146
167
return False
168
+
169
+
170
+ def parse_range_header (header , resource_size ):
171
+ """
172
+ Parses a range header into a list of two-tuples (start, stop) where `start`
173
+ is the starting byte of the range (inclusive) and `stop` is the ending byte
174
+ position of the range (exclusive).
175
+
176
+ Returns None if the value of the header is not syntatically valid.
177
+ """
178
+ if not header or '=' not in header :
179
+ return None
180
+
181
+ ranges = []
182
+ units , range_ = header .split ('=' , 1 )
183
+ units = units .strip ().lower ()
184
+
185
+ if units != "bytes" :
186
+ return None
187
+
188
+ for val in range_ .split ("," ):
189
+ val = val .strip ()
190
+ if '-' not in val :
191
+ return None
192
+
193
+ if val .startswith ("-" ):
194
+ # suffix-byte-range-spec: this form specifies the last N bytes of an
195
+ # entity-body
196
+ start = resource_size + int (val )
197
+ if start < 0 :
198
+ start = 0
199
+ stop = resource_size
200
+ else :
201
+ # byte-range-spec: first-byte-pos "-" [last-byte-pos]
202
+ start , stop = val .split ("-" , 1 )
203
+ start = int (start )
204
+ # the +1 is here since we want the stopping point to be exclusive, whereas in
205
+ # the HTTP spec, the last-byte-pos is inclusive
206
+ stop = int (stop )+ 1 if stop else resource_size
207
+ if start >= stop :
208
+ return None
209
+
210
+ ranges .append ((start , stop ))
211
+
212
+ return ranges
213
+
214
+
215
+ class RangedFileReader :
216
+ """
217
+ Wraps a file like object with an iterator that runs over part (or all) of
218
+ the file defined by start and stop. Blocks of block_size will be returned
219
+ from the starting position, up to, but not including the stop point.
220
+ """
221
+ block_size = 8192
222
+ def __init__ (self , file_like , start = 0 , stop = float ("inf" ), block_size = None ):
223
+ self .f = file_like
224
+ self .block_size = block_size or RangedFileReader .block_size
225
+ self .start = start
226
+ self .stop = stop
227
+
228
+ def __iter__ (self ):
229
+ self .f .seek (self .start )
230
+ position = self .start
231
+ while position < self .stop :
232
+ data = self .f .read (min (self .block_size , self .stop - position ))
233
+ if not data :
234
+ break
235
+
236
+ yield data
237
+ position += self .block_size
0 commit comments