From 647bc578188ba0890de30d76c899c703b2d3fb35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Saillant?= Date: Mon, 15 Jun 2026 00:50:11 +0200 Subject: [PATCH] feat(tower): register server + delta push to box --- tower/lisael_server.py | 58 ++++++++++++++++++++++++++++++++++++++++++ tower/test_delta.py | 14 ++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tower/lisael_server.py create mode 100644 tower/test_delta.py diff --git a/tower/lisael_server.py b/tower/lisael_server.py new file mode 100644 index 0000000..e86492f --- /dev/null +++ b/tower/lisael_server.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Always-on: POST /register {id, have} -> push the staging delta to the box's +file-server (:8080). The box IP is the request source address.""" +import json, os, urllib.request +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +STAGING = os.environ.get("LISAEL_STAGING", "/home/clems/lisael-content/podcasts") +PORT = int(os.environ.get("LISAEL_PORT", "9555")) +BOX_PORT = int(os.environ.get("LISAEL_BOX_PORT", "8080")) + +def staging_files(): + if not os.path.isdir(STAGING): + return [] + return sorted(f for f in os.listdir(STAGING) if not f.startswith(".")) + +def compute_delta(staging, have): + """Files the box is missing; manifest.json only when something else changed.""" + haveset = set(have) + files = [f for f in staging if f != "manifest.json" and f not in haveset] + if files and "manifest.json" in staging: + files.append("manifest.json") + return files + +def push_file(ip, name): + data = open(os.path.join(STAGING, name), "rb").read() + url = f"http://{ip}:{BOX_PORT}/put?name=podcasts/{name}" + with urllib.request.urlopen(urllib.request.Request(url, data=data, method="POST"), + timeout=40) as r: + return r.status + +class H(BaseHTTPRequestHandler): + def do_POST(self): + if self.path.split("?")[0] != "/register": + self.send_error(404); return + ip = self.client_address[0] + try: + n = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(n) or b"{}") + except Exception: + self.send_error(400); return + delta = compute_delta(staging_files(), body.get("have", []) or []) + print(f"register {ip} id={body.get('id')} have={len(body.get('have',[]) or [])} push={len(delta)}", + flush=True) + ok = 0 + for f in delta: + try: + push_file(ip, f); ok += 1 + except Exception as e: + print("push fail", f, e, flush=True) + self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers() + self.wfile.write(json.dumps({"pushed": ok, "of": len(delta)}).encode()) + + def log_message(self, *a): + pass + +if __name__ == "__main__": + print(f"lisael-content on :{PORT} staging={STAGING}", flush=True) + ThreadingHTTPServer(("0.0.0.0", PORT), H).serve_forever() diff --git a/tower/test_delta.py b/tower/test_delta.py new file mode 100644 index 0000000..84963f4 --- /dev/null +++ b/tower/test_delta.py @@ -0,0 +1,14 @@ +from lisael_server import compute_delta + +def test_delta(): + stg = ["oli_01.m4a", "oli.bin", "bestioles_01.mp3", "manifest.json"] + # empty box -> everything, manifest last + assert compute_delta(stg, []) == ["oli_01.m4a", "oli.bin", "bestioles_01.mp3", "manifest.json"] + # up-to-date box -> nothing (not even the manifest) + assert compute_delta(stg, ["oli_01.m4a", "oli.bin", "bestioles_01.mp3"]) == [] + # box missing the cover -> .bin + manifest + assert compute_delta(stg, ["oli_01.m4a", "bestioles_01.mp3"]) == ["oli.bin", "manifest.json"] + print("OK") + +if __name__ == "__main__": + test_delta()