Skip to content

Commit e81d46e

Browse files
committed
Add support for HTTP Range header in SimpleHTTPServer
1 parent 0ac40ac commit e81d46e

3 files changed

Lines changed: 84 additions & 9 deletions

File tree

Lib/http/server.py

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
import mimetypes
9898
import os
9999
import posixpath
100+
import re
100101
import select
101102
import shutil
102103
import socket # For gethostbyaddr()
@@ -682,7 +683,7 @@ def do_GET(self):
682683
f = self.send_head()
683684
if f:
684685
try:
685-
self.copyfile(f, self.wfile)
686+
self.copyfile(f, self.wfile, range=self.range)
686687
finally:
687688
f.close()
688689

@@ -705,6 +706,7 @@ def send_head(self):
705706
"""
706707
path = self.translate_path(self.path)
707708
f = None
709+
self.range = self.get_range()
708710
if os.path.isdir(path):
709711
parts = urllib.parse.urlsplit(self.path)
710712
if not parts.path.endswith('/'):
@@ -769,9 +771,26 @@ def send_head(self):
769771
f.close()
770772
return None
771773

772-
self.send_response(HTTPStatus.OK)
774+
if self.range:
775+
start, end = self.range
776+
if start >= fs.st_size:
777+
# 416 REQUESTED_RANGE_NOT_SATISFIABLE means that none of the range values overlap the extent of the resource
778+
f.close()
779+
self.send_error(HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
780+
return None
781+
if end is None or end >= fs.st_size:
782+
end = fs.st_size - 1
783+
self.send_response(HTTPStatus.PARTIAL_CONTENT)
784+
self.send_header("Content-Range", "bytes %s-%s/%s" % (start, end, fs.st_size))
785+
self.send_header("Content-Length", str(end-start+1))
786+
787+
# Update range to be sent to be used later in copyfile
788+
self.range = (start, end)
789+
else:
790+
self.send_response(HTTPStatus.OK)
791+
self.send_header("Accept-Ranges", "bytes")
792+
self.send_header("Content-Length", str(fs[6]))
773793
self.send_header("Content-type", ctype)
774-
self.send_header("Content-Length", str(fs[6]))
775794
self.send_header("Last-Modified",
776795
self.date_time_string(fs.st_mtime))
777796
self.end_headers()
@@ -868,21 +887,37 @@ def translate_path(self, path):
868887
path += '/'
869888
return path
870889

871-
def copyfile(self, source, outputfile):
872-
"""Copy all data between two file objects.
890+
def copyfile(self, source, outputfile, range=None):
891+
"""Copy all data between two file objects if range is None.
892+
Otherwise, copy data between two file objects based on the
893+
inclusive range (start, end).
873894
874895
The SOURCE argument is a file object open for reading
875-
(or anything with a read() method) and the DESTINATION
876-
argument is a file object open for writing (or
877-
anything with a write() method).
896+
(or anything with read() and seek() method) and the
897+
DESTINATION argument is a file object open for writing
898+
(or anything with a write() method).
878899
879900
The only reason for overriding this would be to change
880901
the block size or perhaps to replace newlines by CRLF
881902
-- note however that this the default server uses this
882903
to copy binary data as well.
883904
884905
"""
885-
shutil.copyfileobj(source, outputfile)
906+
if range is None:
907+
shutil.copyfileobj(source, outputfile)
908+
else:
909+
start, end = range
910+
length = end - start + 1
911+
source.seek(start)
912+
while True:
913+
if length <= 0:
914+
break
915+
buf = source.read(min(length, shutil.COPY_BUFSIZE))
916+
if not buf:
917+
break
918+
length -= len(buf)
919+
outputfile.write(buf)
920+
886921

887922
def guess_type(self, path):
888923
"""Guess the type of a file.
@@ -909,6 +944,24 @@ def guess_type(self, path):
909944
return guess
910945
return 'application/octet-stream'
911946

947+
def get_range(self):
948+
"""Return a tuple of (start, end) representing the range header in
949+
the HTTP request. If the range header is missing or not resolvable,
950+
None is returned. This only supports single part ranges.
951+
952+
"""
953+
range_header = self.headers.get('range')
954+
if not range_header:
955+
return None
956+
m = re.match(r'bytes=(\d+)-(\d*)$', range_header)
957+
if not m:
958+
return None
959+
start = m.group(1)
960+
if not m.group(2):
961+
return int(start), None
962+
end = m.group(2)
963+
return int(start), int(end)
964+
912965

913966
# Utilities for CGIHTTPRequestHandler
914967

Lib/test/test_httpservers.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,27 @@ def test_get(self):
539539
finally:
540540
os.chmod(self.tempdir, 0o755)
541541

542+
def test_range_get(self):
543+
response = self.request(self.base_url + '/test')
544+
self.assertEqual(response.getheader('accept-ranges'), 'bytes')
545+
self.check_status_and_reason(response, HTTPStatus.OK, data=self.data)
546+
547+
response = self.request(self.base_url + '/test', headers={'Range': 'bytes=3-12'})
548+
self.assertEqual(response.getheader('content-range'), 'bytes 3-12/30')
549+
self.assertEqual(response.getheader('content-length'), '10')
550+
self.check_status_and_reason(response, HTTPStatus.PARTIAL_CONTENT, data=self.data[3:13])
551+
552+
response = self.request(self.base_url + '/test', headers={'Range': 'bytes=3-'})
553+
self.assertEqual(response.getheader('content-range'), 'bytes 3-29/30')
554+
self.assertEqual(response.getheader('content-length'), '27')
555+
self.check_status_and_reason(response, HTTPStatus.PARTIAL_CONTENT, data=self.data[3:])
556+
557+
response = self.request(self.base_url + '/test', headers={'Range': 'bytes=100-200'})
558+
self.check_status_and_reason(response, HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
559+
560+
response = self.request(self.base_url + '/test', headers={'Range': 'bytes=wrong format'})
561+
self.check_status_and_reason(response, HTTPStatus.OK, data=self.data)
562+
542563
def test_head(self):
543564
response = self.request(
544565
self.base_url + '/test', method='HEAD')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for HTTP Range header in ``SimpleHTTPServer``

0 commit comments

Comments
 (0)