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)