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

138 lines
4.3 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 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"
DESIGNS_DIR = Path("/data/designs")
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)
return jsonify({
"ok": True,
"service": "scribus-bridge",
"runner_connected": runner_alive,
})
@app.route("/api/scribus/start", methods=["POST"])
def start_scribus():
"""Verify runner socket is available. Optionally launch Scribus GUI for real rendering."""
# The runner process is managed by supervisor and should already be listening.
# Wait briefly for socket if it's still starting up.
for _ in range(10):
if os.path.exists(SOCKET_PATH):
return jsonify({"ok": True, "message": "Runner connected", "runner_connected": True})
time.sleep(0.5)
return jsonify({"ok": False, "error": "Runner socket not available. Check supervisor logs."}), 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)))