diff --git a/cista/api.py b/cista/api.py index 262e5f9..66a3264 100644 --- a/cista/api.py +++ b/cista/api.py @@ -2,11 +2,11 @@ import asyncio import typing import msgspec -from sanic import Blueprint, SanicException +from sanic import Blueprint from cista import watching from cista.fileio import FileServer -from cista.protocol import ControlBase, ErrorMsg, FileRange, StatusMsg +from cista.protocol import ControlBase, FileRange, StatusMsg from cista.util.apphelpers import asend, websocket_wrapper bp = Blueprint("api", url_prefix="/api") diff --git a/cista/serve.py b/cista/serve.py index 7b8ecc9..4bf4af1 100755 --- a/cista/serve.py +++ b/cista/serve.py @@ -1,6 +1,6 @@ import os import re -from pathlib import Path +from pathlib import Path, PurePath from sanic import Sanic @@ -13,14 +13,23 @@ def run(dev=False): url, opts = parse_listen(config.config.listen) # Silence Sanic's warning about running in production rather than debug os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "1" + confdir = config.conffile.parent + wwwroot = PurePath(__file__).parent / "wwwroot" if opts.get("ssl"): # Run plain HTTP redirect/acme server on port 80 server80.app.prepare(port=80, motd=False) domain = opts["host"] - opts["ssl"] = str(config.conffile.parent / domain) # type: ignore - app.prepare(**opts, motd=False, dev=dev, auto_reload=dev, access_log=True) # type: ignore + check_cert(confdir / domain, domain) + opts["ssl"] = str(confdir / domain) # type: ignore + app.prepare(**opts, motd=False, dev=dev, auto_reload=dev, reload_dir={confdir, wwwroot}, access_log=True) # type: ignore Sanic.serve() +def check_cert(certdir, domain): + if (certdir / "privkey.pem").exist() and (certdir / "fullchain.pem").exists(): + return + # TODO: Use certbot to fetch a cert + raise ValueError(f"TLS certificate files privkey.pem and fullchain.pem needed in {certdir}") + def parse_listen(listen): if listen.startswith("/"): unix = Path(listen).resolve() diff --git a/cista/watching.py b/cista/watching.py index 5c1fb0c..7e85758 100755 --- a/cista/watching.py +++ b/cista/watching.py @@ -13,7 +13,7 @@ from cista.protocol import DirEntry, FileEntry, UpdateEntry pubsub = {} tree = {"": None} tree_lock = threading.Lock() -rootpath = None +rootpath: Path = None # type: ignore quit = False modified_flags = "IN_CREATE", "IN_DELETE", "IN_DELETE_SELF", "IN_MODIFY", "IN_MOVE_SELF", "IN_MOVED_FROM", "IN_MOVED_TO" disk_usage = None @@ -22,6 +22,7 @@ def watcher_thread(loop): global disk_usage while True: + rootpath = config.config.path i = inotify.adapters.InotifyTree(rootpath.as_posix()) old = format_tree() if tree[""] else None with tree_lock: @@ -36,18 +37,25 @@ def watcher_thread(loop): for event in i.event_gen(): if quit: return + # Disk usage update du = shutil.disk_usage(rootpath) if du != disk_usage: disk_usage = du asyncio.run_coroutine_threadsafe(broadcast(format_du()), loop) + break + # Do a full refresh? if time.monotonic() > refreshdl: break if event is None: continue _, flags, path, filename = event if not any(f in modified_flags for f in flags): continue + # Update modified path path = PurePosixPath(path) / filename - #print(path, flags) - update(path.relative_to(rootpath), loop) + try: + update(path.relative_to(rootpath), loop) + except Exception as e: + print("Watching error", e) + break i = None # Free the inotify object def format_du(): @@ -84,7 +92,7 @@ def walk(path: Path) -> DirEntry | FileEntry | None: print("OS error walking path", path, e) return None -def update(relpath: Path, loop): +def update(relpath: PurePosixPath, loop): """Called by inotify updates, check the filesystem and broadcast any changes.""" new = walk(rootpath / relpath) with tree_lock: @@ -149,9 +157,7 @@ async def broadcast(msg): await queue.put_nowait(msg) async def start(app, loop): - global rootpath config.load_config() - rootpath = config.config.path app.ctx.watcher = threading.Thread(target=watcher_thread, args=[loop]) app.ctx.watcher.start()