Add project documentation from API (GitHub download failed)

This commit is contained in:
xiaoyi 2026-05-25 15:22:18 +08:00
commit 820efb3fe8
7 changed files with 1278 additions and 0 deletions

146
.taskfiles/backend.yml Normal file
View File

@ -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]

63
.taskfiles/docker.yml Normal file
View File

@ -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}}

300
.taskfiles/frontend.yml Normal file
View File

@ -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

34
DATABASE.md Normal file
View File

@ -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.

444
FILE_SHARING.md Normal file
View File

@ -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`)

228
README.md Normal file
View File

@ -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模式
- ⭐⭐⭐⭐ 多语言国际化

63
SECURITY.md Normal file
View File

@ -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