169 lines
5.2 KiB
Python
169 lines
5.2 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:
|
|
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
|
|
|
|
env["XDG_RUNTIME_DIR"] = "/tmp/runtime-root"
|
|
_scribus_proc = subprocess.Popen(
|
|
["scribus", "--python-script", SCRIBUS_RUNNER],
|
|
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)))
|