mirror of
https://github.com/rasterio/rasterio.git
synced 2026-01-25 14:10:17 +00:00
120 lines
3.6 KiB
Python
120 lines
3.6 KiB
Python
#!/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
|
|
|
|
try:
|
|
# Python3
|
|
from http.server import SimpleHTTPRequestHandler
|
|
|
|
except ImportError:
|
|
# Python 2
|
|
from SimpleHTTPServer import SimpleHTTPRequestHandler
|
|
|
|
|
|
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:
|
|
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 OSError:
|
|
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 {}-{}/{}".format(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)
|