Created
May 5, 2021 10:16
-
-
Save billyc/581cf8e6b4ebf10a5ed8ef52a3d4e1ba to your computer and use it in GitHub Desktop.
Python 2/3 http file server with open CORS and Range Requests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import argparse | |
import os | |
import re | |
import sys | |
print("Mini-File-Server: serving with RANGE and CORS (open access)") | |
print("-----------------------------------------------------------") | |
print("Folder: " + os.getcwd()) | |
try: | |
# Python3 | |
import http.server as SimpleHTTPServer | |
from http.server import SimpleHTTPRequestHandler, test as test_orig | |
except ImportError: | |
# Python 2 | |
from BaseHTTPServer import HTTPServer, test | |
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 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') # moved to CORSandRangeRequestHandler | |
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) | |
class CORSandRangeRequestHandler(RangeRequestHandler): | |
def end_headers(self): | |
self.send_header("Access-Control-Allow-Origin", "*") | |
self.send_header('Accept-Ranges', 'bytes') | |
self.send_header( | |
"Cache-Control", "no-cache, max-age=0, must-revalidate, no-store" | |
) | |
SimpleHTTPRequestHandler.end_headers(self) | |
test(HandlerClass=CORSandRangeRequestHandler) # , port=args.port) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment