commit 820efb3fe89e38f35499edc8a17703782f2567fc Author: xiaoyi Date: Mon May 25 15:22:18 2026 +0800 Add project documentation from API (GitHub download failed) diff --git a/.taskfiles/backend.yml b/.taskfiles/backend.yml new file mode 100644 index 0000000..56db8eb --- /dev/null +++ b/.taskfiles/backend.yml @@ -0,0 +1,146 @@ +version: '3' + +# Gradle invocation strategy: +# - Linux/macOS: `./gradlew` runs the POSIX shell wrapper natively. +# - Windows: `cmd /c ".\gradlew.bat ..."` invokes the .bat wrapper through +# cmd.exe so it inherits the user's Windows-side `JAVA_HOME` +# and PATH. Routing through `bash gradlew` on Windows ends up +# under WSL or Git-Bash, neither of which inherits +# Adoptium/Temurin's default Windows-only Java env - gradlew +# then errors with "JAVA_HOME is not set and no 'java' command +# could be found". +# +# The entire `.\gradlew.bat ...` payload is double-quoted so +# the leading `.\` survives mvdan/sh's POSIX backslash +# stripping; cmd.exe also requires `.\` (not bare `gradlew.bat`) +# because modern Windows excludes cwd from cmd's search path. + +tasks: + dev: + desc: "Start backend dev server" + ignore_error: true + vars: + PORT: '{{.PORT | default "8080"}}' + AIENGINE_URL: '{{.AIENGINE_URL | default ""}}' + env: + SERVER_PORT: '{{.PORT}}' + cmds: + - cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"' + platforms: [windows] + - cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}./gradlew :stirling-pdf:bootRun' + platforms: [linux, darwin] + + dev:bundled: + desc: "Clean + bootRun with frontend bundled into the backend (single :8080 server)" + ignore_error: true + cmds: + - cmd: cmd /c ".\gradlew.bat clean bootRun -PbuildWithFrontend=true" + platforms: [windows] + - cmd: ./gradlew clean bootRun -PbuildWithFrontend=true + platforms: [linux, darwin] + + build: + desc: "Full backend build" + cmds: + - cmd: cmd /c ".\gradlew.bat clean build" + platforms: [windows] + - cmd: ./gradlew clean build + platforms: [linux, darwin] + + build:fast: + desc: "Build without tests" + cmds: + - cmd: cmd /c ".\gradlew.bat clean build -x test" + platforms: [windows] + - cmd: ./gradlew clean build -x test + platforms: [linux, darwin] + + build:ci: + desc: "Build for CI (formatting checked separately)" + cmds: + - cmd: cmd /c ".\gradlew.bat build -PnoSpotless" + platforms: [windows] + - cmd: ./gradlew build -PnoSpotless + platforms: [linux, darwin] + + test: + desc: "Run backend tests" + cmds: + - cmd: cmd /c ".\gradlew.bat test" + platforms: [windows] + - cmd: ./gradlew test + platforms: [linux, darwin] + + format: + desc: "Auto-fix code formatting" + cmds: + - cmd: cmd /c ".\gradlew.bat spotlessApply" + platforms: [windows] + - cmd: ./gradlew spotlessApply + platforms: [linux, darwin] + + format:check: + desc: "Check code formatting" + cmds: + - cmd: cmd /c ".\gradlew.bat spotlessCheck" + platforms: [windows] + - cmd: ./gradlew spotlessCheck + platforms: [linux, darwin] + + fix: + desc: "Auto-fix backend" + cmds: + - task: format + + swagger: + desc: "Generate OpenAPI docs" + cmds: + - cmd: cmd /c ".\gradlew.bat :stirling-pdf:copySwaggerDoc" + platforms: [windows] + - cmd: ./gradlew :stirling-pdf:copySwaggerDoc + platforms: [linux, darwin] + sources: + - app/core/src/main/java/**/*.java + - app/proprietary/src/main/java/**/*.java + - app/common/src/main/java/**/*.java + generates: + - SwaggerDoc.json + + check: + desc: "Backend quality gate" + cmds: + - task: format:check + - task: test + + version: + desc: "Print project version" + silent: true + cmds: + - cmd: cmd /c ".\gradlew.bat printVersion --quiet" | tail -1 + platforms: [windows] + - cmd: ./gradlew printVersion --quiet | tail -1 + platforms: [linux, darwin] + + licenses:check: + desc: "Check dependency licenses" + cmds: + - cmd: cmd /c ".\gradlew.bat checkLicense --no-parallel" + platforms: [windows] + - cmd: ./gradlew checkLicense --no-parallel + platforms: [linux, darwin] + + licenses:generate: + desc: "Check and generate dependency license report" + cmds: + - cmd: cmd /c ".\gradlew.bat checkLicense generateLicenseReport --no-parallel" + platforms: [windows] + - cmd: ./gradlew checkLicense generateLicenseReport --no-parallel + platforms: [linux, darwin] + + clean: + desc: "Clean build artifacts" + cmds: + - cmd: cmd /c ".\gradlew.bat clean" + platforms: [windows] + - cmd: ./gradlew clean + platforms: [linux, darwin] diff --git a/.taskfiles/docker.yml b/.taskfiles/docker.yml new file mode 100644 index 0000000..abe885a --- /dev/null +++ b/.taskfiles/docker.yml @@ -0,0 +1,63 @@ +version: '3' + +vars: + COMPOSE_DIR: docker/compose + EMBEDDED_DIR: docker/embedded + +tasks: + build: + desc: "Build standard Docker image" + cmds: + - docker build -t stirling-pdf -f {{.EMBEDDED_DIR}}/Dockerfile . + + build:fat: + desc: "Build fat Docker image (all features)" + cmds: + - docker build -t stirling-pdf-fat -f {{.EMBEDDED_DIR}}/Dockerfile.fat . + + build:ultra-lite: + desc: "Build ultra-lite Docker image" + cmds: + - docker build -t stirling-pdf-ultra-lite -f {{.EMBEDDED_DIR}}/Dockerfile.ultra-lite . + + build:frontend: + desc: "Build frontend-only Docker image" + cmds: + - docker build -t stirling-pdf-frontend -f docker/frontend/Dockerfile . + + build:engine: + desc: "Build engine Docker image" + dir: engine + cmds: + - docker build -t stirling-pdf-engine . + + up: + desc: "Start standard docker compose stack" + cmds: + - docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml up -d + + up:fat: + desc: "Start fat docker compose stack" + cmds: + - docker compose -f {{.COMPOSE_DIR}}/docker-compose.fat.yml up -d + + up:ultra-lite: + desc: "Start ultra-lite docker compose stack" + cmds: + - docker compose -f {{.COMPOSE_DIR}}/docker-compose.ultra-lite.yml up -d + + down: + desc: "Stop all running docker compose stacks" + cmds: + - docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml down + + logs: + desc: "Tail docker compose logs" + cmds: + - docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml logs -f + + test: + desc: "Run full Docker integration test suite (builds all variants and tests them)" + ignore_error: true + cmds: + - bash testing/test.sh {{.CLI_ARGS}} diff --git a/.taskfiles/frontend.yml b/.taskfiles/frontend.yml new file mode 100644 index 0000000..8c3fbc6 --- /dev/null +++ b/.taskfiles/frontend.yml @@ -0,0 +1,300 @@ +version: '3' + +# Tasks operate from the workspace root (frontend/). Editor commands pass +# `editor` as the vite project root (positional after `build` / before the +# mode flag) or use `--project editor/...` for tsc — so the editor lives +# under frontend/editor/ without each task needing a cd. + +tasks: + install: + desc: "Install dependencies" + run: once + cmds: + - '{{ if eq .CI "true" }}npm ci{{ else }}npm install{{ end }}' + sources: + - package-lock.json + - package.json + status: + - test -d node_modules + env: + CI: '{{ .CI | default "false" }}' + + prepare:env: + internal: true + run: when_changed + deps: [install] + vars: + MODE: '{{.MODE | default ""}}' + cmds: + - npx tsx editor/scripts/setup-env.mts{{if .MODE}} --{{.MODE}}{{end}} + sources: + - editor/scripts/setup-env.mts + generates: + - editor/.env.local + - editor/.env{{if .MODE}}.{{.MODE}}{{end}}.local + + prepare:icons: + internal: true + run: once + deps: [install] + cmds: + - node editor/scripts/generate-icons.js + + prepare: + desc: "Set up dev environment" + run: when_changed + vars: + MODE: '{{.MODE | default ""}}' + deps: + - task: prepare:env + vars: { MODE: '{{.MODE}}' } + - prepare:icons + + # ============================================================ + # Development + # ============================================================ + + dev:_run: + internal: true + ignore_error: true + vars: + MODE: '{{.MODE}}' + PORT: '{{.PORT | default "5173"}}' + BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}' + OPEN: '{{.OPEN | default ""}}' + env: + BACKEND_URL: '{{.BACKEND_URL}}' + cmds: + - npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}} + + dev: + desc: "Start frontend dev server" + cmds: + - task: dev:proprietary + vars: { PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } + + dev:core: + desc: "Start frontend dev server in core mode" + deps: [prepare] + cmds: + - task: dev:_run + vars: { MODE: core, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } + + dev:proprietary: + desc: "Start frontend dev server in proprietary mode" + deps: [prepare] + cmds: + - task: dev:_run + vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } + + dev:saas: + desc: "Start frontend dev server in SaaS mode" + deps: + - task: prepare + vars: { MODE: saas } + cmds: + - task: dev:_run + vars: { MODE: saas, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } + + dev:desktop: + desc: "Start frontend dev server in desktop mode" + deps: + - task: prepare + vars: { MODE: desktop } + cmds: + - task: dev:_run + vars: { MODE: desktop, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } + + dev:prototypes: + desc: "Start frontend dev server in prototypes mode" + deps: [prepare] + cmds: + - task: dev:_run + vars: { MODE: prototypes, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' } + + # ============================================================ + # Build + # ============================================================ + + build: + desc: "Production build (default mode)" + deps: [prepare] + cmds: + - npx vite build editor + + build:core: + desc: "Build for core mode" + deps: [prepare] + cmds: + - npx vite build editor --mode core + + build:proprietary: + desc: "Build for proprietary mode" + deps: [prepare] + cmds: + - npx vite build editor --mode proprietary + + build:saas: + desc: "Build for SaaS mode" + deps: + - task: prepare + vars: { MODE: saas } + cmds: + - npx vite build editor --mode saas + + build:desktop: + desc: "Build for desktop mode" + deps: + - task: prepare + vars: { MODE: desktop } + cmds: + - npx vite build editor --mode desktop + + build:prototypes: + desc: "Build for prototypes mode" + deps: [prepare] + cmds: + - npx vite build editor --mode prototypes + + # ============================================================ + # Code quality + # ============================================================ + + lint: + desc: "Run linting" + deps: [install] + cmds: + - npx eslint --max-warnings=0 + - npx dpdm editor/src --circular --no-warning --no-tree --exit-code circular:1 + + lint:fix: + desc: "Auto-fix lint issues" + deps: [install] + cmds: + - npx eslint --fix + + format: + desc: "Auto-fix code formatting" + deps: [install] + cmds: + - npx prettier --write . + + format:check: + desc: "Check code formatting" + deps: [install] + cmds: + - npx prettier --check . + + fix: + desc: "Auto-fix lint and format" + cmds: + - task: format + - task: lint:fix + + typecheck: + desc: "Typecheck default build of the app" + cmds: + - task: typecheck:proprietary + + typecheck:core: + desc: "Typecheck core build variant" + deps: [prepare] + cmds: + - npx tsc --noEmit --project editor/src/core/tsconfig.json + + typecheck:proprietary: + desc: "Typecheck proprietary build variant" + deps: [prepare] + cmds: + - npx tsc --noEmit --project editor/src/proprietary/tsconfig.json + + typecheck:saas: + desc: "Typecheck SaaS build variant" + deps: + - task: prepare + vars: { MODE: saas } + cmds: + - npx tsc --noEmit --project editor/src/saas/tsconfig.json + + typecheck:desktop: + desc: "Typecheck desktop build variant" + deps: + - task: prepare + vars: { MODE: desktop } + cmds: + - npx tsc --noEmit --project editor/src/desktop/tsconfig.json + + typecheck:scripts: + desc: "Typecheck scripts" + deps: [prepare] + cmds: + - npx tsc --noEmit --project editor/scripts/tsconfig.json + + typecheck:prototypes: + desc: "Typecheck prototypes build variant" + deps: [prepare] + cmds: + - npx tsc --noEmit --project editor/src/prototypes/tsconfig.json + + typecheck:all: + desc: "Typecheck all build variants" + cmds: + - task: typecheck:core + - task: typecheck:proprietary + - task: typecheck:saas + - task: typecheck:desktop + - task: typecheck:scripts + - task: typecheck:prototypes + + # ============================================================ + # Quality Gate + # ============================================================ + + check: + desc: "Quick quality gate for local development" + cmds: + - task: typecheck + - task: lint + - task: format:check + - task: test + + check:all: + desc: "Full CI quality gate" + cmds: + - task: typecheck:all + - task: lint + - task: format:check + - task: build + - task: test + + # ============================================================ + # Test + # ============================================================ + + test: + desc: "Run tests" + deps: [install] + cmds: + - npx vitest run --root editor + + test:watch: + desc: "Run tests in watch mode" + deps: [install] + cmds: + - npx vitest --watch --root editor + + test:coverage: + desc: "Run tests with coverage" + deps: [install] + cmds: + - npx vitest --coverage --root editor + + # ============================================================ + # Code Generation + # ============================================================ + + licenses:generate: + desc: "Generate frontend license report" + deps: [install] + cmds: + - node editor/scripts/generate-licenses.js diff --git a/DATABASE.md b/DATABASE.md new file mode 100644 index 0000000..5c245af --- /dev/null +++ b/DATABASE.md @@ -0,0 +1,34 @@ +# New Database Backup and Import Functionality + +## Functionality Overview + +The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works: + +1. Automatic Backup Creation + - The system automatically creates a database backup on a configurable schedule (default: daily at midnight via `system.databaseBackup.cron`). This ensures that there is always a recent backup available, minimizing the risk of data loss. +2. Manual Backup Export + - Admin actions that modify the user database trigger a manual export of the database. This keeps the backup up-to-date with the latest changes and provides an extra layer of data security. +3. Importing Database Backups + - Admin users can import a database backup either via the web interface or API endpoints. This allows for easy restoration of the database to a previous state in case of data corruption or other issues. + - The import process ensures that the database structure and data are correctly restored, maintaining the integrity of the application. +4. Managing Backup Files + - Admins can view a list of all existing backup files, along with their creation dates and sizes. This helps in managing storage and identifying the most recent or relevant backups. + - Backup files can be downloaded for offline storage or transferred to other environments, providing flexibility in database management. + - Unnecessary backup files can be deleted through the interface to free up storage space and maintain an organized backup directory. + +## User Interface + +### Web Interface + +1. Upload SQL files to import database backups. +2. View details of existing backups, such as file names, creation dates, and sizes. +3. Download backup files for offline storage. +4. Delete outdated or unnecessary backup files. + +### API Endpoints + +1. Import database backups by uploading SQL files. +2. Download backup files. +3. Delete backup files. + +This new functionality streamlines database management, ensuring that backups are always available and easy to manage, thus improving the reliability and resilience of the application. diff --git a/FILE_SHARING.md b/FILE_SHARING.md new file mode 100644 index 0000000..e0d96cf --- /dev/null +++ b/FILE_SHARING.md @@ -0,0 +1,444 @@ +# File Sharing Feature - Architecture & Workflow + +## Overview + +The File Sharing feature enables users to store files server-side and share them with other registered users or via token-based share links. Files are stored using a pluggable storage provider (local filesystem or database) with optional quota enforcement. + +**Key Capabilities:** +- Server-side file storage (upload, update, download, delete) +- Optional history bundle and audit log attachments per file +- Direct user-to-user sharing with access roles +- Token-based share links (requires `system.frontendUrl`) +- Optional email notifications for shares (requires `mail.enabled`) +- Access audit trail (tracks who accessed a share link and how) +- Automatic share link expiration +- Storage quotas (per-user and total) +- Pluggable storage backend (local filesystem or database BLOB) +- Integration with the Shared Signing workflow + +## Architecture + +### Database Schema + +**`stored_files`** +- One record per uploaded file +- Stores file metadata (name, content type, size, storage key) +- Optionally links to a history bundle and audit log as separate stored objects +- `workflow_session_id` — nullable link to a `WorkflowSession` (signing feature) +- `file_purpose` — enum classifying the file's role: `GENERIC`, `SIGNING_ORIGINAL`, `SIGNING_SIGNED`, `SIGNING_HISTORY` + +**`file_shares`** +- One record per sharing relationship +- Two share types, distinguished by which fields are set: + - **User share**: `shared_with_user_id` is set, `share_token` is null + - **Link share**: `share_token` is set (UUID), `shared_with_user_id` is null +- `access_role` — `EDITOR`, `COMMENTER`, or `VIEWER` +- `expires_at` — nullable expiration for link shares +- `workflow_participant_id` — when set, marks this as a **workflow share** (hidden from the file manager, accessible only via workflow endpoints) + +**`file_share_accesses`** +- One record per access event on a share link +- Tracks: user, share link, access type (`VIEW` or `DOWNLOAD`), timestamp + +**`storage_cleanup_entries`** +- Queue of storage keys to be deleted asynchronously +- Used when a file is deleted but the physical storage object cleanup is deferred + +### Access Roles + +| Role | Can Read | Can Write | +|------|----------|-----------| +| `EDITOR` | ✅ | ✅ | +| `COMMENTER` | ✅ | ❌ | +| `VIEWER` | ✅ | ❌ | + +Default role when none is specified: `EDITOR`. + +Owners always have full access regardless of role. + +#### Role Semantics: COMMENTER vs VIEWER + +In the file storage layer, `COMMENTER` and `VIEWER` are equivalent — both grant read-only access and neither can replace file content. The distinction is meaningful in the **signing workflow** context: + +| Context | COMMENTER | VIEWER | +|---------|-----------|--------| +| File storage | Read only (same as VIEWER) | Read only | +| Signing workflow | Can submit a signing action | Read only | + +`WorkflowParticipant.canEdit()` returns `true` for `COMMENTER` (and `EDITOR`) roles, which the signing workflow uses to determine if a participant can still submit a signature. Once a participant has signed or declined, their effective role is automatically downgraded to `VIEWER` regardless of their configured role. + +The rationale: "annotating" a document (submitting a signature) is not the same as "replacing" it. COMMENTER grants annotation rights without file-replacement rights. + +### Backend Architecture + +#### Service Layer + +**FileStorageService** (`1137 lines`) +- Core file management service +- Upload, update, download, and delete operations +- User share management (share, revoke, leave) +- Link share management (create, revoke, access) +- Access recording and listing +- Storage quota enforcement +- Configuration feature gate checks + +**StorageCleanupService** +- Scheduled daily: deletes orphaned storage keys from `storage_cleanup_entries` +- Scheduled daily: purges expired share links from `file_shares` +- Processes cleanup in batches of 50 entries + +#### Storage Providers + +**LocalStorageProvider** +- Files stored on the filesystem under `storage.local.basePath` (default: `./storage`) +- Storage key is a path relative to the base directory + +**DatabaseStorageProvider** +- Files stored as BLOBs in `stored_file_blobs` table +- No filesystem dependency + +Provider is selected at startup via `storage.provider: local | database`. + +#### Controller Layer + +**FileStorageController** (`/api/v1/storage`) +- All endpoints require authentication +- File CRUD and sharing operations + +### Data Flow + +``` +User uploads file → StorageProvider stores bytes → StoredFile record created + ↓ +Owner shares file → FileShare record created (user or link) + ↓ +Recipient accesses file → Access recorded → File bytes streamed +``` + +## File Operations + +### Upload File + +```bash +POST /api/v1/storage/files +Content-Type: multipart/form-data + +file: document.pdf # Required — main file +historyBundle: history.json # Optional — version history +auditLog: audit.json # Optional — audit trail +``` + +**Response:** +```json +{ + "id": 42, + "fileName": "document.pdf", + "contentType": "application/pdf", + "sizeBytes": 102400, + "owner": "alice", + "ownedByCurrentUser": true, + "accessRole": "editor", + "createdAt": "2025-01-01T12:00:00", + "updatedAt": "2025-01-01T12:00:00", + "sharedWithUsers": [], + "sharedUsers": [], + "shareLinks": [] +} +``` + +### Update File + +Replaces the file content. Only the owner can update. + +```bash +PUT /api/v1/storage/files/{fileId} +Content-Type: multipart/form-data + +file: document_v2.pdf +historyBundle: history.json # Optional +auditLog: audit.json # Optional +``` + +### List Files + +Returns all files owned by or shared with the current user. Workflow-shared files (signing participants) are excluded — those are accessible via signing endpoints only. + +```bash +GET /api/v1/storage/files +``` + +Response is sorted by `createdAt` descending. + +### Download File + +```bash +GET /api/v1/storage/files/{fileId}/download?inline=false +``` + +- `inline=false` (default) — `Content-Disposition: attachment` +- `inline=true` — `Content-Disposition: inline` (for browser preview) + +### Delete File + +Only the owner can delete. All associated share links and their access records are deleted first, then the database record, then the physical storage object. + +```bash +DELETE /api/v1/storage/files/{fileId} +``` + +## Sharing Operations + +### Share with User + +```bash +POST /api/v1/storage/files/{fileId}/shares/users +Content-Type: application/json + +{ + "username": "bob", # Username or email address + "accessRole": "editor" # "editor", "commenter", or "viewer" (default: "editor") +} +``` + +**Behaviour:** +- If the target user exists: creates/updates a `FileShare` with `sharedWithUser` set +- If `username` is an email address and the user doesn't exist: creates a share link and sends a notification email (requires `sharing.emailEnabled` and `sharing.linkEnabled`) +- If the target user is the owner: returns 400 +- If sharing is disabled: returns 403 + +### Revoke User Share + +Only the owner can revoke. + +```bash +DELETE /api/v1/storage/files/{fileId}/shares/users/{username} +``` + +### Leave Shared File + +The recipient removes themselves from a shared file. + +```bash +DELETE /api/v1/storage/files/{fileId}/shares/self +``` + +### Create Share Link + +Creates a token-based link for anonymous/authenticated access. Requires `sharing.linkEnabled` and `system.frontendUrl` to be configured. + +```bash +POST /api/v1/storage/files/{fileId}/shares/links +Content-Type: application/json + +{ + "accessRole": "viewer" # Optional (default: "editor") +} +``` + +**Response:** +```json +{ + "token": "550e8400-e29b-41d4-a716-446655440000", + "accessRole": "viewer", + "createdAt": "2025-01-01T12:00:00", + "expiresAt": "2025-01-04T12:00:00" +} +``` + +Expiration is set to `now + sharing.linkExpirationDays` (default: 3 days). + +### Revoke Share Link + +```bash +DELETE /api/v1/storage/files/{fileId}/shares/links/{token} +``` + +Also deletes all access records for that token. + +## Share Link Access + +### Download via Share Link + +Authentication is required (even for share links). Anonymous access is not permitted. + +```bash +GET /api/v1/storage/share-links/{token}?inline=false +``` + +- Returns 401 if unauthenticated +- Returns 403 if authenticated but link doesn't permit access +- Returns 410 if the link has expired +- Records a `FileShareAccess` entry on success + +> **Token-as-credential semantics:** Any authenticated user who holds the token can access the file — the token is the credential. If you need per-user access control (only a specific person can open it), use "Share with User" instead. Share links are appropriate for broader distribution where possession of the token implies authorization. + +### Get Share Link Metadata + +```bash +GET /api/v1/storage/share-links/{token}/metadata +``` + +Returns file name, owner, access role, creation/expiry timestamps, and whether the current user owns the file. + +### List Accessed Share Links + +Returns the most recent access for each non-expired share link the current user has accessed. + +```bash +GET /api/v1/storage/share-links/accessed +``` + +### List Accesses for a Link (Owner Only) + +```bash +GET /api/v1/storage/files/{fileId}/shares/links/{token}/accesses +``` + +Returns per-user access history (username, VIEW/DOWNLOAD, timestamp), sorted descending by time. + +## Workflow Share Integration + +Signing workflow participants access documents via their own `WorkflowParticipant.shareToken`. No `FileShare` record is created for participants; access control is self-contained in the `WorkflowParticipant` entity. + +The `FileShare.workflow_participant_id` column and the `FileShare.isWorkflowShare()` method are **deprecated**. Legacy data (sessions created before this change) may still have `FileShare` records with `workflow_participant_id` set, which continue to work via the existing token lookup path in `UnifiedAccessControlService`. No new records are created. + +`GET /api/v1/storage/files` returns all files owned by or shared with the current user (via `FileShare`). Signing-session PDFs use the `file_purpose` field (`SIGNING_ORIGINAL`, `SIGNING_SIGNED`, etc.) to distinguish them from generic files. The file manager UI can filter on this field if needed. + +## API Reference + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| POST | `/api/v1/storage/files` | Upload file | Required | +| PUT | `/api/v1/storage/files/{id}` | Update file | Required (owner) | +| GET | `/api/v1/storage/files` | List accessible files | Required | +| GET | `/api/v1/storage/files/{id}` | Get file metadata | Required | +| GET | `/api/v1/storage/files/{id}/download` | Download file | Required | +| DELETE | `/api/v1/storage/files/{id}` | Delete file | Required (owner) | +| POST | `/api/v1/storage/files/{id}/shares/users` | Share with user | Required (owner) | +| DELETE | `/api/v1/storage/files/{id}/shares/users/{username}` | Revoke user share | Required (owner) | +| DELETE | `/api/v1/storage/files/{id}/shares/self` | Leave shared file | Required | +| POST | `/api/v1/storage/files/{id}/shares/links` | Create share link | Required (owner) | +| DELETE | `/api/v1/storage/files/{id}/shares/links/{token}` | Revoke share link | Required (owner) | +| GET | `/api/v1/storage/share-links/{token}` | Download via share link | Required | +| GET | `/api/v1/storage/share-links/{token}/metadata` | Get share link metadata | Required | +| GET | `/api/v1/storage/share-links/accessed` | List accessed share links | Required | +| GET | `/api/v1/storage/files/{id}/shares/links/{token}/accesses` | List share accesses | Required (owner) | + +## Configuration + +All storage settings live under the `storage:` key in `settings.yml`: + +```yaml +storage: + enabled: true # Requires security.enableLogin = true + provider: local # 'local' or 'database' + local: + basePath: './storage' # Filesystem base directory (local provider only) + quotas: + maxStorageMbPerUser: -1 # Per-user storage cap in MB; -1 = unlimited + maxStorageMbTotal: -1 # Total storage cap in MB; -1 = unlimited + maxFileMb: -1 # Max size per upload (main + history + audit) in MB; -1 = unlimited + sharing: + enabled: false # Master switch for all sharing (opt-in) + linkEnabled: false # Enable token-based share links (requires system.frontendUrl) + emailEnabled: false # Enable email notifications (requires mail.enabled) + linkExpirationDays: 3 # Days until share links expire +``` + +**Prerequisites:** +- `storage.enabled` requires `security.enableLogin = true` +- `sharing.linkEnabled` requires `system.frontendUrl` to be set (used to build share link URLs) +- `sharing.emailEnabled` requires `mail.enabled = true` + +## Security Considerations + +### Access Control +- All endpoints require authentication — there is no anonymous access +- Owner-only operations enforced in service layer (not just controller) +- `requireReadAccess` / `requireEditorAccess` checked on every download + +### Share Link Security +- Tokens are UUIDs (random, not guessable) +- Expiration enforced on every access +- Expired links return HTTP 410 Gone +- Revoked links delete all access records + +### Quota Enforcement +- Checked before storing (not after) +- Accounts for existing file size when replacing (only the delta counts) +- Covers main file + history bundle + audit log in a single check + +## Automatic Cleanup + +`StorageCleanupService` runs two scheduled jobs daily: + +1. **Orphaned storage cleanup** — processes up to 50 `StorageCleanupEntry` records, deletes the physical storage object, then removes the entry. Failed attempts increment `attemptCount` for retry. + +2. **Expired share link cleanup** — deletes all `FileShare` records where `expiresAt` is in the past and `shareToken` is set. + +## Troubleshooting + +**"Storage is disabled":** +- Check `storage.enabled: true` in settings +- Verify `security.enableLogin: true` + +**"Share links are disabled":** +- Check `sharing.linkEnabled: true` +- Verify `system.frontendUrl` is set and non-empty + +**"Email sharing is disabled":** +- Check `sharing.emailEnabled: true` +- Verify `mail.enabled: true` and mail configuration + +**Signing-session PDF appearing in the general file list:** +- This is expected — signing PDFs are accessible to owners and shared users +- Filter by `file_purpose` (`SIGNING_ORIGINAL`, `SIGNING_SIGNED`) in the UI to distinguish them + +**Share link returns 410:** +- Link has expired — check `expires_at` in `file_shares` table +- Owner must create a new link + +### Debug Queries + +```sql +-- List files and their share counts +SELECT sf.stored_file_id, sf.original_filename, u.username as owner, + COUNT(DISTINCT fs.file_share_id) FILTER (WHERE fs.shared_with_user_id IS NOT NULL) as user_shares, + COUNT(DISTINCT fs.file_share_id) FILTER (WHERE fs.share_token IS NOT NULL) as link_shares +FROM stored_files sf +LEFT JOIN users u ON sf.owner_id = u.user_id +LEFT JOIN file_shares fs ON fs.stored_file_id = sf.stored_file_id +GROUP BY sf.stored_file_id, u.username; + +-- Check share link expiration +SELECT share_token, access_role, created_at, expires_at, + expires_at < NOW() as is_expired +FROM file_shares +WHERE share_token IS NOT NULL; + +-- Check access history for a share link +SELECT u.username, fsa.access_type, fsa.accessed_at +FROM file_share_accesses fsa +JOIN file_shares fs ON fsa.file_share_id = fs.file_share_id +JOIN users u ON fsa.user_id = u.user_id +WHERE fs.share_token = '{token}' +ORDER BY fsa.accessed_at DESC; + +-- Pending cleanup entries +SELECT storage_key, attempt_count, updated_at +FROM storage_cleanup_entries +ORDER BY updated_at ASC; +``` + +## Summary + +The File Sharing feature provides: +- ✅ Server-side file storage with pluggable backend (local/database) +- ✅ History bundle and audit log attachments per file +- ✅ Direct user-to-user sharing with EDITOR/COMMENTER/VIEWER roles +- ✅ Token-based share links with expiration +- ✅ Optional email notifications for shares +- ✅ Per-access audit trail for share links +- ✅ Storage quotas (per-user, total, per-file) +- ✅ Automatic cleanup of expired links and orphaned storage +- ✅ Workflow integration (signing-session PDFs stored via same infrastructure; participant access via `WorkflowParticipant.shareToken`) diff --git a/README.md b/README.md new file mode 100644 index 0000000..9fd69f2 --- /dev/null +++ b/README.md @@ -0,0 +1,228 @@ +# Stirling-PDF + +## 基本信息 + +| 项目 | 内容 | +|------|------| +| **GitHub** | https://github.com/Stirling-Tools/Stirling-PDF | +| **Stars** | **79,420** ⭐ (#1 PDF Application on GitHub) | +| **描述** | 随时随地编辑PDF的开源PDF编辑平台 | +| **技术栈** | Java/Spring Boot + Python AI Engine + React | +| **许可证** | Open-core | + +## 核心功能 + +### 50+ PDF工具 +- **编辑**: 合并、分割、旋转、裁剪 +- **安全**: 签名、编辑、密码保护 +- **转换**: PDF与Word/Excel/图片互转 +- **OCR**: 文字识别 +- **压缩**: 优化PDF大小 +- **自动化**: 无代码工作流 + REST API + +### 部署方式 +- **桌面客户端**: Tauri跨平台桌面应用 +- **浏览器UI**: React SPA +- **Docker**: `docker run -p 8080:8080 docker.stirlingpdf.com/stirlingtools/stirling-pdf` +- **Kubernetes**: Helm charts支持 + +### 企业级功能 +- **SSO**: 单点登录 +- **审计**: 操作审计日志 +- **私有API**: REST API for 几乎所有工具 +- **40+语言**: 国际化支持 + +## 技术架构 + +### 三组件分离设计 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Stirling-PDF │ +├─────────────────────────────────────────────────────────────┤ +│ Frontend (React SPA) │ +│ - React + TypeScript + Vite │ +│ - Mantine UI + TailwindCSS │ +│ - PDF.js (客户端渲染) │ +│ - IndexedDB (本地存储) │ +├─────────────────────────────────────────────────────────────┤ +│ Backend (Java Spring Boot) │ +│ - JDK 25 │ +│ - PDFBox (PDF操作) │ +│ - LibreOffice (文档转换) │ +│ - qpdf (PDF优化) │ +│ - Spring Security (可选) │ +├─────────────────────────────────────────────────────────────┤ +│ Engine (Python FastAPI) │ +│ - AI推理服务 │ +│ - 任务规划和解译 │ +│ - 不持有持久状态 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 技术栈 + +| 组件 | 技术 | +|------|------| +| **Backend** | Spring Boot (JDK 25), PDFBox, LibreOffice, qpdf, Lombok | +| **Frontend** | React, TypeScript, Vite, Mantine, TailwindCSS, PDF.js, i18next | +| **Engine** | Python FastAPI | +| **Desktop** | Tauri (Rust) | +| **Infrastructure** | Docker, Gradle | + +### 目录结构 + +``` +Stirling-PDF/ +├── app/ +│ ├── common/ # 公共代码 +│ ├── core/ # 核心PDF操作 +│ ├── proprietary/ # 专有功能(企业版) +│ └── saas/ # SaaS功能 +├── frontend/ # React前端 +├── engine/ # Python AI引擎 +├── docker/ # Docker构建 +├── gradle/ # Gradle构建 +└── Taskfile.yml # 统一命令 +``` + +## Taskfile 统一命令系统 + +项目使用 [Task](https://taskfile.dev/) 作为统一命令运行器: + +### 快速参考 + +| 命令 | 功能 | +|------|------| +| `task install` | 安装所有依赖 | +| `task dev` | 启动后端+前端 | +| `task dev:all` | 启动后端+前端+引擎 | +| `task build` | 构建所有组件 | +| `task test` | 运行所有测试 | +| `task lint` | 运行所有linter | +| `task format` | 自动修复格式 | +| `task check` | 完整质量检查 | +| `task docker:build` | 构建Docker镜像 | +| `task docker:up` | 启动Docker compose | + +### Docker变体 + +- **standard**: 标准版本 +- **fat**: 包含所有依赖 +- **ultra-lite**: 精简版 + +## 项目亮点 + +### 1. Taskfile 统一命令 +所有构建/开发/测试命令统一管理,跨平台一致体验。 + +### 2. 三组件分离架构 +- Java后端: 核心业务逻辑 +- Python Engine: AI推理 +- 前端: 用户界面 + +### 3. 企业级功能设计 +- SSO单点登录 +- 操作审计 +- 完整REST API + +### 4. Open-core 商业模式 +- 核心开源 +- 企业版付费功能 + +### 5. 完善的国际化 +- 40+语言支持 +- 完善的翻译指南 + +### 6. 安全设计 +- 私有化部署 +- 不上传外部服务 +- 安全模式开发 + +## 开发环境 + +### 前置要求 +- [Task](https://taskfile.dev/installation/) +- Docker +- Git +- Java JDK 25 +- Node.js 18+ +- Gradle 7.0+ +- Python + uv (for engine) + +### 快速开始 + +```bash +# 克隆项目 +git clone https://github.com/Stirling-Tools/Stirling-PDF.git +cd Stirling-PDF + +# 安装依赖 +task install + +# 启动开发 +task dev + +# 运行测试 +task test + +# 质量检查 +task check +``` + +## 添加新工具 + +### 目录结构 +``` +frontend/editor/src/ +├── hooks/tools/[toolName]/ +│ ├── use[ToolName]Parameters.ts # 参数定义 +│ └── use[ToolName]Operation.ts # 操作逻辑 +├── components/tools/[toolName]/ +│ └── [ToolName]Settings.tsx # 设置UI +└── tools/ + └── [ToolName].tsx # 主组件 +``` + +### 核心模式 +```typescript +// 使用 useBaseTool 简化hook管理 +export const use[ToolName]Parameters = () => { + return useBaseParameters({ + defaultParameters, + endpointName: 'your-endpoint-name', + }); +}; + +// 使用 useToolOperation 处理操作 +export const use[ToolName]Operation = () => { + return useToolOperation({ + toolType: ToolType.singleFile, + buildFormData: build[ToolName]FormData, + endpoint: '/api/v1/category/endpoint-name', + }); +}; +``` + +## 相关项目 + +- [Stirling-PDF-chart](https://github.com/Stirling-Tools/Stirling-PDF-chart) - Helm charts +- [Stirling-PDF-Enterprise-and-Login](https://github.com/Stirling-Tools/Stirling-PDF-Enterprise-and-Login) - 企业版 +- [stirling_pdf_addon](https://github.com/pablodelarco/stirling_pdf_addon) - 插件 + +--- + +## 研究笔记 + +**注意**: 由于GitHub下载超时,此仓库仅包含从API获取的文档信息。 + +**研究日期**: 2026-05-25 +**Stars**: 79,420 +**建议**: 高价值项目,架构设计值得学习 + +**借鉴价值**: +- ⭐⭐⭐⭐⭐ Taskfile统一命令系统 +- ⭐⭐⭐⭐⭐ 三组件分离架构 +- ⭐⭐⭐⭐⭐ 企业功能(SSO/审计/API) +- ⭐⭐⭐⭐ Python Engine模式 +- ⭐⭐⭐⭐ 多语言国际化 \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5f532aa --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,63 @@ +# Security Policy + +## Reporting a Vulnerability + +The Stirling-PDF team takes security vulnerabilities seriously. We appreciate your efforts to responsibly disclose your findings. + +### How to Report + +You can report security vulnerabilities through two channels: + +1. **GitHub Security Advisory**: + - Navigate to the [Security tab](https://github.com/Stirling-Tools/Stirling-PDF/security) in our repository + - Click on "Report a vulnerability" + - Provide a detailed description of the vulnerability + +2. **Direct Email**: + - Send your report to security@stirlingpdf.com + - Please include as much information as possible about the vulnerability + +### What to Include + +When reporting a vulnerability, please provide: + +- A clear description of the vulnerability +- Steps to reproduce the issue +- Any potential impact +- If possible, suggestions for addressing the vulnerability +- Your contact information for follow-up questions + +### Response Time + +We aim to acknowledge receipt of your vulnerability report within 48 hours + +### Process + +1. Submit your report through one of the channels above +2. Receive an acknowledgment from our team +3. Our team will investigate and validate the issue +4. We will work on a fix and keep you updated on our progress +5. Once resolved, we will publish the fix and acknowledge your contribution (if desired) + +### Bug Bounty + +At this time, we do not offer a bug bounty program. However, we greatly appreciate your efforts in making Stirling-PDF more secure and will acknowledge your contribution in our release notes (unless you prefer to remain anonymous). + +## Supported Versions + +Only the latest version of Stirling-PDF is supported for security updates. We do not backport security fixes to older versions. + +| Version | Supported | +| ------- | ------------------ | +| Latest | :white_check_mark: | +| Older | :x: | + +**Please note:** Before reporting a security issue, ensure you are using the latest version of Stirling-PDF. Security reports for older versions will not be accepted. + +## Security Best Practices + +When deploying Stirling-PDF: + +1. Always use the latest version +2. Follow our deployment guidelines +3. Regularly check for and apply updates