mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7798d68e7f |
@@ -290,3 +290,6 @@ docs/type3/signatures/
|
||||
|
||||
# Local screenshot artifacts from *-screenshots.spec.ts
|
||||
frontend/editor/screenshots/
|
||||
|
||||
# Generated API client SDKs (task sdk:generate)
|
||||
clients/
|
||||
|
||||
@@ -57,6 +57,12 @@ tasks:
|
||||
- task: frontend:install
|
||||
- task: engine:install
|
||||
|
||||
sdk:generate:
|
||||
desc: "Generate Python/Node/Go client SDKs from the OpenAPI spec"
|
||||
cmds:
|
||||
- task: backend:swagger
|
||||
- bash devTools/generate-sdks.sh
|
||||
|
||||
# ============================================================
|
||||
# Development
|
||||
# ============================================================
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate API client SDKs (Python, TypeScript/Node, Go) from SwaggerDoc.json.
|
||||
# Build-time only: nothing here ships in any image. Output lands in clients/
|
||||
# which is gitignored; publish steps live with each package registry.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SPEC="$ROOT/SwaggerDoc.json"
|
||||
GENERATOR_VERSION=7.24.0
|
||||
JAR="$ROOT/build/openapi-generator-cli-$GENERATOR_VERSION.jar"
|
||||
OUT="$ROOT/clients"
|
||||
|
||||
if [ ! -f "$SPEC" ]; then
|
||||
echo "SwaggerDoc.json missing - run 'task backend:swagger' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$JAR" ]; then
|
||||
mkdir -p "$ROOT/build"
|
||||
echo "Fetching openapi-generator-cli $GENERATOR_VERSION"
|
||||
curl -fsSL -o "$JAR" \
|
||||
"https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/$GENERATOR_VERSION/openapi-generator-cli-$GENERATOR_VERSION.jar"
|
||||
fi
|
||||
|
||||
generate() {
|
||||
local generator="$1" out_dir="$2"; shift 2
|
||||
echo "Generating $generator -> $out_dir"
|
||||
java -jar "$JAR" generate -i "$SPEC" -g "$generator" -o "$OUT/$out_dir" \
|
||||
--skip-validate-spec "$@"
|
||||
}
|
||||
|
||||
rm -rf "$OUT"
|
||||
generate python python \
|
||||
--additional-properties=packageName=stirling_pdf_client,projectName=stirling-pdf-client
|
||||
generate typescript-node node \
|
||||
--additional-properties=npmName=@stirling-pdf/client,supportsES6=true
|
||||
generate go go \
|
||||
--additional-properties=packageName=stirlingpdf
|
||||
|
||||
echo "SDKs generated under clients/"
|
||||
@@ -0,0 +1,41 @@
|
||||
# stirling-pdf (pip launcher)
|
||||
|
||||
Install and run Stirling PDF with pip:
|
||||
|
||||
```bash
|
||||
pip install stirling-pdf
|
||||
stirling-pdf run
|
||||
```
|
||||
|
||||
What it does:
|
||||
|
||||
- **Docker available** (recommended): generates a compose file under
|
||||
`~/.stirling-pdf/`, pulls the official image, and starts it on
|
||||
`http://localhost:8080`. `--variant fat|ultra-lite` picks the image,
|
||||
`--docparse` also runs the AI engine with the DocParse addon (layout
|
||||
parsing, grounded extraction; ~1.6 GB one-time download onto a volume).
|
||||
- **No Docker**: downloads the release jar and runs it with your local
|
||||
Java 21+ (`--no-docker` forces this path). The DocParse advanced tier is
|
||||
Docker-only; the jar still serves everything else.
|
||||
|
||||
Commands:
|
||||
|
||||
```bash
|
||||
stirling-pdf run [--port 8080] [--variant latest|fat|ultra-lite] [--docparse] [--no-docker]
|
||||
stirling-pdf status
|
||||
stirling-pdf update
|
||||
stirling-pdf stop
|
||||
stirling-pdf addons install docparse
|
||||
stirling-pdf addons remove docparse
|
||||
```
|
||||
|
||||
State lives in `~/.stirling-pdf` (override with `STIRLING_PDF_HOME`). The
|
||||
package has zero Python dependencies.
|
||||
|
||||
## Publishing (maintainers)
|
||||
|
||||
```bash
|
||||
cd packaging/pip
|
||||
python -m build
|
||||
python -m twine upload dist/*
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
[project]
|
||||
name = "stirling-pdf"
|
||||
version = "0.1.0"
|
||||
description = "Installer and launcher for Stirling PDF: runs the Docker images when Docker is available, or the release jar with a local Java 21+."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
license = { text = "MIT" }
|
||||
authors = [{ name = "Stirling PDF", email = "hello@stirlingpdf.com" }]
|
||||
keywords = ["pdf", "stirling", "stirling-pdf", "document"]
|
||||
classifiers = [
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
# Deliberately zero dependencies: stdlib only, so `pip install stirling-pdf`
|
||||
# never conflicts with anything.
|
||||
dependencies = []
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://stirlingpdf.com"
|
||||
Source = "https://github.com/Stirling-Tools/Stirling-PDF"
|
||||
|
||||
[project.scripts]
|
||||
stirling-pdf = "stirling_pdf.cli:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/stirling_pdf"]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""stirling-pdf: pip-installable launcher for Stirling PDF."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,263 @@
|
||||
"""Stirling PDF launcher CLI.
|
||||
|
||||
Prefers Docker (compose file generated under ~/.stirling-pdf); falls back to
|
||||
downloading the release jar and running it with a local Java 21+. Stdlib only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
APP_IMAGE = "stirlingtools/stirling-pdf"
|
||||
ENGINE_IMAGE = "stirlingtools/stirling-pdf-engine"
|
||||
JAR_URL_TEMPLATE = "https://github.com/Stirling-Tools/Stirling-PDF/releases/{release}/download/{asset}"
|
||||
JAR_ASSETS = ("Stirling-PDF.jar", "Stirling-PDF-with-login.jar")
|
||||
COMPOSE_PROJECT = "stirling-pdf"
|
||||
|
||||
|
||||
def home_dir() -> Path:
|
||||
home = Path(os.environ.get("STIRLING_PDF_HOME", Path.home() / ".stirling-pdf"))
|
||||
home.mkdir(parents=True, exist_ok=True)
|
||||
return home
|
||||
|
||||
|
||||
def load_state() -> dict:
|
||||
path = home_dir() / "config.json"
|
||||
if path.exists():
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def save_state(state: dict) -> None:
|
||||
(home_dir() / "config.json").write_text(json.dumps(state, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- docker mode
|
||||
|
||||
|
||||
def docker_available() -> bool:
|
||||
if shutil.which("docker") is None:
|
||||
return False
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "info"], capture_output=True, text=True, timeout=30, check=False
|
||||
)
|
||||
return result.returncode == 0
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return False
|
||||
|
||||
|
||||
def compose_yaml(port: int, variant: str, docparse: bool) -> str:
|
||||
tag = "latest" if variant == "latest" else f"latest-{variant}"
|
||||
home = home_dir()
|
||||
for sub in ("data", "config", "logs", "docparse"):
|
||||
(home / sub).mkdir(exist_ok=True)
|
||||
|
||||
app_env = [
|
||||
" DOCKER_ENABLE_SECURITY: \"false\"",
|
||||
]
|
||||
engine_service = ""
|
||||
if docparse:
|
||||
app_env += [
|
||||
" AIENGINE_ENABLED: \"true\"",
|
||||
" AIENGINE_URL: http://stirling-pdf-engine:5001",
|
||||
" DOCPARSE_ENABLED: \"true\"",
|
||||
" DOCPARSE_MODE: auto",
|
||||
]
|
||||
engine_service = f"""
|
||||
stirling-pdf-engine:
|
||||
image: {ENGINE_IMAGE}:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DOCPARSE_AUTO_INSTALL: "true"
|
||||
volumes:
|
||||
- {(home / 'docparse').as_posix()}:/configs/docparse
|
||||
"""
|
||||
|
||||
return f"""# Generated by `stirling-pdf run` - edit and re-run, or delete to reset.
|
||||
services:
|
||||
stirling-pdf:
|
||||
image: {APP_IMAGE}:{tag}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "{port}:8080"
|
||||
volumes:
|
||||
- {(home / 'data').as_posix()}:/usr/share/tessdata
|
||||
- {(home / 'config').as_posix()}:/configs
|
||||
- {(home / 'logs').as_posix()}:/logs
|
||||
environment:
|
||||
{chr(10).join(app_env)}
|
||||
{engine_service}"""
|
||||
|
||||
|
||||
def compose(*args: str) -> int:
|
||||
return subprocess.call(
|
||||
["docker", "compose", "-p", COMPOSE_PROJECT, "-f", str(home_dir() / "docker-compose.yml"), *args]
|
||||
)
|
||||
|
||||
|
||||
def run_docker(port: int, variant: str, docparse: bool) -> int:
|
||||
compose_path = home_dir() / "docker-compose.yml"
|
||||
compose_path.write_text(compose_yaml(port, variant, docparse), encoding="utf-8")
|
||||
save_state({**load_state(), "mode": "docker", "port": port, "variant": variant, "docparse": docparse})
|
||||
code = compose("up", "-d", "--pull", "always")
|
||||
if code == 0:
|
||||
print(f"Stirling PDF is starting: http://localhost:{port}")
|
||||
if docparse:
|
||||
print("DocParse addon: engine is downloading it on first boot (~1.6 GB, one-time).")
|
||||
return code
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- jar mode
|
||||
|
||||
|
||||
def java_major_version() -> int | None:
|
||||
java = shutil.which("java")
|
||||
if java is None:
|
||||
return None
|
||||
try:
|
||||
result = subprocess.run([java, "-version"], capture_output=True, text=True, timeout=30, check=False)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
match = re.search(r'version "(\d+)', result.stderr or result.stdout or "")
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
|
||||
def download(url: str, target: Path) -> bool:
|
||||
print(f"Downloading {url}")
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=600) as response: # noqa: S310
|
||||
total = int(response.headers.get("Content-Length") or 0)
|
||||
read = 0
|
||||
tmp = target.with_suffix(".part")
|
||||
with open(tmp, "wb") as out:
|
||||
while True:
|
||||
chunk = response.read(1 << 20)
|
||||
if not chunk:
|
||||
break
|
||||
out.write(chunk)
|
||||
read += len(chunk)
|
||||
if total:
|
||||
sys.stdout.write(f"\r {read // (1 << 20)} / {total // (1 << 20)} MB")
|
||||
sys.stdout.flush()
|
||||
sys.stdout.write("\n")
|
||||
tmp.replace(target)
|
||||
return True
|
||||
except (urllib.error.URLError, OSError) as error:
|
||||
print(f" download failed: {error}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def run_jar(port: int, version: str) -> int:
|
||||
major = java_major_version()
|
||||
if major is None:
|
||||
print("Neither Docker nor Java found. Install Docker (recommended) or Java 21+.", file=sys.stderr)
|
||||
return 1
|
||||
if major < 21:
|
||||
print(f"Java {major} found but Stirling PDF needs Java 21+.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
jar = home_dir() / "Stirling-PDF.jar"
|
||||
if not jar.exists():
|
||||
release = "latest" if version == "latest" else f"download/v{version}"
|
||||
if not any(download(JAR_URL_TEMPLATE.format(release=release, asset=asset), jar) for asset in JAR_ASSETS):
|
||||
return 1
|
||||
|
||||
save_state({**load_state(), "mode": "jar", "port": port})
|
||||
env = {**os.environ, "SERVER_PORT": str(port), "STIRLING_BASE_PATH": str(home_dir() / "jar-home")}
|
||||
print(f"Starting Stirling PDF on http://localhost:{port} (Ctrl-C to stop)")
|
||||
print("Note: the jar runs the core app; DocParse advanced tier needs the Docker engine image.")
|
||||
return subprocess.call(["java", "-jar", str(jar)], env=env)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ commands
|
||||
|
||||
|
||||
def cmd_run(args: argparse.Namespace) -> int:
|
||||
if not args.no_docker and docker_available():
|
||||
return run_docker(args.port, args.variant, args.docparse)
|
||||
if args.docparse:
|
||||
print("--docparse needs Docker (the addon ships in the engine image).", file=sys.stderr)
|
||||
return 1
|
||||
return run_jar(args.port, args.version)
|
||||
|
||||
|
||||
def cmd_stop(_args: argparse.Namespace) -> int:
|
||||
state = load_state()
|
||||
if state.get("mode") == "docker":
|
||||
return compose("down")
|
||||
print("Jar mode runs in the foreground; stop it with Ctrl-C.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_status(_args: argparse.Namespace) -> int:
|
||||
state = load_state()
|
||||
if state.get("mode") == "docker":
|
||||
return compose("ps")
|
||||
print(json.dumps(state or {"mode": "not configured"}, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_update(_args: argparse.Namespace) -> int:
|
||||
state = load_state()
|
||||
if state.get("mode") == "docker":
|
||||
code = compose("pull")
|
||||
return code or compose("up", "-d")
|
||||
jar = home_dir() / "Stirling-PDF.jar"
|
||||
if jar.exists():
|
||||
jar.unlink()
|
||||
print("Removed cached jar; the next `stirling-pdf run` downloads the latest release.")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_addons(args: argparse.Namespace) -> int:
|
||||
if args.addon != "docparse":
|
||||
print(f"Unknown addon: {args.addon}. Available: docparse", file=sys.stderr)
|
||||
return 1
|
||||
state = load_state()
|
||||
if state.get("mode") != "docker":
|
||||
print("Addons need Docker mode; run `stirling-pdf run` with Docker available first.", file=sys.stderr)
|
||||
return 1
|
||||
enable = args.action == "install"
|
||||
return run_docker(state.get("port", 8080), state.get("variant", "latest"), enable)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(prog="stirling-pdf", description=__doc__)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
run_parser = sub.add_parser("run", help="Start Stirling PDF (Docker preferred, jar fallback)")
|
||||
run_parser.add_argument("--port", type=int, default=8080)
|
||||
run_parser.add_argument("--variant", choices=["latest", "fat", "ultra-lite"], default="latest")
|
||||
run_parser.add_argument("--version", default="latest", help="Release version for jar mode")
|
||||
run_parser.add_argument("--docparse", action="store_true", help="Also run the AI engine with the DocParse addon")
|
||||
run_parser.add_argument("--no-docker", action="store_true", help="Force jar mode")
|
||||
run_parser.set_defaults(func=cmd_run)
|
||||
|
||||
sub.add_parser("stop", help="Stop the Docker deployment").set_defaults(func=cmd_stop)
|
||||
sub.add_parser("status", help="Show what is running").set_defaults(func=cmd_status)
|
||||
sub.add_parser("update", help="Pull/download the newest release").set_defaults(func=cmd_update)
|
||||
|
||||
addons = sub.add_parser("addons", help="Manage optional addons")
|
||||
addons.add_argument("action", choices=["install", "remove"])
|
||||
addons.add_argument("addon")
|
||||
addons.set_defaults(func=cmd_addons)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user