Mercurial > repos > fubar > jbrowse2
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() |
