Mercurial > repos > fubar > jbrowse2
diff x/jb2_webserver.py @ 125:49f3d3878413 draft
planemo upload for repository https://github.com/galaxyproject/tools-iuc/tree/master/tools/jbrowse2 commit 5ea1f9c1eef1de76232e69aa6d34cda77d90d566
author | fubar |
---|---|
date | Sat, 05 Oct 2024 23:58:05 +0000 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/x/jb2_webserver.py Sat Oct 05 23:58:05 2024 +0000 @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +# spec: simplest python web server with range support and multithreading that takes root path, +# port and bind address as command line arguments; by default uses the current dir as webroot, +# port 8000 and bind address of 0.0.0.0 +# borrowed from https://github.com/danvk/RangeHTTPServer +# and reborrowed from https://gist.github.com/glowinthedark/b99900abe935e4ab4857314d647a9068 +# +# The Apache 2.0 license copy in this repository is distributed with this code in accordance with that licence. +# https://www.apache.org/licenses/LICENSE-2.0.txt +# This part is not MIT licenced like the other components. + +# APPENDIX: How to apply the Apache License to your work. + +# To apply the Apache License to your work, attach the following +# boilerplate notice, with the fields enclosed by brackets "[]" +# replaced with your own identifying information. (Don't include +# the brackets!) The text should be enclosed in the appropriate +# comment syntax for the file format. We also recommend that a +# file or class name and description of purpose be included on the +# same "printed page" as the copyright notice for easier +# identification within third-party archives. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import argparse +import functools +import os +import re +import socketserver +import webbrowser +from http.server import SimpleHTTPRequestHandler + + +DEFAULT_PORT = 8081 + + +def copy_byte_range(infile, outfile, start=None, stop=None, bufsize=16 * 1024): + """Like shutil.copyfileobj, but only copy a range of the streams. + + Both start and stop are inclusive. + """ + if start is not None: + infile.seek(start) + while 1: + to_read = min(bufsize, stop + 1 - infile.tell() if stop else bufsize) + buf = infile.read(to_read) + if not buf: + break + outfile.write(buf) + + +BYTE_RANGE_RE = re.compile(r"bytes=(\d+)-(\d+)?$") + + +def parse_byte_range(byte_range): + """Returns the two numbers in 'bytes=123-456' or throws ValueError. + + The last number or both numbers may be None. + """ + if byte_range.strip() == "": + return None, None + + m = BYTE_RANGE_RE.match(byte_range) + if not m: + raise ValueError("Invalid byte range %s" % byte_range) + + first, last = [x and int(x) for x in m.groups()] + if last and last < first: + raise ValueError("Invalid byte range %s" % byte_range) + return first, last + + +class RangeRequestHandler(SimpleHTTPRequestHandler): + """Adds support for HTTP 'Range' requests to SimpleHTTPRequestHandler + + The approach is to: + - Override send_head to look for 'Range' and respond appropriately. + - Override copyfile to only transmit a range when requested. + """ + + def handle(self): + try: + SimpleHTTPRequestHandler.handle(self) + except Exception: + # ignored, thrown whenever the client aborts streaming (broken pipe) + pass + + def send_head(self): + if "Range" not in self.headers: + self.range = None + return SimpleHTTPRequestHandler.send_head(self) + try: + self.range = parse_byte_range(self.headers["Range"]) + except ValueError: + self.send_error(400, "Invalid byte range") + return None + first, last = self.range + + # Mirroring SimpleHTTPServer.py here + path = self.translate_path(self.path) + f = None + ctype = self.guess_type(path) + try: + f = open(path, "rb") + except IOError: + self.send_error(404, "File not found") + return None + + fs = os.fstat(f.fileno()) + file_len = fs[6] + if first >= file_len: + self.send_error(416, "Requested Range Not Satisfiable") + return None + + self.send_response(206) + self.send_header("Content-type", ctype) + + if last is None or last >= file_len: + last = file_len - 1 + response_length = last - first + 1 + + self.send_header("Content-Range", "bytes %s-%s/%s" % (first, last, file_len)) + self.send_header("Content-Length", str(response_length)) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + self.end_headers() + return f + + def end_headers(self): + self.send_header("Accept-Ranges", "bytes") + return SimpleHTTPRequestHandler.end_headers(self) + + def copyfile(self, source, outputfile): + if not self.range: + return SimpleHTTPRequestHandler.copyfile(self, source, outputfile) + + # SimpleHTTPRequestHandler uses shutil.copyfileobj, which doesn't let + # you stop the copying before the end of the file. + start, stop = self.range # set in send_head() + copy_byte_range(source, outputfile, start, stop) + + +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + allow_reuse_address = True + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Tiny Python Web Server supporting range requests, for local viewing of unzipped Galaxy JBrowse2 configurations" + ) + parser.add_argument( + "--root", + default=os.getcwd(), + help="Root path to serve files from (default: current working directory)", + ) + parser.add_argument( + "--port", + type=int, + default=DEFAULT_PORT, + help=f"Port to listen on (default: {DEFAULT_PORT})", + ) + parser.add_argument( + "--bind", + default="127.0.0.1", + help="IP address to bind to (default: 127.0.0.1 - use 0.0.0.0 to allow access on your network)", + ) + args = parser.parse_args() + + handler = functools.partial(RangeRequestHandler, directory=args.root) + + webbrowser.open(f"http://{args.bind}:{args.port}") + + with ThreadedTCPServer((args.bind, args.port), handler) as httpd: + print( + f"Serving HTTP on {args.bind} port {args.port} (http://{args.bind}:{args.port}/)" + ) + httpd.serve_forever()