-
Notifications
You must be signed in to change notification settings - Fork 28
/
RangeHTTPServer.py
executable file
·113 lines (90 loc) · 3.59 KB
/
RangeHTTPServer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/python
'''
Use this in the same way as Python's SimpleHTTPServer:
python -m RangeHTTPServer [port]
The only difference from SimpleHTTPServer is that RangeHTTPServer supports
'Range:' headers to load portions of files. This is helpful for doing local web
development with genomic data files, which tend to be to large to load into the
browser all at once.
'''
import os
import re
import sys
from SimpleHTTPServer import SimpleHTTPRequestHandler
import SimpleHTTPServer
def copy_byte_range(infile, outfile, start=None, stop=None, bufsize=16*1024):
'''Like shutil.copyfileobj, but only copy a range of the streams.
Both start and stop are inclusive.
'''
if start is not None: infile.seek(start)
while 1:
to_read = min(bufsize, stop + 1 - infile.tell() if stop else bufsize)
buf = infile.read(to_read)
if not buf:
break
outfile.write(buf)
BYTE_RANGE_RE = re.compile(r'bytes=(\d+)-(\d+)?$')
def parse_byte_range(byte_range):
'''Returns the two numbers in 'bytes=123-456' or throws ValueError.
The last number or both numbers may be None.
'''
if byte_range.strip() == '':
return None, None
m = BYTE_RANGE_RE.match(byte_range)
if not m:
raise ValueError('Invalid byte range %s' % byte_range)
first, last = [x and int(x) for x in m.groups()]
if last and last < first:
raise ValueError('Invalid byte range %s' % byte_range)
return first, last
class RangeRequestHandler(SimpleHTTPRequestHandler):
"""Adds support for HTTP 'Range' requests to SimpleHTTPRequestHandler
The approach is to:
- Override send_head to look for 'Range' and respond appropriately.
- Override copyfile to only transmit a range when requested.
"""
def send_head(self):
if 'Range' not in self.headers:
self.range = None
return SimpleHTTPRequestHandler.send_head(self)
try:
self.range = parse_byte_range(self.headers['Range'])
except ValueError as e:
self.send_error(400, 'Invalid byte range')
return None
first, last = self.range
# Mirroring SimpleHTTPServer.py here
path = self.translate_path(self.path)
f = None
ctype = self.guess_type(path)
try:
f = open(path, 'rb')
except IOError:
self.send_error(404, 'File not found')
return None
fs = os.fstat(f.fileno())
file_len = fs[6]
if first >= file_len:
self.send_error(416, 'Requested Range Not Satisfiable')
return None
self.send_response(206)
self.send_header('Content-type', ctype)
self.send_header('Accept-Ranges', 'bytes')
if last is None or last >= file_len:
last = file_len - 1
response_length = last - first + 1
self.send_header('Content-Range',
'bytes %s-%s/%s' % (first, last, file_len))
self.send_header('Content-Length', str(response_length))
self.send_header('Last-Modified', self.date_time_string(fs.st_mtime))
self.end_headers()
return f
def copyfile(self, source, outputfile):
if not self.range:
return SimpleHTTPRequestHandler.copyfile(self, source, outputfile)
# SimpleHTTPRequestHandler uses shutil.copyfileobj, which doesn't let
# you stop the copying before the end of the file.
start, stop = self.range # set in send_head()
copy_byte_range(source, outputfile, start, stop)
if __name__ == '__main__':
SimpleHTTPServer.test(HandlerClass=RangeRequestHandler)