""" Scribus Bridge Server — HTTP API for controlling Scribus from rSpace. Architecture: rspace container → HTTP → this Flask server (port 8765) │ Unix socket scribus --python-script scribus_runner.py The runner script executes inside the Scribus process (required by Scribus Python API). It listens on a Unix socket for JSON commands. This Flask server translates HTTP requests into socket commands. """ import json import os import socket import subprocess import time from pathlib import Path from flask import Flask, request, jsonify app = Flask(__name__) BRIDGE_SECRET = os.environ.get("BRIDGE_SECRET", "") SOCKET_PATH = "/tmp/scribus_bridge.sock" SCRIBUS_RUNNER = "/opt/bridge/scribus_runner.py" DESIGNS_DIR = Path("/data/designs") DISPLAY = os.environ.get("DISPLAY", ":1") _scribus_proc = None def _check_auth(): """Verify X-Bridge-Secret header.""" if not BRIDGE_SECRET: return None # No secret configured, allow all token = request.headers.get("X-Bridge-Secret", "") if token != BRIDGE_SECRET: return jsonify({"error": "Unauthorized"}), 401 return None def _send_command(cmd: dict, timeout: float = 30.0) -> dict: """Send a JSON command to the Scribus runner via Unix socket.""" if not os.path.exists(SOCKET_PATH): return {"error": "Scribus runner not connected. Call /api/scribus/start first."} try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(timeout) sock.connect(SOCKET_PATH) payload = json.dumps(cmd) + "\n" sock.sendall(payload.encode("utf-8")) # Read response (newline-delimited JSON) buf = b"" while True: chunk = sock.recv(4096) if not chunk: break buf += chunk if b"\n" in buf: break sock.close() return json.loads(buf.decode("utf-8").strip()) except socket.timeout: return {"error": "Command timed out"} except ConnectionRefusedError: return {"error": "Scribus runner not responding"} except Exception as e: return {"error": f"Bridge error: {str(e)}"} @app.before_request def before_request(): auth = _check_auth() if auth: return auth @app.route("/health", methods=["GET"]) def health(): runner_alive = os.path.exists(SOCKET_PATH) scribus_alive = _scribus_proc is not None and _scribus_proc.poll() is None return jsonify({ "ok": True, "service": "scribus-bridge", "runner_connected": runner_alive, "scribus_alive": scribus_alive, }) @app.route("/api/scribus/start", methods=["POST"]) def start_scribus(): """Launch Scribus with the bridge runner script.""" global _scribus_proc if _scribus_proc and _scribus_proc.poll() is None: if os.path.exists(SOCKET_PATH): return jsonify({"ok": True, "message": "Scribus already running", "pid": _scribus_proc.pid}) # Process alive but runner socket missing — kill and restart _scribus_proc.kill() _scribus_proc.wait(timeout=5) _scribus_proc = None # Clean up stale socket if os.path.exists(SOCKET_PATH): os.remove(SOCKET_PATH) env = os.environ.copy() env["DISPLAY"] = DISPLAY _scribus_proc = subprocess.Popen( ["scribus", "--python-script", SCRIBUS_RUNNER, "--no-gui"], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # Wait for runner socket to appear (up to 15s) for _ in range(30): if os.path.exists(SOCKET_PATH): return jsonify({"ok": True, "message": "Scribus started", "pid": _scribus_proc.pid}) time.sleep(0.5) return jsonify({"ok": False, "error": "Scribus runner did not connect in time"}), 500 @app.route("/api/scribus/command", methods=["POST"]) def scribus_command(): """Execute a Scribus command via the runner.""" body = request.get_json(silent=True) if not body or "action" not in body: return jsonify({"error": "Missing 'action' in request body"}), 400 result = _send_command(body) status = 200 if "error" not in result else 500 return jsonify(result), status @app.route("/api/scribus/state", methods=["GET"]) def scribus_state(): """Return the full document state as JSON.""" result = _send_command({"action": "get_doc_state"}) status = 200 if "error" not in result else 500 return jsonify(result), status @app.route("/api/scribus/screenshot", methods=["GET"]) def scribus_screenshot(): """Export the current page as PNG.""" dpi = request.args.get("dpi", "72", type=str) result = _send_command({"action": "screenshot", "args": {"dpi": int(dpi)}}) if "error" in result: return jsonify(result), 500 png_path = result.get("path") if png_path and os.path.exists(png_path): return app.send_static_file(png_path) if False else \ (open(png_path, "rb").read(), 200, {"Content-Type": "image/png"}) return jsonify({"error": "Screenshot not generated"}), 500 if __name__ == "__main__": DESIGNS_DIR.mkdir(parents=True, exist_ok=True) app.run(host="0.0.0.0", port=int(os.environ.get("BRIDGE_PORT", 8765)))