rspace-online/docker/scribus-novnc/bridge/server.py

163 lines
5.0 KiB
Python

"""
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:
return jsonify({"ok": True, "message": "Scribus already running", "pid": _scribus_proc.pid})
# 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)))