aboutsummaryrefslogtreecommitdiff
path: root/demoware/server.py
diff options
context:
space:
mode:
authorkj_sh6042026-02-13 22:47:17 -0500
committerkj_sh6042026-02-13 22:47:23 -0500
commitbd1e20b26c0f1ff7c1fd6f3b9bc34e32b5e067c9 (patch)
treeae937ac33190ebf008a436bf4c70d1be5dbe19a1 /demoware/server.py
parentbf3c2e1c9b5f2fd22de39c55769af239277a819a (diff)
feat: try making demoware for this
Diffstat (limited to 'demoware/server.py')
-rw-r--r--demoware/server.py210
1 files changed, 210 insertions, 0 deletions
diff --git a/demoware/server.py b/demoware/server.py
new file mode 100644
index 0000000..a4b1029
--- /dev/null
+++ b/demoware/server.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python3
+
+# kjandoc demoware server
+# a tiny threaded http server for the web interface.
+#
+# usage: python server.py [port]
+# default port: 8080
+
+import os
+import sys
+import json
+import uuid
+import time
+import shutil
+import threading
+import subprocess
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+from socketserver import ThreadingMixIn
+from pathlib import Path
+from urllib.parse import unquote
+
+
+PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
+BASE_DIR = Path(__file__).parent.resolve()
+UPLOAD_DIR = BASE_DIR / 'uploads'
+OUTPUT_DIR = BASE_DIR / 'output'
+KJANDOC = (BASE_DIR.parent / 'src' / 'kjandoc').resolve()
+
+UPLOAD_DIR.mkdir(exist_ok=True)
+OUTPUT_DIR.mkdir(exist_ok=True)
+
+# {job_id: {status, command, output, log}}
+jobs = {}
+jobs_lock = threading.Lock()
+
+
+class ThreadedServer(ThreadingMixIn, HTTPServer):
+ daemon_threads = True
+
+
+class Handler(SimpleHTTPRequestHandler):
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, directory=str(BASE_DIR), **kwargs)
+
+ # -- routing --
+
+ def do_POST(self):
+ if self.path == '/api/upload':
+ self._upload()
+ elif self.path == '/api/merge':
+ self._merge()
+ else:
+ self.send_error(404)
+
+ def do_GET(self):
+ if self.path.startswith('/api/status/'):
+ self._status()
+ else:
+ super().do_GET()
+
+ # -- helpers --
+
+ def _json(self, code, data):
+ body = json.dumps(data).encode('utf-8')
+ self.send_response(code)
+ self.send_header('Content-Type', 'application/json')
+ self.send_header('Content-Length', str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ # -- handlers --
+
+ def _upload(self):
+ job_id = self.headers.get('X-Job-Id', '')
+ raw_name = self.headers.get('X-Filename', '')
+ length = int(self.headers.get('Content-Length', 0))
+
+ if not job_id or not raw_name:
+ return self._json(400, {"error": "missing headers"})
+ if length <= 0:
+ return self._json(400, {"error": "empty file"})
+
+ name = Path(unquote(raw_name)).name
+ if not name.lower().endswith('.pptx'):
+ return self._json(400, {"error": "not a .pptx file"})
+
+ job_dir = UPLOAD_DIR / job_id
+ job_dir.mkdir(parents=True, exist_ok=True)
+ dest = job_dir / name
+
+ with open(dest, 'wb') as f:
+ left = length
+ while left > 0:
+ chunk = self.rfile.read(min(left, 65536))
+ if not chunk:
+ break
+ f.write(chunk)
+ left -= len(chunk)
+
+ self._json(200, {"ok": True, "file": name})
+
+ def _merge(self):
+ length = int(self.headers.get('Content-Length', 0))
+ raw = self.rfile.read(length)
+ try:
+ data = json.loads(raw)
+ except json.JSONDecodeError:
+ return self._json(400, {"error": "bad json"})
+
+ job_id = data.get('job_id', '')
+ files = data.get('files', [])
+
+ if not job_id or not files:
+ return self._json(400, {"error": "missing job_id or files"})
+
+ job_dir = UPLOAD_DIR / job_id
+ if not job_dir.exists():
+ return self._json(400, {"error": "upload dir not found — did you upload first?"})
+
+ # validate filenames (no path traversal)
+ for f in files:
+ if '/' in f or '\\' in f or '..' in f:
+ return self._json(400, {"error": f"bad filename: {f}"})
+ if not (job_dir / f).exists():
+ return self._json(400, {"error": f"not found: {f}"})
+
+ # output: epoch_shortid.pptx
+ epoch = int(time.time())
+ uid = uuid.uuid4().hex[:8]
+ out_name = f"{epoch}_{uid}.pptx"
+ out_path = OUTPUT_DIR / out_name
+
+ # build the real command
+ inputs = [str(job_dir / f) for f in files]
+ cmd = [sys.executable, str(KJANDOC)] + inputs + ['-o', str(out_path)]
+
+ # pretty command for display (strip numeric id prefixes like "3_")
+ pretty_names = []
+ for f in files:
+ parts = f.split('_', 1)
+ pretty_names.append(parts[1] if len(parts) == 2 and parts[0].isdigit() else f)
+ cmd_str = f"kjandoc {' '.join(pretty_names)} -o output/{out_name}"
+
+ with jobs_lock:
+ jobs[job_id] = {
+ "status": "running",
+ "command": cmd_str,
+ "output": out_name,
+ "log": "",
+ }
+
+ def do_merge():
+ try:
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
+ log = result.stdout + result.stderr
+ status = "done" if result.returncode == 0 else "error"
+ except subprocess.TimeoutExpired:
+ log = "timed out (10 min limit)"
+ status = "error"
+ except Exception as e:
+ log = str(e)
+ status = "error"
+
+ with jobs_lock:
+ jobs[job_id]["log"] = log
+ jobs[job_id]["status"] = status
+
+ # clean up uploads for this job
+ shutil.rmtree(job_dir, ignore_errors=True)
+
+ threading.Thread(target=do_merge, daemon=True).start()
+
+ self._json(200, {
+ "ok": True,
+ "job_id": job_id,
+ "command": cmd_str,
+ "output": out_name,
+ })
+
+ def _status(self):
+ job_id = self.path.rsplit('/', 1)[-1]
+ with jobs_lock:
+ job = jobs.get(job_id)
+ snapshot = dict(job) if job else None
+ if not snapshot:
+ return self._json(404, {"error": "unknown job"})
+ self._json(200, snapshot)
+
+
+def main():
+ if not KJANDOC.exists():
+ print(f"[!] kjandoc not found: {KJANDOC}", file=sys.stderr)
+ print(f" expected at ../src/kjandoc relative to this script", file=sys.stderr)
+ sys.exit(1)
+
+ srv = ThreadedServer(('', PORT), Handler)
+ print(f"[*] kjandoc demoware")
+ print(f"[*] http://localhost:{PORT}")
+ print(f"[*] kjandoc: {KJANDOC}")
+ print(f"[*] ctrl-c to stop")
+ try:
+ srv.serve_forever()
+ except KeyboardInterrupt:
+ print("\n[*] bye")
+ srv.shutdown()
+
+
+if __name__ == '__main__':
+ main()