Skip to content

Commit 541f6b9

Browse files
committed
Add support for HTTP Range header in SimpleHTTPServer
1 parent 7e894c2 commit 541f6b9

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()
@@ -676,7 +677,7 @@ def do_GET(self):
676677
f = self.send_head()
677678
if f:
678679
try:
679-
self.copyfile(f, self.wfile)
680+
self.copyfile(f, self.wfile, range=self.range)
680681
finally:
681682
f.close()
682683

@@ -699,6 +700,7 @@ def send_head(self):
699700
"""
700701
path = self.translate_path(self.path)
701702
f = None
703+
self.range = self.get_range()
702704
if os.path.isdir(path):
703705
parts = urllib.parse.urlsplit(self.path)
704706
if not parts.path.endswith('/'):
@@ -763,9 +765,26 @@ def send_head(self):
763765
f.close()
764766
return None
765767

766-
self.send_response(HTTPStatus.OK)
768+
if self.range:
769+
start, end = self.range
770+
if start >= fs.st_size:
771+
# 416 REQUESTED_RANGE_NOT_SATISFIABLE means that none of the range values overlap the extent of the resource
772+
f.close()
773+
self.send_error(HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
774+
return None
775+
if end is None or end >= fs.st_size:
776+
end = fs.st_size - 1
777+
self.send_response(HTTPStatus.PARTIAL_CONTENT)
778+
self.send_header("Content-Range", "bytes %s-%s/%s" % (start, end, fs.st_size))
779+
self.send_header("Content-Length", str(end-start+1))
780+
781+
# Update range to be sent to be used later in copyfile
782+
self.range = (start, end)
783+
else:
784+
self.send_response(HTTPStatus.OK)
785+
self.send_header("Accept-Ranges", "bytes")
786+
self.send_header("Content-Length", str(fs[6]))
767787
self.send_header("Content-type", ctype)
768-
self.send_header("Content-Length", str(fs[6]))
769788
self.send_header("Last-Modified",
770789
self.date_time_string(fs.st_mtime))
771790
self.end_headers()
@@ -861,21 +880,37 @@ def translate_path(self, path):
861880
path += '/'
862881
return path
863882

864-
def copyfile(self, source, outputfile):
865-
"""Copy all data between two file objects.
883+
def copyfile(self, source, outputfile, range=None):
884+
"""Copy all data between two file objects if range is None.
885+
Otherwise, copy data between two file objects based on the
886+
inclusive range (start, end).
866887
867888
The SOURCE argument is a file object open for reading
868-
(or anything with a read() method) and the DESTINATION
869-
argument is a file object open for writing (or
870-
anything with a write() method).
889+
(or anything with read() and seek() method) and the
890+
DESTINATION argument is a file object open for writing
891+
(or anything with a write() method).
871892
872893
The only reason for overriding this would be to change
873894
the block size or perhaps to replace newlines by CRLF
874895
-- note however that this the default server uses this
875896
to copy binary data as well.
876897
877898
"""
878-
shutil.copyfileobj(source, outputfile)
899+
if range is None:
900+
shutil.copyfileobj(source, outputfile)
901+
else:
902+
start, end = range
903+
length = end - start + 1
904+
source.seek(start)
905+
while True:
906+
if length <= 0:
907+
break
908+
buf = source.read(min(length, shutil.COPY_BUFSIZE))
909+
if not buf:
910+
break
911+
length -= len(buf)
912+
outputfile.write(buf)
913+
879914

880915
def guess_type(self, path):
881916
"""Guess the type of a file.
@@ -902,6 +937,24 @@ def guess_type(self, path):
902937
return guess
903938
return 'application/octet-stream'
904939

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

906959
# Utilities for CGIHTTPRequestHandler
907960

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)