comparison webserver.py @ 3:2de9f585505b draft

planemo upload for repository https://github.com/galaxyproject/tools-iuc/tree/master/tools/jbrowse2 commit b30d2e277cc360437a02fa812c577c0c8d997cff-dirty
author fubar
date Tue, 09 Jan 2024 02:35:52 +0000
parents
children
comparison
equal deleted inserted replaced
2:37b2c26c7b9b 3:2de9f585505b
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()