github.com/iDigitalFlame/xmt@v0.5.4/tools/content_server.py (about) 1 #!/usr/bin/python 2 # Copyright (C) 2020 - 2023 iDigitalFlame 3 # 4 # This program is free software: you can redistribute it and/or modify 5 # it under the terms of the GNU General Public License as published by 6 # the Free Software Foundation, either version 3 of the License, or 7 # any later version. 8 # 9 # This program is distributed in the hope that it will be useful, 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 # GNU General Public License for more details. 13 # 14 # You should have received a copy of the GNU General Public License 15 # along with this program. If not, see <https://www.gnu.org/licenses/>. 16 # 17 18 from json import loads 19 from threading import Lock 20 from sys import argv, stderr, exit 21 from socketserver import TCPServer 22 from watchdog.observers import Observer 23 from http.server import SimpleHTTPRequestHandler 24 from os.path import expanduser, expandvars, isfile 25 from watchdog.events import FileSystemEventHandler, FileModifiedEvent 26 27 28 class Server(FileSystemEventHandler): 29 __slots__ = ("_dir", "_entries", "_lock", "_file", "_obs") 30 31 def __init__(self, file, dir): 32 self._dir = dir 33 self._lock = Lock() 34 self._file = expanduser(expandvars(file)) 35 self._reload() 36 self._obs = Observer() 37 self._obs.schedule(self, self._file) 38 self._obs.start() 39 40 def stop(self): 41 if self._obs is None: 42 return 43 self._obs.stop() 44 self._obs.unschedule_all() 45 self._obs = None 46 47 def _reload(self): 48 print(f'Reading config file "{self._file}"..') 49 with open(self._file) as f: 50 e = loads(f.read()) 51 if not isinstance(e, dict): 52 raise ValueError(f'file "{self._file}" does not contain a JSON dict') 53 for k, v in e.items(): 54 if not isinstance(k, str) or len(k) == 0: 55 raise ValueError("invalid JSON key") 56 if not isinstance(v, dict): 57 raise ValueError(f'invalid JSON value for "{k}"') 58 if "file" not in v or "type" not in v: 59 raise ValueError(f'missing values for JSON value "{k}"') 60 if not isinstance(v["file"], str) or not isinstance(v["type"], str): 61 raise ValueError(f'invalid value types for JSON value "{k}"') 62 p = expandvars(expanduser(v["file"])) 63 if not isfile(p): 64 raise ValueError(f'path "{p}" for JSON value "{k}" does not exist') 65 v["file"] = p 66 del p 67 self._entries = None 68 self._entries = e 69 print(f"Reading config done, {len(self._entries)} loaded.") 70 71 def start(self, addr, port): 72 with TCPServer((addr, port), self._request) as h: 73 h.serve_forever() 74 75 def on_modified(self, event): 76 if not isinstance(event, FileModifiedEvent): 77 return 78 self._lock.acquire(True) 79 try: 80 self._reload() 81 except Exception as err: 82 print(f"Failed to reload config: {err}!", file=stderr) 83 finally: 84 self._lock.release() 85 86 def _request(self, req, address, server): 87 self._lock.acquire(True) 88 r = _WebRequest(self._entries, self._dir, req, address, server) 89 self._lock.release() 90 return r 91 92 93 class _WebRequest(SimpleHTTPRequestHandler): 94 def __init__(self, entries, base, req, address, server): 95 self._entries = entries 96 SimpleHTTPRequestHandler.__init__( 97 self, request=req, client_address=address, server=server, directory=base 98 ) 99 100 def do_GET(self): 101 e = self._entries.get(self.path.lower()) 102 if not isinstance(e, dict): 103 return super(__class__, self).do_GET() 104 try: 105 with open(e["file"], "rb") as f: 106 b = f.read() 107 except OSError as err: 108 print(f"Server read error {self.path}: {err}", file=stderr) 109 return self.send_error(500, "Server error") 110 else: 111 self.send_response(200) 112 self.send_header("Content-type", e["type"]) 113 finally: 114 del e 115 self.send_header("Content-Length", len(b)) 116 self.end_headers() 117 self.wfile.write(b) 118 119 120 if __name__ == "__main__": 121 if len(argv) < 5: 122 print(f"{argv[0]} <config> <dir> <addr> <port>", file=stderr) 123 exit(1) 124 try: 125 Server(argv[1], argv[2]).start(argv[3], int(argv[4])) 126 except KeyboardInterrupt: 127 print("Interrupted!", file=stderr) 128 exit(1) 129 except Exception as err: 130 print(f"Error: {err}!", file=stderr) 131 exit(1)