annotate jbrowse2/servejb2.py @ 7:234cf4490901 draft

Uploaded
author fubar
date Fri, 05 Jan 2024 04:31:35 +0000
parents
children
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
7
234cf4490901 Uploaded
fubar
parents:
diff changeset
1 #!/usr/bin/env python3
234cf4490901 Uploaded
fubar
parents:
diff changeset
2
234cf4490901 Uploaded
fubar
parents:
diff changeset
3 # spec: simplest python web server with range support and multithreading that takes root path,
234cf4490901 Uploaded
fubar
parents:
diff changeset
4 # port and bind address as command line arguments; by default uses the current dir as webroot,
234cf4490901 Uploaded
fubar
parents:
diff changeset
5 # port 8000 and bind address of 0.0.0.0
234cf4490901 Uploaded
fubar
parents:
diff changeset
6 # borrowed from https://github.com/danvk/RangeHTTPServer
234cf4490901 Uploaded
fubar
parents:
diff changeset
7 # and reborrowed from https://gist.github.com/glowinthedark/b99900abe935e4ab4857314d647a9068
234cf4490901 Uploaded
fubar
parents:
diff changeset
8
234cf4490901 Uploaded
fubar
parents:
diff changeset
9
234cf4490901 Uploaded
fubar
parents:
diff changeset
10 import argparse
234cf4490901 Uploaded
fubar
parents:
diff changeset
11 import functools
234cf4490901 Uploaded
fubar
parents:
diff changeset
12 import os
234cf4490901 Uploaded
fubar
parents:
diff changeset
13 import re
234cf4490901 Uploaded
fubar
parents:
diff changeset
14 import socketserver
234cf4490901 Uploaded
fubar
parents:
diff changeset
15 import webbrowser
234cf4490901 Uploaded
fubar
parents:
diff changeset
16 from http.server import SimpleHTTPRequestHandler
234cf4490901 Uploaded
fubar
parents:
diff changeset
17
234cf4490901 Uploaded
fubar
parents:
diff changeset
18
234cf4490901 Uploaded
fubar
parents:
diff changeset
19 DEFAULT_PORT = 8080
234cf4490901 Uploaded
fubar
parents:
diff changeset
20
234cf4490901 Uploaded
fubar
parents:
diff changeset
21
234cf4490901 Uploaded
fubar
parents:
diff changeset
22 def copy_byte_range(infile, outfile, start=None, stop=None, bufsize=16 * 1024):
234cf4490901 Uploaded
fubar
parents:
diff changeset
23 """Like shutil.copyfileobj, but only copy a range of the streams.
234cf4490901 Uploaded
fubar
parents:
diff changeset
24
234cf4490901 Uploaded
fubar
parents:
diff changeset
25 Both start and stop are inclusive.
234cf4490901 Uploaded
fubar
parents:
diff changeset
26 """
234cf4490901 Uploaded
fubar
parents:
diff changeset
27 if start is not None:
234cf4490901 Uploaded
fubar
parents:
diff changeset
28 infile.seek(start)
234cf4490901 Uploaded
fubar
parents:
diff changeset
29 while 1:
234cf4490901 Uploaded
fubar
parents:
diff changeset
30 to_read = min(bufsize, stop + 1 - infile.tell() if stop else bufsize)
234cf4490901 Uploaded
fubar
parents:
diff changeset
31 buf = infile.read(to_read)
234cf4490901 Uploaded
fubar
parents:
diff changeset
32 if not buf:
234cf4490901 Uploaded
fubar
parents:
diff changeset
33 break
234cf4490901 Uploaded
fubar
parents:
diff changeset
34 outfile.write(buf)
234cf4490901 Uploaded
fubar
parents:
diff changeset
35
234cf4490901 Uploaded
fubar
parents:
diff changeset
36
234cf4490901 Uploaded
fubar
parents:
diff changeset
37 BYTE_RANGE_RE = re.compile(r"bytes=(\d+)-(\d+)?$")
234cf4490901 Uploaded
fubar
parents:
diff changeset
38
234cf4490901 Uploaded
fubar
parents:
diff changeset
39
234cf4490901 Uploaded
fubar
parents:
diff changeset
40 def parse_byte_range(byte_range):
234cf4490901 Uploaded
fubar
parents:
diff changeset
41 """Returns the two numbers in 'bytes=123-456' or throws ValueError.
234cf4490901 Uploaded
fubar
parents:
diff changeset
42
234cf4490901 Uploaded
fubar
parents:
diff changeset
43 The last number or both numbers may be None.
234cf4490901 Uploaded
fubar
parents:
diff changeset
44 """
234cf4490901 Uploaded
fubar
parents:
diff changeset
45 if byte_range.strip() == "":
234cf4490901 Uploaded
fubar
parents:
diff changeset
46 return None, None
234cf4490901 Uploaded
fubar
parents:
diff changeset
47
234cf4490901 Uploaded
fubar
parents:
diff changeset
48 m = BYTE_RANGE_RE.match(byte_range)
234cf4490901 Uploaded
fubar
parents:
diff changeset
49 if not m:
234cf4490901 Uploaded
fubar
parents:
diff changeset
50 raise ValueError("Invalid byte range %s" % byte_range)
234cf4490901 Uploaded
fubar
parents:
diff changeset
51
234cf4490901 Uploaded
fubar
parents:
diff changeset
52 first, last = [x and int(x) for x in m.groups()]
234cf4490901 Uploaded
fubar
parents:
diff changeset
53 if last and last < first:
234cf4490901 Uploaded
fubar
parents:
diff changeset
54 raise ValueError("Invalid byte range %s" % byte_range)
234cf4490901 Uploaded
fubar
parents:
diff changeset
55 return first, last
234cf4490901 Uploaded
fubar
parents:
diff changeset
56
234cf4490901 Uploaded
fubar
parents:
diff changeset
57
234cf4490901 Uploaded
fubar
parents:
diff changeset
58 class RangeRequestHandler(SimpleHTTPRequestHandler):
234cf4490901 Uploaded
fubar
parents:
diff changeset
59 """Adds support for HTTP 'Range' requests to SimpleHTTPRequestHandler
234cf4490901 Uploaded
fubar
parents:
diff changeset
60
234cf4490901 Uploaded
fubar
parents:
diff changeset
61 The approach is to:
234cf4490901 Uploaded
fubar
parents:
diff changeset
62 - Override send_head to look for 'Range' and respond appropriately.
234cf4490901 Uploaded
fubar
parents:
diff changeset
63 - Override copyfile to only transmit a range when requested.
234cf4490901 Uploaded
fubar
parents:
diff changeset
64 """
234cf4490901 Uploaded
fubar
parents:
diff changeset
65
234cf4490901 Uploaded
fubar
parents:
diff changeset
66 def handle(self):
234cf4490901 Uploaded
fubar
parents:
diff changeset
67 try:
234cf4490901 Uploaded
fubar
parents:
diff changeset
68 SimpleHTTPRequestHandler.handle(self)
234cf4490901 Uploaded
fubar
parents:
diff changeset
69 except Exception:
234cf4490901 Uploaded
fubar
parents:
diff changeset
70 # ignored, thrown whenever the client aborts streaming (broken pipe)
234cf4490901 Uploaded
fubar
parents:
diff changeset
71 pass
234cf4490901 Uploaded
fubar
parents:
diff changeset
72
234cf4490901 Uploaded
fubar
parents:
diff changeset
73 def send_head(self):
234cf4490901 Uploaded
fubar
parents:
diff changeset
74 if "Range" not in self.headers:
234cf4490901 Uploaded
fubar
parents:
diff changeset
75 self.range = None
234cf4490901 Uploaded
fubar
parents:
diff changeset
76 return SimpleHTTPRequestHandler.send_head(self)
234cf4490901 Uploaded
fubar
parents:
diff changeset
77 try:
234cf4490901 Uploaded
fubar
parents:
diff changeset
78 self.range = parse_byte_range(self.headers["Range"])
234cf4490901 Uploaded
fubar
parents:
diff changeset
79 except ValueError:
234cf4490901 Uploaded
fubar
parents:
diff changeset
80 self.send_error(400, "Invalid byte range")
234cf4490901 Uploaded
fubar
parents:
diff changeset
81 return None
234cf4490901 Uploaded
fubar
parents:
diff changeset
82 first, last = self.range
234cf4490901 Uploaded
fubar
parents:
diff changeset
83
234cf4490901 Uploaded
fubar
parents:
diff changeset
84 # Mirroring SimpleHTTPServer.py here
234cf4490901 Uploaded
fubar
parents:
diff changeset
85 path = self.translate_path(self.path)
234cf4490901 Uploaded
fubar
parents:
diff changeset
86 f = None
234cf4490901 Uploaded
fubar
parents:
diff changeset
87 ctype = self.guess_type(path)
234cf4490901 Uploaded
fubar
parents:
diff changeset
88 try:
234cf4490901 Uploaded
fubar
parents:
diff changeset
89 f = open(path, "rb")
234cf4490901 Uploaded
fubar
parents:
diff changeset
90 except IOError:
234cf4490901 Uploaded
fubar
parents:
diff changeset
91 self.send_error(404, "File not found")
234cf4490901 Uploaded
fubar
parents:
diff changeset
92 return None
234cf4490901 Uploaded
fubar
parents:
diff changeset
93
234cf4490901 Uploaded
fubar
parents:
diff changeset
94 fs = os.fstat(f.fileno())
234cf4490901 Uploaded
fubar
parents:
diff changeset
95 file_len = fs[6]
234cf4490901 Uploaded
fubar
parents:
diff changeset
96 if first >= file_len:
234cf4490901 Uploaded
fubar
parents:
diff changeset
97 self.send_error(416, "Requested Range Not Satisfiable")
234cf4490901 Uploaded
fubar
parents:
diff changeset
98 return None
234cf4490901 Uploaded
fubar
parents:
diff changeset
99
234cf4490901 Uploaded
fubar
parents:
diff changeset
100 self.send_response(206)
234cf4490901 Uploaded
fubar
parents:
diff changeset
101 self.send_header("Content-type", ctype)
234cf4490901 Uploaded
fubar
parents:
diff changeset
102
234cf4490901 Uploaded
fubar
parents:
diff changeset
103 if last is None or last >= file_len:
234cf4490901 Uploaded
fubar
parents:
diff changeset
104 last = file_len - 1
234cf4490901 Uploaded
fubar
parents:
diff changeset
105 response_length = last - first + 1
234cf4490901 Uploaded
fubar
parents:
diff changeset
106
234cf4490901 Uploaded
fubar
parents:
diff changeset
107 self.send_header("Content-Range", "bytes %s-%s/%s" % (first, last, file_len))
234cf4490901 Uploaded
fubar
parents:
diff changeset
108 self.send_header("Content-Length", str(response_length))
234cf4490901 Uploaded
fubar
parents:
diff changeset
109 self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
234cf4490901 Uploaded
fubar
parents:
diff changeset
110 self.end_headers()
234cf4490901 Uploaded
fubar
parents:
diff changeset
111 return f
234cf4490901 Uploaded
fubar
parents:
diff changeset
112
234cf4490901 Uploaded
fubar
parents:
diff changeset
113 def end_headers(self):
234cf4490901 Uploaded
fubar
parents:
diff changeset
114 self.send_header("Accept-Ranges", "bytes")
234cf4490901 Uploaded
fubar
parents:
diff changeset
115 return SimpleHTTPRequestHandler.end_headers(self)
234cf4490901 Uploaded
fubar
parents:
diff changeset
116
234cf4490901 Uploaded
fubar
parents:
diff changeset
117 def copyfile(self, source, outputfile):
234cf4490901 Uploaded
fubar
parents:
diff changeset
118 if not self.range:
234cf4490901 Uploaded
fubar
parents:
diff changeset
119 return SimpleHTTPRequestHandler.copyfile(self, source, outputfile)
234cf4490901 Uploaded
fubar
parents:
diff changeset
120
234cf4490901 Uploaded
fubar
parents:
diff changeset
121 # SimpleHTTPRequestHandler uses shutil.copyfileobj, which doesn't let
234cf4490901 Uploaded
fubar
parents:
diff changeset
122 # you stop the copying before the end of the file.
234cf4490901 Uploaded
fubar
parents:
diff changeset
123 start, stop = self.range # set in send_head()
234cf4490901 Uploaded
fubar
parents:
diff changeset
124 copy_byte_range(source, outputfile, start, stop)
234cf4490901 Uploaded
fubar
parents:
diff changeset
125
234cf4490901 Uploaded
fubar
parents:
diff changeset
126
234cf4490901 Uploaded
fubar
parents:
diff changeset
127 class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
234cf4490901 Uploaded
fubar
parents:
diff changeset
128 allow_reuse_address = True
234cf4490901 Uploaded
fubar
parents:
diff changeset
129
234cf4490901 Uploaded
fubar
parents:
diff changeset
130
234cf4490901 Uploaded
fubar
parents:
diff changeset
131 if __name__ == "__main__":
234cf4490901 Uploaded
fubar
parents:
diff changeset
132 parser = argparse.ArgumentParser(
234cf4490901 Uploaded
fubar
parents:
diff changeset
133 description="Simple Python Web Server with Range Support"
234cf4490901 Uploaded
fubar
parents:
diff changeset
134 )
234cf4490901 Uploaded
fubar
parents:
diff changeset
135 parser.add_argument(
234cf4490901 Uploaded
fubar
parents:
diff changeset
136 "--root",
234cf4490901 Uploaded
fubar
parents:
diff changeset
137 default=os.getcwd(),
234cf4490901 Uploaded
fubar
parents:
diff changeset
138 help="Root path to serve files from (default: current working directory)",
234cf4490901 Uploaded
fubar
parents:
diff changeset
139 )
234cf4490901 Uploaded
fubar
parents:
diff changeset
140 parser.add_argument(
234cf4490901 Uploaded
fubar
parents:
diff changeset
141 "--port",
234cf4490901 Uploaded
fubar
parents:
diff changeset
142 type=int,
234cf4490901 Uploaded
fubar
parents:
diff changeset
143 default=DEFAULT_PORT,
234cf4490901 Uploaded
fubar
parents:
diff changeset
144 help=f"Port to listen on (default: {DEFAULT_PORT})",
234cf4490901 Uploaded
fubar
parents:
diff changeset
145 )
234cf4490901 Uploaded
fubar
parents:
diff changeset
146 parser.add_argument(
234cf4490901 Uploaded
fubar
parents:
diff changeset
147 "--bind", default="0.0.0.0", help="IP address to bind to (default: 0.0.0.0)"
234cf4490901 Uploaded
fubar
parents:
diff changeset
148 )
234cf4490901 Uploaded
fubar
parents:
diff changeset
149 args = parser.parse_args()
234cf4490901 Uploaded
fubar
parents:
diff changeset
150
234cf4490901 Uploaded
fubar
parents:
diff changeset
151 handler = functools.partial(RangeRequestHandler, directory=args.root)
234cf4490901 Uploaded
fubar
parents:
diff changeset
152
234cf4490901 Uploaded
fubar
parents:
diff changeset
153 webbrowser.open(f"http://{args.bind}:{args.port}")
234cf4490901 Uploaded
fubar
parents:
diff changeset
154
234cf4490901 Uploaded
fubar
parents:
diff changeset
155 with ThreadedTCPServer((args.bind, args.port), handler) as httpd:
234cf4490901 Uploaded
fubar
parents:
diff changeset
156 print(
234cf4490901 Uploaded
fubar
parents:
diff changeset
157 f"Serving HTTP on {args.bind} port {args.port} (http://{args.bind}:{args.port}/)"
234cf4490901 Uploaded
fubar
parents:
diff changeset
158 )
234cf4490901 Uploaded
fubar
parents:
diff changeset
159 httpd.serve_forever()