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