Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ SimpleHTTPServer with support for Range requests
Quickstart:

$ pip install rangehttpserver
$ python -m RangeHTTPServer
$ python -m rangehttpserver
Serving HTTP on 0.0.0.0 port 8000 ...

See the GitHub repo for more information:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SimpleHTTPServer with support for Range requests
Quickstart:

$ pip install rangehttpserver
$ python -m RangeHTTPServer
$ python -m rangehttpserver
Serving HTTP on 0.0.0.0 port 8000 ...

# Alternatives
Expand Down
116 changes: 0 additions & 116 deletions RangeHTTPServer/__init__.py

This file was deleted.

1,523 changes: 1,523 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[tool.poetry]
name = "rangehttpserver"
version = "1.4.0"
description = ""
authors = ["Noah Parker <nparker@cyvl.ai>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.8"
pytest = "^7.4.2"
pytest-cov = "^4.1.0"
coveralls = "^3.3.1"
requests-toolbelt = "^1.0.0"
Comment on lines +8 to +13
Copy link

@agoose77 agoose77 Oct 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you unpin the upper bound on these dependencies? Although this is more of an "application" than a "library", it's still likely to be used as a library. As such, we want to avoid pinning downstream users without good reason c.f. https://iscinumpy.dev/post/poetry-versions/

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, are any of these actually needed for runtime besides the Python version?



[tool.poetry.group.dev.dependencies]
black = { extras = ["jupyter"], version = "^23.1.0" }
mypy = "^1.1.1"
pytest = "^7.2.2"
pylint = "^2.17.0"
boto3-stubs = { extras = ["codepipeline"], version = "^1.26.89" }
poethepoet = "^0.24.1"

[tool.poe.tasks]
# Or `pylint mypackage`
lint = 'pylint --recursive=y --fail-under 5 .'
format = 'black .'
check-formatting = 'black --check .'
check-types = 'mypy -p cyvl_data_fusion -p tests'
unit-tests = 'pytest --cov'
Empty file added rangehttpserver/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions rangehttpserver/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from rangehttpserver.servers.server import start

if __name__ == '__main__':
start()
162 changes: 162 additions & 0 deletions rangehttpserver/request_handlers/range_request_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import re


try:
# Python3
from http.server import SimpleHTTPRequestHandler

except ImportError:
# Python 2
from SimpleHTTPServer import SimpleHTTPRequestHandler

import os

MULTIPART_BOUNDARY_STRING = "python-boundary-string-1234"
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.ranges = None
return SimpleHTTPRequestHandler.send_head(self)
try:
self.ranges = parse_byte_range(self.headers['Range'])
except ValueError as e:
self.send_error(400, 'Invalid byte range')
return None

# Mirroring SimpleHTTPServer.py here
path = self.translate_path(self.path)
f = None
ctype = self.guess_type(path)
self.ctype = ctype
try:
f = open(path, 'rb')
except IOError:
self.send_error(404, 'File not found')
return None

response_length = 0

sent_response = False

for range in self.ranges:

first, last = range

fs = os.fstat(f.fileno())
file_len = fs[6]
if first >= file_len:
self.send_error(416, 'Requested Range Not Satisfiable')
return None
if last is None or last >= file_len:
last = file_len - 1

value = 'bytes %s-%s/%s' % (first, last, file_len)

message_length = last - first + 1

if sent_response is False:
self.send_response(206)
if len(self.ranges) == 1:
self.send_header('Content-type', ctype)
else:
self.send_header('Content-type', 'multipart/byteranges; boundary='+MULTIPART_BOUNDARY_STRING)
sent_response = True

if len(self.ranges) > 1:
message_length += len( self.get_section_boundary_string(ctype, first, last, file_len) )

response_length += message_length

self.send_header('Content-Range',
value)

if len(self.ranges) > 1:
response_length += len(("\r\n--"+MULTIPART_BOUNDARY_STRING+"--").encode())

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 end_headers(self):
self.send_header('Accept-Ranges', 'bytes')
return SimpleHTTPRequestHandler.end_headers(self)

def copyfile(self, source, outputfile):
if not self.ranges:
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()
#get total length from source

total_byte_length = os.fstat(source.fileno())[6]
for range in self.ranges:
start, stop = range

if len(self.ranges) > 1:
boundary_string = self.get_section_boundary_string(self.ctype, start, stop, total_byte_length)
outputfile.write(boundary_string.encode())

copy_byte_range(source, outputfile, start, stop)

if len(self.ranges) > 1:
#copy in final boundary string
outputfile.write(("\r\n--"+MULTIPART_BOUNDARY_STRING+"--").encode())

def get_section_boundary_string(self, content_type, start, end, total):
boundary_string = MULTIPART_BOUNDARY_STRING
formatted_boundary_string = "\r\n--"+boundary_string+"\r\n"
content_header = f"Content-Type: {content_type}\r\n"
range_header = "Content-Range: bytes "+str(start)+"-"+str(end)+"/"+str(total)+"\r\n\r\n"
return formatted_boundary_string+content_header+range_header

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)]

#get index of 'bytes=' str
start = byte_range.index('bytes=')
range_data = byte_range[start+6:]
print(range_data)
range_list = range_data.split(",")

ranges = []
for range in range_list:
if len(range.split("-")) < 2:
raise ValueError('Invalid byte range %s' % byte_range)
first, last = [x and int(x) for x in range.split("-")]
if last == '':
last = None
if last and last < first:
raise ValueError('Invalid byte range %s' % byte_range)

ranges.append((first, last))

return ranges
20 changes: 12 additions & 8 deletions RangeHTTPServer/__main__.py → rangehttpserver/servers/server.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
browser all at once.
"""

from rangehttpserver.request_handlers.range_request_handler import RangeRequestHandler

try:
# Python3
import http.server as SimpleHTTPServer
Expand All @@ -17,13 +19,15 @@
# Python 2
import SimpleHTTPServer

from . import RangeRequestHandler

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('port', action='store',
default=8000, type=int,
nargs='?', help='Specify alternate port [default: 8000]')

args = parser.parse_args()
SimpleHTTPServer.test(HandlerClass=RangeRequestHandler, port=args.port)
def start():

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('port', action='store',
default=8000, type=int,
nargs='?', help='Specify alternate port [default: 8000]')

args = parser.parse_args()
SimpleHTTPServer.test(HandlerClass=RangeRequestHandler, port=args.port)
Loading