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