#!/usr/bin/env python3 """Static file server for the rendered Bible site. Stdlib only, so the container needs nothing but a Python base image. Its one job beyond serving files is getting cache headers right: the service worker owns the offline copy, and if the worker or the pages that register it are served stale, a new build can never reach a device that already installed the old one. Expects to sit behind something that terminates TLS. That is not optional for the offline app: browsers only register a service worker in a secure context, so a device loading this over plain http:// from a LAN address installs the page but never gets an offline copy. """ from __future__ import annotations import argparse import os import signal import sys from functools import partial from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer # Revalidate every time: these are what carry an update to an installed app. NO_CACHE_NAMES = {"sw.js"} NO_CACHE_EXTS = {".html", ".webmanifest"} class Handler(SimpleHTTPRequestHandler): extensions_map = { **SimpleHTTPRequestHandler.extensions_map, ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".webmanifest": "application/manifest+json; charset=utf-8", ".png": "image/png", ".svg": "image/svg+xml", } server_version = "bible-static" sys_version = "" def end_headers(self): path = self.path.split("?", 1)[0] name = os.path.basename(path) ext = os.path.splitext(name)[1].lower() if name in NO_CACHE_NAMES or ext in NO_CACHE_EXTS or path.endswith("/"): self.send_header("Cache-Control", "no-cache") else: self.send_header("Cache-Control", "public, max-age=604800") # Lets the worker claim the whole origin even if it moves off root. self.send_header("Service-Worker-Allowed", "/") self.send_header("X-Content-Type-Options", "nosniff") super().end_headers() def log_message(self, fmt, *args): sys.stdout.write("%s %s\n" % (self.address_string(), fmt % args)) sys.stdout.flush() def main(argv=None): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--root", default=os.environ.get("SITE_ROOT", "books"), help="directory to serve (default: books)") ap.add_argument("--host", default=os.environ.get("HOST", "0.0.0.0")) ap.add_argument("--port", type=int, default=int(os.environ.get("PORT", "8080"))) args = ap.parse_args(argv) root = os.path.abspath(args.root) if not os.path.isdir(root): sys.exit("site root not found: %s" % root) httpd = ThreadingHTTPServer((args.host, args.port), partial(Handler, directory=root)) httpd.daemon_threads = True def stop(_signum, _frame): httpd.shutdown() signal.signal(signal.SIGTERM, stop) signal.signal(signal.SIGINT, stop) print("serving %s at http://%s:%d" % (root, args.host, args.port)) sys.stdout.flush() try: httpd.serve_forever() finally: httpd.server_close() return 0 if __name__ == "__main__": sys.exit(main())