comparison jb2_webserver.py @ 7:b04fd993b31e draft

planemo upload for repository https://github.com/galaxyproject/tools-iuc/tree/master/tools/jbrowse2 commit 53a108d8153c955044ae7eb8cb06bdcfd0036717
author fubar
date Wed, 17 Jan 2024 07:50:52 +0000
parents
children 4c201a3d4755
comparison
equal deleted inserted replaced
6:79f7265f90bd 7:b04fd993b31e
1 #!/usr/bin/env python3# spec: simplest python web server with range support and multithreading that takes root path,
2 # port and bind address as command line arguments; by default uses the current dir as webroot,
3 # port 8000 and bind address of 0.0.0.0
4 # borrowed from https://github.com/danvk/RangeHTTPServer
5 # and reborrowed from https://gist.github.com/glowinthedark/b99900abe935e4ab4857314d647a9068
6 #
7 # The Apache 2.0 license copy in this repository is distributed with this code in accordance with that licence.
8 # https://www.apache.org/licenses/LICENSE-2.0.txt
9 # This part is not MIT licenced like the other components.
10
11 # APPENDIX: How to apply the Apache License to your work.
12
13 # To apply the Apache License to your work, attach the following
14 # boilerplate notice, with the fields enclosed by brackets "[]"
15 # replaced with your own identifying information. (Don't include
16 # the brackets!) The text should be enclosed in the appropriate
17 # comment syntax for the file format. We also recommend that a
18 # file or class name and description of purpose be included on the
19 # same "printed page" as the copyright notice for easier
20 # identification within third-party archives.
21
22 # Licensed under the Apache License, Version 2.0 (the "License");
23 # you may not use this file except in compliance with the License.
24 # You may obtain a copy of the License at
25
26 # http://www.apache.org/licenses/LICENSE-2.0
27
28 # Unless required by applicable law or agreed to in writing, software
29 # distributed under the License is distributed on an "AS IS" BASIS,
30 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31 # See the License for the specific language governing permissions and
32 # limitations under the License.
33
34
35 import argparse
36 import functools
37 import os
38 import re
39 import socketserver
40 import webbrowser
41 from http.server import SimpleHTTPRequestHandler
42
43
44 DEFAULT_PORT = 8080
45
46
47 def copy_byte_range(infile, outfile, start=None, stop=None, bufsize=16 * 1024):
48 """Like shutil.copyfileobj, but only copy a range of the streams.
49
50 Both start and stop are inclusive.
51 """
52 if start is not None:
53 infile.seek(start)
54 while 1:
55 to_read = min(bufsize, stop + 1 - infile.tell() if stop else bufsize)
56 buf = infile.read(to_read)
57 if not buf:
58 break
59 outfile.write(buf)
60
61
62 BYTE_RANGE_RE = re.compile(r"bytes=(\d+)-(\d+)?$")
63
64
65 def parse_byte_range(byte_range):
66 """Returns the two numbers in 'bytes=123-456' or throws ValueError.
67
68 The last number or both numbers may be None.
69 """
70 if byte_range.strip() == "":
71 return None, None
72
73 m = BYTE_RANGE_RE.match(byte_range)
74 if not m:
75 raise ValueError("Invalid byte range %s" % byte_range)
76
77 first, last = [x and int(x) for x in m.groups()]
78 if last and last < first:
79 raise ValueError("Invalid byte range %s" % byte_range)
80 return first, last
81
82
83 class RangeRequestHandler(SimpleHTTPRequestHandler):
84 """Adds support for HTTP 'Range' requests to SimpleHTTPRequestHandler
85
86 The approach is to:
87 - Override send_head to look for 'Range' and respond appropriately.
88 - Override copyfile to only transmit a range when requested.
89 """
90
91 def handle(self):
92 try:
93 SimpleHTTPRequestHandler.handle(self)
94 except Exception:
95 # ignored, thrown whenever the client aborts streaming (broken pipe)
96 pass
97
98 def send_head(self):
99 if "Range" not in self.headers:
100 self.range = None
101 return SimpleHTTPRequestHandler.send_head(self)
102 try:
103 self.range = parse_byte_range(self.headers["Range"])
104 except ValueError:
105 self.send_error(400, "Invalid byte range")
106 return None
107 first, last = self.range
108
109 # Mirroring SimpleHTTPServer.py here
110 path = self.translate_path(self.path)
111 f = None
112 ctype = self.guess_type(path)
113 try:
114 f = open(path, "rb")
115 except IOError:
116 self.send_error(404, "File not found")
117 return None
118
119 fs = os.fstat(f.fileno())
120 file_len = fs[6]
121 if first >= file_len:
122 self.send_error(416, "Requested Range Not Satisfiable")
123 return None
124
125 self.send_response(206)
126 self.send_header("Content-type", ctype)
127
128 if last is None or last >= file_len:
129 last = file_len - 1
130 response_length = last - first + 1
131
132 self.send_header("Content-Range", "bytes %s-%s/%s" % (first, last, file_len))
133 self.send_header("Content-Length", str(response_length))
134 self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
135 self.end_headers()
136 return f
137
138 def end_headers(self):
139 self.send_header("Accept-Ranges", "bytes")
140 return SimpleHTTPRequestHandler.end_headers(self)
141
142 def copyfile(self, source, outputfile):
143 if not self.range:
144 return SimpleHTTPRequestHandler.copyfile(self, source, outputfile)
145
146 # SimpleHTTPRequestHandler uses shutil.copyfileobj, which doesn't let
147 # you stop the copying before the end of the file.
148 start, stop = self.range # set in send_head()
149 copy_byte_range(source, outputfile, start, stop)
150
151
152 class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
153 allow_reuse_address = True
154
155
156 if __name__ == "__main__":
157 parser = argparse.ArgumentParser(
158 description="Simple Python Web Server with Range Support"
159 )
160 parser.add_argument(
161 "--root",
162 default=os.getcwd(),
163 help="Root path to serve files from (default: current working directory)",
164 )
165 parser.add_argument(
166 "--port",
167 type=int,
168 default=DEFAULT_PORT,
169 help=f"Port to listen on (default: {DEFAULT_PORT})",
170 )
171 parser.add_argument(
172 "--bind", default="0.0.0.0", help="IP address to bind to (default: 0.0.0.0)"
173 )
174 args = parser.parse_args()
175
176 handler = functools.partial(RangeRequestHandler, directory=args.root)
177
178 webbrowser.open(f"http://{args.bind}:{args.port}")
179
180 with ThreadedTCPServer((args.bind, args.port), handler) as httpd:
181 print(
182 f"Serving HTTP on {args.bind} port {args.port} (http://{args.bind}:{args.port}/)"
183 )
184 httpd.serve_forever()