mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
feat(docker): update base images to Java 25, Spring 4, Jackson 3, Gradle 9 and optimize JVM options (Project Lilliput) (#5725)
Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
parent
24128dd318
commit
1f9b90ad57
+40
-63
@@ -1,75 +1,52 @@
|
|||||||
# Node modules and build artifacts
|
# Version control
|
||||||
node_modules
|
.git/
|
||||||
frontend/node_modules
|
|
||||||
frontend/dist
|
|
||||||
frontend/build
|
|
||||||
frontend/.vite
|
|
||||||
frontend/.tauri
|
|
||||||
frontend/src-tauri/target
|
|
||||||
|
|
||||||
# Gradle build artifacts
|
|
||||||
.gradle
|
|
||||||
build
|
|
||||||
bin
|
|
||||||
target
|
|
||||||
out
|
|
||||||
|
|
||||||
# Git
|
|
||||||
.git
|
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|
||||||
# IDE
|
# Build outputs
|
||||||
.vscode
|
build/
|
||||||
.idea
|
*/build/
|
||||||
|
**/build/
|
||||||
|
out/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Gradle caches (local, not what's in the container)
|
||||||
|
.gradle/
|
||||||
|
**/.gradle/
|
||||||
|
|
||||||
|
# Node / frontend
|
||||||
|
node_modules/
|
||||||
|
**/node_modules/
|
||||||
|
.npm/
|
||||||
|
.yarn/
|
||||||
|
|
||||||
|
# IDE and editor
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
*.iml
|
*.iml
|
||||||
*.iws
|
|
||||||
*.ipr
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
|
||||||
# Logs
|
# Logs and temp files
|
||||||
*.log
|
*.log
|
||||||
logs
|
*.tmp
|
||||||
|
*.pid
|
||||||
# Environment files
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# OS files
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Java compiled files
|
# Docker itself
|
||||||
*.class
|
Dockerfile*
|
||||||
*.jar
|
|
||||||
*.war
|
|
||||||
*.ear
|
|
||||||
|
|
||||||
# Test reports
|
|
||||||
test-results
|
|
||||||
coverage
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
docker-compose.override.yml
|
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
|
||||||
# Temporary files
|
# CI / CD configs (not needed in build context)
|
||||||
tmp
|
.github/
|
||||||
temp
|
.circleci/
|
||||||
*.tmp
|
|
||||||
*.swp
|
|
||||||
*~
|
|
||||||
|
|
||||||
# Runtime database and config files (locked by running app)
|
|
||||||
app/core/configs/**
|
|
||||||
stirling/**
|
|
||||||
stirling-pdf-DB*.mv.db
|
|
||||||
stirling-pdf-DB*.trace.db
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
*.md
|
|
||||||
!README.md
|
|
||||||
docs
|
|
||||||
|
|
||||||
# CI/CD
|
|
||||||
.github
|
|
||||||
.gitlab-ci.yml
|
.gitlab-ci.yml
|
||||||
|
|
||||||
|
# Test reports
|
||||||
|
**/test-results/
|
||||||
|
**/jacoco/
|
||||||
|
|
||||||
|
# Local env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|||||||
@@ -150,10 +150,10 @@ jobs:
|
|||||||
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
||||||
token: ${{ steps.setup-bot.outputs.token }}
|
token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "25"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
jdk-version: [17, 21]
|
jdk-version: [21, 25]
|
||||||
spring-security: [true, false]
|
spring-security: [true, false]
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
@@ -140,7 +140,7 @@ jobs:
|
|||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "25"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
@@ -209,10 +209,10 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "25"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
@@ -221,7 +221,12 @@ jobs:
|
|||||||
gradle-version: 8.14
|
gradle-version: 8.14
|
||||||
|
|
||||||
- name: check the licenses for compatibility
|
- name: check the licenses for compatibility
|
||||||
run: ./gradlew checkLicense
|
# NOTE: --no-parallel is intentional here. Running the checkLicense task in parallel with other
|
||||||
|
# Gradle tasks has been observed to cause intermittent failures with the dependency license
|
||||||
|
# checking plugin on this Gradle version. Disabling parallel execution trades some build speed
|
||||||
|
# for more reliable, deterministic license checks. If upgrading Gradle or the plugin, consider
|
||||||
|
# re-evaluating whether this flag is still required before removing it.
|
||||||
|
run: ./gradlew checkLicense --no-parallel
|
||||||
env:
|
env:
|
||||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||||
@@ -265,10 +270,10 @@ jobs:
|
|||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "25"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
@@ -355,10 +360,10 @@ jobs:
|
|||||||
docker system prune -af || true
|
docker system prune -af || true
|
||||||
echo "Disk space after cleanup:" && df -h
|
echo "Disk space after cleanup:" && df -h
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "25"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
|
|||||||
@@ -330,10 +330,10 @@ jobs:
|
|||||||
app-id: ${{ secrets.GH_APP_ID }}
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "25"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
@@ -344,7 +344,12 @@ jobs:
|
|||||||
- name: Check licenses and generate report
|
- name: Check licenses and generate report
|
||||||
id: license-check
|
id: license-check
|
||||||
run: |
|
run: |
|
||||||
./gradlew checkLicense generateLicenseReport || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
|
# NOTE: --no-parallel is intentional here. Running the license-checking tasks in parallel has
|
||||||
|
# previously caused intermittent concurrency issues in CI (e.g. flaky failures in the license
|
||||||
|
# plugin/Gradle when multiple projects are evaluated concurrently). Disabling parallelism trades
|
||||||
|
# some build speed for more reliable license reports. If the underlying issues are resolved in
|
||||||
|
# future Gradle or plugin versions, this flag can be reconsidered.
|
||||||
|
./gradlew checkLicense generateLicenseReport --no-parallel || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV
|
||||||
env:
|
env:
|
||||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "25"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
@@ -117,10 +117,10 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "25"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
@@ -197,10 +197,10 @@ jobs:
|
|||||||
toolchain: stable
|
toolchain: stable
|
||||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "25"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "25"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 25
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "25"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ spotless {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'org.springframework.boot:spring-boot-starter-web'
|
api 'org.springframework.boot:spring-boot-starter-webmvc'
|
||||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
api 'org.springframework.boot:spring-boot-starter-aspectj'
|
||||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||||
api 'com.fathzer:javaluator:3.0.6'
|
api 'com.fathzer:javaluator:3.0.6'
|
||||||
api 'com.posthog.java:posthog:1.2.0'
|
api 'com.posthog.java:posthog:1.2.0'
|
||||||
@@ -42,7 +42,7 @@ dependencies {
|
|||||||
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
|
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
|
||||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||||
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
|
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
|
||||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.15"
|
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1"
|
||||||
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
|
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
|
||||||
api 'org.simplejavamail:simple-java-mail:8.12.6'
|
api 'org.simplejavamail:simple-java-mail:8.12.6'
|
||||||
api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support
|
api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support
|
||||||
|
|||||||
+2
-2
@@ -91,9 +91,9 @@ public class RuntimePathConfig {
|
|||||||
|
|
||||||
// Initialize Operation paths
|
// Initialize Operation paths
|
||||||
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
||||||
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
String defaultUnoConvertPath = isDocker ? "/usr/local/bin/unoconvert" : "unoconvert";
|
||||||
String defaultCalibrePath = isDocker ? "/opt/calibre/ebook-convert" : "ebook-convert";
|
String defaultCalibrePath = isDocker ? "/opt/calibre/ebook-convert" : "ebook-convert";
|
||||||
String defaultOcrMyPdfPath = isDocker ? "/usr/bin/ocrmypdf" : "ocrmypdf";
|
String defaultOcrMyPdfPath = isDocker ? "/opt/venv/bin/ocrmypdf" : "ocrmypdf";
|
||||||
String defaultSOfficePath = isDocker ? "/usr/bin/soffice" : "soffice";
|
String defaultSOfficePath = isDocker ? "/usr/bin/soffice" : "soffice";
|
||||||
|
|
||||||
Operations operations = customPaths.getOperations();
|
Operations operations = customPaths.getOperations();
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package stirling.software.common.configuration;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.TaskScheduler;
|
||||||
|
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the scheduler used by all {@code @Scheduled} methods. Uses virtual threads so that
|
||||||
|
* long-running scheduled tasks (e.g. cleanup, license checks, file monitoring) never block each
|
||||||
|
* other — each runs on its own lightweight virtual thread.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class SchedulingConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TaskScheduler taskScheduler() {
|
||||||
|
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
|
||||||
|
scheduler.setVirtualThreads(true);
|
||||||
|
scheduler.setThreadNamePrefix("scheduled-vt-");
|
||||||
|
return scheduler;
|
||||||
|
}
|
||||||
|
}
|
||||||
+609
-450
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@ public class JobExecutorService {
|
|||||||
private final HttpServletRequest request;
|
private final HttpServletRequest request;
|
||||||
private final ResourceMonitor resourceMonitor;
|
private final ResourceMonitor resourceMonitor;
|
||||||
private final JobQueue jobQueue;
|
private final JobQueue jobQueue;
|
||||||
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
private final ExecutorService executor = ExecutorFactory.newVirtualThreadExecutor();
|
||||||
private final long effectiveTimeoutMs;
|
private final long effectiveTimeoutMs;
|
||||||
|
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
|
|||||||
@@ -44,8 +44,10 @@ public class JobQueue implements SmartLifecycle {
|
|||||||
|
|
||||||
private volatile BlockingQueue<QueuedJob> jobQueue;
|
private volatile BlockingQueue<QueuedJob> jobQueue;
|
||||||
private final Map<String, QueuedJob> jobMap = new ConcurrentHashMap<>();
|
private final Map<String, QueuedJob> jobMap = new ConcurrentHashMap<>();
|
||||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService scheduler =
|
||||||
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
Executors.newSingleThreadScheduledExecutor(
|
||||||
|
Thread.ofVirtual().name("job-queue-scheduler-", 0).factory());
|
||||||
|
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualThreadExecutor();
|
||||||
private final Object queueLock = new Object(); // Lock for synchronizing queue operations
|
private final Object queueLock = new Object(); // Lock for synchronizing queue operations
|
||||||
|
|
||||||
private boolean shuttingDown = false;
|
private boolean shuttingDown = false;
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ public class ResourceMonitor {
|
|||||||
@Value("${stirling.resource.monitor.interval-ms:60000}")
|
@Value("${stirling.resource.monitor.interval-ms:60000}")
|
||||||
private long monitorIntervalMs = 60000; // 60 seconds
|
private long monitorIntervalMs = 60000; // 60 seconds
|
||||||
|
|
||||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService scheduler =
|
||||||
|
Executors.newSingleThreadScheduledExecutor(
|
||||||
|
Thread.ofVirtual().name("resource-monitor-", 0).factory());
|
||||||
private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
|
private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
|
||||||
private final OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
|
private final OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ public class TaskManager {
|
|||||||
|
|
||||||
private final FileStorage fileStorage;
|
private final FileStorage fileStorage;
|
||||||
private final ScheduledExecutorService cleanupExecutor =
|
private final ScheduledExecutorService cleanupExecutor =
|
||||||
Executors.newSingleThreadScheduledExecutor();
|
Executors.newSingleThreadScheduledExecutor(
|
||||||
|
Thread.ofVirtual().name("task-cleanup-", 0).factory());
|
||||||
|
|
||||||
/** Initialize the task manager and start the cleanup scheduler */
|
/** Initialize the task manager and start the cleanup scheduler */
|
||||||
public TaskManager(FileStorage fileStorage) {
|
public TaskManager(FileStorage fileStorage) {
|
||||||
|
|||||||
@@ -2,30 +2,28 @@ package stirling.software.common.util;
|
|||||||
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class ExecutorFactory {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an ExecutorService using virtual threads if available (Java 21+), or falls back to a
|
* Factory for creating executors backed by virtual threads (Java 21+). Virtual threads are
|
||||||
* cached thread pool on older Java versions.
|
* lightweight, managed by the JVM, and ideal for I/O-bound tasks. They eliminate the need for
|
||||||
|
* thread pool sizing since thousands can run concurrently with minimal overhead.
|
||||||
*/
|
*/
|
||||||
public static ExecutorService newVirtualOrCachedThreadExecutor() {
|
public final class ExecutorFactory {
|
||||||
try {
|
|
||||||
ExecutorService executor =
|
private ExecutorFactory() {}
|
||||||
(ExecutorService)
|
|
||||||
Executors.class
|
/** Creates an {@link ExecutorService} that starts a new virtual thread for each task. */
|
||||||
.getMethod("newVirtualThreadPerTaskExecutor")
|
public static ExecutorService newVirtualThreadExecutor() {
|
||||||
.invoke(null);
|
return Executors.newVirtualThreadPerTaskExecutor();
|
||||||
return executor;
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
log.debug("Virtual threads not available; falling back to cached thread pool.");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.debug("Error initializing virtual thread executor: {}", e.getMessage(), e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Executors.newCachedThreadPool();
|
/**
|
||||||
|
* Creates a {@link ScheduledExecutorService} backed by a single virtual thread. Useful for
|
||||||
|
* periodic/delayed tasks that should not pin a platform thread.
|
||||||
|
*/
|
||||||
|
public static ScheduledExecutorService newSingleVirtualThreadScheduledExecutor() {
|
||||||
|
return Executors.newSingleThreadScheduledExecutor(
|
||||||
|
Thread.ofVirtual().name("scheduled-vt-", 0).factory());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,17 @@ public class FileMonitor {
|
|||||||
|
|
||||||
if (this.rootDirs.isEmpty()) {
|
if (this.rootDirs.isEmpty()) {
|
||||||
log.error("No valid directories to monitor - FileMonitor will not function");
|
log.error("No valid directories to monitor - FileMonitor will not function");
|
||||||
|
} else {
|
||||||
|
// Register directories eagerly so the first @Scheduled tick does not warn.
|
||||||
|
for (Path rootDir : this.rootDirs) {
|
||||||
|
if (Files.exists(rootDir)) {
|
||||||
|
try {
|
||||||
|
recursivelyRegisterEntry(rootDir);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to register monitoring for {}", rootDir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -649,7 +649,8 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) {
|
public List<Integer> parsePageList(String[] pages, int totalPages, boolean oneBased) {
|
||||||
List<Integer> result = new ArrayList<>();
|
// Use LinkedHashSet to prevent duplicates from inflating size and triggering maxSize guard
|
||||||
|
Set<Integer> result = new LinkedHashSet<>();
|
||||||
int offset = oneBased ? 1 : 0;
|
int offset = oneBased ? 1 : 0;
|
||||||
int maxSize = Math.max(1000, totalPages * 3);
|
int maxSize = Math.max(1000, totalPages * 3);
|
||||||
for (String page : pages) {
|
for (String page : pages) {
|
||||||
@@ -672,7 +673,7 @@ public class GeneralUtils {
|
|||||||
"Page list exceeds maximum allowed size of " + maxSize);
|
"Page list exceeds maximum allowed size of " + maxSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return new ArrayList<>(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -226,7 +226,8 @@ public class ProcessExecutor {
|
|||||||
List<String> outputLines = new ArrayList<>();
|
List<String> outputLines = new ArrayList<>();
|
||||||
|
|
||||||
Thread errorReaderThread =
|
Thread errorReaderThread =
|
||||||
new Thread(
|
Thread.ofVirtual()
|
||||||
|
.unstarted(
|
||||||
() -> {
|
() -> {
|
||||||
try (BufferedReader errorReader =
|
try (BufferedReader errorReader =
|
||||||
new BufferedReader(
|
new BufferedReader(
|
||||||
@@ -242,14 +243,16 @@ public class ProcessExecutor {
|
|||||||
if (liveUpdates) log.info(line);
|
if (liveUpdates) log.info(line);
|
||||||
}
|
}
|
||||||
} catch (InterruptedIOException e) {
|
} catch (InterruptedIOException e) {
|
||||||
log.warn("Error reader thread was interrupted due to timeout.");
|
log.warn(
|
||||||
|
"Error reader thread was interrupted due to timeout.");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("exception", e);
|
log.error("exception", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Thread outputReaderThread =
|
Thread outputReaderThread =
|
||||||
new Thread(
|
Thread.ofVirtual()
|
||||||
|
.unstarted(
|
||||||
() -> {
|
() -> {
|
||||||
try (BufferedReader outputReader =
|
try (BufferedReader outputReader =
|
||||||
new BufferedReader(
|
new BufferedReader(
|
||||||
@@ -265,7 +268,8 @@ public class ProcessExecutor {
|
|||||||
if (liveUpdates) log.info(line);
|
if (liveUpdates) log.info(line);
|
||||||
}
|
}
|
||||||
} catch (InterruptedIOException e) {
|
} catch (InterruptedIOException e) {
|
||||||
log.warn("Error reader thread was interrupted due to timeout.");
|
log.warn(
|
||||||
|
"Error reader thread was interrupted due to timeout.");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("exception", e);
|
log.error("exception", e);
|
||||||
}
|
}
|
||||||
@@ -370,7 +374,7 @@ public class ProcessExecutor {
|
|||||||
}
|
}
|
||||||
// Match common unoconvert variants (but NOT soffice)
|
// Match common unoconvert variants (but NOT soffice)
|
||||||
String lowerBasename = basename.toLowerCase(java.util.Locale.ROOT);
|
String lowerBasename = basename.toLowerCase(java.util.Locale.ROOT);
|
||||||
if (lowerBasename.contains("unoconvert") || lowerBasename.equals("unoconv")) {
|
if (lowerBasename.contains("unoconvert") || "unoconv".equals(lowerBasename)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-6
@@ -4,18 +4,23 @@ import java.beans.PropertyEditorSupport;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.api.security.RedactionArea;
|
import stirling.software.common.model.api.security.RedactionArea;
|
||||||
|
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.DeserializationFeature;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
|
public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper =
|
||||||
|
JsonMapper.builder()
|
||||||
|
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||||
|
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
|
||||||
|
.build();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAsText(String text) throws IllegalArgumentException {
|
public void setAsText(String text) throws IllegalArgumentException {
|
||||||
@@ -24,7 +29,6 @@ public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
|
|
||||||
TypeReference<ArrayList<RedactionArea>> typeRef = new TypeReference<>() {};
|
TypeReference<ArrayList<RedactionArea>> typeRef = new TypeReference<>() {};
|
||||||
List<RedactionArea> list = objectMapper.readValue(text, typeRef);
|
List<RedactionArea> list = objectMapper.readValue(text, typeRef);
|
||||||
setValue(list);
|
setValue(list);
|
||||||
|
|||||||
+4
-3
@@ -4,12 +4,13 @@ import java.beans.PropertyEditorSupport;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import tools.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
public class StringToMapPropertyEditor extends PropertyEditorSupport {
|
public class StringToMapPropertyEditor extends PropertyEditorSupport {
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAsText(String text) throws IllegalArgumentException {
|
public void setAsText(String text) throws IllegalArgumentException {
|
||||||
|
|||||||
+4
-3
@@ -12,11 +12,12 @@ class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
|||||||
public StrategyType lastStrategyUsed;
|
public StrategyType lastStrategyUsed;
|
||||||
|
|
||||||
public SpyPDFDocumentFactory(PdfMetadataService service) {
|
public SpyPDFDocumentFactory(PdfMetadataService service) {
|
||||||
super(service);
|
super(service); // TempFileManager falls back to Files.createTempFile in tests
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
|
protected StreamCacheCreateFunction getStreamCacheFunction(
|
||||||
|
long contentSize, MemorySnapshot mem) {
|
||||||
StrategyType type;
|
StrategyType type;
|
||||||
if (contentSize < 10 * 1024 * 1024) {
|
if (contentSize < 10 * 1024 * 1024) {
|
||||||
type = StrategyType.MEMORY_ONLY;
|
type = StrategyType.MEMORY_ONLY;
|
||||||
@@ -26,6 +27,6 @@ class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
|||||||
type = StrategyType.TEMP_FILE;
|
type = StrategyType.TEMP_FILE;
|
||||||
}
|
}
|
||||||
this.lastStrategyUsed = type;
|
this.lastStrategyUsed = type;
|
||||||
return super.getStreamCacheFunction(contentSize); // delegate to real behavior
|
return super.getStreamCacheFunction(contentSize, mem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,11 +121,13 @@ public class UnoServerPoolTest {
|
|||||||
|
|
||||||
// Try to acquire a third endpoint in separate thread (should block)
|
// Try to acquire a third endpoint in separate thread (should block)
|
||||||
Thread blockingThread =
|
Thread blockingThread =
|
||||||
new Thread(
|
Thread.ofVirtual()
|
||||||
|
.unstarted(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
acquireLatch.countDown(); // Signal we're about to block
|
acquireLatch.countDown(); // Signal we're about to block
|
||||||
UnoServerPool.UnoServerLease lease3 = pool.acquireEndpoint();
|
UnoServerPool.UnoServerLease lease3 =
|
||||||
|
pool.acquireEndpoint();
|
||||||
acquired.incrementAndGet();
|
acquired.incrementAndGet();
|
||||||
lease3.close();
|
lease3.close();
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
|||||||
+14
-4
@@ -143,11 +143,21 @@ class StringToArrayListPropertyEditorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSetAsText_InvalidStructure() {
|
void testSetAsText_UnknownProperties() {
|
||||||
// Arrange - this JSON doesn't match RedactionArea structure
|
// Arrange - this JSON contains properties not in RedactionArea
|
||||||
|
// With FAIL_ON_UNKNOWN_PROPERTIES disabled, this should ignore the unknown properties
|
||||||
String json = "[{\"invalid\":\"structure\"}]";
|
String json = "[{\"invalid\":\"structure\"}]";
|
||||||
|
|
||||||
// Act & Assert
|
// Act
|
||||||
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(json));
|
editor.setAsText(json);
|
||||||
|
Object value = editor.getValue();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(value, "Value should not be null");
|
||||||
|
assertInstanceOf(List.class, value, "Value should be a List");
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<RedactionArea> list = (List<RedactionArea>) value;
|
||||||
|
assertEquals(1, list.size(), "List should have 1 entry (empty object)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-4
@@ -46,8 +46,23 @@ dependencies {
|
|||||||
|
|
||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-jetty'
|
implementation 'org.springframework.boot:spring-boot-starter-jetty'
|
||||||
implementation 'com.posthog.java:posthog:1.2.0'
|
implementation 'org.eclipse.jetty.http2:jetty-http2-server'
|
||||||
implementation 'org.telegram:telegrambots:6.9.7.1'
|
implementation ('org.telegram:telegrambots:6.9.7.1') {
|
||||||
|
// Grizzly server + Jersey JAX-RS stack: only used for webhook mode;
|
||||||
|
// Stirling-PDF uses long-polling mode so these are dead weight (~3 MB)
|
||||||
|
exclude group: 'org.glassfish.jersey.inject'
|
||||||
|
exclude group: 'org.glassfish.jersey.media'
|
||||||
|
exclude group: 'org.glassfish.jersey.containers'
|
||||||
|
exclude group: 'org.glassfish.jersey.core'
|
||||||
|
exclude group: 'org.glassfish.jersey.ext'
|
||||||
|
exclude group: 'org.glassfish.grizzly'
|
||||||
|
exclude group: 'org.glassfish.hk2'
|
||||||
|
exclude group: 'org.glassfish.hk2.external'
|
||||||
|
exclude group: 'org.javassist', module: 'javassist'
|
||||||
|
// Old javax JAX-RS Jackson bindings (not needed, project uses jakarta)
|
||||||
|
exclude group: 'com.fasterxml.jackson.jaxrs'
|
||||||
|
exclude group: 'com.fasterxml.jackson.module', module: 'jackson-module-jaxb-annotations'
|
||||||
|
}
|
||||||
implementation 'commons-io:commons-io:2.21.0'
|
implementation 'commons-io:commons-io:2.21.0'
|
||||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||||
@@ -78,8 +93,9 @@ dependencies {
|
|||||||
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
|
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||||
implementation 'org.apache.poi:poi-ooxml:5.5.1'
|
implementation 'org.apache.poi:poi-ooxml:5.5.1'
|
||||||
|
|
||||||
// Batik
|
// Batik only bridge module needed (transitively pulls anim, gvt, util, css, dom, svg-dom)
|
||||||
implementation 'org.apache.xmlgraphics:batik-all:1.19'
|
// Replaces batik-all which included unused codec, svggen, transcoder, script modules
|
||||||
|
implementation 'org.apache.xmlgraphics:batik-bridge:1.19'
|
||||||
|
|
||||||
// PDFBox Graphics2D bridge for Batik SVG to PDF conversion
|
// PDFBox Graphics2D bridge for Batik SVG to PDF conversion
|
||||||
implementation 'de.rototor.pdfbox:graphics2d:3.0.5'
|
implementation 'de.rototor.pdfbox:graphics2d:3.0.5'
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ public class LibreOfficeListener {
|
|||||||
process = SystemCommand.runCommand(Runtime.getRuntime(), "unoconv --listener");
|
process = SystemCommand.runCommand(Runtime.getRuntime(), "unoconv --listener");
|
||||||
lastActivityTime = System.currentTimeMillis();
|
lastActivityTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Start a background thread to monitor the activity timeout
|
// Start a virtual thread to monitor the activity timeout
|
||||||
executorService = Executors.newSingleThreadExecutor();
|
executorService = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
executorService.submit(
|
executorService.submit(
|
||||||
() -> {
|
() -> {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.web.context.WebServerInitializedEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
@@ -176,11 +176,14 @@ public class SPDFApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@EventListener
|
@EventListener
|
||||||
public void onWebServerInitialized(WebServerInitializedEvent event) {
|
public void onApplicationReady(ApplicationReadyEvent event) {
|
||||||
int actualPort = event.getWebServer().getPort();
|
String port =
|
||||||
serverPortStatic = String.valueOf(actualPort);
|
event.getApplicationContext().getEnvironment().getProperty("local.server.port");
|
||||||
|
if (port != null) {
|
||||||
|
serverPortStatic = port;
|
||||||
|
}
|
||||||
// Log the actual runtime port for Tauri to parse
|
// Log the actual runtime port for Tauri to parse
|
||||||
log.info("Stirling-PDF running on port: {}", actualPort);
|
log.info("Stirling-PDF running on port: {}", serverPortStatic);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void printStartupLogs() {
|
private static void printStartupLogs() {
|
||||||
|
|||||||
@@ -51,9 +51,7 @@ public class ExternalAppDepConfig {
|
|||||||
*/
|
*/
|
||||||
private final Map<String, List<String>> commandToGroupMapping;
|
private final Map<String, List<String>> commandToGroupMapping;
|
||||||
|
|
||||||
private final ExecutorService pool =
|
private final ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
Executors.newFixedThreadPool(
|
|
||||||
Math.max(2, Runtime.getRuntime().availableProcessors() / 2));
|
|
||||||
|
|
||||||
public ExternalAppDepConfig(
|
public ExternalAppDepConfig(
|
||||||
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
|
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.web.servlet.MultipartConfigFactory;
|
import org.springframework.boot.servlet.MultipartConfigFactory;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.DependsOn;
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ public class TauriProcessMonitor {
|
|||||||
scheduler =
|
scheduler =
|
||||||
Executors.newSingleThreadScheduledExecutor(
|
Executors.newSingleThreadScheduledExecutor(
|
||||||
r -> {
|
r -> {
|
||||||
Thread t = new Thread(r, "tauri-process-monitor");
|
Thread t =
|
||||||
t.setDaemon(true);
|
Thread.ofVirtual().name("tauri-process-monitor").unstarted(r);
|
||||||
return t;
|
return t;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -154,8 +154,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
.maxAge(3600);
|
.maxAge(3600);
|
||||||
} else {
|
} else {
|
||||||
// Default to allowing all origins when nothing is configured
|
// Default to allowing all origins when nothing is configured
|
||||||
logger.info(
|
logger.debug(
|
||||||
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); allowing all origins.");
|
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); WebMvcConfig allowing all origins.");
|
||||||
registry.addMapping("/**")
|
registry.addMapping("/**")
|
||||||
.allowedOriginPatterns("*")
|
.allowedOriginPatterns("*")
|
||||||
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
||||||
|
|||||||
+3
-3
@@ -15,9 +15,6 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -32,6 +29,9 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
|||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@GeneralApi
|
@GeneralApi
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ import org.springframework.core.io.ResourceLoader;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -35,6 +32,9 @@ import stirling.software.common.service.UserServiceInterface;
|
|||||||
import stirling.software.common.util.ExceptionUtils;
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@UiDataApi
|
@UiDataApi
|
||||||
public class UIDataController {
|
public class UIDataController {
|
||||||
@@ -44,18 +44,21 @@ public class UIDataController {
|
|||||||
private final UserServiceInterface userService;
|
private final UserServiceInterface userService;
|
||||||
private final ResourceLoader resourceLoader;
|
private final ResourceLoader resourceLoader;
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public UIDataController(
|
public UIDataController(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
SharedSignatureService signatureService,
|
SharedSignatureService signatureService,
|
||||||
@Autowired(required = false) UserServiceInterface userService,
|
@Autowired(required = false) UserServiceInterface userService,
|
||||||
ResourceLoader resourceLoader,
|
ResourceLoader resourceLoader,
|
||||||
RuntimePathConfig runtimePathConfig) {
|
RuntimePathConfig runtimePathConfig,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.signatureService = signatureService;
|
this.signatureService = signatureService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.resourceLoader = resourceLoader;
|
this.resourceLoader = resourceLoader;
|
||||||
this.runtimePathConfig = runtimePathConfig;
|
this.runtimePathConfig = runtimePathConfig;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/footer-info")
|
@GetMapping("/footer-info")
|
||||||
@@ -93,9 +96,8 @@ public class UIDataController {
|
|||||||
|
|
||||||
try (InputStream is = resource.getInputStream()) {
|
try (InputStream is = resource.getInputStream()) {
|
||||||
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
|
||||||
Map<String, List<Dependency>> licenseData =
|
Map<String, List<Dependency>> licenseData =
|
||||||
mapper.readValue(json, new TypeReference<>() {});
|
objectMapper.readValue(json, new TypeReference<>() {});
|
||||||
data.setDependencies(licenseData.get("dependencies"));
|
data.setDependencies(licenseData.get("dependencies"));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Failed to load licenses data", e);
|
log.error("Failed to load licenses data", e);
|
||||||
@@ -127,8 +129,8 @@ public class UIDataController {
|
|||||||
|
|
||||||
for (String config : pipelineConfigs) {
|
for (String config : pipelineConfigs) {
|
||||||
Map<String, Object> jsonContent =
|
Map<String, Object> jsonContent =
|
||||||
new ObjectMapper()
|
objectMapper.readValue(
|
||||||
.readValue(config, new TypeReference<Map<String, Object>>() {});
|
config, new TypeReference<Map<String, Object>>() {});
|
||||||
String name = (String) jsonContent.get("name");
|
String name = (String) jsonContent.get("name");
|
||||||
if (name == null || name.length() < 1) {
|
if (name == null || name.length() < 1) {
|
||||||
String filename =
|
String filename =
|
||||||
|
|||||||
+23
-6
@@ -91,10 +91,12 @@ public class ConvertOfficeController {
|
|||||||
|
|
||||||
Path libreOfficeProfile = null;
|
Path libreOfficeProfile = null;
|
||||||
try {
|
try {
|
||||||
ProcessExecutorResult result;
|
ProcessExecutorResult result = null;
|
||||||
// Run Unoconvert command
|
IOException unoconvertException = null;
|
||||||
|
|
||||||
|
// Try unoconvert first if available
|
||||||
if (isUnoconvertAvailable()) {
|
if (isUnoconvertAvailable()) {
|
||||||
// Unoconvert: schreibe direkt in outputPath innerhalb des workDir
|
try {
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add(runtimePathConfig.getUnoConvertPath());
|
command.add(runtimePathConfig.getUnoConvertPath());
|
||||||
command.add("--convert-to");
|
command.add("--convert-to");
|
||||||
@@ -105,8 +107,16 @@ public class ConvertOfficeController {
|
|||||||
result =
|
result =
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
} // Run soffice command
|
} catch (IOException e) {
|
||||||
else {
|
unoconvertException = e;
|
||||||
|
log.warn(
|
||||||
|
"Unoconvert command failed ({}). Falling back to soffice command.",
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to soffice if unoconvert was unavailable or failed
|
||||||
|
if (result == null) {
|
||||||
libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_");
|
libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_");
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add(runtimePathConfig.getSOfficePath());
|
command.add(runtimePathConfig.getSOfficePath());
|
||||||
@@ -114,14 +124,21 @@ public class ConvertOfficeController {
|
|||||||
command.add("--headless");
|
command.add("--headless");
|
||||||
command.add("--nologo");
|
command.add("--nologo");
|
||||||
command.add("--convert-to");
|
command.add("--convert-to");
|
||||||
command.add("pdf:writer_pdf_Export");
|
command.add("pdf");
|
||||||
command.add("--outdir");
|
command.add("--outdir");
|
||||||
command.add(workDir.toString());
|
command.add(workDir.toString());
|
||||||
command.add(inputPath.toString());
|
command.add(inputPath.toString());
|
||||||
|
|
||||||
|
try {
|
||||||
result =
|
result =
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (unoconvertException != null) {
|
||||||
|
e.addSuppressed(unoconvertException);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the result
|
// Check the result
|
||||||
|
|||||||
+1
-4
@@ -70,10 +70,7 @@ public class ConvertPDFToExcelController {
|
|||||||
tables.size() == 1
|
tables.size() == 1
|
||||||
? String.format(Locale.ROOT, "Page %d", pageNum)
|
? String.format(Locale.ROOT, "Page %d", pageNum)
|
||||||
: String.format(
|
: String.format(
|
||||||
Locale.ROOT,
|
Locale.ROOT, "Page %d Table %d", pageNum, tableIdx + 1);
|
||||||
"Page %d Table %d",
|
|
||||||
pageNum,
|
|
||||||
tableIdx + 1);
|
|
||||||
|
|
||||||
sheetName = getUniqueSheetName(workbook, sheetName);
|
sheetName = getUniqueSheetName(workbook, sheetName);
|
||||||
Sheet sheet = workbook.createSheet(sheetName);
|
Sheet sheet = workbook.createSheet(sheetName);
|
||||||
|
|||||||
+2
-2
@@ -9,13 +9,13 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
|
|||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.exception.CacheUnavailableException;
|
import stirling.software.SPDF.exception.CacheUnavailableException;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@ControllerAdvice(assignableTypes = ConvertPdfJsonController.class)
|
@ControllerAdvice(assignableTypes = ConvertPdfJsonController.class)
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
|||||||
+3
-2
@@ -19,8 +19,6 @@ import org.springframework.web.bind.annotation.RequestPart;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.opencsv.CSVWriter;
|
import com.opencsv.CSVWriter;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
import io.github.pixee.security.Filenames;
|
||||||
@@ -38,6 +36,9 @@ import stirling.software.common.util.ExceptionUtils;
|
|||||||
import stirling.software.common.util.FormUtils;
|
import stirling.software.common.util.FormUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/form")
|
@RequestMapping("/api/v1/form")
|
||||||
@Tag(
|
@Tag(
|
||||||
|
|||||||
+16
-17
@@ -1,6 +1,5 @@
|
|||||||
package stirling.software.SPDF.controller.api.form;
|
package stirling.software.SPDF.controller.api.form;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -8,13 +7,14 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import stirling.software.common.util.ExceptionUtils;
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.FormUtils;
|
import stirling.software.common.util.FormUtils;
|
||||||
|
|
||||||
|
import tools.jackson.core.JacksonException;
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.JsonNode;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
final class FormPayloadParser {
|
final class FormPayloadParser {
|
||||||
|
|
||||||
private static final String KEY_FIELDS = "fields";
|
private static final String KEY_FIELDS = "fields";
|
||||||
@@ -32,8 +32,7 @@ final class FormPayloadParser {
|
|||||||
|
|
||||||
private FormPayloadParser() {}
|
private FormPayloadParser() {}
|
||||||
|
|
||||||
static Map<String, Object> parseValueMap(ObjectMapper objectMapper, String json)
|
static Map<String, Object> parseValueMap(ObjectMapper objectMapper, String json) {
|
||||||
throws IOException {
|
|
||||||
if (json == null || json.isBlank()) {
|
if (json == null || json.isBlank()) {
|
||||||
return Map.of();
|
return Map.of();
|
||||||
}
|
}
|
||||||
@@ -41,7 +40,7 @@ final class FormPayloadParser {
|
|||||||
JsonNode root;
|
JsonNode root;
|
||||||
try {
|
try {
|
||||||
root = objectMapper.readTree(json);
|
root = objectMapper.readTree(json);
|
||||||
} catch (IOException e) {
|
} catch (JacksonException e) {
|
||||||
// Fallback to legacy direct map parse (will throw again if invalid)
|
// Fallback to legacy direct map parse (will throw again if invalid)
|
||||||
return objectMapper.readValue(json, MAP_TYPE);
|
return objectMapper.readValue(json, MAP_TYPE);
|
||||||
}
|
}
|
||||||
@@ -88,14 +87,14 @@ final class FormPayloadParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static List<FormUtils.ModifyFormFieldDefinition> parseModificationDefinitions(
|
static List<FormUtils.ModifyFormFieldDefinition> parseModificationDefinitions(
|
||||||
ObjectMapper objectMapper, String json) throws IOException {
|
ObjectMapper objectMapper, String json) {
|
||||||
if (json == null || json.isBlank()) {
|
if (json == null || json.isBlank()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
return objectMapper.readValue(json, MODIFY_FIELD_LIST_TYPE);
|
return objectMapper.readValue(json, MODIFY_FIELD_LIST_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<String> parseNameList(ObjectMapper objectMapper, String json) throws IOException {
|
static List<String> parseNameList(ObjectMapper objectMapper, String json) {
|
||||||
if (json == null || json.isBlank()) {
|
if (json == null || json.isBlank()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
@@ -119,7 +118,7 @@ final class FormPayloadParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (root.isTextual()) {
|
} else if (root.isTextual()) {
|
||||||
final String single = trimToNull(root.asText());
|
final String single = trimToNull(root.asText(""));
|
||||||
if (single != null) {
|
if (single != null) {
|
||||||
names.add(single);
|
names.add(single);
|
||||||
}
|
}
|
||||||
@@ -131,7 +130,7 @@ final class FormPayloadParser {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return objectMapper.readValue(json, STRING_LIST_TYPE);
|
return objectMapper.readValue(json, STRING_LIST_TYPE);
|
||||||
} catch (IOException e) {
|
} catch (JacksonException e) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.invalidFormat",
|
"error.invalidFormat",
|
||||||
"Invalid {0} format: {1}",
|
"Invalid {0} format: {1}",
|
||||||
@@ -199,7 +198,7 @@ final class FormPayloadParser {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (node.isTextual()) {
|
if (node.isTextual()) {
|
||||||
return trimToEmpty(node.asText());
|
return trimToEmpty(node.asText(""));
|
||||||
}
|
}
|
||||||
if (node.isNumber()) {
|
if (node.isNumber()) {
|
||||||
return node.numberValue().toString();
|
return node.numberValue().toString();
|
||||||
@@ -208,7 +207,7 @@ final class FormPayloadParser {
|
|||||||
return Boolean.toString(node.booleanValue());
|
return Boolean.toString(node.booleanValue());
|
||||||
}
|
}
|
||||||
// Fallback for other scalar-like nodes
|
// Fallback for other scalar-like nodes
|
||||||
return trimToEmpty(node.asText());
|
return trimToEmpty(node.asText(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void collectNames(JsonNode arrayNode, Set<String> sink) {
|
private static void collectNames(JsonNode arrayNode, Set<String> sink) {
|
||||||
@@ -229,7 +228,7 @@ final class FormPayloadParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.isTextual()) {
|
if (node.isTextual()) {
|
||||||
return trimToNull(node.asText());
|
return trimToNull(node.asText(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.isObject()) {
|
if (node.isObject()) {
|
||||||
@@ -264,8 +263,8 @@ final class FormPayloadParser {
|
|||||||
private static Map<String, Object> objectToLinkedMap(JsonNode objectNode) {
|
private static Map<String, Object> objectToLinkedMap(JsonNode objectNode) {
|
||||||
final Map<String, Object> result = new LinkedHashMap<>();
|
final Map<String, Object> result = new LinkedHashMap<>();
|
||||||
objectNode
|
objectNode
|
||||||
.fieldNames()
|
.propertyNames()
|
||||||
.forEachRemaining(
|
.forEach(
|
||||||
key -> {
|
key -> {
|
||||||
final JsonNode v = objectNode.get(key);
|
final JsonNode v = objectNode.get(key);
|
||||||
if (v == null || v.isNull()) {
|
if (v == null || v.isNull()) {
|
||||||
|
|||||||
+2
-3
@@ -82,9 +82,8 @@ public class ExtractImagesController {
|
|||||||
Set<byte[]> processedImages = new HashSet<>();
|
Set<byte[]> processedImages = new HashSet<>();
|
||||||
|
|
||||||
if (useMultithreading) {
|
if (useMultithreading) {
|
||||||
// Executor service to handle multithreading
|
// Virtual thread executor — lightweight threads ideal for I/O-bound image extraction
|
||||||
ExecutorService executor =
|
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
|
||||||
Set<Future<Void>> futures = new HashSet<>();
|
Set<Future<Void>> futures = new HashSet<>();
|
||||||
|
|
||||||
// Safely iterate over each page, handling corrupt PDFs where page count might be wrong
|
// Safely iterate over each page, handling corrupt PDFs where page count might be wrong
|
||||||
|
|||||||
+20
-5
@@ -246,10 +246,12 @@ public class OCRController {
|
|||||||
command.add("--clean-final");
|
command.add("--clean-final");
|
||||||
}
|
}
|
||||||
if (ocrType != null && !ocrType.isEmpty()) {
|
if (ocrType != null && !ocrType.isEmpty()) {
|
||||||
if ("skip-text".equals(ocrType)) {
|
if ("force-ocr".equals(ocrType)) {
|
||||||
command.add("--skip-text");
|
|
||||||
} else if ("force-ocr".equals(ocrType)) {
|
|
||||||
command.add("--force-ocr");
|
command.add("--force-ocr");
|
||||||
|
} else {
|
||||||
|
// Default for 'Normal' and 'skip-text': use --skip-text
|
||||||
|
// ocrmypdf 17+ requires explicit flag when PDF already contains text
|
||||||
|
command.add("--skip-text");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
command.add("--invalidate-digital-signatures");
|
command.add("--invalidate-digital-signatures");
|
||||||
@@ -378,11 +380,12 @@ public class OCRController {
|
|||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add("tesseract");
|
command.add("tesseract");
|
||||||
command.add(imagePath.toString());
|
command.add(imagePath.toString());
|
||||||
command.add(
|
String outputBase =
|
||||||
new File(
|
new File(
|
||||||
tempOutputDir,
|
tempOutputDir,
|
||||||
String.format(Locale.ROOT, "page_%d", pageNum))
|
String.format(Locale.ROOT, "page_%d", pageNum))
|
||||||
.toString());
|
.toString();
|
||||||
|
command.add(outputBase);
|
||||||
command.add("-l");
|
command.add("-l");
|
||||||
command.add(String.join("+", selectedLanguages));
|
command.add(String.join("+", selectedLanguages));
|
||||||
command.add("pdf"); // Always output PDF
|
command.add("pdf"); // Always output PDF
|
||||||
@@ -400,6 +403,18 @@ public class OCRController {
|
|||||||
result.getRc());
|
result.getRc());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify the OCR'd PDF was created
|
||||||
|
if (!pageOutputPath.exists()) {
|
||||||
|
log.warn(
|
||||||
|
"Tesseract did not create expected output file: {}. Page may be blank or unreadable.",
|
||||||
|
pageOutputPath.getAbsolutePath());
|
||||||
|
// Save original page without OCR as fallback
|
||||||
|
try (PDDocument pageDoc = new PDDocument()) {
|
||||||
|
pageDoc.addPage(page);
|
||||||
|
pageDoc.save(pageOutputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add OCR'd PDF to merger
|
// Add OCR'd PDF to merger
|
||||||
merger.addSource(pageOutputPath);
|
merger.addSource(pageOutputPath);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+5
-5
@@ -14,10 +14,6 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -34,6 +30,10 @@ import stirling.software.common.service.PostHogService;
|
|||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
|
import tools.jackson.core.JacksonException;
|
||||||
|
import tools.jackson.databind.DatabindException;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@PipelineApi
|
@PipelineApi
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -54,7 +54,7 @@ public class PipelineController {
|
|||||||
+ "Users provide files and a JSON configuration defining the sequence of operations to perform. "
|
+ "Users provide files and a JSON configuration defining the sequence of operations to perform. "
|
||||||
+ "Input:PDF Output:PDF/ZIP Type:MIMO")
|
+ "Input:PDF Output:PDF/ZIP Type:MIMO")
|
||||||
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
||||||
throws JsonMappingException, JsonProcessingException {
|
throws DatabindException, JacksonException {
|
||||||
MultipartFile[] files = request.getFileInput();
|
MultipartFile[] files = request.getFileInput();
|
||||||
String jsonString = request.getJson();
|
String jsonString = request.getJson();
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
|
|||||||
+2
-2
@@ -28,8 +28,6 @@ import org.springframework.core.io.Resource;
|
|||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.PipelineConfig;
|
import stirling.software.SPDF.model.PipelineConfig;
|
||||||
@@ -40,6 +38,8 @@ import stirling.software.common.configuration.RuntimePathConfig;
|
|||||||
import stirling.software.common.service.PostHogService;
|
import stirling.software.common.service.PostHogService;
|
||||||
import stirling.software.common.util.FileMonitor;
|
import stirling.software.common.util.FileMonitor;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class PipelineDirectoryProcessor {
|
public class PipelineDirectoryProcessor {
|
||||||
|
|||||||
+6
-5
@@ -47,10 +47,6 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -66,6 +62,11 @@ import stirling.software.common.util.ExceptionUtils;
|
|||||||
import stirling.software.common.util.RegexPatternUtils;
|
import stirling.software.common.util.RegexPatternUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
import tools.jackson.databind.node.ArrayNode;
|
||||||
|
import tools.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
@SecurityApi
|
@SecurityApi
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -77,7 +78,7 @@ public class GetInfoOnPDF {
|
|||||||
private static final String PAGE_PREFIX = "Page ";
|
private static final String PAGE_PREFIX = "Page ";
|
||||||
private static final long MAX_FILE_SIZE = 100L * 1024 * 1024;
|
private static final long MAX_FILE_SIZE = 100L * 1024 * 1024;
|
||||||
|
|
||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
private static final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private final VeraPDFService veraPDFService;
|
private final VeraPDFService veraPDFService;
|
||||||
|
|||||||
+20
-1
@@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -54,10 +56,27 @@ public class MetricsController {
|
|||||||
}
|
}
|
||||||
Map<String, String> status = new HashMap<>();
|
Map<String, String> status = new HashMap<>();
|
||||||
status.put("status", "UP");
|
status.put("status", "UP");
|
||||||
status.put("version", getClass().getPackage().getImplementationVersion());
|
String version = getClass().getPackage().getImplementationVersion();
|
||||||
|
if (version == null) {
|
||||||
|
version = getVersionFromProperties();
|
||||||
|
}
|
||||||
|
status.put("version", version);
|
||||||
return ResponseEntity.ok(status);
|
return ResponseEntity.ok(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getVersionFromProperties() {
|
||||||
|
try (InputStream is = getClass().getResourceAsStream("/version.properties")) {
|
||||||
|
if (is != null) {
|
||||||
|
Properties props = new Properties();
|
||||||
|
props.load(is);
|
||||||
|
return props.getProperty("version");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to load version.properties", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/load")
|
@GetMapping("/load")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "GET request count",
|
summary = "GET request count",
|
||||||
|
|||||||
@@ -190,6 +190,31 @@ public class GlobalExceptionHandler {
|
|||||||
return problemDetail;
|
return problemDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given IOException indicates that the client disconnected before the
|
||||||
|
* response could be written (broken pipe, connection reset, etc.). When this happens there is
|
||||||
|
* no point in serialising a {@link ProblemDetail} body because the socket is already closed —
|
||||||
|
* and attempting to do so may trigger a secondary {@code HttpMessageNotWritableException} if
|
||||||
|
* the response Content-Type was already committed as a non-JSON type (e.g. image/png).
|
||||||
|
*/
|
||||||
|
private static boolean isClientDisconnectException(IOException ex) {
|
||||||
|
// Walk the causal chain — Jetty/Tomcat may wrap the low-level SocketException
|
||||||
|
Throwable current = ex;
|
||||||
|
while (current != null) {
|
||||||
|
String msg = current.getMessage();
|
||||||
|
if (msg != null) {
|
||||||
|
String lower = msg.toLowerCase(java.util.Locale.ROOT);
|
||||||
|
if (lower.contains("broken pipe")
|
||||||
|
|| lower.contains("connection reset")
|
||||||
|
|| lower.contains("an established connection was aborted")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current.getCause();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to create a standardized ProblemDetail response for exceptions with error
|
* Helper method to create a standardized ProblemDetail response for exceptions with error
|
||||||
* codes.
|
* codes.
|
||||||
@@ -1107,6 +1132,15 @@ public class GlobalExceptionHandler {
|
|||||||
public ResponseEntity<ProblemDetail> handleIOException(
|
public ResponseEntity<ProblemDetail> handleIOException(
|
||||||
IOException ex, HttpServletRequest request) {
|
IOException ex, HttpServletRequest request) {
|
||||||
|
|
||||||
|
// Broken pipe / connection reset means the client disconnected.
|
||||||
|
// Attempting to write a ProblemDetail response will fail because the
|
||||||
|
// response Content-Type may already be committed (e.g. image/png) and
|
||||||
|
// the client is gone anyway. Log at WARN and return an empty body.
|
||||||
|
if (isClientDisconnectException(ex)) {
|
||||||
|
log.warn("Client disconnected at {}: {}", request.getRequestURI(), ex.getMessage());
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a PDF-specific error and wrap it appropriately
|
// Check if this is a PDF-specific error and wrap it appropriately
|
||||||
IOException processedException =
|
IOException processedException =
|
||||||
ExceptionUtils.handlePdfException(ex, request.getRequestURI());
|
ExceptionUtils.handlePdfException(ex, request.getRequestURI());
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package stirling.software.SPDF.model;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import tools.jackson.databind.JsonNode;
|
||||||
|
|
||||||
public class ApiEndpoint {
|
public class ApiEndpoint {
|
||||||
private final String name;
|
private final String name;
|
||||||
private Map<String, JsonNode> parameters;
|
private Map<String, JsonNode> parameters;
|
||||||
@@ -18,10 +18,10 @@ public class ApiEndpoint {
|
|||||||
postNode.path("parameters")
|
postNode.path("parameters")
|
||||||
.forEach(
|
.forEach(
|
||||||
paramNode -> {
|
paramNode -> {
|
||||||
String paramName = paramNode.path("name").asText();
|
String paramName = paramNode.path("name").asText("");
|
||||||
parameters.put(paramName, paramNode);
|
parameters.put(paramName, paramNode);
|
||||||
});
|
});
|
||||||
this.description = postNode.path("description").asText();
|
this.description = postNode.path("description").asText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean areParametersValid(Map<String, Object> providedParams) {
|
public boolean areParametersValid(Map<String, Object> providedParams) {
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletContext;
|
import jakarta.servlet.ServletContext;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -28,6 +25,9 @@ import stirling.software.common.model.enumeration.Role;
|
|||||||
import stirling.software.common.service.UserServiceInterface;
|
import stirling.software.common.service.UserServiceInterface;
|
||||||
import stirling.software.common.util.RegexPatternUtils;
|
import stirling.software.common.util.RegexPatternUtils;
|
||||||
|
|
||||||
|
import tools.jackson.databind.JsonNode;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ApiDocService {
|
public class ApiDocService {
|
||||||
@@ -36,12 +36,15 @@ public class ApiDocService {
|
|||||||
|
|
||||||
private final ServletContext servletContext;
|
private final ServletContext servletContext;
|
||||||
private final UserServiceInterface userService;
|
private final UserServiceInterface userService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
Map<String, List<String>> outputToFileTypes = new HashMap<>();
|
Map<String, List<String>> outputToFileTypes = new HashMap<>();
|
||||||
JsonNode apiDocsJsonRootNode;
|
JsonNode apiDocsJsonRootNode;
|
||||||
|
|
||||||
public ApiDocService(
|
public ApiDocService(
|
||||||
|
ObjectMapper objectMapper,
|
||||||
ServletContext servletContext,
|
ServletContext servletContext,
|
||||||
@Autowired(required = false) UserServiceInterface userService) {
|
@Autowired(required = false) UserServiceInterface userService) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
this.servletContext = servletContext;
|
this.servletContext = servletContext;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
@@ -116,8 +119,7 @@ public class ApiDocService {
|
|||||||
ResponseEntity<String> response =
|
ResponseEntity<String> response =
|
||||||
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
|
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
|
||||||
apiDocsJson = response.getBody();
|
apiDocsJson = response.getBody();
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
apiDocsJsonRootNode = objectMapper.readTree(apiDocsJson);
|
||||||
apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
|
|
||||||
JsonNode paths = apiDocsJsonRootNode.path("paths");
|
JsonNode paths = apiDocsJsonRootNode.path("paths");
|
||||||
paths.propertyStream()
|
paths.propertyStream()
|
||||||
.forEach(
|
.forEach(
|
||||||
|
|||||||
+2
-1
@@ -427,7 +427,8 @@ public class CertificateValidationService {
|
|||||||
try (InputStream certStream =
|
try (InputStream certStream =
|
||||||
getClass().getClassLoader().getResourceAsStream("certs/cacert.pem")) {
|
getClass().getClassLoader().getResourceAsStream("certs/cacert.pem")) {
|
||||||
if (certStream == null) {
|
if (certStream == null) {
|
||||||
log.warn("Bundled Mozilla CA certificate file not found in resources");
|
log.debug(
|
||||||
|
"Bundled Mozilla CA certificate file not found in resources — using Java system trust store only");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,8 +90,6 @@ import org.apache.pdfbox.util.Matrix;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -129,6 +127,8 @@ import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
|||||||
import stirling.software.common.util.TempFile;
|
import stirling.software.common.util.TempFile;
|
||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -6834,7 +6834,8 @@ public class PdfJsonConversionService {
|
|||||||
|
|
||||||
/** Schedules automatic cleanup of cached documents after 30 minutes. */
|
/** Schedules automatic cleanup of cached documents after 30 minutes. */
|
||||||
private void scheduleDocumentCleanup(String jobId) {
|
private void scheduleDocumentCleanup(String jobId) {
|
||||||
new Thread(
|
Thread.ofVirtual()
|
||||||
|
.start(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(TimeUnit.MINUTES.toMillis(30));
|
Thread.sleep(TimeUnit.MINUTES.toMillis(30));
|
||||||
@@ -6843,7 +6844,6 @@ public class PdfJsonConversionService {
|
|||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import java.util.stream.Stream;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.SignatureFile;
|
import stirling.software.SPDF.model.SignatureFile;
|
||||||
@@ -24,6 +22,8 @@ import stirling.software.SPDF.model.api.signature.SavedSignatureRequest;
|
|||||||
import stirling.software.SPDF.model.api.signature.SavedSignatureResponse;
|
import stirling.software.SPDF.model.api.signature.SavedSignatureResponse;
|
||||||
import stirling.software.common.configuration.InstallationPathConfig;
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SharedSignatureService {
|
public class SharedSignatureService {
|
||||||
@@ -33,9 +33,9 @@ public class SharedSignatureService {
|
|||||||
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public SharedSignatureService() {
|
public SharedSignatureService(ObjectMapper objectMapper) {
|
||||||
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
|
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
|
||||||
this.objectMapper = new ObjectMapper();
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasAccessToFile(String username, String fileName) throws IOException {
|
public boolean hasAccessToFile(String username, String fileName) throws IOException {
|
||||||
|
|||||||
+5
-5
@@ -18,8 +18,6 @@ import org.apache.pdfbox.pdmodel.font.PDFont;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -38,6 +36,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
|||||||
import stirling.software.common.service.TaskManager;
|
import stirling.software.common.service.TaskManager;
|
||||||
import stirling.software.common.util.ExceptionUtils;
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for lazy loading PDF pages. Caches PDF documents and extracts pages on-demand to reduce
|
* Service for lazy loading PDF pages. Caches PDF documents and extracts pages on-demand to reduce
|
||||||
* memory usage for large PDFs.
|
* memory usage for large PDFs.
|
||||||
@@ -255,7 +255,8 @@ public class PdfLazyLoadingService {
|
|||||||
|
|
||||||
/** Schedules automatic cleanup of cached documents after 30 minutes. */
|
/** Schedules automatic cleanup of cached documents after 30 minutes. */
|
||||||
private void scheduleDocumentCleanup(String jobId) {
|
private void scheduleDocumentCleanup(String jobId) {
|
||||||
new Thread(
|
Thread.ofVirtual()
|
||||||
|
.start(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(TimeUnit.MINUTES.toMillis(30));
|
Thread.sleep(TimeUnit.MINUTES.toMillis(30));
|
||||||
@@ -264,8 +265,7 @@ public class PdfLazyLoadingService {
|
|||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+22
-8
@@ -18,14 +18,14 @@ import org.springframework.core.io.Resource;
|
|||||||
import org.springframework.core.io.ResourceLoader;
|
import org.springframework.core.io.ResourceLoader;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.service.pdfjson.type3.Type3FontSignatureCalculator;
|
import stirling.software.SPDF.service.pdfjson.type3.Type3FontSignatureCalculator;
|
||||||
|
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -202,20 +202,34 @@ public class Type3FontLibrary {
|
|||||||
if (payload == null) {
|
if (payload == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
byte[] data = null;
|
String base64;
|
||||||
if (payload.base64 != null && !payload.base64.isBlank()) {
|
if (payload.base64 != null && !payload.base64.isBlank()) {
|
||||||
|
// Validate the base64 string without wasteful full decode
|
||||||
|
// Only decode a small prefix to verify encoding is valid
|
||||||
try {
|
try {
|
||||||
data = Base64.getDecoder().decode(payload.base64);
|
byte[] probe =
|
||||||
|
Base64.getDecoder()
|
||||||
|
.decode(
|
||||||
|
payload.base64.substring(
|
||||||
|
0, Math.min(4, payload.base64.length())));
|
||||||
|
if (probe.length == 0 && payload.base64.length() <= 4) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
log.warn("[TYPE3] Invalid base64 payload in Type3 library: {}", ex.getMessage());
|
log.warn("[TYPE3] Invalid base64 payload in Type3 library: {}", ex.getMessage());
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
// Keep the original base64 string directly — avoids 3x memory pressure
|
||||||
|
base64 = payload.base64;
|
||||||
} else if (payload.resource != null && !payload.resource.isBlank()) {
|
} else if (payload.resource != null && !payload.resource.isBlank()) {
|
||||||
data = loadResourceBytes(payload.resource);
|
byte[] data = loadResourceBytes(payload.resource);
|
||||||
}
|
|
||||||
if (data == null || data.length == 0) {
|
if (data == null || data.length == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String base64 = Base64.getEncoder().encodeToString(data);
|
base64 = Base64.getEncoder().encodeToString(data);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return new Type3FontLibraryPayload(base64, normalizeFormat(payload.format));
|
return new Type3FontLibraryPayload(base64, normalizeFormat(payload.format));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -26,14 +26,15 @@ import org.apache.pdfbox.pdmodel.font.PDType3Font;
|
|||||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.service.pdfjson.type3.Type3FontSignatureCalculator;
|
import stirling.software.SPDF.service.pdfjson.type3.Type3FontSignatureCalculator;
|
||||||
import stirling.software.SPDF.service.pdfjson.type3.Type3GlyphExtractor;
|
import stirling.software.SPDF.service.pdfjson.type3.Type3GlyphExtractor;
|
||||||
import stirling.software.SPDF.service.pdfjson.type3.model.Type3GlyphOutline;
|
import stirling.software.SPDF.service.pdfjson.type3.model.Type3GlyphOutline;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.ObjectWriter;
|
||||||
|
import tools.jackson.databind.SerializationFeature;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Small CLI helper that scans a PDF for Type3 fonts, computes their signatures, and optionally
|
* Small CLI helper that scans a PDF for Type3 fonts, computes their signatures, and optionally
|
||||||
* emits JSON describing the glyph coverage. This allows Type3 library entries to be added without
|
* emits JSON describing the glyph coverage. This allows Type3 library entries to be added without
|
||||||
@@ -48,7 +49,7 @@ import stirling.software.SPDF.service.pdfjson.type3.model.Type3GlyphOutline;
|
|||||||
public final class Type3SignatureTool {
|
public final class Type3SignatureTool {
|
||||||
|
|
||||||
private static final ObjectMapper OBJECT_MAPPER =
|
private static final ObjectMapper OBJECT_MAPPER =
|
||||||
new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build();
|
||||||
|
|
||||||
private Type3SignatureTool() {}
|
private Type3SignatureTool() {}
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ public class SvgToPdf {
|
|||||||
private GraphicsNode buildGvtWithTimeout(BridgeContext ctx, SVGDocument svgDoc)
|
private GraphicsNode buildGvtWithTimeout(BridgeContext ctx, SVGDocument svgDoc)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
GVTBuilder builder = new GVTBuilder();
|
GVTBuilder builder = new GVTBuilder();
|
||||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
|
||||||
Callable<GraphicsNode> buildTask = () -> builder.build(ctx, svgDoc);
|
Callable<GraphicsNode> buildTask = () -> builder.build(ctx, svgDoc);
|
||||||
Future<GraphicsNode> future = executor.submit(buildTask);
|
Future<GraphicsNode> future = executor.submit(buildTask);
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ logging.level.stirling.software.common.service.JobExecutorService=INFO
|
|||||||
logging.level.stirling.software.common.service.TaskManager=INFO
|
logging.level.stirling.software.common.service.TaskManager=INFO
|
||||||
spring.jpa.open-in-view=false
|
spring.jpa.open-in-view=false
|
||||||
server.forward-headers-strategy=NATIVE
|
server.forward-headers-strategy=NATIVE
|
||||||
|
|
||||||
|
# Enable HTTP/2 for improved performance (multiplexed streams, header compression)
|
||||||
|
server.http2.enabled=true
|
||||||
|
|
||||||
|
# Enable virtual threads (Java 21+, pinning fix in Java 25)
|
||||||
|
spring.threads.virtual.enabled=true
|
||||||
|
|
||||||
|
# Response compression
|
||||||
|
server.compression.enabled=true
|
||||||
|
server.compression.min-response-size=1024
|
||||||
|
server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript
|
||||||
|
|
||||||
server.error.path=/error
|
server.error.path=/error
|
||||||
server.error.whitelabel.enabled=false
|
server.error.whitelabel.enabled=false
|
||||||
server.error.include-stacktrace=always
|
server.error.include-stacktrace=always
|
||||||
@@ -38,7 +50,7 @@ spring.devtools.livereload.enabled=true
|
|||||||
spring.devtools.restart.exclude=stirling.software.proprietary.security/**
|
spring.devtools.restart.exclude=stirling.software.proprietary.security/**
|
||||||
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
||||||
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
||||||
server.tomcat.max-http-header-size=32768
|
server.jetty.max-http-request-header-size=32768
|
||||||
|
|
||||||
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL
|
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL
|
||||||
spring.datasource.driver-class-name=org.h2.Driver
|
spring.datasource.driver-class-name=org.h2.Driver
|
||||||
|
|||||||
+3
-3
@@ -27,13 +27,13 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.controller.api.EditTableOfContentsController.BookmarkItem;
|
import stirling.software.SPDF.controller.api.EditTableOfContentsController.BookmarkItem;
|
||||||
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class EditTableOfContentsControllerTest {
|
class EditTableOfContentsControllerTest {
|
||||||
|
|
||||||
|
|||||||
+18
-16
@@ -40,14 +40,15 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.security.PDFVerificationResult;
|
import stirling.software.SPDF.model.api.security.PDFVerificationResult;
|
||||||
import stirling.software.SPDF.service.VeraPDFService;
|
import stirling.software.SPDF.service.VeraPDFService;
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
|
import tools.jackson.databind.JsonNode;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
@DisplayName("GetInfoOnPDF Controller Tests")
|
@DisplayName("GetInfoOnPDF Controller Tests")
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class GetInfoOnPDFTest {
|
class GetInfoOnPDFTest {
|
||||||
@@ -64,7 +65,7 @@ class GetInfoOnPDFTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
objectMapper = new ObjectMapper();
|
objectMapper = JsonMapper.builder().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper method to load a PDF file from test resources */
|
/** Helper method to load a PDF file from test resources */
|
||||||
@@ -216,8 +217,8 @@ class GetInfoOnPDFTest {
|
|||||||
Assertions.assertTrue(jsonNode.has("Permissions"));
|
Assertions.assertTrue(jsonNode.has("Permissions"));
|
||||||
|
|
||||||
JsonNode metadata = jsonNode.get("Metadata");
|
JsonNode metadata = jsonNode.get("Metadata");
|
||||||
Assertions.assertEquals("Test Title", metadata.get("Title").asText());
|
Assertions.assertEquals("Test Title", metadata.get("Title").asText(""));
|
||||||
Assertions.assertEquals("Test Author", metadata.get("Author").asText());
|
Assertions.assertEquals("Test Author", metadata.get("Author").asText(""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,12 +316,12 @@ class GetInfoOnPDFTest {
|
|||||||
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
|
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
|
||||||
JsonNode metadata = jsonNode.get("Metadata");
|
JsonNode metadata = jsonNode.get("Metadata");
|
||||||
|
|
||||||
Assertions.assertEquals("Test Title", metadata.get("Title").asText());
|
Assertions.assertEquals("Test Title", metadata.get("Title").asText(""));
|
||||||
Assertions.assertEquals("Test Author", metadata.get("Author").asText());
|
Assertions.assertEquals("Test Author", metadata.get("Author").asText(""));
|
||||||
Assertions.assertEquals("Test Subject", metadata.get("Subject").asText());
|
Assertions.assertEquals("Test Subject", metadata.get("Subject").asText(""));
|
||||||
Assertions.assertEquals("test, pdf, metadata", metadata.get("Keywords").asText());
|
Assertions.assertEquals("test, pdf, metadata", metadata.get("Keywords").asText(""));
|
||||||
Assertions.assertEquals("Test Creator", metadata.get("Creator").asText());
|
Assertions.assertEquals("Test Creator", metadata.get("Creator").asText(""));
|
||||||
Assertions.assertEquals("Test Producer", metadata.get("Producer").asText());
|
Assertions.assertEquals("Test Producer", metadata.get("Producer").asText(""));
|
||||||
Assertions.assertTrue(metadata.has("CreationDate"));
|
Assertions.assertTrue(metadata.has("CreationDate"));
|
||||||
Assertions.assertTrue(metadata.has("ModificationDate"));
|
Assertions.assertTrue(metadata.has("ModificationDate"));
|
||||||
|
|
||||||
@@ -512,10 +513,10 @@ class GetInfoOnPDFTest {
|
|||||||
JsonNode page1 = perPageInfo.get("Page 1");
|
JsonNode page1 = perPageInfo.get("Page 1");
|
||||||
Assertions.assertTrue(page1.has("Size"));
|
Assertions.assertTrue(page1.has("Size"));
|
||||||
Assertions.assertTrue(page1.get("Size").has("Standard Page"));
|
Assertions.assertTrue(page1.get("Size").has("Standard Page"));
|
||||||
Assertions.assertEquals("A4", page1.get("Size").get("Standard Page").asText());
|
Assertions.assertEquals("A4", page1.get("Size").get("Standard Page").asText(""));
|
||||||
|
|
||||||
JsonNode page2 = perPageInfo.get("Page 2");
|
JsonNode page2 = perPageInfo.get("Page 2");
|
||||||
Assertions.assertEquals("Letter", page2.get("Size").get("Standard Page").asText());
|
Assertions.assertEquals("Letter", page2.get("Size").get("Standard Page").asText(""));
|
||||||
|
|
||||||
loadedDoc.close();
|
loadedDoc.close();
|
||||||
}
|
}
|
||||||
@@ -569,7 +570,8 @@ class GetInfoOnPDFTest {
|
|||||||
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
|
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
|
||||||
|
|
||||||
Assertions.assertTrue(jsonNode.has("error"));
|
Assertions.assertTrue(jsonNode.has("error"));
|
||||||
Assertions.assertTrue(jsonNode.get("error").asText().contains("PDF file is required"));
|
Assertions.assertTrue(
|
||||||
|
jsonNode.get("error").asText("").contains("PDF file is required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -645,7 +647,7 @@ class GetInfoOnPDFTest {
|
|||||||
|
|
||||||
Assertions.assertTrue(jsonNode.has("error"));
|
Assertions.assertTrue(jsonNode.has("error"));
|
||||||
Assertions.assertTrue(
|
Assertions.assertTrue(
|
||||||
jsonNode.get("error").asText().contains("exceeds maximum allowed size"));
|
jsonNode.get("error").asText("").contains("exceeds maximum allowed size"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import java.util.Map;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import tools.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import tools.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import tools.jackson.databind.node.ArrayNode;
|
||||||
|
import tools.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
class ApiEndpointTest {
|
class ApiEndpointTest {
|
||||||
|
|
||||||
private final ObjectMapper mapper = new ObjectMapper();
|
private final ObjectMapper mapper = JsonMapper.builder().build();
|
||||||
|
|
||||||
private JsonNode postNodeWithParams(String description, String... names) {
|
private JsonNode postNodeWithParams(String description, String... names) {
|
||||||
ObjectNode post = mapper.createObjectNode();
|
ObjectNode post = mapper.createObjectNode();
|
||||||
|
|||||||
@@ -12,14 +12,15 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletContext;
|
import jakarta.servlet.ServletContext;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApiEndpoint;
|
import stirling.software.SPDF.model.ApiEndpoint;
|
||||||
import stirling.software.common.service.UserServiceInterface;
|
import stirling.software.common.service.UserServiceInterface;
|
||||||
|
|
||||||
|
import tools.jackson.databind.JsonNode;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ApiDocServiceTest {
|
class ApiDocServiceTest {
|
||||||
|
|
||||||
@@ -27,11 +28,11 @@ class ApiDocServiceTest {
|
|||||||
@Mock UserServiceInterface userService;
|
@Mock UserServiceInterface userService;
|
||||||
|
|
||||||
ApiDocService apiDocService;
|
ApiDocService apiDocService;
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = JsonMapper.builder().build();
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
apiDocService = new ApiDocService(servletContext, userService);
|
apiDocService = new ApiDocService(mapper, servletContext, userService);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setApiDocumentation(Map<String, ApiEndpoint> docs) throws Exception {
|
private void setApiDocumentation(Map<String, ApiEndpoint> docs) throws Exception {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import org.mockito.MockedStatic;
|
|||||||
import stirling.software.SPDF.model.SignatureFile;
|
import stirling.software.SPDF.model.SignatureFile;
|
||||||
import stirling.software.common.configuration.InstallationPathConfig;
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
|
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
class SignatureServiceTest {
|
class SignatureServiceTest {
|
||||||
|
|
||||||
@TempDir Path tempDir;
|
@TempDir Path tempDir;
|
||||||
@@ -53,7 +55,7 @@ class SignatureServiceTest {
|
|||||||
.thenReturn(tempDir.toString());
|
.thenReturn(tempDir.toString());
|
||||||
|
|
||||||
// Initialize the service with our temp directory
|
// Initialize the service with our temp directory
|
||||||
signatureService = new SharedSignatureService();
|
signatureService = new SharedSignatureService(JsonMapper.builder().build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ dependencies {
|
|||||||
api 'org.springframework.boot:spring-boot-starter-jetty'
|
api 'org.springframework.boot:spring-boot-starter-jetty'
|
||||||
api 'org.springframework.boot:spring-boot-starter-security'
|
api 'org.springframework.boot:spring-boot-starter-security'
|
||||||
api 'org.springframework.boot:spring-boot-starter-data-jpa'
|
api 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
api 'org.springframework.boot:spring-boot-starter-oauth2-client'
|
api 'org.springframework.boot:spring-boot-starter-security-oauth2-client'
|
||||||
api 'org.springframework.boot:spring-boot-starter-mail'
|
api 'org.springframework.boot:spring-boot-starter-mail'
|
||||||
api 'org.springframework.boot:spring-boot-starter-cache'
|
api 'org.springframework.boot:spring-boot-starter-cache'
|
||||||
api 'com.github.ben-manes.caffeine:caffeine'
|
api 'com.github.ben-manes.caffeine:caffeine'
|
||||||
|
|||||||
+8
-14
@@ -2,21 +2,22 @@ package stirling.software.proprietary.config;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import org.slf4j.MDC;
|
import org.slf4j.MDC;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.task.TaskDecorator;
|
import org.springframework.core.task.TaskDecorator;
|
||||||
|
import org.springframework.core.task.support.TaskExecutorAdapter;
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
public class AsyncConfig {
|
public class AsyncConfig {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MDC context-propagating task decorator Copies MDC context from the caller thread to the async
|
* MDC context-propagating task decorator. Copies MDC context from the caller thread to the
|
||||||
* executor thread
|
* virtual thread executing the task.
|
||||||
*/
|
*/
|
||||||
static class MDCContextTaskDecorator implements TaskDecorator {
|
static class MDCContextTaskDecorator implements TaskDecorator {
|
||||||
@Override
|
@Override
|
||||||
@@ -42,16 +43,9 @@ public class AsyncConfig {
|
|||||||
|
|
||||||
@Bean(name = "auditExecutor")
|
@Bean(name = "auditExecutor")
|
||||||
public Executor auditExecutor() {
|
public Executor auditExecutor() {
|
||||||
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
|
TaskExecutorAdapter adapter =
|
||||||
exec.setCorePoolSize(2);
|
new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
|
||||||
exec.setMaxPoolSize(8);
|
adapter.setTaskDecorator(new MDCContextTaskDecorator());
|
||||||
exec.setQueueCapacity(1_000);
|
return adapter;
|
||||||
exec.setThreadNamePrefix("audit-");
|
|
||||||
|
|
||||||
// Set the task decorator to propagate MDC context
|
|
||||||
exec.setTaskDecorator(new MDCContextTaskDecorator());
|
|
||||||
|
|
||||||
exec.initialize();
|
|
||||||
return exec;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-6
@@ -1,15 +1,12 @@
|
|||||||
package stirling.software.proprietary.config;
|
package stirling.software.proprietary.config;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|
||||||
/** Configuration to enable scheduling for the audit system. */
|
/** Configuration for audit system transaction management. */
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableTransactionManagement
|
@EnableTransactionManagement
|
||||||
@EnableScheduling
|
|
||||||
public class AuditJpaConfig {
|
public class AuditJpaConfig {
|
||||||
// This configuration enables scheduling for audit cleanup tasks
|
// Scheduling is enabled on SPDFApplication — no duplicate @EnableScheduling needed.
|
||||||
// JPA repositories are now managed by DatabaseConfig to avoid conflicts
|
// JPA repositories are now managed by DatabaseConfig to avoid conflicts.
|
||||||
// No additional beans or methods needed
|
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -12,8 +12,6 @@ import org.springframework.scheduling.annotation.Async;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -21,6 +19,8 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
|||||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||||
import stirling.software.proprietary.util.SecretMasker;
|
import stirling.software.proprietary.util.SecretMasker;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Primary
|
@Primary
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -68,7 +68,10 @@ public class CustomAuditEventRepository implements AuditEventRepository {
|
|||||||
.build();
|
.build();
|
||||||
repo.save(ent);
|
repo.save(ent);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace(); // fail-open
|
log.error(
|
||||||
|
"Failed to persist audit event (fail-open); principal={}",
|
||||||
|
ev.getPrincipal(),
|
||||||
|
e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -28,9 +28,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -47,6 +44,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
|||||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
|
||||||
|
import tools.jackson.core.JacksonException;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
/** REST endpoints for the audit dashboard. */
|
/** REST endpoints for the audit dashboard. */
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@@ -266,7 +266,7 @@ public class AuditDashboardController {
|
|||||||
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
||||||
|
|
||||||
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JacksonException e) {
|
||||||
log.error("Error serializing audit events to JSON", e);
|
log.error("Error serializing audit events to JSON", e);
|
||||||
return ResponseEntity.internalServerError().build();
|
return ResponseEntity.internalServerError().build();
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -20,9 +20,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -32,6 +29,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
|||||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
|
||||||
|
import tools.jackson.core.JacksonException;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
/** REST API controller for audit data used by React frontend. */
|
/** REST API controller for audit data used by React frontend. */
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ProprietaryUiDataApi
|
@ProprietaryUiDataApi
|
||||||
@@ -332,7 +332,7 @@ public class AuditRestController {
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> parsed = objectMapper.readValue(event.getData(), Map.class);
|
Map<String, Object> parsed = objectMapper.readValue(event.getData(), Map.class);
|
||||||
details = parsed;
|
details = parsed;
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JacksonException e) {
|
||||||
log.warn("Failed to parse audit event data as JSON: {}", event.getData());
|
log.warn("Failed to parse audit event data as JSON: {}", event.getData());
|
||||||
details.put("rawData", event.getData());
|
details.put("rawData", event.getData());
|
||||||
}
|
}
|
||||||
@@ -380,7 +380,7 @@ public class AuditRestController {
|
|||||||
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
||||||
|
|
||||||
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JacksonException e) {
|
||||||
log.error("Error serializing audit events to JSON", e);
|
log.error("Error serializing audit events to JSON", e);
|
||||||
return ResponseEntity.internalServerError().build();
|
return ResponseEntity.internalServerError().build();
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -15,9 +15,6 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -54,6 +51,9 @@ import stirling.software.proprietary.security.service.TeamService;
|
|||||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.proprietary.service.UserLicenseSettingsService;
|
import stirling.software.proprietary.service.UserLicenseSettingsService;
|
||||||
|
|
||||||
|
import tools.jackson.core.JacksonException;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ProprietaryUiDataApi
|
@ProprietaryUiDataApi
|
||||||
public class ProprietaryUIDataController {
|
public class ProprietaryUIDataController {
|
||||||
@@ -414,7 +414,7 @@ public class ProprietaryUIDataController {
|
|||||||
String settingsJson;
|
String settingsJson;
|
||||||
try {
|
try {
|
||||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JacksonException e) {
|
||||||
log.error("Error converting settings map", e);
|
log.error("Error converting settings map", e);
|
||||||
return ResponseEntity.status(500).build();
|
return ResponseEntity.status(500).build();
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -8,9 +8,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -20,6 +17,9 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
|||||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
|
||||||
|
import tools.jackson.core.JacksonException;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
/** REST API controller for usage analytics data used by React frontend. */
|
/** REST API controller for usage analytics data used by React frontend. */
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ProprietaryUiDataApi
|
@ProprietaryUiDataApi
|
||||||
@@ -135,7 +135,7 @@ public class UsageRestController {
|
|||||||
return normalizeEndpoint(requestUri.toString());
|
return normalizeEndpoint(requestUri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JacksonException e) {
|
||||||
log.debug("Failed to parse audit data JSON: {}", dataJson, e);
|
log.debug("Failed to parse audit data JSON: {}", dataJson, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -6,9 +6,9 @@ import javax.sql.DataSource;
|
|||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
|
||||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||||
import org.springframework.boot.jdbc.DatabaseDriver;
|
import org.springframework.boot.jdbc.DatabaseDriver;
|
||||||
|
import org.springframework.boot.persistence.autoconfigure.EntityScan;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Primary;
|
import org.springframework.context.annotation.Primary;
|
||||||
@@ -75,10 +75,18 @@ public class DatabaseConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private DataSource useDefaultDataSource(DataSourceBuilder<?> dataSourceBuilder) {
|
private DataSource useDefaultDataSource(DataSourceBuilder<?> dataSourceBuilder) {
|
||||||
|
// Support AOT training: override URL via system property to avoid H2 file lock
|
||||||
|
// conflicts when the AOT RECORD phase starts a second Spring context
|
||||||
|
String overrideUrl = System.getProperty("stirling.datasource.url");
|
||||||
|
String url =
|
||||||
|
(overrideUrl != null && !overrideUrl.isBlank())
|
||||||
|
? overrideUrl
|
||||||
|
: DATASOURCE_DEFAULT_URL;
|
||||||
|
|
||||||
log.info("Using default H2 database");
|
log.info("Using default H2 database");
|
||||||
|
|
||||||
dataSourceBuilder
|
dataSourceBuilder
|
||||||
.url(DATASOURCE_DEFAULT_URL)
|
.url(url)
|
||||||
.driverClassName(DatabaseDriver.H2.getDriverClassName())
|
.driverClassName(DatabaseDriver.H2.getDriverClassName())
|
||||||
.username(DEFAULT_USERNAME);
|
.username(DEFAULT_USERNAME);
|
||||||
|
|
||||||
|
|||||||
+16
-13
@@ -14,15 +14,17 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
|||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.CorsConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider;
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
@@ -82,7 +84,7 @@ public class SecurityConfiguration {
|
|||||||
private final PersistentLoginRepository persistentLoginRepository;
|
private final PersistentLoginRepository persistentLoginRepository;
|
||||||
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
||||||
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
||||||
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
private final OpenSaml5AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||||
private final stirling.software.proprietary.service.UserLicenseSettingsService
|
private final stirling.software.proprietary.service.UserLicenseSettingsService
|
||||||
licenseSettingsService;
|
licenseSettingsService;
|
||||||
private final ClientRegistrationRepository clientRegistrationRepository;
|
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||||
@@ -105,7 +107,7 @@ public class SecurityConfiguration {
|
|||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver,
|
OpenSaml5AuthenticationRequestResolver saml2AuthenticationRequestResolver,
|
||||||
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository,
|
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository,
|
||||||
stirling.software.proprietary.service.UserLicenseSettingsService
|
stirling.software.proprietary.service.UserLicenseSettingsService
|
||||||
licenseSettingsService) {
|
licenseSettingsService) {
|
||||||
@@ -151,7 +153,8 @@ public class SecurityConfiguration {
|
|||||||
// Default to allowing all origins when nothing is configured
|
// Default to allowing all origins when nothing is configured
|
||||||
cfg.setAllowedOriginPatterns(List.of("*"));
|
cfg.setAllowedOriginPatterns(List.of("*"));
|
||||||
log.info(
|
log.info(
|
||||||
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); allowing all origins.");
|
"No CORS allowed origins configured in settings.yml"
|
||||||
|
+ " (system.corsAllowedOrigins); allowing all origins.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly configure supported HTTP methods (include OPTIONS for preflight)
|
// Explicitly configure supported HTTP methods (include OPTIONS for preflight)
|
||||||
@@ -225,7 +228,7 @@ public class SecurityConfiguration {
|
|||||||
http.cors(cors -> cors.configurationSource(corsSource));
|
http.cors(cors -> cors.configurationSource(corsSource));
|
||||||
} else {
|
} else {
|
||||||
// Explicitly disable CORS when no origins are configured
|
// Explicitly disable CORS when no origins are configured
|
||||||
http.cors(cors -> cors.disable());
|
http.cors(CorsConfigurer::disable);
|
||||||
}
|
}
|
||||||
|
|
||||||
http.csrf(CsrfConfigurer::disable);
|
http.csrf(CsrfConfigurer::disable);
|
||||||
@@ -233,24 +236,24 @@ public class SecurityConfiguration {
|
|||||||
// Configure X-Frame-Options based on settings.yml configuration
|
// Configure X-Frame-Options based on settings.yml configuration
|
||||||
// When login is disabled, automatically disable X-Frame-Options to allow embedding
|
// When login is disabled, automatically disable X-Frame-Options to allow embedding
|
||||||
if (!loginEnabledValue) {
|
if (!loginEnabledValue) {
|
||||||
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()));
|
http.headers(headers -> headers.frameOptions(FrameOptionsConfig::disable));
|
||||||
} else {
|
} else {
|
||||||
String xFrameOption = securityProperties.getXFrameOptions();
|
String xFrameOption = securityProperties.getXFrameOptions();
|
||||||
if (xFrameOption != null) {
|
if (xFrameOption != null) {
|
||||||
http.headers(
|
http.headers(
|
||||||
headers -> {
|
headers -> {
|
||||||
if ("DISABLED".equalsIgnoreCase(xFrameOption)) {
|
if ("DISABLED".equalsIgnoreCase(xFrameOption)) {
|
||||||
headers.frameOptions(frameOptions -> frameOptions.disable());
|
headers.frameOptions(FrameOptionsConfig::disable);
|
||||||
} else if ("SAMEORIGIN".equalsIgnoreCase(xFrameOption)) {
|
} else if ("SAMEORIGIN".equalsIgnoreCase(xFrameOption)) {
|
||||||
headers.frameOptions(frameOptions -> frameOptions.sameOrigin());
|
headers.frameOptions(FrameOptionsConfig::sameOrigin);
|
||||||
} else {
|
} else {
|
||||||
// Default to DENY
|
// Default to DENY
|
||||||
headers.frameOptions(frameOptions -> frameOptions.deny());
|
headers.frameOptions(FrameOptionsConfig::deny);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If not configured, use default DENY
|
// If not configured, use default DENY
|
||||||
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.deny()));
|
http.headers(headers -> headers.frameOptions(FrameOptionsConfig::deny));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,8 +384,8 @@ public class SecurityConfiguration {
|
|||||||
}
|
}
|
||||||
// Handle SAML
|
// Handle SAML
|
||||||
if (securityProperties.isSaml2Active() && runningProOrHigher) {
|
if (securityProperties.isSaml2Active() && runningProOrHigher) {
|
||||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
OpenSaml5AuthenticationProvider authenticationProvider =
|
||||||
new OpenSaml4AuthenticationProvider();
|
new OpenSaml5AuthenticationProvider();
|
||||||
authenticationProvider.setResponseAuthenticationConverter(
|
authenticationProvider.setResponseAuthenticationConverter(
|
||||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||||
http.authenticationProvider(authenticationProvider)
|
http.authenticationProvider(authenticationProvider)
|
||||||
|
|||||||
+90
-87
@@ -12,11 +12,6 @@ import org.bouncycastle.crypto.signers.Ed25519Signer;
|
|||||||
import org.bouncycastle.util.encoders.Hex;
|
import org.bouncycastle.util.encoders.Hex;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.posthog.java.shaded.org.json.JSONException;
|
|
||||||
import com.posthog.java.shaded.org.json.JSONObject;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -24,6 +19,9 @@ import stirling.software.common.model.ApplicationProperties;
|
|||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.RegexPatternUtils;
|
import stirling.software.common.util.RegexPatternUtils;
|
||||||
|
|
||||||
|
import tools.jackson.databind.JsonNode;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -47,7 +45,7 @@ public class KeygenLicenseVerifier {
|
|||||||
|
|
||||||
private static final String JWT_PREFIX = "key/";
|
private static final String JWT_PREFIX = "key/";
|
||||||
|
|
||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
// Shared HTTP client for connection pooling
|
// Shared HTTP client for connection pooling
|
||||||
@@ -135,11 +133,11 @@ public class KeygenLicenseVerifier {
|
|||||||
String algorithm = "";
|
String algorithm = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JSONObject attrs = new JSONObject(payload);
|
JsonNode attrs = objectMapper.readTree(payload);
|
||||||
encryptedData = (String) attrs.get("enc");
|
encryptedData = attrs.path("enc").asText("");
|
||||||
encodedSignature = (String) attrs.get("sig");
|
encodedSignature = attrs.path("sig").asText("");
|
||||||
algorithm = (String) attrs.get("alg");
|
algorithm = attrs.path("alg").asText("");
|
||||||
} catch (JSONException e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to parse license file: {}", e.getMessage());
|
log.error("Failed to parse license file: {}", e.getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -215,11 +213,17 @@ public class KeygenLicenseVerifier {
|
|||||||
|
|
||||||
private boolean processCertificateData(String certData, LicenseContext context) {
|
private boolean processCertificateData(String certData, LicenseContext context) {
|
||||||
try {
|
try {
|
||||||
JSONObject licenseData = new JSONObject(certData);
|
JsonNode licenseData = objectMapper.readTree(certData);
|
||||||
JSONObject metaObj = licenseData.optJSONObject("meta");
|
JsonNode metaObj = licenseData.path("meta");
|
||||||
if (metaObj != null) {
|
if (!metaObj.isMissingNode() && metaObj.isObject()) {
|
||||||
String issuedStr = metaObj.optString("issued", null);
|
String issuedStr =
|
||||||
String expiryStr = metaObj.optString("expiry", null);
|
metaObj.path("issued").isNull()
|
||||||
|
? null
|
||||||
|
: metaObj.path("issued").asText(null);
|
||||||
|
String expiryStr =
|
||||||
|
metaObj.path("expiry").isNull()
|
||||||
|
? null
|
||||||
|
: metaObj.path("expiry").asText(null);
|
||||||
|
|
||||||
if (issuedStr != null && expiryStr != null) {
|
if (issuedStr != null && expiryStr != null) {
|
||||||
java.time.Instant issued = java.time.Instant.parse(issuedStr);
|
java.time.Instant issued = java.time.Instant.parse(issuedStr);
|
||||||
@@ -244,31 +248,32 @@ public class KeygenLicenseVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the main license data
|
// Get the main license data
|
||||||
JSONObject dataObj = licenseData.optJSONObject("data");
|
JsonNode dataObj = licenseData.path("data");
|
||||||
if (dataObj == null) {
|
if (dataObj.isMissingNode() || !dataObj.isObject()) {
|
||||||
log.error("No data object found in certificate");
|
log.error("No data object found in certificate");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract license or machine information
|
// Extract license or machine information
|
||||||
JSONObject attributesObj = dataObj.optJSONObject("attributes");
|
JsonNode attributesObj = dataObj.path("attributes");
|
||||||
if (attributesObj != null) {
|
if (!attributesObj.isMissingNode() && attributesObj.isObject()) {
|
||||||
log.info("Found attributes in certificate data");
|
log.info("Found attributes in certificate data");
|
||||||
|
|
||||||
// Check for floating license
|
// Check for floating license
|
||||||
context.isFloatingLicense = attributesObj.optBoolean("floating", false);
|
context.isFloatingLicense = attributesObj.path("floating").asBoolean(false);
|
||||||
context.maxMachines = attributesObj.optInt("maxMachines", 1);
|
context.maxMachines = attributesObj.path("maxMachines").asInt(1);
|
||||||
|
|
||||||
// Extract metadata
|
// Extract metadata
|
||||||
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
|
JsonNode metadataObj = attributesObj.path("metadata");
|
||||||
if (metadataObj != null) {
|
if (!metadataObj.isMissingNode() && metadataObj.isObject()) {
|
||||||
// Check if this is an old license (no planType) with isEnterprise flag
|
// Check if this is an old license (no planType) with isEnterprise flag
|
||||||
context.isEnterpriseLicense = metadataObj.optBoolean("isEnterprise", false);
|
context.isEnterpriseLicense = metadataObj.path("isEnterprise").asBoolean(false);
|
||||||
|
|
||||||
// Extract user count - default based on license type
|
// Extract user count - default based on license type
|
||||||
// Old licenses: Only had isEnterprise flag
|
// Old licenses: Only had isEnterprise flag
|
||||||
// New licenses: Have planType field with "server" or "enterprise"
|
// New licenses: Have planType field with "server" or "enterprise"
|
||||||
int users = metadataObj.optInt("users", context.isEnterpriseLicense ? 1 : 0);
|
int users =
|
||||||
|
metadataObj.path("users").asInt(context.isEnterpriseLicense ? 1 : 0);
|
||||||
|
|
||||||
// SERVER license (isEnterprise=false, users=0) = unlimited
|
// SERVER license (isEnterprise=false, users=0) = unlimited
|
||||||
// ENTERPRISE license (isEnterprise=true, users>0) = limited seats
|
// ENTERPRISE license (isEnterprise=true, users>0) = limited seats
|
||||||
@@ -282,7 +287,7 @@ public class KeygenLicenseVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check license status if available
|
// Check license status if available
|
||||||
String status = attributesObj.optString("status", null);
|
String status = attributesObj.path("status").asText(null);
|
||||||
if (status != null
|
if (status != null
|
||||||
&& !"ACTIVE".equals(status)
|
&& !"ACTIVE".equals(status)
|
||||||
&& !"EXPIRING".equals(status)) { // Accept "EXPIRING" status as valid
|
&& !"EXPIRING".equals(status)) { // Accept "EXPIRING" status as valid
|
||||||
@@ -372,11 +377,11 @@ public class KeygenLicenseVerifier {
|
|||||||
try {
|
try {
|
||||||
log.info("Processing license payload: {}", payload);
|
log.info("Processing license payload: {}", payload);
|
||||||
|
|
||||||
JSONObject licenseData = new JSONObject(payload);
|
JsonNode licenseData = objectMapper.readTree(payload);
|
||||||
|
|
||||||
JSONObject licenseObj = licenseData.optJSONObject("license");
|
JsonNode licenseObj = licenseData.path("license");
|
||||||
if (licenseObj == null) {
|
if (licenseObj.isMissingNode() || !licenseObj.isObject()) {
|
||||||
String id = licenseData.optString("id", null);
|
String id = licenseData.path("id").asText(null);
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
log.info("Found license ID: {}", id);
|
log.info("Found license ID: {}", id);
|
||||||
licenseObj = licenseData; // Use the root object as the license object
|
licenseObj = licenseData; // Use the root object as the license object
|
||||||
@@ -386,18 +391,18 @@ public class KeygenLicenseVerifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String licenseId = licenseObj.optString("id", "unknown");
|
String licenseId = licenseObj.path("id").asText("unknown");
|
||||||
log.info("Processing license with ID: {}", licenseId);
|
log.info("Processing license with ID: {}", licenseId);
|
||||||
|
|
||||||
// Check for floating license in license object
|
// Check for floating license in license object
|
||||||
context.isFloatingLicense = licenseObj.optBoolean("floating", false);
|
context.isFloatingLicense = licenseObj.path("floating").asBoolean(false);
|
||||||
context.maxMachines = licenseObj.optInt("maxMachines", 1);
|
context.maxMachines = licenseObj.path("maxMachines").asInt(1);
|
||||||
if (context.isFloatingLicense) {
|
if (context.isFloatingLicense) {
|
||||||
log.info("Detected floating license with max machines: {}", context.maxMachines);
|
log.info("Detected floating license with max machines: {}", context.maxMachines);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check expiry date
|
// Check expiry date
|
||||||
String expiryStr = licenseObj.optString("expiry", null);
|
String expiryStr = licenseObj.path("expiry").asText(null);
|
||||||
if (expiryStr != null && !"null".equals(expiryStr)) {
|
if (expiryStr != null && !"null".equals(expiryStr)) {
|
||||||
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
|
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
|
||||||
java.time.Instant now = java.time.Instant.now();
|
java.time.Instant now = java.time.Instant.now();
|
||||||
@@ -413,9 +418,9 @@ public class KeygenLicenseVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract account, product, policy info
|
// Extract account, product, policy info
|
||||||
JSONObject accountObj = licenseData.optJSONObject("account");
|
JsonNode accountObj = licenseData.path("account");
|
||||||
if (accountObj != null) {
|
if (!accountObj.isMissingNode() && accountObj.isObject()) {
|
||||||
String accountId = accountObj.optString("id", "unknown");
|
String accountId = accountObj.path("id").asText("unknown");
|
||||||
log.info("License belongs to account: {}", accountId);
|
log.info("License belongs to account: {}", accountId);
|
||||||
|
|
||||||
// Verify this matches your expected account ID
|
// Verify this matches your expected account ID
|
||||||
@@ -426,14 +431,14 @@ public class KeygenLicenseVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract policy information if available
|
// Extract policy information if available
|
||||||
JSONObject policyObj = licenseData.optJSONObject("policy");
|
JsonNode policyObj = licenseData.path("policy");
|
||||||
if (policyObj != null) {
|
if (!policyObj.isMissingNode() && policyObj.isObject()) {
|
||||||
String policyId = policyObj.optString("id", "unknown");
|
String policyId = policyObj.path("id").asText("unknown");
|
||||||
log.info("License uses policy: {}", policyId);
|
log.info("License uses policy: {}", policyId);
|
||||||
|
|
||||||
// Check for floating license in policy
|
// Check for floating license in policy
|
||||||
boolean policyFloating = policyObj.optBoolean("floating", false);
|
boolean policyFloating = policyObj.path("floating").asBoolean(false);
|
||||||
int policyMaxMachines = policyObj.optInt("maxMachines", 1);
|
int policyMaxMachines = policyObj.path("maxMachines").asInt(1);
|
||||||
|
|
||||||
// Policy settings take precedence
|
// Policy settings take precedence
|
||||||
if (policyFloating) {
|
if (policyFloating) {
|
||||||
@@ -445,16 +450,21 @@ public class KeygenLicenseVerifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract max users and isEnterprise from policy or metadata
|
// Extract max users and isEnterprise from policy or metadata
|
||||||
context.isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false);
|
context.isEnterpriseLicense = policyObj.path("isEnterprise").asBoolean(false);
|
||||||
int users = policyObj.optInt("users", -1);
|
int users = policyObj.path("users").asInt(-1);
|
||||||
|
|
||||||
if (users == -1) {
|
if (users == -1) {
|
||||||
// Try to get users from metadata if not at policy level
|
// Try to get users from metadata if not at policy level
|
||||||
Object metadataObj = policyObj.opt("metadata");
|
JsonNode metadataObj = policyObj.path("metadata");
|
||||||
if (metadataObj instanceof JSONObject metadata) {
|
if (!metadataObj.isMissingNode() && metadataObj.isObject()) {
|
||||||
context.isEnterpriseLicense =
|
context.isEnterpriseLicense =
|
||||||
metadata.optBoolean("isEnterprise", context.isEnterpriseLicense);
|
metadataObj
|
||||||
users = metadata.optInt("users", context.isEnterpriseLicense ? 1 : 0);
|
.path("isEnterprise")
|
||||||
|
.asBoolean(context.isEnterpriseLicense);
|
||||||
|
users =
|
||||||
|
metadataObj
|
||||||
|
.path("users")
|
||||||
|
.asInt(context.isEnterpriseLicense ? 1 : 0);
|
||||||
} else {
|
} else {
|
||||||
// Default based on license type
|
// Default based on license type
|
||||||
users = context.isEnterpriseLicense ? 1 : 0;
|
users = context.isEnterpriseLicense ? 1 : 0;
|
||||||
@@ -493,9 +503,9 @@ public class KeygenLicenseVerifier {
|
|||||||
validateLicense(licenseKey, machineFingerprint, context);
|
validateLicense(licenseKey, machineFingerprint, context);
|
||||||
if (validationResponse != null) {
|
if (validationResponse != null) {
|
||||||
boolean isValid = validationResponse.path("meta").path("valid").asBoolean();
|
boolean isValid = validationResponse.path("meta").path("valid").asBoolean();
|
||||||
String licenseId = validationResponse.path("data").path("id").asText();
|
String licenseId = validationResponse.path("data").path("id").asText("");
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
String code = validationResponse.path("meta").path("code").asText();
|
String code = validationResponse.path("meta").path("code").asText("");
|
||||||
log.info(code);
|
log.info(code);
|
||||||
if ("NO_MACHINE".equals(code)
|
if ("NO_MACHINE".equals(code)
|
||||||
|| "NO_MACHINES".equals(code)
|
|| "NO_MACHINES".equals(code)
|
||||||
@@ -579,8 +589,8 @@ public class KeygenLicenseVerifier {
|
|||||||
JsonNode metaNode = jsonResponse.path("meta");
|
JsonNode metaNode = jsonResponse.path("meta");
|
||||||
boolean isValid = metaNode.path("valid").asBoolean();
|
boolean isValid = metaNode.path("valid").asBoolean();
|
||||||
|
|
||||||
String detail = metaNode.path("detail").asText();
|
String detail = metaNode.path("detail").asText("");
|
||||||
String code = metaNode.path("code").asText();
|
String code = metaNode.path("code").asText("");
|
||||||
|
|
||||||
log.info("License validity: {}", isValid);
|
log.info("License validity: {}", isValid);
|
||||||
log.info("Validation detail: {}", detail);
|
log.info("Validation detail: {}", detail);
|
||||||
@@ -604,7 +614,7 @@ public class KeygenLicenseVerifier {
|
|||||||
|
|
||||||
if (includedNode.isArray()) {
|
if (includedNode.isArray()) {
|
||||||
for (JsonNode node : includedNode) {
|
for (JsonNode node : includedNode) {
|
||||||
if ("policies".equals(node.path("type").asText())) {
|
if ("policies".equals(node.path("type").asText(""))) {
|
||||||
policyNode = node;
|
policyNode = node;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -690,9 +700,9 @@ public class KeygenLicenseVerifier {
|
|||||||
|
|
||||||
for (JsonNode machine : machines) {
|
for (JsonNode machine : machines) {
|
||||||
if (machineFingerprint.equals(
|
if (machineFingerprint.equals(
|
||||||
machine.path("attributes").path("fingerprint").asText())) {
|
machine.path("attributes").path("fingerprint").asText(""))) {
|
||||||
isCurrentMachineActivated = true;
|
isCurrentMachineActivated = true;
|
||||||
currentMachineId = machine.path("id").asText();
|
currentMachineId = machine.path("id").asText("");
|
||||||
log.info(
|
log.info(
|
||||||
"Current machine is already activated with ID: {}",
|
"Current machine is already activated with ID: {}",
|
||||||
currentMachineId);
|
currentMachineId);
|
||||||
@@ -726,7 +736,7 @@ public class KeygenLicenseVerifier {
|
|||||||
java.time.Instant.parse(createdStr);
|
java.time.Instant.parse(createdStr);
|
||||||
if (oldestTime == null || createdTime.isBefore(oldestTime)) {
|
if (oldestTime == null || createdTime.isBefore(oldestTime)) {
|
||||||
oldestTime = createdTime;
|
oldestTime = createdTime;
|
||||||
oldestMachineId = machine.path("id").asText();
|
oldestMachineId = machine.path("id").asText("");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn(
|
log.warn(
|
||||||
@@ -740,7 +750,7 @@ public class KeygenLicenseVerifier {
|
|||||||
if (oldestMachineId == null) {
|
if (oldestMachineId == null) {
|
||||||
log.warn(
|
log.warn(
|
||||||
"Could not determine oldest machine by timestamp, using first machine in list");
|
"Could not determine oldest machine by timestamp, using first machine in list");
|
||||||
oldestMachineId = machines.path(0).path("id").asText();
|
oldestMachineId = machines.path(0).path("id").asText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Deregistering machine with ID: {}", oldestMachineId);
|
log.info("Deregistering machine with ID: {}", oldestMachineId);
|
||||||
@@ -770,35 +780,28 @@ public class KeygenLicenseVerifier {
|
|||||||
hostname = "Unknown";
|
hostname = "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
JSONObject body =
|
tools.jackson.databind.node.ObjectNode attributes = objectMapper.createObjectNode();
|
||||||
new JSONObject()
|
attributes.put("fingerprint", machineFingerprint);
|
||||||
.put(
|
attributes.put("platform", System.getProperty("os.name"));
|
||||||
"data",
|
attributes.put("name", hostname);
|
||||||
new JSONObject()
|
|
||||||
.put("type", "machines")
|
tools.jackson.databind.node.ObjectNode licenseRef = objectMapper.createObjectNode();
|
||||||
.put(
|
licenseRef.put("type", "licenses");
|
||||||
"attributes",
|
licenseRef.put("id", licenseId);
|
||||||
new JSONObject()
|
|
||||||
.put("fingerprint", machineFingerprint)
|
tools.jackson.databind.node.ObjectNode licenseRelation = objectMapper.createObjectNode();
|
||||||
.put(
|
licenseRelation.set("data", licenseRef);
|
||||||
"platform",
|
|
||||||
System.getProperty("os.name"))
|
tools.jackson.databind.node.ObjectNode relationships = objectMapper.createObjectNode();
|
||||||
.put("name", hostname))
|
relationships.set("license", licenseRelation);
|
||||||
.put(
|
|
||||||
"relationships",
|
tools.jackson.databind.node.ObjectNode data = objectMapper.createObjectNode();
|
||||||
new JSONObject()
|
data.put("type", "machines");
|
||||||
.put(
|
data.set("attributes", attributes);
|
||||||
"license",
|
data.set("relationships", relationships);
|
||||||
new JSONObject()
|
|
||||||
.put(
|
tools.jackson.databind.node.ObjectNode body = objectMapper.createObjectNode();
|
||||||
"data",
|
body.set("data", data);
|
||||||
new JSONObject()
|
|
||||||
.put(
|
|
||||||
"type",
|
|
||||||
"licenses")
|
|
||||||
.put(
|
|
||||||
"id",
|
|
||||||
licenseId)))));
|
|
||||||
|
|
||||||
HttpRequest request =
|
HttpRequest request =
|
||||||
HttpRequest.newBuilder()
|
HttpRequest.newBuilder()
|
||||||
|
|||||||
+6
-6
@@ -27,9 +27,6 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.util.HtmlUtils;
|
import org.springframework.web.util.HtmlUtils;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
@@ -49,6 +46,9 @@ import stirling.software.proprietary.security.model.api.admin.SettingValueRespon
|
|||||||
import stirling.software.proprietary.security.model.api.admin.UpdateSettingValueRequest;
|
import stirling.software.proprietary.security.model.api.admin.UpdateSettingValueRequest;
|
||||||
import stirling.software.proprietary.security.model.api.admin.UpdateSettingsRequest;
|
import stirling.software.proprietary.security.model.api.admin.UpdateSettingsRequest;
|
||||||
|
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@AdminApi
|
@AdminApi
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@@ -543,7 +543,8 @@ public class AdminSettingsController {
|
|||||||
pendingChanges.clear();
|
pendingChanges.clear();
|
||||||
|
|
||||||
// Give the HTTP response time to complete, then exit
|
// Give the HTTP response time to complete, then exit
|
||||||
new Thread(
|
Thread.ofVirtual()
|
||||||
|
.start(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
@@ -554,8 +555,7 @@ public class AdminSettingsController {
|
|||||||
log.error("Restart interrupted: {}", e.getMessage(), e);
|
log.error("Restart interrupted: {}", e.getMessage(), e);
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.start();
|
|
||||||
|
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
Map.of(
|
Map.of(
|
||||||
|
|||||||
+6
-5
@@ -20,9 +20,6 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -31,6 +28,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
|
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/ui-data")
|
@RequestMapping("/api/v1/ui-data")
|
||||||
@@ -39,6 +39,7 @@ public class UIDataTessdataController {
|
|||||||
|
|
||||||
private static final Pattern INVALID_LANG_CHARS_PATTERN = Pattern.compile("[^A-Za-z0-9_+\\-]");
|
private static final Pattern INVALID_LANG_CHARS_PATTERN = Pattern.compile("[^A-Za-z0-9_+\\-]");
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
private static volatile List<String> cachedRemoteTessdata = null;
|
private static volatile List<String> cachedRemoteTessdata = null;
|
||||||
private static volatile long cachedRemoteTessdataExpiry = 0L;
|
private static volatile long cachedRemoteTessdataExpiry = 0L;
|
||||||
private static final long REMOTE_TESSDATA_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
private static final long REMOTE_TESSDATA_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
||||||
@@ -223,9 +224,9 @@ public class UIDataTessdataController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try (InputStream is = connection.getInputStream()) {
|
try (InputStream is = connection.getInputStream()) {
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
|
||||||
List<Map<String, Object>> items =
|
List<Map<String, Object>> items =
|
||||||
mapper.readValue(is, new TypeReference<List<Map<String, Object>>>() {});
|
objectMapper.readValue(
|
||||||
|
is, new TypeReference<List<Map<String, Object>>>() {});
|
||||||
List<String> languages =
|
List<String> languages =
|
||||||
items.stream()
|
items.stream()
|
||||||
.map(item -> (String) item.get("name"))
|
.map(item -> (String) item.get("name"))
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
|
|||||||
private Object credentials;
|
private Object credentials;
|
||||||
|
|
||||||
public ApiKeyAuthenticationToken(String apiKey) {
|
public ApiKeyAuthenticationToken(String apiKey) {
|
||||||
super(null);
|
super((Collection<? extends GrantedAuthority>) null);
|
||||||
this.principal = null;
|
this.principal = null;
|
||||||
this.credentials = apiKey;
|
this.credentials = apiKey;
|
||||||
setAuthenticated(false);
|
setAuthenticated(false);
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ import org.opensaml.saml.saml2.core.AuthnStatement;
|
|||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.core.convert.converter.Converter;
|
import org.springframework.core.convert.converter.Converter;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
|
import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseToken;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|||||||
+4
-4
@@ -15,7 +15,7 @@ import org.springframework.security.saml2.provider.service.registration.InMemory
|
|||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
@@ -151,10 +151,10 @@ public class Saml2Configuration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
public OpenSaml5AuthenticationRequestResolver authenticationRequestResolver(
|
||||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||||
OpenSaml4AuthenticationRequestResolver resolver =
|
OpenSaml5AuthenticationRequestResolver resolver =
|
||||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
new OpenSaml5AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||||
|
|
||||||
resolver.setRelayStateResolver(
|
resolver.setRelayStateResolver(
|
||||||
request -> {
|
request -> {
|
||||||
|
|||||||
+8
-7
@@ -20,9 +20,6 @@ import org.springframework.security.core.userdetails.UserDetails;
|
|||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import io.jsonwebtoken.ExpiredJwtException;
|
import io.jsonwebtoken.ExpiredJwtException;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
@@ -40,21 +37,25 @@ import stirling.software.proprietary.security.model.JwtVerificationKey;
|
|||||||
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
|
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
|
||||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
|
|
||||||
|
import tools.jackson.core.type.TypeReference;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class JwtService implements JwtServiceInterface {
|
public class JwtService implements JwtServiceInterface {
|
||||||
|
|
||||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private final KeyPersistenceServiceInterface keyPersistenceService;
|
private final KeyPersistenceServiceInterface keyPersistenceService;
|
||||||
private final boolean v2Enabled;
|
private final boolean v2Enabled;
|
||||||
private final ApplicationProperties.Security securityProperties;
|
private final ApplicationProperties.Security securityProperties;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public JwtService(
|
public JwtService(
|
||||||
|
ObjectMapper objectMapper,
|
||||||
@Qualifier("v2Enabled") boolean v2Enabled,
|
@Qualifier("v2Enabled") boolean v2Enabled,
|
||||||
KeyPersistenceServiceInterface keyPersistenceService,
|
KeyPersistenceServiceInterface keyPersistenceService,
|
||||||
ApplicationProperties applicationProperties) {
|
ApplicationProperties applicationProperties) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
this.v2Enabled = v2Enabled;
|
this.v2Enabled = v2Enabled;
|
||||||
this.keyPersistenceService = keyPersistenceService;
|
this.keyPersistenceService = keyPersistenceService;
|
||||||
this.securityProperties = applicationProperties.getSecurity();
|
this.securityProperties = applicationProperties.getSecurity();
|
||||||
@@ -374,14 +375,14 @@ public class JwtService implements JwtServiceInterface {
|
|||||||
|
|
||||||
byte[] headerBytes = Base64.getUrlDecoder().decode(tokenParts[0]);
|
byte[] headerBytes = Base64.getUrlDecoder().decode(tokenParts[0]);
|
||||||
Map<String, Object> header =
|
Map<String, Object> header =
|
||||||
OBJECT_MAPPER.readValue(
|
objectMapper.readValue(
|
||||||
headerBytes, new TypeReference<Map<String, Object>>() {});
|
headerBytes, new TypeReference<Map<String, Object>>() {});
|
||||||
Object keyId = header.get("kid");
|
Object keyId = header.get("kid");
|
||||||
return keyId instanceof String ? (String) keyId : null;
|
return keyId instanceof String ? (String) keyId : null;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.debug("Failed to decode Base64 JWT header: {}", e.getMessage());
|
log.debug("Failed to decode Base64 JWT header: {}", e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
} catch (java.io.IOException e) {
|
} catch (tools.jackson.core.JacksonException e) {
|
||||||
log.debug("Failed to parse JWT header as JSON: {}", e.getMessage());
|
log.debug("Failed to parse JWT header as JSON: {}", e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -15,8 +15,6 @@ import java.util.stream.Stream;
|
|||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.configuration.InstallationPathConfig;
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
@@ -24,6 +22,8 @@ import stirling.software.common.service.PersonalSignatureServiceInterface;
|
|||||||
import stirling.software.proprietary.model.api.signature.SavedSignatureRequest;
|
import stirling.software.proprietary.model.api.signature.SavedSignatureRequest;
|
||||||
import stirling.software.proprietary.model.api.signature.SavedSignatureResponse;
|
import stirling.software.proprietary.model.api.signature.SavedSignatureResponse;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for managing user signatures with authentication and storage limits. This proprietary
|
* Service for managing user signatures with authentication and storage limits. This proprietary
|
||||||
* version enforces per-user quotas and requires authentication. Provides access to personal
|
* version enforces per-user quotas and requires authentication. Provides access to personal
|
||||||
@@ -36,15 +36,16 @@ public class SignatureService implements PersonalSignatureServiceInterface {
|
|||||||
private static final Pattern FILENAME_VALIDATION_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]+$");
|
private static final Pattern FILENAME_VALIDATION_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]+$");
|
||||||
private final String SIGNATURE_BASE_PATH;
|
private final String SIGNATURE_BASE_PATH;
|
||||||
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
// Storage limits per user
|
// Storage limits per user
|
||||||
private static final int MAX_SIGNATURES_PER_USER = 20;
|
private static final int MAX_SIGNATURES_PER_USER = 20;
|
||||||
private static final long MAX_SIGNATURE_SIZE_BYTES = 2_000_000; // 2MB per signature
|
private static final long MAX_SIGNATURE_SIZE_BYTES = 2_000_000; // 2MB per signature
|
||||||
private static final long MAX_TOTAL_USER_STORAGE_BYTES = 20_000_000; // 20MB total per user
|
private static final long MAX_TOTAL_USER_STORAGE_BYTES = 20_000_000; // 20MB total per user
|
||||||
|
|
||||||
public SignatureService() {
|
public SignatureService(ObjectMapper objectMapper) {
|
||||||
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
|
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+4
-3
@@ -16,8 +16,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.model.enumeration.Role;
|
import stirling.software.common.model.enumeration.Role;
|
||||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||||
@@ -35,6 +33,9 @@ import stirling.software.proprietary.security.service.MfaService;
|
|||||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.proprietary.service.UserLicenseSettingsService;
|
import stirling.software.proprietary.service.UserLicenseSettingsService;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ProprietaryUIDataControllerTest {
|
class ProprietaryUIDataControllerTest {
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ class ProprietaryUIDataControllerTest {
|
|||||||
applicationProperties.getSecurity().getSaml2().setEnabled(false);
|
applicationProperties.getSecurity().getSaml2().setEnabled(false);
|
||||||
|
|
||||||
auditConfig = new AuditConfigurationProperties(applicationProperties);
|
auditConfig = new AuditConfigurationProperties(applicationProperties);
|
||||||
objectMapper = new ObjectMapper();
|
objectMapper = JsonMapper.builder().build();
|
||||||
|
|
||||||
controller =
|
controller =
|
||||||
new ProprietaryUIDataController(
|
new ProprietaryUIDataController(
|
||||||
|
|||||||
+4
-3
@@ -26,8 +26,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
|||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.model.enumeration.Role;
|
import stirling.software.common.model.enumeration.Role;
|
||||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||||
@@ -42,10 +40,13 @@ import stirling.software.proprietary.security.service.RefreshRateLimitService;
|
|||||||
import stirling.software.proprietary.security.service.TotpService;
|
import stirling.software.proprietary.security.service.TotpService;
|
||||||
import stirling.software.proprietary.security.service.UserService;
|
import stirling.software.proprietary.security.service.UserService;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class AuthControllerLoginTest {
|
class AuthControllerLoginTest {
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||||
|
|
||||||
private MockMvc mockMvc;
|
private MockMvc mockMvc;
|
||||||
private ApplicationProperties.Security securityProperties;
|
private ApplicationProperties.Security securityProperties;
|
||||||
|
|||||||
+4
-3
@@ -26,8 +26,6 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||||
import stirling.software.proprietary.security.model.User;
|
import stirling.software.proprietary.security.model.User;
|
||||||
import stirling.software.proprietary.security.service.CustomUserDetailsService;
|
import stirling.software.proprietary.security.service.CustomUserDetailsService;
|
||||||
@@ -37,12 +35,15 @@ import stirling.software.proprietary.security.service.MfaService;
|
|||||||
import stirling.software.proprietary.security.service.TotpService;
|
import stirling.software.proprietary.security.service.TotpService;
|
||||||
import stirling.software.proprietary.security.service.UserService;
|
import stirling.software.proprietary.security.service.UserService;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class AuthControllerMfaTest {
|
class AuthControllerMfaTest {
|
||||||
|
|
||||||
private static final String USERNAME = "[email protected]";
|
private static final String USERNAME = "[email protected]";
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||||
|
|
||||||
private MockMvc mockMvc;
|
private MockMvc mockMvc;
|
||||||
private Authentication authentication;
|
private Authentication authentication;
|
||||||
|
|||||||
+15
-13
@@ -19,6 +19,8 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|||||||
|
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
|
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
class UIDataTessdataControllerTest {
|
class UIDataTessdataControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -27,7 +29,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn("ignored/path");
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn("ignored/path");
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> getRemoteTessdataLanguages() {
|
protected List<String> getRemoteTessdataLanguages() {
|
||||||
return List.of("eng");
|
return List.of("eng");
|
||||||
@@ -49,7 +51,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> getRemoteTessdataLanguages() {
|
protected List<String> getRemoteTessdataLanguages() {
|
||||||
return List.of("eng");
|
return List.of("eng");
|
||||||
@@ -77,7 +79,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> getRemoteTessdataLanguages() {
|
protected List<String> getRemoteTessdataLanguages() {
|
||||||
return List.of("eng");
|
return List.of("eng");
|
||||||
@@ -100,7 +102,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> getRemoteTessdataLanguages() {
|
protected List<String> getRemoteTessdataLanguages() {
|
||||||
return List.of("eng", "fra");
|
return List.of("eng", "fra");
|
||||||
@@ -139,7 +141,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> getRemoteTessdataLanguages() {
|
protected List<String> getRemoteTessdataLanguages() {
|
||||||
return List.of("eng");
|
return List.of("eng");
|
||||||
@@ -164,7 +166,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected boolean isWritableDirectory(Path dir) {
|
protected boolean isWritableDirectory(Path dir) {
|
||||||
return false;
|
return false;
|
||||||
@@ -186,7 +188,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> getRemoteTessdataLanguages() {
|
protected List<String> getRemoteTessdataLanguages() {
|
||||||
return List.of("eng");
|
return List.of("eng");
|
||||||
@@ -217,7 +219,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> getRemoteTessdataLanguages() {
|
protected List<String> getRemoteTessdataLanguages() {
|
||||||
return List.of("eng");
|
return List.of("eng");
|
||||||
@@ -258,7 +260,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> getRemoteTessdataLanguages() {
|
protected List<String> getRemoteTessdataLanguages() {
|
||||||
return List.of("eng", "fra");
|
return List.of("eng", "fra");
|
||||||
@@ -282,7 +284,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> getRemoteTessdataLanguages() {
|
protected List<String> getRemoteTessdataLanguages() {
|
||||||
return List.of("eng");
|
return List.of("eng");
|
||||||
@@ -307,7 +309,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> getRemoteTessdataLanguages() {
|
protected List<String> getRemoteTessdataLanguages() {
|
||||||
return List.of("eng");
|
return List.of("eng");
|
||||||
@@ -330,7 +332,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(missingDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(missingDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected List<String> getRemoteTessdataLanguages() {
|
protected List<String> getRemoteTessdataLanguages() {
|
||||||
return List.of("eng");
|
return List.of("eng");
|
||||||
@@ -352,7 +354,7 @@ class UIDataTessdataControllerTest {
|
|||||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
UIDataTessdataController controller =
|
UIDataTessdataController controller =
|
||||||
new UIDataTessdataController(runtimePathConfig) {
|
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||||
@Override
|
@Override
|
||||||
protected boolean isWritableDirectory(Path dir) {
|
protected boolean isWritableDirectory(Path dir) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+4
-3
@@ -21,8 +21,6 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.proprietary.model.Team;
|
import stirling.software.proprietary.model.Team;
|
||||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||||
@@ -35,10 +33,13 @@ import stirling.software.proprietary.security.service.UserService;
|
|||||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.proprietary.service.UserLicenseSettingsService;
|
import stirling.software.proprietary.service.UserLicenseSettingsService;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class UserControllerTest {
|
class UserControllerTest {
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||||
|
|
||||||
@Mock private UserService userService;
|
@Mock private UserService userService;
|
||||||
@Mock private SessionPersistentRegistry sessionRegistry;
|
@Mock private SessionPersistentRegistry sessionRegistry;
|
||||||
|
|||||||
+5
-1
@@ -36,6 +36,9 @@ import stirling.software.proprietary.security.model.JwtVerificationKey;
|
|||||||
import stirling.software.proprietary.security.model.User;
|
import stirling.software.proprietary.security.model.User;
|
||||||
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
|
import stirling.software.proprietary.security.model.exception.AuthenticationFailureException;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class JwtServiceTest {
|
class JwtServiceTest {
|
||||||
|
|
||||||
@@ -66,7 +69,8 @@ class JwtServiceTest {
|
|||||||
testVerificationKey = new JwtVerificationKey("test-key-id", encodedPublicKey);
|
testVerificationKey = new JwtVerificationKey("test-key-id", encodedPublicKey);
|
||||||
|
|
||||||
ApplicationProperties applicationProperties = new ApplicationProperties();
|
ApplicationProperties applicationProperties = new ApplicationProperties();
|
||||||
jwtService = new JwtService(true, keystoreService, applicationProperties);
|
ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||||
|
jwtService = new JwtService(objectMapper, true, keystoreService, applicationProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+51
-16
@@ -2,7 +2,7 @@ plugins {
|
|||||||
id "java"
|
id "java"
|
||||||
id "jacoco"
|
id "jacoco"
|
||||||
id "io.spring.dependency-management" version "1.1.7"
|
id "io.spring.dependency-management" version "1.1.7"
|
||||||
id "org.springframework.boot" version "3.5.9"
|
id "org.springframework.boot" version "4.0.3"
|
||||||
id "org.springdoc.openapi-gradle-plugin" version "1.9.0"
|
id "org.springdoc.openapi-gradle-plugin" version "1.9.0"
|
||||||
id "io.swagger.swaggerhub" version "1.3.2"
|
id "io.swagger.swaggerhub" version "1.3.2"
|
||||||
id "com.diffplug.spotless" version "8.1.0"
|
id "com.diffplug.spotless" version "8.1.0"
|
||||||
@@ -15,20 +15,31 @@ import com.github.jk1.license.render.*
|
|||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
import groovy.json.JsonSlurper
|
import groovy.json.JsonSlurper
|
||||||
import groovy.xml.XmlSlurper
|
import groovy.xml.XmlSlurper
|
||||||
|
import org.gradle.api.JavaVersion
|
||||||
import org.gradle.api.tasks.testing.Test
|
import org.gradle.api.tasks.testing.Test
|
||||||
|
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
springBootVersion = "3.5.9"
|
springBootVersion = "4.0.3"
|
||||||
pdfboxVersion = "3.0.6"
|
pdfboxVersion = "3.0.6"
|
||||||
imageioVersion = "3.13.0"
|
imageioVersion = "3.13.0"
|
||||||
lombokVersion = "1.18.42"
|
lombokVersion = "1.18.42"
|
||||||
bouncycastleVersion = "1.83"
|
bouncycastleVersion = "1.83"
|
||||||
springSecuritySamlVersion = "6.5.6"
|
springSecuritySamlVersion = "7.0.2"
|
||||||
openSamlVersion = "4.3.2"
|
openSamlVersion = "4.3.2"
|
||||||
commonmarkVersion = "0.27.0"
|
commonmarkVersion = "0.27.1"
|
||||||
googleJavaFormatVersion = "1.28.0"
|
googleJavaFormatVersion = "1.34.1"
|
||||||
logback = "1.5.25"
|
logback = "1.5.28"
|
||||||
junitPlatformVersion = "1.12.2"
|
junitPlatformVersion = "1.12.2"
|
||||||
|
modernJavaVersion = 21
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(project.findProperty('javaVersion')?.toString() ?: '25')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.isSecurityDisabled = { ->
|
ext.isSecurityDisabled = { ->
|
||||||
@@ -70,7 +81,6 @@ allprojects {
|
|||||||
version = '2.5.3'
|
version = '2.5.3'
|
||||||
|
|
||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
|
||||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,8 +148,11 @@ subprojects {
|
|||||||
apply plugin: 'jacoco'
|
apply plugin: 'jacoco'
|
||||||
|
|
||||||
java {
|
java {
|
||||||
// 17 is lowest but we support and recommend 21
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(25)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (project.name != "stirling-pdf") {
|
if (project.name != "stirling-pdf") {
|
||||||
@@ -167,12 +180,14 @@ subprojects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
|
||||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
||||||
// Exclude vulnerable BouncyCastle version used in tableau
|
// Exclude vulnerable BouncyCastle version used in tableau
|
||||||
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
|
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
|
||||||
exclude group: 'org.bouncycastle', module: 'bcutil-jdk15on'
|
exclude group: 'org.bouncycastle', module: 'bcutil-jdk15on'
|
||||||
exclude group: 'org.bouncycastle', module: 'bcmail-jdk15on'
|
exclude group: 'org.bouncycastle', module: 'bcmail-jdk15on'
|
||||||
|
// google-java-format 1.34+ requires Guava 33.x (ImmutableSortedMapFauxverideShim);
|
||||||
|
// force it here so Spotless's FeatureClassLoader resolves the correct version.
|
||||||
|
resolutionStrategy.force 'com.google.guava:guava:33.4.8-jre'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencyManagement {
|
dependencyManagement {
|
||||||
@@ -191,7 +206,11 @@ subprojects {
|
|||||||
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||||
|
|
||||||
|
// Jackson 3 (Spring Boot 4 uses tools.jackson)
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-jackson'
|
||||||
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
|
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
|
||||||
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
||||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
|
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
|
||||||
|
|
||||||
@@ -201,13 +220,15 @@ subprojects {
|
|||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
tasks.withType(JavaCompile).configureEach {
|
||||||
options.encoding = "UTF-8"
|
options.encoding = "UTF-8"
|
||||||
|
options.release = rootProject.ext.modernJavaVersion
|
||||||
if (!project.hasProperty("noSpotless")) {
|
if (!project.hasProperty("noSpotless")) {
|
||||||
dependsOn "spotlessApply"
|
dependsOn "spotlessApply"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named("compileJava", JavaCompile).configure {
|
tasks.named("compileJava", JavaCompile).configure {
|
||||||
options.compilerArgs.add("-parameters")
|
// options.compilerArgs.add("-Xlint:deprecation")
|
||||||
|
// options.compilerArgs.add("-Xlint:unchecked")
|
||||||
}
|
}
|
||||||
|
|
||||||
def jacocoReport = tasks.named("jacocoTestReport")
|
def jacocoReport = tasks.named("jacocoTestReport")
|
||||||
@@ -387,6 +408,17 @@ subprojects {
|
|||||||
finalizedBy("copySwaggerDoc")
|
finalizedBy("copySwaggerDoc")
|
||||||
doNotTrackState("OpenAPI plugin writes outside build directory")
|
doNotTrackState("OpenAPI plugin writes outside build directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named("bootRun") {
|
||||||
|
jvmArgs = [
|
||||||
|
"-XX:+UseG1GC",
|
||||||
|
"-XX:MaxGCPauseMillis=200",
|
||||||
|
"-XX:G1HeapRegionSize=4m",
|
||||||
|
"-XX:+ExplicitGCInvokesConcurrent",
|
||||||
|
"-XX:+UseStringDeduplication",
|
||||||
|
"-XX:+UseCompactObjectHeaders"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,9 +565,12 @@ tasks.register('compileRestartHelper', JavaCompile) {
|
|||||||
|
|
||||||
source = fileTree(dir: 'scripts', include: 'RestartHelper.java')
|
source = fileTree(dir: 'scripts', include: 'RestartHelper.java')
|
||||||
classpath = files()
|
classpath = files()
|
||||||
destinationDirectory = file("${buildDir}/restart-helper-classes")
|
destinationDirectory = layout.buildDirectory.dir("restart-helper-classes")
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
def restartMajorVersion = project.ext.modernJavaVersion
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
def restartCompatibility = JavaVersion.toVersion(restartMajorVersion.toString())
|
||||||
|
sourceCompatibility = restartCompatibility
|
||||||
|
targetCompatibility = restartCompatibility
|
||||||
|
options.release.set(restartMajorVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task to create restart-helper.jar
|
// Task to create restart-helper.jar
|
||||||
@@ -544,9 +579,9 @@ tasks.register('buildRestartHelper', Jar) {
|
|||||||
description = 'Builds the restart-helper.jar'
|
description = 'Builds the restart-helper.jar'
|
||||||
dependsOn 'compileRestartHelper'
|
dependsOn 'compileRestartHelper'
|
||||||
|
|
||||||
from "${buildDir}/restart-helper-classes"
|
from layout.buildDirectory.dir("restart-helper-classes")
|
||||||
archiveFileName = 'restart-helper.jar'
|
archiveFileName = 'restart-helper.jar'
|
||||||
destinationDirectory = file("${buildDir}/libs")
|
destinationDirectory = layout.buildDirectory.dir("libs")
|
||||||
|
|
||||||
manifest {
|
manifest {
|
||||||
attributes 'Main-Class': 'RestartHelper'
|
attributes 'Main-Class': 'RestartHelper'
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ COPY gradle gradle/
|
|||||||
COPY app/core/build.gradle core/.
|
COPY app/core/build.gradle core/.
|
||||||
COPY app/common/build.gradle common/.
|
COPY app/common/build.gradle common/.
|
||||||
COPY app/proprietary/build.gradle proprietary/.
|
COPY app/proprietary/build.gradle proprietary/.
|
||||||
|
|
||||||
|
ENV JAVA_TOOL_OPTIONS="--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
|
||||||
|
|
||||||
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -54,7 +61,11 @@ LABEL org.opencontainers.image.keywords="PDF, manipulation, unified, API, Spring
|
|||||||
# Copy backend files
|
# Copy backend files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||||
COPY --from=backend-build /app/app/core/build/libs/*.jar app.jar
|
|
||||||
|
# Copy built JAR
|
||||||
|
# Use numeric UID:GID (1000:1000) since the named user doesn't exist yet at COPY time
|
||||||
|
COPY --from=backend-build --chown=1000:1000 \
|
||||||
|
/app/app/core/build/libs/*.jar app.jar
|
||||||
|
|
||||||
# Copy frontend files
|
# Copy frontend files
|
||||||
COPY --from=frontend-build /app/dist /usr/share/nginx/html
|
COPY --from=frontend-build /app/dist /usr/share/nginx/html
|
||||||
@@ -84,6 +95,7 @@ ENV VERSION_TAG=$VERSION_TAG \
|
|||||||
VITE_API_BASE_URL=http://localhost:8080
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
|
|
||||||
# Install all dependencies
|
# Install all dependencies
|
||||||
|
# Removed wasteful pip upgrade; chown moved to COPY above
|
||||||
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
@@ -122,11 +134,14 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
py3-pillow@testing \
|
py3-pillow@testing \
|
||||||
py3-pdf2image@testing && \
|
py3-pdf2image@testing && \
|
||||||
python3 -m venv /opt/venv && \
|
python3 -m venv /opt/venv && \
|
||||||
/opt/venv/bin/pip install --upgrade pip setuptools && \
|
/opt/venv/bin/pip install --no-cache-dir unoserver weasyprint && \
|
||||||
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
|
||||||
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
|
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||||
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
|
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||||
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
|
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
|
||||||
|
# Clean up pip + setuptools from venv
|
||||||
|
find /opt/venv -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true; \
|
||||||
|
rm -rf /opt/venv/lib/python*/site-packages/pip \
|
||||||
|
/opt/venv/lib/python*/site-packages/setuptools && \
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
mkdir -p $HOME /configs /configs/heap_dumps /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /pipeline/watchedFolders /pipeline/finishedFolders && \
|
mkdir -p $HOME /configs /configs/heap_dumps /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
mkdir -p /var/lib/nginx/tmp /var/log/nginx && \
|
mkdir -p /var/lib/nginx/tmp /var/log/nginx && \
|
||||||
@@ -135,8 +150,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
chmod +x /entrypoint.sh && \
|
chmod +x /entrypoint.sh && \
|
||||||
# User permissions
|
# User permissions
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf /var/lib/nginx /var/log/nginx /usr/share/nginx && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf /var/lib/nginx /var/log/nginx /usr/share/nginx
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ COPY gradle gradle/
|
|||||||
COPY app/core/build.gradle core/.
|
COPY app/core/build.gradle core/.
|
||||||
COPY app/common/build.gradle common/.
|
COPY app/common/build.gradle common/.
|
||||||
COPY app/proprietary/build.gradle proprietary/.
|
COPY app/proprietary/build.gradle proprietary/.
|
||||||
|
|
||||||
|
ENV JAVA_TOOL_OPTIONS="--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
|
||||||
|
|
||||||
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -53,7 +60,11 @@ LABEL org.opencontainers.image.keywords="PDF, manipulation, unified, ultra-lite,
|
|||||||
# Copy backend files
|
# Copy backend files
|
||||||
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||||
COPY scripts/installFonts.sh /scripts/installFonts.sh
|
COPY scripts/installFonts.sh /scripts/installFonts.sh
|
||||||
COPY --from=backend-build /app/app/core/build/libs/*.jar app.jar
|
|
||||||
|
# Copy built JAR
|
||||||
|
# Use numeric UID:GID (1000:1000) since the named user doesn't exist yet at COPY time
|
||||||
|
COPY --from=backend-build --chown=1000:1000 \
|
||||||
|
/app/app/core/build/libs/*.jar app.jar
|
||||||
|
|
||||||
# Copy frontend files
|
# Copy frontend files
|
||||||
COPY --from=frontend-build /app/dist /usr/share/nginx/html
|
COPY --from=frontend-build /app/dist /usr/share/nginx/html
|
||||||
@@ -80,6 +91,7 @@ ENV DISABLE_ADDITIONAL_FEATURES=false \
|
|||||||
ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||||
|
|
||||||
# Install minimal dependencies
|
# Install minimal dependencies
|
||||||
|
# /app.jar chown moved to COPY above
|
||||||
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
@@ -100,8 +112,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
chmod +x /entrypoint.sh && \
|
chmod +x /entrypoint.sh && \
|
||||||
# User permissions
|
# User permissions
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /configs /customFiles /pipeline /tmp/stirling-pdf /var/lib/nginx /var/log/nginx /usr/share/nginx && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /configs /customFiles /pipeline /tmp/stirling-pdf /var/lib/nginx /var/log/nginx /usr/share/nginx
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
# ==============================================================================
|
|
||||||
# Multi-stage Dockerfile for Stirling-PDF – image with everything included
|
|
||||||
# Includes: LibreOffice, Calibre, Tesseract, OCRmyPDF, unoserver, WeasyPrint, etc.
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
# ========================================
|
|
||||||
# STAGE 1: Build stage - Alpine with Gradle
|
|
||||||
# ========================================
|
|
||||||
FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build
|
|
||||||
|
|
||||||
COPY build.gradle .
|
|
||||||
COPY settings.gradle .
|
|
||||||
COPY gradlew .
|
|
||||||
COPY gradle gradle/
|
|
||||||
COPY app/core/build.gradle core/.
|
|
||||||
COPY app/common/build.gradle common/.
|
|
||||||
COPY app/proprietary/build.gradle proprietary/.
|
|
||||||
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
|
||||||
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the entire project to the working directory
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the application (server-only JAR - no UI, includes security features controlled at runtime)
|
|
||||||
RUN DISABLE_ADDITIONAL_FEATURES=false \
|
|
||||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
|
||||||
|
|
||||||
# ========================================
|
|
||||||
# STAGE 2: Runtime image based on Debian stable-slim
|
|
||||||
# Contains Java runtime + LibreOffice + Calibre + all PDF tools
|
|
||||||
# ========================================
|
|
||||||
FROM debian:stable-slim@sha256:7cb087f19bcc175b96fbe4c2aef42ed00733a659581a80f6ebccfd8fe3185a3d
|
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
ENV TESS_BASE_PATH=/usr/share/tesseract-ocr/5/tessdata
|
|
||||||
|
|
||||||
# Install core runtime dependencies + tools required by Stirling-PDF features
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates tzdata tini bash fontconfig \
|
|
||||||
openjdk-21-jre-headless \
|
|
||||||
ffmpeg poppler-utils ocrmypdf \
|
|
||||||
libreoffice-nogui libreoffice-java-common \
|
|
||||||
python3 python3-venv python3-uno \
|
|
||||||
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-fra \
|
|
||||||
tesseract-ocr-por tesseract-ocr-chi-sim \
|
|
||||||
libcairo2 libpango-1.0-0 libpangoft2-1.0-0 libgdk-pixbuf-2.0-0 \
|
|
||||||
gosu unpaper \
|
|
||||||
# AWT headless support (required for some Java graphics operations)
|
|
||||||
libfreetype6 libfontconfig1 libx11-6 libxt6 libxext6 libxrender1 libxtst6 libxi6 \
|
|
||||||
libxinerama1 libxkbcommon0 libxkbfile1 libsm6 libice6 \
|
|
||||||
# Qt WebEngine dependencies for Calibre
|
|
||||||
libegl1 libopengl0 libgl1 libxdamage1 libxfixes3 libxshmfence1 libdrm2 libgbm1 \
|
|
||||||
libxkbcommon-x11-0 libxrandr2 libxcomposite1 libnss3 libx11-xcb1 \
|
|
||||||
libxcb-cursor0 libdbus-1-3 libglib2.0-0 \
|
|
||||||
# Virtual framebuffer (required for headless LibreOffice)
|
|
||||||
xvfb x11-utils coreutils \
|
|
||||||
# Temporary packages only needed for Calibre installer
|
|
||||||
xz-utils gpgv curl xdg-utils \
|
|
||||||
\
|
|
||||||
# Install Calibre from official installer script
|
|
||||||
&& curl -fsSL https://download.calibre-ebook.com/linux-installer.sh | sh /dev/stdin \
|
|
||||||
\
|
|
||||||
# Clean up installer-only packages
|
|
||||||
&& apt-get purge -y xz-utils gpgv xdg-utils \
|
|
||||||
&& apt-get autoremove -y \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Make ebook-convert available in PATH
|
|
||||||
RUN ln -sf /opt/calibre/ebook-convert /usr/bin/ebook-convert \
|
|
||||||
&& /opt/calibre/ebook-convert --version
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Create non-root user (stirlingpdfuser) with configurable UID/GID
|
|
||||||
# ==============================================================================
|
|
||||||
ARG PUID=1000
|
|
||||||
ARG PGID=1000
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
# Create group if it doesn't exist
|
|
||||||
if ! getent group stirlingpdfgroup >/dev/null 2>&1; then \
|
|
||||||
if getent group "${PGID}" >/dev/null 2>&1; then \
|
|
||||||
groupadd -o -g "${PGID}" stirlingpdfgroup; \
|
|
||||||
else \
|
|
||||||
groupadd -g "${PGID}" stirlingpdfgroup; \
|
|
||||||
fi; \
|
|
||||||
fi; \
|
|
||||||
# Create user if it doesn't exist, avoid UID conflicts
|
|
||||||
if ! id -u stirlingpdfuser >/dev/null 2>&1; then \
|
|
||||||
if getent passwd | awk -F: -v id="${PUID}" '$3==id{found=1} END{exit !found}'; then \
|
|
||||||
echo "UID ${PUID} already in use – creating stirlingpdfuser with automatic UID"; \
|
|
||||||
useradd -m -g stirlingpdfgroup -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
|
||||||
else \
|
|
||||||
useradd -m -u "${PUID}" -g stirlingpdfgroup -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
|
||||||
fi; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Compatibility alias for older entrypoint scripts expecting su-exec
|
|
||||||
RUN ln -sf /usr/sbin/gosu /usr/local/bin/su-exec
|
|
||||||
|
|
||||||
# Copy application files from build stage
|
|
||||||
COPY --from=build --chown=stirlingpdfuser:stirlingpdfgroup /app/app/core/build/libs/*.jar /app.jar
|
|
||||||
COPY --from=build --chown=stirlingpdfuser:stirlingpdfgroup /app/build/libs/restart-helper.jar /restart-helper.jar
|
|
||||||
COPY scripts/ /scripts/
|
|
||||||
COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/truetype/
|
|
||||||
|
|
||||||
# Optional version tag (can be passed at build time)
|
|
||||||
ARG VERSION_TAG
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.title="Stirling-PDF Backend"
|
|
||||||
LABEL org.opencontainers.image.description="Backend service for Stirling-PDF - Java Spring Boot with PDF processing capabilities"
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
|
|
||||||
LABEL org.opencontainers.image.licenses="MIT"
|
|
||||||
LABEL org.opencontainers.image.vendor="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
|
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
|
|
||||||
LABEL maintainer="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.authors="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.version="${VERSION_TAG}"
|
|
||||||
LABEL org.opencontainers.image.keywords="PDF, manipulation, backend, API, Spring Boot"
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Runtime environment variables
|
|
||||||
# ==============================================================================
|
|
||||||
ENV VERSION_TAG=$VERSION_TAG \
|
|
||||||
DISABLE_ADDITIONAL_FEATURES=true \
|
|
||||||
JAVA_BASE_OPTS="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps \
|
|
||||||
-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 \
|
|
||||||
-XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 \
|
|
||||||
-XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70 \
|
|
||||||
-Djava.awt.headless=true" \
|
|
||||||
JAVA_CUSTOM_OPTS="" \
|
|
||||||
HOME=/home/stirlingpdfuser \
|
|
||||||
PUID=${PUID} \
|
|
||||||
PGID=${PGID} \
|
|
||||||
UMASK=022 \
|
|
||||||
UNO_PATH=/usr/lib/libreoffice/program \
|
|
||||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
|
||||||
TMPDIR=/tmp/stirling-pdf \
|
|
||||||
TEMP=/tmp/stirling-pdf \
|
|
||||||
TMP=/tmp/stirling-pdf
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Python virtual environment for additional Python tools (WeasyPrint, OpenCV, etc.)
|
|
||||||
# ==============================================================================
|
|
||||||
RUN python3 -m venv /opt/venv --system-site-packages \
|
|
||||||
&& /opt/venv/bin/pip install --no-cache-dir weasyprint pdf2image opencv-python-headless \
|
|
||||||
&& /opt/venv/bin/python -c "import cv2; print('OpenCV version:', cv2.__version__)"
|
|
||||||
|
|
||||||
# Separate venv for unoserver (keeps it isolated)
|
|
||||||
RUN python3 -m venv /opt/unoserver-venv --system-site-packages \
|
|
||||||
&& /opt/unoserver-venv/bin/pip install --no-cache-dir unoserver
|
|
||||||
|
|
||||||
# Make unoserver tools available in main venv PATH
|
|
||||||
RUN ln -sf /opt/unoserver-venv/bin/unoconvert /opt/venv/bin/unoconvert \
|
|
||||||
&& ln -sf /opt/unoserver-venv/bin/unoserver /opt/venv/bin/unoserver
|
|
||||||
|
|
||||||
# Extend PATH to include both virtual environments
|
|
||||||
ENV PATH="/opt/venv/bin:/opt/unoserver-venv/bin:${PATH}"
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Final permissions, directories and font cache
|
|
||||||
# ==============================================================================
|
|
||||||
RUN set -eux; \
|
|
||||||
chmod +x /scripts/*; \
|
|
||||||
mkdir -p /configs /configs/heap_dumps /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf; \
|
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup \
|
|
||||||
/home/stirlingpdfuser /configs /logs /customFiles /pipeline /tmp/stirling-pdf \
|
|
||||||
/app.jar /restart-helper.jar /usr/share/fonts/truetype /scripts; \
|
|
||||||
chmod -R 755 /tmp/stirling-pdf
|
|
||||||
|
|
||||||
# Rebuild font cache
|
|
||||||
RUN fc-cache -f -v
|
|
||||||
|
|
||||||
# Force Qt/WebEngine to run headlessly (required for Calibre in Docker)
|
|
||||||
ENV QT_QPA_PLATFORM=offscreen \
|
|
||||||
QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu --disable-dev-shm-usage"
|
|
||||||
|
|
||||||
# Expose web UI port
|
|
||||||
EXPOSE 8080/tcp
|
|
||||||
|
|
||||||
STOPSIGNAL SIGTERM
|
|
||||||
|
|
||||||
# Use tini as init (handles signals and zombies correctly)
|
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
|
||||||
|
|
||||||
# CMD is empty – actual start command is defined in init.sh
|
|
||||||
CMD []
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
# ==============================================================================
|
|
||||||
# Multi-stage Dockerfile for Stirling-PDF – "fat" image with everything included
|
|
||||||
# Includes: LibreOffice, Calibre, Tesseract, OCRmyPDF, unoserver, WeasyPrint, etc.
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
# ========================================
|
|
||||||
# STAGE 1: Build stage - Gradle
|
|
||||||
# ========================================
|
|
||||||
FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build
|
|
||||||
|
|
||||||
COPY build.gradle .
|
|
||||||
COPY settings.gradle .
|
|
||||||
COPY gradlew .
|
|
||||||
COPY gradle gradle/
|
|
||||||
COPY app/core/build.gradle core/.
|
|
||||||
COPY app/common/build.gradle common/.
|
|
||||||
COPY app/proprietary/build.gradle proprietary/.
|
|
||||||
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
|
||||||
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the entire project to the working directory
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the application (server-only JAR - no UI, includes security features controlled at runtime)
|
|
||||||
RUN DISABLE_ADDITIONAL_FEATURES=false \
|
|
||||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
|
||||||
|
|
||||||
# ========================================
|
|
||||||
# STAGE 2: Runtime image based on Debian stable-slim
|
|
||||||
# Contains Java runtime + LibreOffice + Calibre + all PDF tools
|
|
||||||
# ========================================
|
|
||||||
FROM debian:stable-slim@sha256:7cb087f19bcc175b96fbe4c2aef42ed00733a659581a80f6ebccfd8fe3185a3d
|
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
# Install core runtime dependencies + tools required by Stirling-PDF features
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates tzdata tini bash fontconfig \
|
|
||||||
openjdk-21-jre-headless \
|
|
||||||
ffmpeg poppler-utils qpdf ghostscript ocrmypdf \
|
|
||||||
libreoffice-nogui libreoffice-java-common \
|
|
||||||
python3 python3-venv python3-uno \
|
|
||||||
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-fra \
|
|
||||||
tesseract-ocr-por tesseract-ocr-chi-sim \
|
|
||||||
libcairo2 libpango-1.0-0 libpangoft2-1.0-0 libgdk-pixbuf-2.0-0 \
|
|
||||||
gosu unpaper \
|
|
||||||
# AWT headless support (required for some Java graphics operations)
|
|
||||||
libfreetype6 libfontconfig1 libx11-6 libxt6 libxext6 libxrender1 libxtst6 libxi6 \
|
|
||||||
libxinerama1 libxkbcommon0 libxkbfile1 libsm6 libice6 \
|
|
||||||
# Qt WebEngine dependencies for Calibre
|
|
||||||
libegl1 libopengl0 libgl1 libxdamage1 libxfixes3 libxshmfence1 libdrm2 libgbm1 \
|
|
||||||
libxkbcommon-x11-0 libxrandr2 libxcomposite1 libnss3 libx11-xcb1 \
|
|
||||||
libxcb-cursor0 libdbus-1-3 libglib2.0-0 \
|
|
||||||
# Virtual framebuffer (required for headless LibreOffice)
|
|
||||||
xvfb x11-utils coreutils \
|
|
||||||
# Temporary packages only needed for Calibre installer
|
|
||||||
xz-utils gpgv curl xdg-utils \
|
|
||||||
\
|
|
||||||
# Install Calibre from official installer script
|
|
||||||
&& curl -fsSL https://download.calibre-ebook.com/linux-installer.sh | sh /dev/stdin \
|
|
||||||
\
|
|
||||||
# Clean up installer-only packages
|
|
||||||
&& apt-get purge -y xz-utils gpgv xdg-utils \
|
|
||||||
&& apt-get autoremove -y \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Make ebook-convert available in PATH
|
|
||||||
RUN ln -sf /opt/calibre/ebook-convert /usr/bin/ebook-convert \
|
|
||||||
&& /opt/calibre/ebook-convert --version
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Create non-root user (stirlingpdfuser) with configurable UID/GID
|
|
||||||
# ==============================================================================
|
|
||||||
ARG PUID=1000
|
|
||||||
ARG PGID=1000
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
# Create group if it doesn't exist
|
|
||||||
if ! getent group stirlingpdfgroup >/dev/null 2>&1; then \
|
|
||||||
if getent group "${PGID}" >/dev/null 2>&1; then \
|
|
||||||
groupadd -o -g "${PGID}" stirlingpdfgroup; \
|
|
||||||
else \
|
|
||||||
groupadd -g "${PGID}" stirlingpdfgroup; \
|
|
||||||
fi; \
|
|
||||||
fi; \
|
|
||||||
# Create user if it doesn't exist, avoid UID conflicts
|
|
||||||
if ! id -u stirlingpdfuser >/dev/null 2>&1; then \
|
|
||||||
if getent passwd | awk -F: -v id="${PUID}" '$3==id{found=1} END{exit !found}'; then \
|
|
||||||
echo "UID ${PUID} already in use – creating stirlingpdfuser with automatic UID"; \
|
|
||||||
useradd -m -g stirlingpdfgroup -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
|
||||||
else \
|
|
||||||
useradd -m -u "${PUID}" -g stirlingpdfgroup -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
|
||||||
fi; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Compatibility alias for older entrypoint scripts expecting su-exec
|
|
||||||
RUN ln -sf /usr/sbin/gosu /usr/local/bin/su-exec
|
|
||||||
|
|
||||||
# Copy application files from build stage
|
|
||||||
COPY --from=build --chown=stirlingpdfuser:stirlingpdfgroup /app/app/core/build/libs/*.jar /app.jar
|
|
||||||
COPY --from=build --chown=stirlingpdfuser:stirlingpdfgroup /app/build/libs/restart-helper.jar /restart-helper.jar
|
|
||||||
COPY scripts/ /scripts/
|
|
||||||
COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/truetype/
|
|
||||||
|
|
||||||
# Optional version tag (can be passed at build time)
|
|
||||||
ARG VERSION_TAG
|
|
||||||
|
|
||||||
# Metadata labels
|
|
||||||
LABEL org.opencontainers.image.title="Stirling-PDF"
|
|
||||||
LABEL org.opencontainers.image.description="A powerful locally hosted web-based PDF manipulation tool supporting 50+ operations including merging, splitting, conversion, OCR, watermarking, and more."
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
|
|
||||||
LABEL org.opencontainers.image.licenses="MIT"
|
|
||||||
LABEL org.opencontainers.image.vendor="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
|
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
|
|
||||||
LABEL maintainer="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.authors="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.version="${VERSION_TAG}"
|
|
||||||
LABEL org.opencontainers.image.keywords="PDF, manipulation, merge, split, convert, OCR, watermark"
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Runtime environment variables
|
|
||||||
# ==============================================================================
|
|
||||||
ENV VERSION_TAG=$VERSION_TAG \
|
|
||||||
DISABLE_ADDITIONAL_FEATURES=true \
|
|
||||||
JAVA_BASE_OPTS="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps \
|
|
||||||
-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 \
|
|
||||||
-XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 \
|
|
||||||
-XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70 \
|
|
||||||
-Djava.awt.headless=true" \
|
|
||||||
JAVA_CUSTOM_OPTS="" \
|
|
||||||
HOME=/home/stirlingpdfuser \
|
|
||||||
PUID=${PUID} \
|
|
||||||
PGID=${PGID} \
|
|
||||||
UMASK=022 \
|
|
||||||
FAT_DOCKER=true \
|
|
||||||
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
|
||||||
UNO_PATH=/usr/lib/libreoffice/program \
|
|
||||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
|
||||||
TMPDIR=/tmp/stirling-pdf \
|
|
||||||
TEMP=/tmp/stirling-pdf \
|
|
||||||
TMP=/tmp/stirling-pdf
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Python virtual environment for additional Python tools (WeasyPrint, OpenCV, etc.)
|
|
||||||
# ==============================================================================
|
|
||||||
RUN python3 -m venv /opt/venv --system-site-packages \
|
|
||||||
&& /opt/venv/bin/pip install --no-cache-dir weasyprint pdf2image opencv-python-headless \
|
|
||||||
&& /opt/venv/bin/python -c "import cv2; print('OpenCV version:', cv2.__version__)"
|
|
||||||
|
|
||||||
# Separate venv for unoserver (keeps it isolated)
|
|
||||||
RUN python3 -m venv /opt/unoserver-venv --system-site-packages \
|
|
||||||
&& /opt/unoserver-venv/bin/pip install --no-cache-dir unoserver
|
|
||||||
|
|
||||||
# Make unoserver tools available in main venv PATH
|
|
||||||
RUN ln -sf /opt/unoserver-venv/bin/unoconvert /opt/venv/bin/unoconvert \
|
|
||||||
&& ln -sf /opt/unoserver-venv/bin/unoserver /opt/venv/bin/unoserver
|
|
||||||
|
|
||||||
# Extend PATH to include both virtual environments
|
|
||||||
ENV PATH="/opt/venv/bin:/opt/unoserver-venv/bin:${PATH}"
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Final permissions, directories and font cache
|
|
||||||
# ==============================================================================
|
|
||||||
RUN set -eux; \
|
|
||||||
chmod +x /scripts/*; \
|
|
||||||
mkdir -p /configs /configs/heap_dumps /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf; \
|
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup \
|
|
||||||
/home/stirlingpdfuser /configs /logs /customFiles /pipeline /tmp/stirling-pdf \
|
|
||||||
/app.jar /restart-helper.jar /usr/share/fonts/truetype /scripts; \
|
|
||||||
chmod -R 755 /tmp/stirling-pdf
|
|
||||||
|
|
||||||
# Rebuild font cache
|
|
||||||
RUN fc-cache -f -v
|
|
||||||
|
|
||||||
# Force Qt/WebEngine to run headlessly (required for Calibre in Docker)
|
|
||||||
ENV QT_QPA_PLATFORM=offscreen \
|
|
||||||
QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu --disable-dev-shm-usage"
|
|
||||||
|
|
||||||
# Expose web UI port
|
|
||||||
EXPOSE 8080/tcp
|
|
||||||
|
|
||||||
STOPSIGNAL SIGTERM
|
|
||||||
|
|
||||||
# Use tini as init (handles signals and zombies correctly)
|
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
|
||||||
|
|
||||||
# CMD is empty – actual start command is defined in init.sh
|
|
||||||
CMD []
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# Backend ultra-lite Dockerfile - Java Spring Boot with minimal dependencies and build stage
|
|
||||||
# ========================================
|
|
||||||
# STAGE 1: Build stage - Gradle
|
|
||||||
# ========================================
|
|
||||||
FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build
|
|
||||||
|
|
||||||
COPY build.gradle .
|
|
||||||
COPY settings.gradle .
|
|
||||||
COPY gradlew .
|
|
||||||
COPY gradle gradle/
|
|
||||||
COPY app/core/build.gradle core/.
|
|
||||||
COPY app/common/build.gradle common/.
|
|
||||||
COPY app/proprietary/build.gradle proprietary/.
|
|
||||||
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
|
||||||
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the entire project to the working directory
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the application with DISABLE_ADDITIONAL_FEATURES=true
|
|
||||||
RUN DISABLE_ADDITIONAL_FEATURES=true \
|
|
||||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
|
||||||
|
|
||||||
# ========================================
|
|
||||||
# STAGE 2: Runtime stage - Alpine minimal
|
|
||||||
# ========================================
|
|
||||||
FROM alpine:3.23.2@sha256:865b95f46d98cf867a156fe4a135ad3fe50d2056aa3f25ed31662dff6da4eb62
|
|
||||||
|
|
||||||
ARG VERSION_TAG
|
|
||||||
|
|
||||||
# Set Environment Variables
|
|
||||||
ENV HOME=/home/stirlingpdfuser \
|
|
||||||
VERSION_TAG=$VERSION_TAG \
|
|
||||||
JAVA_BASE_OPTS="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
|
|
||||||
JAVA_CUSTOM_OPTS="" \
|
|
||||||
PUID=1000 \
|
|
||||||
PGID=1000 \
|
|
||||||
UMASK=022 \
|
|
||||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
|
||||||
TMPDIR=/tmp/stirling-pdf \
|
|
||||||
TEMP=/tmp/stirling-pdf \
|
|
||||||
TMP=/tmp/stirling-pdf
|
|
||||||
|
|
||||||
# Copy necessary files
|
|
||||||
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
|
||||||
COPY scripts/installFonts.sh /scripts/installFonts.sh
|
|
||||||
COPY --from=build --chown=stirlingpdfuser:stirlingpdfgroup /app/app/core/build/libs/*.jar /app.jar
|
|
||||||
COPY --from=build --chown=stirlingpdfuser:stirlingpdfgroup /app/build/libs/restart-helper.jar /restart-helper.jar
|
|
||||||
|
|
||||||
# Set up necessary directories and permissions
|
|
||||||
RUN apk add --no-cache bash \
|
|
||||||
&& ln -sf /bin/bash /bin/sh \
|
|
||||||
&& printf '%s\n' \
|
|
||||||
'https://dl-cdn.alpinelinux.org/alpine/edge/main' \
|
|
||||||
'https://dl-cdn.alpinelinux.org/alpine/edge/community' \
|
|
||||||
'https://dl-cdn.alpinelinux.org/alpine/edge/testing' \
|
|
||||||
> /etc/apk/repositories && \
|
|
||||||
apk upgrade --no-cache -a && \
|
|
||||||
apk add --no-cache \
|
|
||||||
ca-certificates \
|
|
||||||
tzdata \
|
|
||||||
tini \
|
|
||||||
bash \
|
|
||||||
curl \
|
|
||||||
shadow \
|
|
||||||
su-exec \
|
|
||||||
openjdk21-jre \
|
|
||||||
ghostscript \
|
|
||||||
fontforge && \
|
|
||||||
# User permissions
|
|
||||||
mkdir -p /configs /configs/heap_dumps /logs /customFiles /usr/share/fonts/opentype/noto /tmp/stirling-pdf /pipeline/watchedFolders /pipeline/finishedFolders && \
|
|
||||||
chmod +x /scripts/*.sh && \
|
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar && \
|
|
||||||
ln -sf /bin/busybox /bin/sh
|
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
|
||||||
|
|
||||||
# Run the application
|
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
|
|
||||||
CMD []
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
stirling-pdf:
|
||||||
build:
|
build:
|
||||||
context: ../..
|
context: ../..
|
||||||
dockerfile: docker/backend/Dockerfile.fat
|
dockerfile: docker/embedded/Dockerfile.fat
|
||||||
container_name: stirling-pdf-backend-fat
|
container_name: stirling-pdf-fat
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
@@ -15,9 +15,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 16
|
retries: 16
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # TODO: Remove in production - for debugging only
|
- "8080:8080"
|
||||||
expose:
|
|
||||||
- "8080"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ../../stirling/latest/data:/usr/share/tessdata:rw
|
- ../../stirling/latest/data:/usr/share/tessdata:rw
|
||||||
- ../../stirling/latest/config:/configs:rw
|
- ../../stirling/latest/config:/configs:rw
|
||||||
@@ -37,24 +35,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- stirling-network
|
- stirling-network
|
||||||
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: docker/frontend/Dockerfile
|
|
||||||
container_name: stirling-pdf-frontend-fat
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "3000:80"
|
|
||||||
environment:
|
|
||||||
BACKEND_URL: http://backend:8080
|
|
||||||
#VITE_GOOGLE_DRIVE_CLIENT_ID: <INSERT_YOUR_CLIENT_ID_HERE>
|
|
||||||
#VITE_GOOGLE_DRIVE_API_KEY: <INSERT_YOUR_API_KEY_HERE>
|
|
||||||
#VITE_GOOGLE_DRIVE_APP_ID: <INSERT_YOUR_APP_ID_HERE>
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
networks:
|
|
||||||
- stirling-network
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
stirling-network:
|
stirling-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
stirling-pdf:
|
||||||
build:
|
build:
|
||||||
context: ../..
|
context: ../..
|
||||||
dockerfile: docker/backend/Dockerfile.ultra-lite
|
dockerfile: docker/embedded/Dockerfile.ultra-lite
|
||||||
container_name: stirling-pdf-backend-ultra-lite
|
container_name: stirling-pdf-ultra-lite
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
@@ -15,9 +15,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 16
|
retries: 16
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # TODO: Remove in production - for debugging only
|
- "8080:8080"
|
||||||
expose:
|
|
||||||
- "8080"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ../../stirling/latest/config:/configs:rw
|
- ../../stirling/latest/config:/configs:rw
|
||||||
- ../../stirling/latest/logs:/logs:rw
|
- ../../stirling/latest/logs:/logs:rw
|
||||||
@@ -34,24 +32,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- stirling-network
|
- stirling-network
|
||||||
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: docker/frontend/Dockerfile
|
|
||||||
container_name: stirling-pdf-frontend-ultra-lite
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "3000:80"
|
|
||||||
environment:
|
|
||||||
BACKEND_URL: http://backend:8080
|
|
||||||
#VITE_GOOGLE_DRIVE_CLIENT_ID: <INSERT_YOUR_CLIENT_ID_HERE>
|
|
||||||
#VITE_GOOGLE_DRIVE_API_KEY: <INSERT_YOUR_API_KEY_HERE>
|
|
||||||
#VITE_GOOGLE_DRIVE_APP_ID: <INSERT_YOUR_APP_ID_HERE>
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
networks:
|
|
||||||
- stirling-network
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
stirling-network:
|
stirling-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
stirling-pdf:
|
||||||
build:
|
build:
|
||||||
context: ../..
|
context: ../..
|
||||||
dockerfile: docker/backend/Dockerfile
|
dockerfile: docker/embedded/Dockerfile
|
||||||
container_name: stirling-pdf-backend
|
container_name: stirling-pdf
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
|
||||||
@@ -11,9 +11,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 16
|
retries: 16
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080" # TODO: Remove in production - for debugging only
|
- "8080:8080"
|
||||||
expose:
|
|
||||||
- "8080"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ../../stirling/latest/data:/usr/share/tessdata:rw
|
- ../../stirling/latest/data:/usr/share/tessdata:rw
|
||||||
- ../../stirling/latest/config:/configs:rw
|
- ../../stirling/latest/config:/configs:rw
|
||||||
@@ -32,24 +30,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- stirling-network
|
- stirling-network
|
||||||
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: docker/frontend/Dockerfile
|
|
||||||
container_name: stirling-pdf-frontend
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "3000:80"
|
|
||||||
environment:
|
|
||||||
BACKEND_URL: http://backend:8080
|
|
||||||
#VITE_GOOGLE_DRIVE_CLIENT_ID: <INSERT_YOUR_CLIENT_ID_HERE>
|
|
||||||
#VITE_GOOGLE_DRIVE_API_KEY: <INSERT_YOUR_API_KEY_HERE>
|
|
||||||
#VITE_GOOGLE_DRIVE_APP_ID: <INSERT_YOUR_APP_ID_HERE>
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
networks:
|
|
||||||
- stirling-network
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
stirling-network:
|
stirling-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
+553
-174
@@ -1,213 +1,592 @@
|
|||||||
# Stirling-PDF Dockerfile - Full version with embedded frontend
|
# Stirling-PDF - Full version (embedded frontend)
|
||||||
# Single JAR contains both frontend and backend
|
|
||||||
|
|
||||||
# Stage 1: Build application with embedded frontend
|
FROM ubuntu:noble AS calibre-build
|
||||||
FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build
|
|
||||||
|
|
||||||
# Install Node.js and npm for frontend build
|
ARG CALIBRE_VERSION=9.3.1
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
||||||
&& apt-get install -y nodejs \
|
|
||||||
&& npm --version \
|
|
||||||
&& node --version \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy gradle files for dependency resolution
|
|
||||||
COPY build.gradle .
|
|
||||||
COPY settings.gradle .
|
|
||||||
COPY gradlew .
|
|
||||||
COPY gradle gradle/
|
|
||||||
COPY app/core/build.gradle core/.
|
|
||||||
COPY app/common/build.gradle common/.
|
|
||||||
COPY app/proprietary/build.gradle proprietary/.
|
|
||||||
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy entire project
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build JAR with embedded frontend (includes security features controlled at runtime)
|
|
||||||
RUN DISABLE_ADDITIONAL_FEATURES=false \
|
|
||||||
./gradlew clean build -PbuildWithFrontend=true -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
|
||||||
|
|
||||||
# Stage 2: Runtime image based on Debian stable-slim
|
|
||||||
# Contains Java runtime + LibreOffice + Calibre + all PDF tools
|
|
||||||
FROM debian:stable-slim@sha256:ed542b2d269ff08139fc5ab8c762efe8c8986b564a423d5241a5ce9fb09b6c08
|
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
ENV LANG=C.UTF-8 \
|
|
||||||
LC_ALL=C.UTF-8
|
|
||||||
|
|
||||||
ENV TESS_BASE_PATH=/usr/share/tesseract-ocr/5/tessdata
|
|
||||||
|
|
||||||
# Install core runtime dependencies + tools required by Stirling-PDF features
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates tzdata tini bash fontconfig \
|
|
||||||
openjdk-21-jre-headless \
|
|
||||||
ffmpeg poppler-utils ocrmypdf imagemagick fontforge ghostscript \
|
|
||||||
fonts-dejavu \
|
|
||||||
fonts-liberation fonts-liberation2 \
|
|
||||||
fonts-crosextra-caladea fonts-crosextra-carlito \
|
|
||||||
fonts-linuxlibertine \
|
|
||||||
fonts-noto-core fonts-noto-cjk fonts-noto-mono fonts-noto-ui-core \
|
|
||||||
fonts-noto-color-emoji \
|
|
||||||
ttf-wqy-zenhei \
|
|
||||||
fonts-arphic-ukai fonts-arphic-uming \
|
|
||||||
python3 python3-venv python3-uno \
|
|
||||||
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-fra \
|
|
||||||
tesseract-ocr-por tesseract-ocr-chi-sim \
|
|
||||||
libcairo2 libpango-1.0-0 libpangoft2-1.0-0 libgdk-pixbuf-2.0-0 \
|
|
||||||
gosu unpaper qpdf \
|
|
||||||
# AWT headless support (required for some Java graphics operations)
|
|
||||||
libfreetype6 libfontconfig1 libx11-6 libxt6 libxext6 libxrender1 libxtst6 libxi6 \
|
|
||||||
libxinerama1 libxkbcommon0 libxkbfile1 libsm6 libice6 \
|
|
||||||
# Qt WebEngine dependencies for Calibre
|
|
||||||
libegl1 libopengl0 libgl1 libxdamage1 libxfixes3 libxshmfence1 libdrm2 libgbm1 \
|
|
||||||
libxkbcommon-x11-0 libxrandr2 libxcomposite1 libnss3 libx11-xcb1 \
|
|
||||||
libxcb-cursor0 libdbus-1-3 libglib2.0-0 \
|
|
||||||
# Virtual framebuffer (required for headless LibreOffice)
|
|
||||||
xvfb x11-utils coreutils \
|
|
||||||
# Temporary packages only needed for Calibre installer
|
|
||||||
xz-utils gpgv curl xdg-utils \
|
|
||||||
\
|
|
||||||
# Install Calibre from official installer script
|
|
||||||
&& curl -fsSL https://download.calibre-ebook.com/linux-installer.sh | sh /dev/stdin \
|
|
||||||
\
|
|
||||||
# Clean up installer-only packages
|
|
||||||
&& apt-get purge -y xz-utils gpgv xdg-utils \
|
|
||||||
&& apt-get autoremove -y \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
. /etc/os-release; \
|
|
||||||
echo "deb http://deb.debian.org/debian ${VERSION_CODENAME}-backports main" > /etc/apt/sources.list.d/backports.list; \
|
|
||||||
apt-get update; \
|
apt-get update; \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends -t ${VERSION_CODENAME}-backports \
|
apt-get install -y --no-install-recommends \
|
||||||
libreoffice libreoffice-java-common; \
|
ca-certificates curl xz-utils libnss3 libfontconfig1 \
|
||||||
|
libgl1 libegl1 libdbus-1-3 libasound2t64 libxcomposite1 \
|
||||||
|
libxrandr2 libxkbcommon0 libxi6 libxtst6 libopengl0; \
|
||||||
rm -rf /var/lib/apt/lists/*; \
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
libreoffice --version
|
\
|
||||||
|
case "$(uname -m)" in \
|
||||||
|
x86_64) CALIBRE_ARCH="x86_64" ;; \
|
||||||
|
aarch64) CALIBRE_ARCH="arm64" ;; \
|
||||||
|
*) echo "Unsupported arch: $(uname -m)"; exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
\
|
||||||
|
curl -fsSL \
|
||||||
|
"https://download.calibre-ebook.com/${CALIBRE_VERSION}/calibre-${CALIBRE_VERSION}-${CALIBRE_ARCH}.txz" \
|
||||||
|
-o /tmp/calibre.txz; \
|
||||||
|
mkdir -p /opt/calibre; \
|
||||||
|
tar xJf /tmp/calibre.txz -C /opt/calibre; \
|
||||||
|
rm /tmp/calibre.txz; \
|
||||||
|
\
|
||||||
|
# Remove GUI-only shared libraries.
|
||||||
|
# Libs required by WebEngine PDF output are preserved.
|
||||||
|
rm -f /opt/calibre/lib/libQt6Designer* \
|
||||||
|
/opt/calibre/lib/libQt6Multimedia* \
|
||||||
|
/opt/calibre/lib/libQt6SpatialAudio.so.* \
|
||||||
|
/opt/calibre/lib/libQt6NetworkAuth.so.* \
|
||||||
|
/opt/calibre/lib/libQt6Concurrent.so.* \
|
||||||
|
/opt/calibre/lib/libQt6OpenGLWidgets.so.* \
|
||||||
|
/opt/calibre/lib/libQt6QuickWidgets.so.* \
|
||||||
|
/opt/calibre/lib/libavcodec.so.* \
|
||||||
|
/opt/calibre/lib/libavfilter.so.* \
|
||||||
|
/opt/calibre/lib/libavformat.so.* \
|
||||||
|
/opt/calibre/lib/libavutil.so.* \
|
||||||
|
/opt/calibre/lib/libavdevice.so.* \
|
||||||
|
/opt/calibre/lib/libpostproc.so.* \
|
||||||
|
/opt/calibre/lib/libswresample.so.* \
|
||||||
|
/opt/calibre/lib/libswscale.so.* \
|
||||||
|
/opt/calibre/lib/libspeex.so.* \
|
||||||
|
/opt/calibre/lib/libFLAC.so.* \
|
||||||
|
/opt/calibre/lib/libopus.so.* \
|
||||||
|
/opt/calibre/lib/libvorbis*.so.* \
|
||||||
|
/opt/calibre/lib/libasyncns.so.* \
|
||||||
|
/opt/calibre/lib/libspeechd.so.* \
|
||||||
|
/opt/calibre/lib/libespeak-ng.so.* \
|
||||||
|
/opt/calibre/lib/libonnxruntime.so.* \
|
||||||
|
/opt/calibre/lib/libgio-2.0.so.* \
|
||||||
|
/opt/calibre/lib/libzstd.so.* \
|
||||||
|
/opt/calibre/lib/libhunspell-1.7.so.* \
|
||||||
|
/opt/calibre/lib/libbrotlienc.so.* \
|
||||||
|
/opt/calibre/lib/libbrotlicommon.so.* \
|
||||||
|
/opt/calibre/lib/libbrotlidec.so.* \
|
||||||
|
/opt/calibre/lib/libstemmer.so.* \
|
||||||
|
/opt/calibre/lib/libmtp.so.* \
|
||||||
|
/opt/calibre/lib/libncursesw.so.* \
|
||||||
|
/opt/calibre/lib/libchm.so.* \
|
||||||
|
/opt/calibre/lib/libgcrypt.so.* \
|
||||||
|
/opt/calibre/lib/libgpg-error.so.* \
|
||||||
|
/opt/calibre/lib/libicuio.so.* \
|
||||||
|
/opt/calibre/lib/libreadline.so.* \
|
||||||
|
/opt/calibre/lib/libusb-1.0.so.*; \
|
||||||
|
rm -rf /opt/calibre/lib/qt6/plugins/platformthemes \
|
||||||
|
/opt/calibre/lib/qt6/plugins/multimedia \
|
||||||
|
/opt/calibre/lib/qt6/plugins/designer \
|
||||||
|
/opt/calibre/lib/qt6/plugins/qmltooling; \
|
||||||
|
\
|
||||||
|
# Remove GUI executables but keep ebook-convert, ebook-meta, and calibre-parallel.
|
||||||
|
rm -f /opt/calibre/calibre \
|
||||||
|
/opt/calibre/calibre-server \
|
||||||
|
/opt/calibre/calibre-smtp \
|
||||||
|
/opt/calibre/calibre-debug \
|
||||||
|
/opt/calibre/calibre-customize \
|
||||||
|
/opt/calibre/calibredb \
|
||||||
|
/opt/calibre/ebook-viewer \
|
||||||
|
/opt/calibre/ebook-edit \
|
||||||
|
/opt/calibre/ebook-polish \
|
||||||
|
/opt/calibre/ebook-device \
|
||||||
|
/opt/calibre/fetch-ebook-metadata \
|
||||||
|
/opt/calibre/lrf2lrs \
|
||||||
|
/opt/calibre/lrs2lrf \
|
||||||
|
/opt/calibre/markdown-calibre \
|
||||||
|
/opt/calibre/web2disk; \
|
||||||
|
\
|
||||||
|
# Remove Python modules not needed for conversion.
|
||||||
|
rm -rf /opt/calibre/lib/calibre/gui2 \
|
||||||
|
/opt/calibre/lib/calibre/devices \
|
||||||
|
/opt/calibre/lib/calibre/library \
|
||||||
|
/opt/calibre/lib/calibre/db \
|
||||||
|
/opt/calibre/lib/calibre/srv \
|
||||||
|
/opt/calibre/lib/calibre/spell \
|
||||||
|
/opt/calibre/lib/calibre/live; \
|
||||||
|
\
|
||||||
|
# Remove resources not needed for CLI conversion.
|
||||||
|
rm -rf /opt/calibre/resources/images \
|
||||||
|
/opt/calibre/resources/icons \
|
||||||
|
/opt/calibre/resources/icons.rcc \
|
||||||
|
/opt/calibre/resources/content-server \
|
||||||
|
/opt/calibre/resources/editor* \
|
||||||
|
/opt/calibre/resources/viewer \
|
||||||
|
/opt/calibre/resources/viewer.js \
|
||||||
|
/opt/calibre/resources/viewer.html \
|
||||||
|
/opt/calibre/resources/recipes \
|
||||||
|
/opt/calibre/resources/dictionaries \
|
||||||
|
/opt/calibre/resources/hyphenation \
|
||||||
|
/opt/calibre/resources/catalog \
|
||||||
|
/opt/calibre/resources/calibre-mimetypes.xml \
|
||||||
|
/opt/calibre/resources/changelog.json \
|
||||||
|
/opt/calibre/resources/user-agent-data.json \
|
||||||
|
/opt/calibre/resources/builtin_recipes.zip \
|
||||||
|
/opt/calibre/resources/builtin_recipes.xml \
|
||||||
|
/opt/calibre/resources/builtin_recipes.xml \
|
||||||
|
/opt/calibre/resources/stylelint-bundle.min.js \
|
||||||
|
/opt/calibre/resources/stylelint.js \
|
||||||
|
/opt/calibre/resources/rapydscript \
|
||||||
|
/opt/calibre/resources/quick_start \
|
||||||
|
/opt/calibre/resources/piper-voices.json \
|
||||||
|
/opt/calibre/resources/images.qrc \
|
||||||
|
/opt/calibre/resources/mozilla-ca-certs.pem \
|
||||||
|
/opt/calibre/resources/ebook-convert-complete.calibre_msgpack \
|
||||||
|
/opt/calibre/resources/mathjax \
|
||||||
|
/opt/calibre/resources/common-english-words.txt \
|
||||||
|
/opt/calibre/resources/calibre-portable.sh \
|
||||||
|
/opt/calibre/resources/calibre-portable.bat \
|
||||||
|
/opt/calibre/resources/metadata_sqlite.sql \
|
||||||
|
/opt/calibre/resources/notes_sqlite.sql \
|
||||||
|
/opt/calibre/resources/fts_sqlite.sql \
|
||||||
|
/opt/calibre/resources/fts_triggers.sql \
|
||||||
|
/opt/calibre/resources/jacket \
|
||||||
|
/opt/calibre/resources/editor-functions.json \
|
||||||
|
/opt/calibre/resources/calibre-ebook-root-CA.crt \
|
||||||
|
/opt/calibre/resources/csscolorparser.js \
|
||||||
|
/opt/calibre/resources/lookup.js \
|
||||||
|
/opt/calibre/resources/pdf-mathjax-loader.js \
|
||||||
|
/opt/calibre/resources/scraper.js \
|
||||||
|
/opt/calibre/resources/toc.js \
|
||||||
|
/opt/calibre/resources/user-manual-translation-stats.json \
|
||||||
|
/opt/calibre/resources/pin-template.svg \
|
||||||
|
/opt/calibre/resources/scripts.calibre_msgpack \
|
||||||
|
/opt/calibre/lib/calibre/ebooks/docx/images \
|
||||||
|
/opt/calibre/share \
|
||||||
|
/opt/calibre/man; \
|
||||||
|
\
|
||||||
|
# Remove translations and localization while keeping required libraries.
|
||||||
|
# Keep iso639.calibre_msgpack (required)
|
||||||
|
# Keep qtwebengine_locales (required for WebEngine)
|
||||||
|
rm -rf /opt/calibre/lib/qt6/translations; \
|
||||||
|
find /opt/calibre/translations -mindepth 1 -maxdepth 1 ! -name 'qtwebengine_locales' -exec rm -rf {} +; \
|
||||||
|
if [ -d /opt/calibre/resources/localization ]; then \
|
||||||
|
rm -rf /opt/calibre/resources/localization/locales.zip \
|
||||||
|
/opt/calibre/resources/localization/stats.calibre_msgpack \
|
||||||
|
/opt/calibre/resources/localization/website-languages.txt; \
|
||||||
|
find /opt/calibre/resources/localization -mindepth 1 -maxdepth 1 ! -name 'iso639.calibre_msgpack' -exec rm -rf {} +; \
|
||||||
|
fi; \
|
||||||
|
\
|
||||||
|
# Strip debug symbols from calibre extension modules.
|
||||||
|
# Exclude Qt6 libs: libQt6WebEngineCore and friends embed Chromium V8 JIT code
|
||||||
|
# and internal resource blobs that strip corrupts, causing segfaults at render time.
|
||||||
|
find /opt/calibre/lib -name '*.so*' \
|
||||||
|
! -name 'libQt6*' \
|
||||||
|
-exec strip --strip-unneeded {} + 2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
# Remove Python bytecode caches.
|
||||||
|
find /opt/calibre -type d -name __pycache__ \
|
||||||
|
-exec rm -rf {} + 2>/dev/null || true; \
|
||||||
|
find /opt/calibre -name '*.pyc' -delete 2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
# ── Verify conversion still works ──
|
||||||
|
# NOTE: txt→epub used intentionally NOT txt→pdf.
|
||||||
|
# Calibre 7+ uses WebEngine (Chromium) for PDF output, which requires kernel
|
||||||
|
# capabilities unavailable in Docker RUN steps and segfaults under QEMU.
|
||||||
|
# epub output exercises the same Python/plugin stack without touching WebEngine.
|
||||||
|
/opt/calibre/ebook-convert --version; \
|
||||||
|
echo "Hello" > /tmp/test.txt; \
|
||||||
|
/opt/calibre/ebook-convert /tmp/test.txt /tmp/test.epub; \
|
||||||
|
rm -f /tmp/test.txt /tmp/test.epub; \
|
||||||
|
echo "=== Calibre stripped successfully ==="
|
||||||
|
|
||||||
# Make ebook-convert available in PATH
|
|
||||||
RUN ln -sf /opt/calibre/ebook-convert /usr/bin/ebook-convert \
|
|
||||||
&& /opt/calibre/ebook-convert --version
|
|
||||||
|
|
||||||
# ==============================================================================
|
# Build the Java application and frontend.
|
||||||
# Create non-root user (stirlingpdfuser) with configurable UID/GID
|
FROM gradle:9.3.1-jdk25 AS app-build
|
||||||
# ==============================================================================
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl ca-certificates \
|
||||||
|
&& update-ca-certificates \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# JDK 25+: --add-exports is no longer accepted via JAVA_TOOL_OPTIONS; use JDK_JAVA_OPTIONS instead
|
||||||
|
ENV JDK_JAVA_OPTIONS="--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY build.gradle settings.gradle gradlew ./
|
||||||
|
COPY gradle/ gradle/
|
||||||
|
COPY app/core/build.gradle app/core/
|
||||||
|
COPY app/common/build.gradle app/common/
|
||||||
|
COPY app/proprietary/build.gradle app/proprietary/
|
||||||
|
|
||||||
|
# Use system gradle instead of gradlew to avoid SSL issues downloading gradle distribution on emulated arm64
|
||||||
|
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
|
||||||
|
--mount=type=cache,target=/home/gradle/.gradle/wrapper \
|
||||||
|
gradle dependencies --no-daemon || true
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
|
||||||
|
--mount=type=cache,target=/home/gradle/.gradle/wrapper \
|
||||||
|
DISABLE_ADDITIONAL_FEATURES=false \
|
||||||
|
gradle clean build \
|
||||||
|
-PbuildWithFrontend=true \
|
||||||
|
-x spotlessApply -x spotlessCheck -x test -x sonarqube \
|
||||||
|
--no-daemon
|
||||||
|
|
||||||
|
# Extract Spring Boot Layers.
|
||||||
|
FROM eclipse-temurin:25-jre-noble AS jar-extract
|
||||||
|
WORKDIR /tmp
|
||||||
|
COPY --from=app-build /app/app/core/build/libs/*.jar app.jar
|
||||||
|
RUN java -Djarmode=tools -jar app.jar extract --layers --destination /layers
|
||||||
|
|
||||||
|
|
||||||
|
# Build Ghostscript 10.06.0 from source in an isolated stage (avoids library conflicts).
|
||||||
|
FROM ubuntu:noble AS gs-build
|
||||||
|
ARG GS_VERSION=10.06.0
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential curl ca-certificates libfontconfig1-dev && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
GS_TAG="gs$(printf '%s' "${GS_VERSION}" | tr -d '.')" && \
|
||||||
|
curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/${GS_TAG}/ghostscript-${GS_VERSION}.tar.gz" | tar xz && \
|
||||||
|
cd "ghostscript-${GS_VERSION}" && \
|
||||||
|
./configure \
|
||||||
|
--prefix=/usr/local \
|
||||||
|
--without-x \
|
||||||
|
--disable-cups \
|
||||||
|
--disable-gtk && \
|
||||||
|
make -j"$(nproc)" && \
|
||||||
|
make install && \
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
|
||||||
|
# Build PDF Tools (QPDF and ImageMagick 7).
|
||||||
|
FROM ubuntu:noble AS pdf-tools-build
|
||||||
|
ARG QPDF_VERSION=12.3.2
|
||||||
|
ARG IM_VERSION=7.1.2-13
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential cmake libssl-dev libjpeg-dev zlib1g-dev curl ca-certificates pkg-config \
|
||||||
|
libpng-dev libtiff-dev libwebp-dev libxml2-dev libfreetype6-dev liblcms2-dev libzip-dev liblqr-1-0-dev \
|
||||||
|
libltdl-dev libtool && \
|
||||||
|
# Build QPDF
|
||||||
|
curl -fsSL "https://github.com/qpdf/qpdf/releases/download/v${QPDF_VERSION}/qpdf-${QPDF_VERSION}.tar.gz" | tar xz && \
|
||||||
|
cd "qpdf-${QPDF_VERSION}" && \
|
||||||
|
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DALLOW_CRYPTO_OPENSSL=ON -DDEFAULT_CRYPTO=openssl && \
|
||||||
|
cmake --build build --parallel "$(nproc)" && \
|
||||||
|
cmake --install build && \
|
||||||
|
cd .. && \
|
||||||
|
# Build ImageMagick 7
|
||||||
|
curl -fsSL "https://github.com/ImageMagick/ImageMagick/archive/refs/tags/${IM_VERSION}.tar.gz" | tar xz && \
|
||||||
|
cd "ImageMagick-${IM_VERSION}" && \
|
||||||
|
./configure --prefix=/usr/local --with-modules --with-perl=no --with-magick-plus-plus=no --with-quantum-depth=16 --disable-static --enable-shared && \
|
||||||
|
make -j"$(nproc)" && \
|
||||||
|
make install && \
|
||||||
|
# Enable PDF/PS/EPS in policy
|
||||||
|
sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /usr/local/etc/ImageMagick-7/policy.xml && \
|
||||||
|
sed -i 's/rights="none" pattern="PS"/rights="read|write" pattern="PS"/' /usr/local/etc/ImageMagick-7/policy.xml && \
|
||||||
|
sed -i 's/rights="none" pattern="EPS"/rights="read|write" pattern="EPS"/' /usr/local/etc/ImageMagick-7/policy.xml && \
|
||||||
|
cd .. && \
|
||||||
|
ldconfig /usr/local/lib
|
||||||
|
|
||||||
|
|
||||||
|
# Final runtime image.
|
||||||
|
FROM eclipse-temurin:25-jre-noble AS runtime
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
|
LANG=C.UTF-8 \
|
||||||
|
LC_ALL=C.UTF-8 \
|
||||||
|
TESS_BASE_PATH=/usr/share/tesseract-ocr/5/tessdata
|
||||||
|
|
||||||
|
ARG UNOSERVER_VERSION=3.6
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
apt-get update; \
|
||||||
|
# Add LibreOffice Fresh PPA for latest version (26.2.x)
|
||||||
|
apt-get install -y --no-install-recommends software-properties-common; \
|
||||||
|
add-apt-repository -y ppa:libreoffice/ppa; \
|
||||||
|
apt-get update; \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
# Core tools
|
||||||
|
ca-certificates tzdata tini bash fontconfig curl \
|
||||||
|
ffmpeg poppler-utils fontforge \
|
||||||
|
gosu unpaper \
|
||||||
|
# Fonts: full CJK coverage retained
|
||||||
|
fonts-dejavu \
|
||||||
|
fonts-liberation2 \
|
||||||
|
fonts-crosextra-caladea fonts-crosextra-carlito \
|
||||||
|
fonts-noto-core fonts-noto-mono fonts-noto-extra \
|
||||||
|
fonts-noto-cjk poppler-data \
|
||||||
|
# We install these via apt to avoid downloading "fat wheels" from pip
|
||||||
|
# python3-full replaced with minimal set
|
||||||
|
python3 python3-dev python3-venv python3-uno \
|
||||||
|
# Python dependencies via pip to avoid conflicts, so we don't install them here
|
||||||
|
# OCR
|
||||||
|
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-fra \
|
||||||
|
tesseract-ocr-por tesseract-ocr-chi-sim \
|
||||||
|
# Tesseract OSD for orientation detection
|
||||||
|
tesseract-ocr-osd \
|
||||||
|
# Graphics / AWT headless
|
||||||
|
libcairo2 libpango-1.0-0 libpangoft2-1.0-0 libgdk-pixbuf-2.0-0 \
|
||||||
|
libfreetype6 libfontconfig1 libx11-6 libxt6 libxext6 libxrender1 \
|
||||||
|
libxtst6 libxi6 libxinerama1 libxkbcommon0 libsm6 libice6 \
|
||||||
|
# Qt/EGL for Calibre CLI
|
||||||
|
libegl1 libgl1 libopengl0 libdbus-1-3 libglib2.0-0 libnss3 \
|
||||||
|
libasound2t64 libxcomposite1 libxrandr2 \
|
||||||
|
# Virtual framebuffer (required for headless LibreOffice Impress/Draw)
|
||||||
|
xvfb x11-utils coreutils \
|
||||||
|
libreoffice-writer-nogui libreoffice-calc-nogui \
|
||||||
|
libreoffice-impress-nogui libreoffice-draw-nogui \
|
||||||
|
libreoffice-java-common \
|
||||||
|
; \
|
||||||
|
\
|
||||||
|
\
|
||||||
|
# Note: We do NOT install numpy/pillow/cv2 here; it uses the system versions
|
||||||
|
python3 -m venv /opt/venv --system-site-packages; \
|
||||||
|
/opt/venv/bin/pip install --no-cache-dir \
|
||||||
|
weasyprint pdf2image opencv-python-headless ocrmypdf \
|
||||||
|
"unoserver==${UNOSERVER_VERSION}"; \
|
||||||
|
\
|
||||||
|
ln -sf /opt/venv/bin/unoconvert /usr/local/bin/unoconvert; \
|
||||||
|
ln -sf /opt/venv/bin/unoserver /usr/local/bin/unoserver; \
|
||||||
|
\
|
||||||
|
# Verify and fix LibreOffice
|
||||||
|
libreoffice --version; \
|
||||||
|
soffice --version 2>/dev/null || true; \
|
||||||
|
# Rebuild UNO bridge type database
|
||||||
|
/usr/lib/libreoffice/program/soffice.bin --headless --convert-to pdf /dev/null 2>/dev/null || true; \
|
||||||
|
# Force font cache rebuild and verify filters are available
|
||||||
|
fc-cache -f -v 2>&1 | awk 'NR <= 20'; \
|
||||||
|
/opt/venv/bin/python -c "import cv2; print('OpenCV', cv2.__version__)"; \
|
||||||
|
/opt/venv/bin/python -c "import ocrmypdf; print('ocrmypdf OK')"; \
|
||||||
|
\
|
||||||
|
# Cleanup stage.
|
||||||
|
\
|
||||||
|
# apt clean needed without cache mounts
|
||||||
|
apt-get clean; \
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
\
|
||||||
|
# Docs / man / info / icons / themes / GUI assets (headless server)
|
||||||
|
rm -rf /usr/share/doc/* /usr/share/man/* /usr/share/info/* \
|
||||||
|
/usr/share/lintian/* /usr/share/linda/* \
|
||||||
|
/usr/share/icons/* /usr/share/themes/* \
|
||||||
|
/usr/share/javascript/* \
|
||||||
|
/usr/share/gtk-3.0/* \
|
||||||
|
/usr/share/fontforge/pixmaps \
|
||||||
|
/usr/share/liblangtag/* \
|
||||||
|
/usr/share/tcltk/* \
|
||||||
|
/usr/share/python-wheels/*; \
|
||||||
|
\
|
||||||
|
find /usr/share/locale -mindepth 1 -maxdepth 1 -type d \
|
||||||
|
! -name 'en*' -exec rm -rf {} + 2>/dev/null || true; \
|
||||||
|
rm -rf /usr/share/i18n/locales /usr/share/i18n/charmaps; \
|
||||||
|
\
|
||||||
|
# LibreOffice extras: Only remove specific directories as requested
|
||||||
|
rm -rf /usr/lib/libreoffice/share/gallery \
|
||||||
|
/usr/lib/libreoffice/share/template \
|
||||||
|
/usr/lib/libreoffice/share/wizards \
|
||||||
|
/usr/lib/libreoffice/share/autotext \
|
||||||
|
/usr/lib/libreoffice/help \
|
||||||
|
/usr/lib/libreoffice/share/config/images_*.zip \
|
||||||
|
/usr/lib/libreoffice/share/basic \
|
||||||
|
/usr/lib/libreoffice/share/Scripts \
|
||||||
|
/usr/lib/libreoffice/share/autocorr \
|
||||||
|
/usr/lib/libreoffice/share/classification \
|
||||||
|
/usr/lib/libreoffice/share/wordbook \
|
||||||
|
/usr/lib/libreoffice/share/fingerprint \
|
||||||
|
/usr/lib/libreoffice/share/xdg \
|
||||||
|
/usr/lib/libreoffice/share/numbertext \
|
||||||
|
/usr/lib/libreoffice/share/shell \
|
||||||
|
/usr/lib/libreoffice/share/palette \
|
||||||
|
/usr/lib/libreoffice/share/theme_definitions \
|
||||||
|
/usr/lib/libreoffice/share/xslt \
|
||||||
|
/usr/lib/libreoffice/share/labels \
|
||||||
|
/usr/lib/libreoffice/share/dtd \
|
||||||
|
/usr/lib/libreoffice/share/tipoftheday \
|
||||||
|
/usr/lib/libreoffice/share/toolbarmode \
|
||||||
|
/usr/lib/libreoffice/share/psprint; \
|
||||||
|
\
|
||||||
|
# Preserving soffice.cfg because LibreOffice needs it to load documents.
|
||||||
|
\
|
||||||
|
\
|
||||||
|
# Python caches + pip/setuptools cleanup
|
||||||
|
find /opt/venv -type d -name __pycache__ \
|
||||||
|
-exec rm -rf {} + 2>/dev/null || true; \
|
||||||
|
find /opt/venv \
|
||||||
|
\( -name '*.pyc' -o -name '*.pyi' \) -delete 2>/dev/null || true; \
|
||||||
|
rm -rf /opt/venv/lib/python*/site-packages/pip \
|
||||||
|
/opt/venv/lib/python*/site-packages/pip-*.dist-info \
|
||||||
|
/opt/venv/lib/python*/site-packages/setuptools \
|
||||||
|
/opt/venv/lib/python*/site-packages/setuptools-*.dist-info; \
|
||||||
|
\
|
||||||
|
# Python stdlib: remove unused modules (~71 MB)
|
||||||
|
rm -rf /usr/lib/python3.12/test \
|
||||||
|
/usr/lib/python3.12/idlelib \
|
||||||
|
/usr/lib/python3.12/tkinter \
|
||||||
|
/usr/lib/python3.12/lib2to3 \
|
||||||
|
/usr/lib/python3.12/pydoc_data; \
|
||||||
|
\
|
||||||
|
# System Python packages not needed at runtime (~153 MB)
|
||||||
|
rm -rf /usr/lib/python3/dist-packages/scipy \
|
||||||
|
/usr/lib/python3/dist-packages/sympy \
|
||||||
|
/usr/lib/python3/dist-packages/mpmath; \
|
||||||
|
\
|
||||||
|
# Remove system cffi superseded by venv cffi 2.0
|
||||||
|
rm -rf \
|
||||||
|
/usr/lib/python3/dist-packages/cffi \
|
||||||
|
/usr/lib/python3/dist-packages/cffi-*.dist-info \
|
||||||
|
/usr/lib/python3/dist-packages/_cffi_backend*.so \
|
||||||
|
/usr/lib/python3/dist-packages/_cffi_backend*.cpython*.so \
|
||||||
|
2>/dev/null || true; \
|
||||||
|
# Verify cffi is still importable from the venv after system package removal
|
||||||
|
/opt/venv/bin/python -c "import cffi; print('cffi OK:', cffi.__version__)" \
|
||||||
|
|| { echo 'ERROR: cffi broken after system package cleanup'; exit 1; }; \
|
||||||
|
\
|
||||||
|
# Strip debug symbols from ALL shared libraries
|
||||||
|
find /usr/lib -name '*.so*' -type f \
|
||||||
|
-not -path '*/jvm/*' \
|
||||||
|
-not -path '*/libreoffice/*' \
|
||||||
|
-exec strip --strip-unneeded {} + 2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
# Preserving ffmpeg codec libs as they are directly linked.
|
||||||
|
\
|
||||||
|
# Remove GPU backends not needed for headless operation.
|
||||||
|
MULTIARCH_LIBDIR=$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null \
|
||||||
|
|| find /usr/lib -maxdepth 1 -type d -name '*-linux-gnu' | head -1); \
|
||||||
|
rm -f \
|
||||||
|
"${MULTIARCH_LIBDIR}"/libLLVM*.so* \
|
||||||
|
"${MULTIARCH_LIBDIR}"/libgallium*.so* \
|
||||||
|
2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
# System-wide Python cache cleanup
|
||||||
|
find /usr/lib/python3* -type d -name __pycache__ \
|
||||||
|
-exec rm -rf {} + 2>/dev/null || true; \
|
||||||
|
find /usr/lib/python3* \( -name '*.pyc' -o -name '*.pyi' \) \
|
||||||
|
-delete 2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
# Additional metadata cleanup
|
||||||
|
# FIX: Only remove ImageMagick doc/www, NOT the whole dir (preserves policy.xml/delegates.xml)
|
||||||
|
rm -rf /usr/share/bug /usr/share/lintian /usr/share/linda \
|
||||||
|
/var/lib/dpkg/info/*.md5sums \
|
||||||
|
/var/log/dpkg.log /var/log/apt/* \
|
||||||
|
/usr/local/share/ghostscript/*/doc \
|
||||||
|
/usr/local/share/ghostscript/*/examples \
|
||||||
|
/usr/share/ImageMagick-*/doc \
|
||||||
|
/usr/share/ImageMagick-*/www; \
|
||||||
|
\
|
||||||
|
\
|
||||||
|
# NEW: Tesseract training configs (not needed for OCR, but keep configs/ for hocr/txt output)
|
||||||
|
rm -rf /usr/share/tesseract-ocr/*/tessdata/tessconfigs; \
|
||||||
|
\
|
||||||
|
# Trim CJK fonts to Regular weight only (FIX: Broadened path)
|
||||||
|
find /usr/share/fonts -name '*CJK*' \
|
||||||
|
! -name '*Regular*' -type f -delete 2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
# Misc caches
|
||||||
|
rm -rf /var/cache/fontconfig/* /tmp/*
|
||||||
|
|
||||||
|
# Calibre and QPDF tools.
|
||||||
|
COPY --from=calibre-build /opt/calibre /opt/calibre
|
||||||
|
COPY --from=pdf-tools-build /usr/local/bin/qpdf /usr/bin/qpdf
|
||||||
|
COPY --from=pdf-tools-build /usr/local/bin/magick /usr/bin/magick
|
||||||
|
COPY --from=pdf-tools-build /usr/local/lib/libMagick* /usr/local/lib/
|
||||||
|
# Copy loadable coder/filter modules (required when built with --with-modules)
|
||||||
|
COPY --from=pdf-tools-build /usr/local/lib/ImageMagick-7* /usr/local/lib/
|
||||||
|
COPY --from=pdf-tools-build /usr/local/etc/ImageMagick-7 /usr/local/etc/ImageMagick-7
|
||||||
|
COPY --from=gs-build /usr/local/bin/gs /usr/local/bin/gs
|
||||||
|
COPY --from=gs-build /usr/local/share/ghostscript /usr/local/share/ghostscript
|
||||||
|
RUN ldconfig /usr/local/lib
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# Non-root user
|
||||||
|
# ---
|
||||||
ARG PUID=1000
|
ARG PUID=1000
|
||||||
ARG PGID=1000
|
ARG PGID=1000
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
# Create group if it doesn't exist
|
|
||||||
if ! getent group stirlingpdfgroup >/dev/null 2>&1; then \
|
if ! getent group stirlingpdfgroup >/dev/null 2>&1; then \
|
||||||
if getent group "${PGID}" >/dev/null 2>&1; then \
|
groupadd -g "${PGID}" stirlingpdfgroup 2>/dev/null \
|
||||||
groupadd -o -g "${PGID}" stirlingpdfgroup; \
|
|| groupadd stirlingpdfgroup; \
|
||||||
else \
|
|
||||||
groupadd -g "${PGID}" stirlingpdfgroup; \
|
|
||||||
fi; \
|
fi; \
|
||||||
fi; \
|
|
||||||
# Create user if it doesn't exist, avoid UID conflicts
|
|
||||||
if ! id -u stirlingpdfuser >/dev/null 2>&1; then \
|
if ! id -u stirlingpdfuser >/dev/null 2>&1; then \
|
||||||
if getent passwd | awk -F: -v id="${PUID}" '$3==id{found=1} END{exit !found}'; then \
|
useradd -m -u "${PUID}" -g stirlingpdfgroup \
|
||||||
echo "UID ${PUID} already in use – creating stirlingpdfuser with automatic UID"; \
|
-d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser 2>/dev/null \
|
||||||
useradd -m -g stirlingpdfgroup -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
|| useradd -m -g stirlingpdfgroup \
|
||||||
else \
|
-d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
||||||
useradd -m -u "${PUID}" -g stirlingpdfgroup -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
|
||||||
fi; \
|
fi; \
|
||||||
fi
|
ln -sf /usr/sbin/gosu /usr/local/bin/su-exec
|
||||||
|
|
||||||
# Compatibility alias for older entrypoint scripts expecting su-exec
|
# Application files.
|
||||||
RUN ln -sf /usr/sbin/gosu /usr/local/bin/su-exec
|
WORKDIR /app
|
||||||
|
COPY --from=jar-extract --chown=1000:1000 /layers/dependencies/ /app/
|
||||||
|
COPY --from=jar-extract --chown=1000:1000 /layers/spring-boot-loader/ /app/
|
||||||
|
COPY --from=jar-extract --chown=1000:1000 /layers/snapshot-dependencies/ /app/
|
||||||
|
COPY --from=jar-extract --chown=1000:1000 /layers/application/ /app/
|
||||||
|
|
||||||
# Copy application files from build stage
|
COPY --from=app-build --chown=1000:1000 \
|
||||||
COPY --from=build --chown=stirlingpdfuser:stirlingpdfgroup /app/app/core/build/libs/*.jar /app.jar
|
/app/build/libs/restart-helper.jar /restart-helper.jar
|
||||||
COPY --from=build --chown=stirlingpdfuser:stirlingpdfgroup /app/build/libs/restart-helper.jar /restart-helper.jar
|
COPY --chown=1000:1000 scripts/ /scripts/
|
||||||
COPY scripts/ /scripts/
|
|
||||||
|
# Fonts go to system dir — root ownership is correct (world-readable)
|
||||||
COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/truetype/
|
COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/truetype/
|
||||||
|
|
||||||
# Optional version tag (can be passed at build time)
|
# Permissions and configuration.
|
||||||
|
RUN set -eux; \
|
||||||
|
ln -sf /opt/calibre/ebook-convert /usr/bin/ebook-convert; \
|
||||||
|
ln -sf /opt/venv/bin/unoconvert /usr/local/bin/unoconvert; \
|
||||||
|
ln -sf /opt/venv/bin/unoserver /usr/local/bin/unoserver; \
|
||||||
|
ln -sf /opt/venv/bin/ocrmypdf /usr/local/bin/ocrmypdf; \
|
||||||
|
ln -sf /opt/venv/bin/weasyprint /usr/local/bin/weasyprint; \
|
||||||
|
ln -sf /opt/venv/bin/unoping /usr/local/bin/unoping; \
|
||||||
|
chmod +x /scripts/*; \
|
||||||
|
mkdir -p /configs /logs /customFiles \
|
||||||
|
/pipeline/watchedFolders /pipeline/finishedFolders \
|
||||||
|
/tmp/stirling-pdf/heap_dumps; \
|
||||||
|
# Create symlinks to allow app to find these in /app/
|
||||||
|
ln -s /logs /app/logs; \
|
||||||
|
ln -s /configs /app/configs; \
|
||||||
|
ln -s /customFiles /app/customFiles; \
|
||||||
|
ln -s /pipeline /app/pipeline; \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup \
|
||||||
|
/home/stirlingpdfuser /configs /logs /customFiles /pipeline \
|
||||||
|
/tmp/stirling-pdf; \
|
||||||
|
chown -h stirlingpdfuser:stirlingpdfgroup /app/logs /app/configs /app/customFiles /app/pipeline; \
|
||||||
|
chown stirlingpdfuser:stirlingpdfgroup /app; \
|
||||||
|
chmod 750 /tmp/stirling-pdf; \
|
||||||
|
chmod 750 /tmp/stirling-pdf/heap_dumps; \
|
||||||
|
fc-cache -f
|
||||||
|
# NOTE: Project Leyden AOT cache is generated in the background on first boot
|
||||||
|
# by init-without-ocr.sh. The cache is picked up on subsequent boots for
|
||||||
|
# 15-25% faster startup. See: JEP 483 + 514 + 515 (JDK 25).
|
||||||
|
|
||||||
|
# Environment variables.
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
# Metadata labels
|
|
||||||
LABEL org.opencontainers.image.title="Stirling-PDF"
|
|
||||||
LABEL org.opencontainers.image.description="Stirling-PDF with embedded frontend - Full version with Calibre, LibreOffice, Tesseract, OCRmyPDF, and more"
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
|
|
||||||
LABEL org.opencontainers.image.licenses="MIT"
|
|
||||||
LABEL org.opencontainers.image.vendor="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
|
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
|
|
||||||
LABEL maintainer="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.authors="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.version="${VERSION_TAG}"
|
|
||||||
LABEL org.opencontainers.image.keywords="PDF, manipulation, API, Spring Boot, React"
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Runtime environment variables
|
|
||||||
# ==============================================================================
|
|
||||||
ENV VERSION_TAG=$VERSION_TAG \
|
ENV VERSION_TAG=$VERSION_TAG \
|
||||||
JAVA_BASE_OPTS="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70 -Djava.awt.headless=true" \
|
STIRLING_JVM_PROFILE="balanced" \
|
||||||
|
_JVM_OPTS_BALANCED="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/stirling-pdf/heap_dumps -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4m -XX:G1PeriodicGCInterval=60000 -XX:+UseStringDeduplication -XX:+UseCompactObjectHeaders -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \
|
||||||
|
_JVM_OPTS_PERFORMANCE="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/stirling-pdf/heap_dumps -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \
|
||||||
JAVA_CUSTOM_OPTS="" \
|
JAVA_CUSTOM_OPTS="" \
|
||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
PUID=${PUID} \
|
PUID=${PUID} \
|
||||||
PGID=${PGID} \
|
PGID=${PGID} \
|
||||||
UMASK=022 \
|
UMASK=022 \
|
||||||
|
PATH="/opt/venv/bin:${PATH}" \
|
||||||
UNO_PATH=/usr/lib/libreoffice/program \
|
UNO_PATH=/usr/lib/libreoffice/program \
|
||||||
LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin \
|
LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin \
|
||||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||||
TMPDIR=/tmp/stirling-pdf \
|
TMPDIR=/tmp/stirling-pdf \
|
||||||
TEMP=/tmp/stirling-pdf \
|
TEMP=/tmp/stirling-pdf \
|
||||||
TMP=/tmp/stirling-pdf
|
TMP=/tmp/stirling-pdf \
|
||||||
|
QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu --disable-software-rasterizer" \
|
||||||
|
DBUS_SESSION_BUS_ADDRESS=/dev/null
|
||||||
|
|
||||||
# ==============================================================================
|
# Metadata labels.
|
||||||
# Python virtual environment for additional Python tools (WeasyPrint, OpenCV, etc.)
|
LABEL org.opencontainers.image.title="Stirling-PDF" \
|
||||||
# ==============================================================================
|
org.opencontainers.image.description="Full version with Calibre, LibreOffice, Tesseract, OCRmyPDF" \
|
||||||
RUN python3 -m venv /opt/venv --system-site-packages \
|
org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" \
|
||||||
&& /opt/venv/bin/pip install --no-cache-dir weasyprint pdf2image opencv-python-headless \
|
org.opencontainers.image.licenses="MIT" \
|
||||||
&& /opt/venv/bin/python -c "import cv2; print('OpenCV version:', cv2.__version__)"
|
org.opencontainers.image.vendor="Stirling-Tools" \
|
||||||
|
org.opencontainers.image.url="https://www.stirlingpdf.com" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.stirlingpdf.com" \
|
||||||
|
maintainer="Stirling-Tools" \
|
||||||
|
org.opencontainers.image.authors="Stirling-Tools" \
|
||||||
|
org.opencontainers.image.version="${VERSION_TAG}" \
|
||||||
|
org.opencontainers.image.keywords="PDF, manipulation, API, Spring Boot, React"
|
||||||
|
|
||||||
# Separate venv for unoserver (keeps it isolated)
|
|
||||||
ARG UNOSERVER_VERSION=3.6
|
|
||||||
RUN python3 -m venv /opt/unoserver-venv --system-site-packages \
|
|
||||||
&& /opt/unoserver-venv/bin/pip install --no-cache-dir "unoserver==${UNOSERVER_VERSION}"
|
|
||||||
|
|
||||||
# Make unoserver tools available in main venv PATH
|
|
||||||
RUN ln -sf /opt/unoserver-venv/bin/unoconvert /opt/venv/bin/unoconvert \
|
|
||||||
&& ln -sf /opt/unoserver-venv/bin/unoserver /opt/venv/bin/unoserver
|
|
||||||
|
|
||||||
# Extend PATH to include both virtual environments
|
|
||||||
ENV PATH="/opt/venv/bin:/opt/unoserver-venv/bin:${PATH}"
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Final permissions, directories and font cache
|
|
||||||
# ==============================================================================
|
|
||||||
RUN set -eux; \
|
|
||||||
chmod +x /scripts/*; \
|
|
||||||
mkdir -p /configs /configs/heap_dumps /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf; \
|
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup \
|
|
||||||
/home/stirlingpdfuser /configs /logs /customFiles /pipeline /tmp/stirling-pdf \
|
|
||||||
/app.jar /restart-helper.jar /usr/share/fonts/truetype /scripts; \
|
|
||||||
chmod -R 755 /tmp/stirling-pdf
|
|
||||||
|
|
||||||
# Rebuild font cache
|
|
||||||
RUN fc-cache -f -v
|
|
||||||
|
|
||||||
# Force Qt/WebEngine to run headlessly (required for Calibre in Docker)
|
|
||||||
ENV QT_QPA_PLATFORM=offscreen \
|
|
||||||
QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu --disable-dev-shm-usage"
|
|
||||||
|
|
||||||
# Expose web UI port
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
STOPSIGNAL SIGTERM
|
STOPSIGNAL SIGTERM
|
||||||
|
|
||||||
# Use tini as init (handles signals and zombies correctly)
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
CMD curl -fs --show-error http://localhost:8080/api/v1/info/status || exit 1
|
||||||
|
|
||||||
# CMD is empty – actual start command is defined in init.sh
|
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||||
CMD []
|
CMD []
|
||||||
|
|||||||
+558
-176
@@ -1,158 +1,563 @@
|
|||||||
# Stirling-PDF Dockerfile - Fat version with embedded frontend
|
# Stirling-PDF - Fat version (embedded frontend)
|
||||||
# Single JAR contains both frontend and backend with extra fonts for air-gapped environments
|
# Extra fonts for air-gapped environments
|
||||||
|
|
||||||
# Stage 1: Build application with embedded frontend
|
FROM ubuntu:noble AS calibre-build
|
||||||
FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build
|
|
||||||
|
|
||||||
# Install Node.js and npm for frontend build
|
ARG CALIBRE_VERSION=9.3.1
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
||||||
&& apt-get install -y nodejs \
|
|
||||||
&& npm --version \
|
|
||||||
&& node --version \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Copy gradle files for dependency resolution
|
|
||||||
COPY build.gradle .
|
|
||||||
COPY settings.gradle .
|
|
||||||
COPY gradlew .
|
|
||||||
COPY gradle gradle/
|
|
||||||
COPY app/core/build.gradle core/.
|
|
||||||
COPY app/common/build.gradle common/.
|
|
||||||
COPY app/proprietary/build.gradle proprietary/.
|
|
||||||
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy entire project
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build JAR with embedded frontend (includes security features controlled at runtime)
|
|
||||||
RUN DISABLE_ADDITIONAL_FEATURES=false \
|
|
||||||
./gradlew clean build -PbuildWithFrontend=true -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
|
||||||
|
|
||||||
# Stage 2: Runtime image based on Debian stable-slim
|
|
||||||
# Contains Java runtime + LibreOffice + Calibre + all PDF tools + extra fonts for air-gapped environments
|
|
||||||
FROM debian:stable-slim@sha256:ed542b2d269ff08139fc5ab8c762efe8c8986b564a423d5241a5ce9fb09b6c08
|
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
ENV LANG=C.UTF-8 \
|
|
||||||
LC_ALL=C.UTF-8
|
|
||||||
|
|
||||||
ENV TESS_BASE_PATH=/usr/share/tesseract-ocr/5/tessdata
|
|
||||||
|
|
||||||
# Install core runtime dependencies + tools required by Stirling-PDF features + extra fonts for fat version
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates tzdata tini bash fontconfig \
|
|
||||||
openjdk-21-jre-headless \
|
|
||||||
ffmpeg poppler-utils ocrmypdf imagemagick fontforge ghostscript \
|
|
||||||
fonts-dejavu \
|
|
||||||
fonts-liberation fonts-liberation2 \
|
|
||||||
fonts-crosextra-caladea fonts-crosextra-carlito \
|
|
||||||
fonts-linuxlibertine \
|
|
||||||
fonts-noto-core fonts-noto-cjk fonts-noto-mono fonts-noto-ui-core \
|
|
||||||
fonts-noto-color-emoji \
|
|
||||||
ttf-wqy-zenhei \
|
|
||||||
fonts-arphic-ukai fonts-arphic-uming \
|
|
||||||
fonts-freefont-ttf fonts-terminus \
|
|
||||||
python3 python3-venv python3-uno \
|
|
||||||
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-fra \
|
|
||||||
tesseract-ocr-por tesseract-ocr-chi-sim \
|
|
||||||
libcairo2 libpango-1.0-0 libpangoft2-1.0-0 libgdk-pixbuf-2.0-0 \
|
|
||||||
gosu unpaper qpdf \
|
|
||||||
# AWT headless support (required for some Java graphics operations)
|
|
||||||
libfreetype6 libfontconfig1 libx11-6 libxt6 libxext6 libxrender1 libxtst6 libxi6 \
|
|
||||||
libxinerama1 libxkbcommon0 libxkbfile1 libsm6 libice6 \
|
|
||||||
# Qt WebEngine dependencies for Calibre
|
|
||||||
libegl1 libopengl0 libgl1 libxdamage1 libxfixes3 libxshmfence1 libdrm2 libgbm1 \
|
|
||||||
libxkbcommon-x11-0 libxrandr2 libxcomposite1 libnss3 libx11-xcb1 \
|
|
||||||
libxcb-cursor0 libdbus-1-3 libglib2.0-0 \
|
|
||||||
# Virtual framebuffer (required for headless LibreOffice)
|
|
||||||
xvfb x11-utils coreutils \
|
|
||||||
# Temporary packages only needed for Calibre installer
|
|
||||||
xz-utils gpgv curl xdg-utils \
|
|
||||||
\
|
|
||||||
# Install Calibre from official installer script
|
|
||||||
&& curl -fsSL https://download.calibre-ebook.com/linux-installer.sh | sh /dev/stdin \
|
|
||||||
\
|
|
||||||
# Clean up installer-only packages
|
|
||||||
&& apt-get purge -y xz-utils gpgv xdg-utils \
|
|
||||||
&& apt-get autoremove -y \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
. /etc/os-release; \
|
|
||||||
echo "deb http://deb.debian.org/debian ${VERSION_CODENAME}-backports main" > /etc/apt/sources.list.d/backports.list; \
|
|
||||||
apt-get update; \
|
apt-get update; \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends -t ${VERSION_CODENAME}-backports \
|
apt-get install -y --no-install-recommends \
|
||||||
libreoffice libreoffice-java-common; \
|
ca-certificates curl xz-utils libnss3 libfontconfig1 \
|
||||||
|
libgl1 libegl1 libdbus-1-3 libasound2t64 libxcomposite1 \
|
||||||
|
libxrandr2 libxkbcommon0 libxi6 libxtst6 libopengl0; \
|
||||||
rm -rf /var/lib/apt/lists/*; \
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
libreoffice --version
|
\
|
||||||
|
case "$(uname -m)" in \
|
||||||
|
x86_64) CALIBRE_ARCH="x86_64" ;; \
|
||||||
|
aarch64) CALIBRE_ARCH="arm64" ;; \
|
||||||
|
*) echo "Unsupported arch: $(uname -m)"; exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
\
|
||||||
|
curl -fsSL \
|
||||||
|
"https://download.calibre-ebook.com/${CALIBRE_VERSION}/calibre-${CALIBRE_VERSION}-${CALIBRE_ARCH}.txz" \
|
||||||
|
-o /tmp/calibre.txz; \
|
||||||
|
mkdir -p /opt/calibre; \
|
||||||
|
tar xJf /tmp/calibre.txz -C /opt/calibre; \
|
||||||
|
rm /tmp/calibre.txz; \
|
||||||
|
\
|
||||||
|
# Remove GUI-only shared libraries.
|
||||||
|
# Libs required by WebEngine PDF output are preserved.
|
||||||
|
rm -f /opt/calibre/lib/libQt6Designer* \
|
||||||
|
/opt/calibre/lib/libQt6Multimedia* \
|
||||||
|
/opt/calibre/lib/libQt6SpatialAudio.so.* \
|
||||||
|
/opt/calibre/lib/libQt6NetworkAuth.so.* \
|
||||||
|
/opt/calibre/lib/libQt6Concurrent.so.* \
|
||||||
|
/opt/calibre/lib/libQt6OpenGLWidgets.so.* \
|
||||||
|
/opt/calibre/lib/libQt6QuickWidgets.so.* \
|
||||||
|
# AV / multimedia
|
||||||
|
/opt/calibre/lib/libavcodec.so.* \
|
||||||
|
/opt/calibre/lib/libavfilter.so.* \
|
||||||
|
/opt/calibre/lib/libavformat.so.* \
|
||||||
|
/opt/calibre/lib/libavutil.so.* \
|
||||||
|
/opt/calibre/lib/libavdevice.so.* \
|
||||||
|
/opt/calibre/lib/libpostproc.so.* \
|
||||||
|
/opt/calibre/lib/libswresample.so.* \
|
||||||
|
/opt/calibre/lib/libswscale.so.* \
|
||||||
|
# Audio / speech / TTS
|
||||||
|
/opt/calibre/lib/libspeex.so.* \
|
||||||
|
/opt/calibre/lib/libFLAC.so.* \
|
||||||
|
/opt/calibre/lib/libopus.so.* \
|
||||||
|
/opt/calibre/lib/libvorbis*.so.* \
|
||||||
|
/opt/calibre/lib/libasyncns.so.* \
|
||||||
|
/opt/calibre/lib/libspeechd.so.* \
|
||||||
|
/opt/calibre/lib/libespeak-ng.so.* \
|
||||||
|
# Other unused libs
|
||||||
|
/opt/calibre/lib/libonnxruntime.so.* \
|
||||||
|
/opt/calibre/lib/libgio-2.0.so.* \
|
||||||
|
/opt/calibre/lib/libzstd.so.* \
|
||||||
|
/opt/calibre/lib/libhunspell-1.7.so.* \
|
||||||
|
/opt/calibre/lib/libbrotlienc.so.* \
|
||||||
|
/opt/calibre/lib/libbrotlicommon.so.* \
|
||||||
|
/opt/calibre/lib/libbrotlidec.so.* \
|
||||||
|
/opt/calibre/lib/libstemmer.so.* \
|
||||||
|
/opt/calibre/lib/libmtp.so.* \
|
||||||
|
/opt/calibre/lib/libncursesw.so.* \
|
||||||
|
/opt/calibre/lib/libchm.so.* \
|
||||||
|
/opt/calibre/lib/libgcrypt.so.* \
|
||||||
|
/opt/calibre/lib/libgpg-error.so.* \
|
||||||
|
/opt/calibre/lib/libicuio.so.* \
|
||||||
|
/opt/calibre/lib/libreadline.so.* \
|
||||||
|
/opt/calibre/lib/libusb-1.0.so.*; \
|
||||||
|
rm -rf /opt/calibre/lib/qt6/plugins/platformthemes \
|
||||||
|
/opt/calibre/lib/qt6/plugins/multimedia \
|
||||||
|
/opt/calibre/lib/qt6/plugins/designer \
|
||||||
|
/opt/calibre/lib/qt6/plugins/qmltooling; \
|
||||||
|
\
|
||||||
|
# Remove GUI executables but keep ebook-convert, ebook-meta, and calibre-parallel.
|
||||||
|
rm -f /opt/calibre/calibre \
|
||||||
|
/opt/calibre/calibre-server \
|
||||||
|
/opt/calibre/calibre-smtp \
|
||||||
|
/opt/calibre/calibre-debug \
|
||||||
|
/opt/calibre/calibre-customize \
|
||||||
|
/opt/calibre/calibredb \
|
||||||
|
/opt/calibre/ebook-viewer \
|
||||||
|
/opt/calibre/ebook-edit \
|
||||||
|
/opt/calibre/ebook-polish \
|
||||||
|
/opt/calibre/ebook-device \
|
||||||
|
/opt/calibre/fetch-ebook-metadata \
|
||||||
|
/opt/calibre/lrf2lrs \
|
||||||
|
/opt/calibre/lrs2lrf \
|
||||||
|
/opt/calibre/markdown-calibre \
|
||||||
|
/opt/calibre/web2disk; \
|
||||||
|
\
|
||||||
|
# Remove Python modules not needed for conversion.
|
||||||
|
rm -rf /opt/calibre/lib/calibre/gui2 \
|
||||||
|
/opt/calibre/lib/calibre/devices \
|
||||||
|
/opt/calibre/lib/calibre/library \
|
||||||
|
/opt/calibre/lib/calibre/db \
|
||||||
|
/opt/calibre/lib/calibre/srv \
|
||||||
|
/opt/calibre/lib/calibre/spell \
|
||||||
|
/opt/calibre/lib/calibre/live; \
|
||||||
|
\
|
||||||
|
# Remove resources not needed for CLI conversion.
|
||||||
|
rm -rf /opt/calibre/resources/images \
|
||||||
|
/opt/calibre/resources/icons \
|
||||||
|
/opt/calibre/resources/icons.rcc \
|
||||||
|
/opt/calibre/resources/content-server \
|
||||||
|
/opt/calibre/resources/editor* \
|
||||||
|
/opt/calibre/resources/viewer \
|
||||||
|
/opt/calibre/resources/viewer.js \
|
||||||
|
/opt/calibre/resources/viewer.html \
|
||||||
|
/opt/calibre/resources/recipes \
|
||||||
|
/opt/calibre/resources/dictionaries \
|
||||||
|
/opt/calibre/resources/hyphenation \
|
||||||
|
/opt/calibre/resources/catalog \
|
||||||
|
/opt/calibre/resources/calibre-mimetypes.xml \
|
||||||
|
/opt/calibre/resources/changelog.json \
|
||||||
|
/opt/calibre/resources/user-agent-data.json \
|
||||||
|
/opt/calibre/resources/builtin_recipes.zip \
|
||||||
|
/opt/calibre/resources/builtin_recipes.xml \
|
||||||
|
/opt/calibre/resources/builtin_recipes.xml \
|
||||||
|
/opt/calibre/resources/stylelint-bundle.min.js \
|
||||||
|
/opt/calibre/resources/stylelint.js \
|
||||||
|
/opt/calibre/resources/rapydscript \
|
||||||
|
/opt/calibre/resources/quick_start \
|
||||||
|
/opt/calibre/resources/piper-voices.json \
|
||||||
|
/opt/calibre/resources/images.qrc \
|
||||||
|
/opt/calibre/resources/mozilla-ca-certs.pem \
|
||||||
|
/opt/calibre/resources/ebook-convert-complete.calibre_msgpack \
|
||||||
|
/opt/calibre/resources/mathjax \
|
||||||
|
/opt/calibre/resources/common-english-words.txt \
|
||||||
|
/opt/calibre/resources/calibre-portable.sh \
|
||||||
|
/opt/calibre/resources/calibre-portable.bat \
|
||||||
|
/opt/calibre/resources/metadata_sqlite.sql \
|
||||||
|
/opt/calibre/resources/notes_sqlite.sql \
|
||||||
|
/opt/calibre/resources/fts_sqlite.sql \
|
||||||
|
/opt/calibre/resources/fts_triggers.sql \
|
||||||
|
/opt/calibre/resources/jacket \
|
||||||
|
/opt/calibre/resources/editor-functions.json \
|
||||||
|
/opt/calibre/resources/calibre-ebook-root-CA.crt \
|
||||||
|
/opt/calibre/resources/csscolorparser.js \
|
||||||
|
/opt/calibre/resources/lookup.js \
|
||||||
|
/opt/calibre/resources/pdf-mathjax-loader.js \
|
||||||
|
/opt/calibre/resources/scraper.js \
|
||||||
|
/opt/calibre/resources/toc.js \
|
||||||
|
/opt/calibre/resources/user-manual-translation-stats.json \
|
||||||
|
/opt/calibre/resources/pin-template.svg \
|
||||||
|
/opt/calibre/resources/scripts.calibre_msgpack \
|
||||||
|
/opt/calibre/lib/calibre/ebooks/docx/images \
|
||||||
|
/opt/calibre/share \
|
||||||
|
/opt/calibre/man; \
|
||||||
|
\
|
||||||
|
# Remove translations and localization while keeping required libraries.
|
||||||
|
rm -rf /opt/calibre/lib/qt6/translations; \
|
||||||
|
find /opt/calibre/translations -mindepth 1 -maxdepth 1 ! -name 'qtwebengine_locales' -exec rm -rf {} +; \
|
||||||
|
rm -rf /opt/calibre/resources/localization/locales.zip \
|
||||||
|
/opt/calibre/resources/localization/stats.calibre_msgpack \
|
||||||
|
/opt/calibre/resources/localization/website-languages.txt; \
|
||||||
|
find /opt/calibre/resources/localization -mindepth 1 -maxdepth 1 ! -name 'iso639.calibre_msgpack' -exec rm -rf {} +; \
|
||||||
|
\
|
||||||
|
# Strip debug symbols from calibre extension modules.
|
||||||
|
# Exclude Qt6 libs and all qt6/ subdirectory files to prevent Chromium renderer crashes.
|
||||||
|
find /opt/calibre/lib -name '*.so*' \
|
||||||
|
! -name 'libQt6*' \
|
||||||
|
! -path '*/qt6/*' \
|
||||||
|
-exec strip --strip-unneeded {} + 2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
find /opt/calibre -type d -name __pycache__ \
|
||||||
|
-exec rm -rf {} + 2>/dev/null || true; \
|
||||||
|
find /opt/calibre -name '*.pyc' -delete 2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
# Verify conversion functionality.
|
||||||
|
# NOTE: txt→epub used intentionally NOT txt→pdf.
|
||||||
|
# Calibre 7+ uses WebEngine (Chromium) for PDF output, which requires kernel
|
||||||
|
# capabilities unavailable in Docker RUN steps and segfaults under QEMU.
|
||||||
|
# epub output exercises the same Python/plugin stack without touching WebEngine.
|
||||||
|
/opt/calibre/ebook-convert --version; \
|
||||||
|
echo "Hello" > /tmp/test.txt; \
|
||||||
|
/opt/calibre/ebook-convert /tmp/test.txt /tmp/test.epub; \
|
||||||
|
rm -f /tmp/test.txt /tmp/test.epub; \
|
||||||
|
echo "=== Calibre stripped successfully ==="
|
||||||
|
|
||||||
# Make ebook-convert available in PATH
|
|
||||||
RUN ln -sf /opt/calibre/ebook-convert /usr/bin/ebook-convert \
|
|
||||||
&& /opt/calibre/ebook-convert --version
|
|
||||||
|
|
||||||
# ==============================================================================
|
# Build the Java application and frontend.
|
||||||
# Create non-root user (stirlingpdfuser) with configurable UID/GID
|
FROM gradle:9.3.1-jdk25 AS app-build
|
||||||
# ==============================================================================
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl ca-certificates \
|
||||||
|
&& update-ca-certificates \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# JDK 25+: --add-exports is no longer accepted via JAVA_TOOL_OPTIONS; use JDK_JAVA_OPTIONS instead
|
||||||
|
ENV JDK_JAVA_OPTIONS="--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY build.gradle settings.gradle gradlew ./
|
||||||
|
COPY gradle/ gradle/
|
||||||
|
COPY app/core/build.gradle app/core/
|
||||||
|
COPY app/common/build.gradle app/common/
|
||||||
|
COPY app/proprietary/build.gradle app/proprietary/
|
||||||
|
|
||||||
|
# Use system gradle instead of gradlew to avoid SSL issues downloading gradle distribution on emulated arm64
|
||||||
|
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
|
||||||
|
--mount=type=cache,target=/home/gradle/.gradle/wrapper \
|
||||||
|
gradle dependencies --no-daemon || true
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
|
||||||
|
--mount=type=cache,target=/home/gradle/.gradle/wrapper \
|
||||||
|
DISABLE_ADDITIONAL_FEATURES=false \
|
||||||
|
gradle clean build \
|
||||||
|
-PbuildWithFrontend=true \
|
||||||
|
-x spotlessApply -x spotlessCheck -x test -x sonarqube \
|
||||||
|
--no-daemon
|
||||||
|
|
||||||
|
|
||||||
|
# Python Builder stage.
|
||||||
|
FROM ubuntu:noble AS python-build
|
||||||
|
|
||||||
|
ARG UNOSERVER_VERSION=3.6
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
python3 python3-venv python3-dev \
|
||||||
|
python3-packaging \
|
||||||
|
build-essential \
|
||||||
|
# Build dependencies for ocrmypdf/weasyprint/opencv
|
||||||
|
zlib1g-dev libjpeg-dev libffi-dev libpango1.0-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN python3 -m venv /opt/venv --system-site-packages
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Build all heavy python packages here
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
|
pip install \
|
||||||
|
weasyprint pdf2image opencv-python-headless ocrmypdf \
|
||||||
|
"unoserver==${UNOSERVER_VERSION}"
|
||||||
|
|
||||||
|
|
||||||
|
# Build Ghostscript 10.06.0 from source in an isolated stage (avoids library conflicts).
|
||||||
|
FROM ubuntu:noble AS gs-build
|
||||||
|
ARG GS_VERSION=10.06.0
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential curl ca-certificates libfontconfig1-dev && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
GS_TAG="gs$(printf '%s' "${GS_VERSION}" | tr -d '.')" && \
|
||||||
|
curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/${GS_TAG}/ghostscript-${GS_VERSION}.tar.gz" | tar xz && \
|
||||||
|
cd "ghostscript-${GS_VERSION}" && \
|
||||||
|
./configure \
|
||||||
|
--prefix=/usr/local \
|
||||||
|
--without-x \
|
||||||
|
--disable-cups \
|
||||||
|
--disable-gtk && \
|
||||||
|
make -j"$(nproc)" && \
|
||||||
|
make install && \
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
|
||||||
|
# Build PDF Tools (QPDF and ImageMagick 7).
|
||||||
|
FROM ubuntu:noble AS pdf-tools-build
|
||||||
|
ARG QPDF_VERSION=12.3.2
|
||||||
|
ARG IM_VERSION=7.1.2-13
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential cmake libssl-dev libjpeg-dev zlib1g-dev curl ca-certificates pkg-config \
|
||||||
|
libpng-dev libtiff-dev libwebp-dev libxml2-dev libfreetype6-dev liblcms2-dev libzip-dev liblqr-1-0-dev \
|
||||||
|
libltdl-dev libtool && \
|
||||||
|
# Build QPDF
|
||||||
|
curl -fsSL "https://github.com/qpdf/qpdf/releases/download/v${QPDF_VERSION}/qpdf-${QPDF_VERSION}.tar.gz" | tar xz && \
|
||||||
|
cd "qpdf-${QPDF_VERSION}" && \
|
||||||
|
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DALLOW_CRYPTO_OPENSSL=ON -DDEFAULT_CRYPTO=openssl && \
|
||||||
|
cmake --build build --parallel "$(nproc)" && \
|
||||||
|
cmake --install build && \
|
||||||
|
cd .. && \
|
||||||
|
# Build ImageMagick 7
|
||||||
|
curl -fsSL "https://github.com/ImageMagick/ImageMagick/archive/refs/tags/${IM_VERSION}.tar.gz" | tar xz && \
|
||||||
|
cd "ImageMagick-${IM_VERSION}" && \
|
||||||
|
./configure --prefix=/usr/local --with-modules --with-perl=no --with-magick-plus-plus=no --with-quantum-depth=16 --disable-static --enable-shared && \
|
||||||
|
make -j"$(nproc)" && \
|
||||||
|
make install && \
|
||||||
|
# Enable PDF/PS/EPS in policy
|
||||||
|
sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /usr/local/etc/ImageMagick-7/policy.xml && \
|
||||||
|
sed -i 's/rights="none" pattern="PS"/rights="read|write" pattern="PS"/' /usr/local/etc/ImageMagick-7/policy.xml && \
|
||||||
|
sed -i 's/rights="none" pattern="EPS"/rights="read|write" pattern="EPS"/' /usr/local/etc/ImageMagick-7/policy.xml && \
|
||||||
|
cd .. && \
|
||||||
|
ldconfig /usr/local/lib
|
||||||
|
|
||||||
|
|
||||||
|
# Final runtime image.
|
||||||
|
FROM eclipse-temurin:25-jre AS runtime
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
|
LANG=C.UTF-8 \
|
||||||
|
LC_ALL=C.UTF-8 \
|
||||||
|
TESS_BASE_PATH=/usr/share/tesseract-ocr/5/tessdata
|
||||||
|
|
||||||
|
ARG UNOSERVER_VERSION=3.6
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/root/.cache/pip \
|
||||||
|
set -eux; \
|
||||||
|
apt-get update; \
|
||||||
|
# Add LibreOffice Fresh PPA for latest version (26.2.x)
|
||||||
|
apt-get install -y --no-install-recommends software-properties-common; \
|
||||||
|
add-apt-repository -y ppa:libreoffice/ppa; \
|
||||||
|
apt-get update; \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
# Core tools
|
||||||
|
ca-certificates tzdata tini bash fontconfig curl \
|
||||||
|
ffmpeg poppler-utils fontforge \
|
||||||
|
gosu unpaper pngquant \
|
||||||
|
fonts-liberation2 \
|
||||||
|
fonts-crosextra-caladea fonts-crosextra-carlito \
|
||||||
|
fonts-noto-core fonts-noto-mono fonts-noto-extra \
|
||||||
|
fonts-noto-cjk poppler-data \
|
||||||
|
fonts-freefont-ttf fonts-terminus \
|
||||||
|
# Python runtime & UNO bridge (python3-full -> python3 optimization)
|
||||||
|
python3 python3-uno python3-packaging \
|
||||||
|
# OCR
|
||||||
|
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-fra \
|
||||||
|
tesseract-ocr-por tesseract-ocr-chi-sim \
|
||||||
|
# Graphics / AWT headless
|
||||||
|
libcairo2 libpango-1.0-0 libpangoft2-1.0-0 \
|
||||||
|
libfreetype6 libfontconfig1 libx11-6 libxt6 libxext6 libxrender1 \
|
||||||
|
libxtst6 libxi6 libxinerama1 libxkbcommon0 libsm6 libice6 \
|
||||||
|
# Qt/EGL for Calibre CLI
|
||||||
|
libegl1 libgl1 libopengl0 libdbus-1-3 libglib2.0-0 libnss3 \
|
||||||
|
libasound2t64 libxcomposite1 libxrandr2 \
|
||||||
|
# Virtual framebuffer (required for headless LibreOffice Impress/Draw)
|
||||||
|
xvfb x11-utils coreutils \
|
||||||
|
libreoffice-writer-nogui libreoffice-calc-nogui \
|
||||||
|
libreoffice-impress-nogui libreoffice-draw-nogui \
|
||||||
|
libreoffice-base-nogui libreoffice-java-common \
|
||||||
|
; \
|
||||||
|
\
|
||||||
|
# Fix LibreOffice UNO bridge and filter availability
|
||||||
|
libreoffice --version; \
|
||||||
|
soffice --version 2>/dev/null || true; \
|
||||||
|
# Rebuild UNO bridge type database
|
||||||
|
/usr/lib/libreoffice/program/soffice.bin --headless --convert-to pdf /dev/null 2>/dev/null || true; \
|
||||||
|
# Force font cache rebuild and verify filters are available
|
||||||
|
fc-cache -f -v 2>&1 | awk 'NR <= 20'; \
|
||||||
|
\
|
||||||
|
# Cleanup stage.
|
||||||
|
\
|
||||||
|
# apt clean not strictly needed with cache mounts, but good for reducing layer metadata
|
||||||
|
apt-get clean; \
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
\
|
||||||
|
# Docs / man / info / icons / themes / GUI assets (headless server)
|
||||||
|
rm -rf /usr/share/doc/* /usr/share/man/* /usr/share/info/* \
|
||||||
|
/usr/share/lintian/* /usr/share/linda/* \
|
||||||
|
/usr/share/icons/* /usr/share/themes/* \
|
||||||
|
/usr/share/javascript/* \
|
||||||
|
/usr/share/gtk-3.0/* \
|
||||||
|
/usr/share/fontforge/pixmaps \
|
||||||
|
/usr/share/liblangtag/* \
|
||||||
|
/usr/share/tcltk/* \
|
||||||
|
/usr/share/python-wheels/*; \
|
||||||
|
\
|
||||||
|
# Clean up system locale data (LANG=C.UTF-8 doesn't use them)
|
||||||
|
find /usr/share/locale -mindepth 1 -maxdepth 1 -type d \
|
||||||
|
! -name 'en*' -exec rm -rf {} + 2>/dev/null || true; \
|
||||||
|
rm -rf /usr/share/i18n/locales /usr/share/i18n/charmaps; \
|
||||||
|
\
|
||||||
|
rm -rf /usr/lib/libreoffice/share/gallery \
|
||||||
|
/usr/lib/libreoffice/share/template \
|
||||||
|
/usr/lib/libreoffice/share/wizards \
|
||||||
|
/usr/lib/libreoffice/share/autotext \
|
||||||
|
/usr/lib/libreoffice/help \
|
||||||
|
/usr/lib/libreoffice/share/config/images_*.zip \
|
||||||
|
/usr/lib/libreoffice/share/basic \
|
||||||
|
/usr/lib/libreoffice/share/Scripts \
|
||||||
|
/usr/lib/libreoffice/share/autocorr \
|
||||||
|
/usr/lib/libreoffice/share/classification \
|
||||||
|
/usr/lib/libreoffice/share/wordbook \
|
||||||
|
/usr/lib/libreoffice/share/fingerprint \
|
||||||
|
/usr/lib/libreoffice/share/xdg \
|
||||||
|
/usr/lib/libreoffice/share/numbertext \
|
||||||
|
/usr/lib/libreoffice/share/shell \
|
||||||
|
/usr/lib/libreoffice/share/palette \
|
||||||
|
/usr/lib/libreoffice/share/theme_definitions \
|
||||||
|
/usr/lib/libreoffice/share/xslt \
|
||||||
|
/usr/lib/libreoffice/share/labels \
|
||||||
|
/usr/lib/libreoffice/share/dtd \
|
||||||
|
/usr/lib/libreoffice/share/tipoftheday \
|
||||||
|
/usr/lib/libreoffice/share/toolbarmode \
|
||||||
|
/usr/lib/libreoffice/share/psprint; \
|
||||||
|
\
|
||||||
|
# Preserving soffice.cfg because LibreOffice needs it to load documents.
|
||||||
|
\
|
||||||
|
\
|
||||||
|
\
|
||||||
|
find /usr/lib -name '*.so*' -type f \
|
||||||
|
-not -path '*/jvm/*' \
|
||||||
|
-not -path '*/libreoffice/*' \
|
||||||
|
-exec strip --strip-unneeded {} + 2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
# Preserving ffmpeg codec libs as they are directly linked.
|
||||||
|
\
|
||||||
|
# Remove Mesa/LLVM GPU backends (~179 MB, not needed for headless/offscreen)
|
||||||
|
MULTIARCH_LIBDIR=$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null \
|
||||||
|
|| find /usr/lib -maxdepth 1 -type d -name '*-linux-gnu' | head -1); \
|
||||||
|
rm -f \
|
||||||
|
"${MULTIARCH_LIBDIR}"/libLLVM*.so* \
|
||||||
|
"${MULTIARCH_LIBDIR}"/libgallium*.so* \
|
||||||
|
2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
# Python stdlib: remove unused modules (~71 MB)
|
||||||
|
rm -rf /usr/lib/python3.12/test \
|
||||||
|
/usr/lib/python3.12/idlelib \
|
||||||
|
/usr/lib/python3.12/tkinter \
|
||||||
|
/usr/lib/python3.12/lib2to3 \
|
||||||
|
/usr/lib/python3.12/pydoc_data; \
|
||||||
|
\
|
||||||
|
# System Python packages not needed at runtime (~153 MB)
|
||||||
|
rm -rf /usr/lib/python3/dist-packages/scipy \
|
||||||
|
/usr/lib/python3/dist-packages/sympy \
|
||||||
|
/usr/lib/python3/dist-packages/mpmath; \
|
||||||
|
\
|
||||||
|
# Duplicate system packages (superseded by venv versions, ~55 MB)
|
||||||
|
rm -rf /usr/lib/python3/dist-packages/numpy \
|
||||||
|
/usr/lib/python3/dist-packages/fontTools \
|
||||||
|
/usr/lib/python3/dist-packages/PIL; \
|
||||||
|
\
|
||||||
|
# System-wide Python cache cleanup
|
||||||
|
find /usr/lib/python3* -type d -name __pycache__ \
|
||||||
|
-exec rm -rf {} + 2>/dev/null || true; \
|
||||||
|
find /usr/lib/python3* \( -name '*.pyc' -o -name '*.pyi' \) \
|
||||||
|
-delete 2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
# Additional metadata cleanup
|
||||||
|
# FIX: Only remove ImageMagick doc/www, NOT the whole dir (preserves policy.xml/delegates.xml)
|
||||||
|
rm -rf /usr/share/bug /usr/share/lintian /usr/share/linda \
|
||||||
|
/var/lib/dpkg/info/*.md5sums \
|
||||||
|
/var/log/dpkg.log /var/log/apt/* \
|
||||||
|
/usr/local/share/ghostscript/*/doc \
|
||||||
|
/usr/local/share/ghostscript/*/examples \
|
||||||
|
/usr/share/ImageMagick-*/doc \
|
||||||
|
/usr/share/ImageMagick-*/www; \
|
||||||
|
\
|
||||||
|
\
|
||||||
|
# NEW: Tesseract training configs (not needed for OCR, but keep configs/ for hocr/txt output)
|
||||||
|
rm -rf /usr/share/tesseract-ocr/*/tessdata/tessconfigs; \
|
||||||
|
\
|
||||||
|
# Trim CJK fonts to Regular weight only (FIX: Broadened path)
|
||||||
|
find /usr/share/fonts -name '*CJK*' \
|
||||||
|
! -name '*Regular*' -type f -delete 2>/dev/null || true; \
|
||||||
|
\
|
||||||
|
# Misc caches
|
||||||
|
rm -rf /var/cache/fontconfig/* /tmp/*
|
||||||
|
|
||||||
|
# Python virtual environment.
|
||||||
|
COPY --from=python-build /opt/venv /opt/venv
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
ln -sf /opt/venv/bin/unoconvert /usr/local/bin/unoconvert; \
|
||||||
|
ln -sf /opt/venv/bin/unoserver /usr/local/bin/unoserver; \
|
||||||
|
# Verify python libs are accessible
|
||||||
|
/opt/venv/bin/python -c "import cv2; import ocrmypdf; import weasyprint; print('Python libs verified')"; \
|
||||||
|
# Cleanup venv from builder leftovers
|
||||||
|
find /opt/venv -type d -name __pycache__ \
|
||||||
|
-exec rm -rf {} + 2>/dev/null || true; \
|
||||||
|
find /opt/venv \( -name '*.pyc' -o -name '*.pyi' \) -delete 2>/dev/null || true; \
|
||||||
|
rm -rf /opt/venv/lib/python*/site-packages/pip \
|
||||||
|
/opt/venv/lib/python*/site-packages/pip-*.dist-info \
|
||||||
|
/opt/venv/lib/python*/site-packages/setuptools \
|
||||||
|
/opt/venv/lib/python*/site-packages/setuptools-*.dist-info;
|
||||||
|
|
||||||
|
# Calibre and PDF Tools.
|
||||||
|
COPY --link --from=calibre-build /opt/calibre /opt/calibre
|
||||||
|
COPY --link --from=pdf-tools-build /usr/local/bin/qpdf /usr/bin/qpdf
|
||||||
|
COPY --link --from=pdf-tools-build /usr/local/bin/magick /usr/bin/magick
|
||||||
|
COPY --link --from=pdf-tools-build /usr/local/lib/libMagick* /usr/local/lib/
|
||||||
|
COPY --link --from=pdf-tools-build /usr/local/etc/ImageMagick-7 /usr/local/etc/ImageMagick-7
|
||||||
|
COPY --link --from=gs-build /usr/local/bin/gs /usr/local/bin/gs
|
||||||
|
COPY --link --from=gs-build /usr/local/share/ghostscript /usr/local/share/ghostscript
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
ldconfig /usr/local/lib; \
|
||||||
|
# Clean pycache that may have been generated during stage-1 verify
|
||||||
|
find /opt/calibre -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true;
|
||||||
|
|
||||||
|
# Non-root user.
|
||||||
ARG PUID=1000
|
ARG PUID=1000
|
||||||
ARG PGID=1000
|
ARG PGID=1000
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
# Create group if it doesn't exist
|
|
||||||
if ! getent group stirlingpdfgroup >/dev/null 2>&1; then \
|
if ! getent group stirlingpdfgroup >/dev/null 2>&1; then \
|
||||||
if getent group "${PGID}" >/dev/null 2>&1; then \
|
groupadd -g "${PGID}" stirlingpdfgroup 2>/dev/null \
|
||||||
groupadd -o -g "${PGID}" stirlingpdfgroup; \
|
|| groupadd stirlingpdfgroup; \
|
||||||
else \
|
|
||||||
groupadd -g "${PGID}" stirlingpdfgroup; \
|
|
||||||
fi; \
|
fi; \
|
||||||
fi; \
|
|
||||||
# Create user if it doesn't exist, avoid UID conflicts
|
|
||||||
if ! id -u stirlingpdfuser >/dev/null 2>&1; then \
|
if ! id -u stirlingpdfuser >/dev/null 2>&1; then \
|
||||||
if getent passwd | awk -F: -v id="${PUID}" '$3==id{found=1} END{exit !found}'; then \
|
useradd -m -u "${PUID}" -g stirlingpdfgroup \
|
||||||
echo "UID ${PUID} already in use – creating stirlingpdfuser with automatic UID"; \
|
-d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser 2>/dev/null \
|
||||||
useradd -m -g stirlingpdfgroup -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
|| useradd -m -g stirlingpdfgroup \
|
||||||
else \
|
-d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
||||||
useradd -m -u "${PUID}" -g stirlingpdfgroup -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
|
||||||
fi; \
|
fi; \
|
||||||
fi
|
ln -sf /usr/sbin/gosu /usr/local/bin/su-exec
|
||||||
|
|
||||||
# Compatibility alias for older entrypoint scripts expecting su-exec
|
# Application files.
|
||||||
RUN ln -sf /usr/sbin/gosu /usr/local/bin/su-exec
|
COPY --link --from=app-build --chown=1000:1000 \
|
||||||
|
/app/app/core/build/libs/*.jar /app.jar
|
||||||
|
COPY --link --from=app-build --chown=1000:1000 \
|
||||||
|
/app/build/libs/restart-helper.jar /restart-helper.jar
|
||||||
|
COPY --link --chown=1000:1000 scripts/ /scripts/
|
||||||
|
|
||||||
# Copy application files from build stage
|
# Fonts go to system dir — root ownership is correct (world-readable)
|
||||||
COPY --from=build --chown=stirlingpdfuser:stirlingpdfgroup /app/app/core/build/libs/*.jar /app.jar
|
COPY --link app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/truetype/
|
||||||
COPY --from=build --chown=stirlingpdfuser:stirlingpdfgroup /app/build/libs/restart-helper.jar /restart-helper.jar
|
|
||||||
COPY scripts/ /scripts/
|
|
||||||
COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/truetype/
|
|
||||||
|
|
||||||
# Optional version tag (can be passed at build time)
|
# Permissions and configuration.
|
||||||
|
RUN set -eux; \
|
||||||
|
ln -sf /opt/calibre/ebook-convert /usr/bin/ebook-convert; \
|
||||||
|
ln -sf /opt/venv/bin/unoconvert /usr/local/bin/unoconvert; \
|
||||||
|
ln -sf /opt/venv/bin/unoserver /usr/local/bin/unoserver; \
|
||||||
|
ln -sf /opt/venv/bin/ocrmypdf /usr/local/bin/ocrmypdf; \
|
||||||
|
ln -sf /opt/venv/bin/weasyprint /usr/local/bin/weasyprint; \
|
||||||
|
ln -sf /opt/venv/bin/unoping /usr/local/bin/unoping; \
|
||||||
|
chmod +x /scripts/*; \
|
||||||
|
mkdir -p /configs /logs /customFiles \
|
||||||
|
/pipeline/watchedFolders /pipeline/finishedFolders \
|
||||||
|
/tmp/stirling-pdf/heap_dumps; \
|
||||||
|
# Create symlinks to allow app to find these in /app/
|
||||||
|
mkdir -p /app; \
|
||||||
|
ln -s /logs /app/logs; \
|
||||||
|
ln -s /configs /app/configs; \
|
||||||
|
ln -s /customFiles /app/customFiles; \
|
||||||
|
ln -s /pipeline /app/pipeline; \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup \
|
||||||
|
/home/stirlingpdfuser /configs /logs /customFiles /pipeline \
|
||||||
|
/tmp/stirling-pdf; \
|
||||||
|
chown -h stirlingpdfuser:stirlingpdfgroup /app/logs /app/configs /app/customFiles /app/pipeline; \
|
||||||
|
chown stirlingpdfuser:stirlingpdfgroup /app; \
|
||||||
|
chmod 1777 /tmp/stirling-pdf; \
|
||||||
|
fc-cache -f; \
|
||||||
|
# NOTE: Project Leyden AOT cache is generated in the background on first boot
|
||||||
|
# by init-without-ocr.sh. The cache is picked up on subsequent boots for
|
||||||
|
# 15-25% faster startup. See: JEP 483 + 514 + 515 (JDK 25).
|
||||||
|
\
|
||||||
|
# Clean Calibre pycache that may have been generated during stage-1 verify
|
||||||
|
find /opt/calibre -type d -name __pycache__ \
|
||||||
|
-exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
||||||
|
# Environment variables.
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
# Metadata labels
|
|
||||||
LABEL org.opencontainers.image.title="Stirling-PDF Fat"
|
|
||||||
LABEL org.opencontainers.image.description="Stirling-PDF with embedded frontend - Fat version with extra fonts for air-gapped environments, includes Calibre, LibreOffice, Tesseract, OCRmyPDF, and more"
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
|
|
||||||
LABEL org.opencontainers.image.licenses="MIT"
|
|
||||||
LABEL org.opencontainers.image.vendor="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
|
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
|
|
||||||
LABEL maintainer="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.authors="Stirling-Tools"
|
|
||||||
LABEL org.opencontainers.image.version="${VERSION_TAG}"
|
|
||||||
LABEL org.opencontainers.image.keywords="PDF, manipulation, fat, air-gapped, API, Spring Boot, React"
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Runtime environment variables
|
|
||||||
# ==============================================================================
|
|
||||||
ENV VERSION_TAG=$VERSION_TAG \
|
ENV VERSION_TAG=$VERSION_TAG \
|
||||||
JAVA_BASE_OPTS="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70 -Djava.awt.headless=true" \
|
STIRLING_JVM_PROFILE="balanced" \
|
||||||
|
_JVM_OPTS_BALANCED="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/stirling-pdf/heap_dumps -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4m -XX:G1PeriodicGCInterval=60000 -XX:+UseStringDeduplication -XX:+UseCompactObjectHeaders -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \
|
||||||
|
_JVM_OPTS_PERFORMANCE="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/stirling-pdf/heap_dumps -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \
|
||||||
JAVA_CUSTOM_OPTS="" \
|
JAVA_CUSTOM_OPTS="" \
|
||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
PUID=${PUID} \
|
PUID=${PUID} \
|
||||||
@@ -160,57 +565,34 @@ ENV VERSION_TAG=$VERSION_TAG \
|
|||||||
UMASK=022 \
|
UMASK=022 \
|
||||||
FAT_DOCKER=true \
|
FAT_DOCKER=true \
|
||||||
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
||||||
|
PATH="/opt/venv/bin:${PATH}" \
|
||||||
UNO_PATH=/usr/lib/libreoffice/program \
|
UNO_PATH=/usr/lib/libreoffice/program \
|
||||||
LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin \
|
LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin \
|
||||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||||
TMPDIR=/tmp/stirling-pdf \
|
TMPDIR=/tmp/stirling-pdf \
|
||||||
TEMP=/tmp/stirling-pdf \
|
TEMP=/tmp/stirling-pdf \
|
||||||
TMP=/tmp/stirling-pdf
|
TMP=/tmp/stirling-pdf \
|
||||||
|
QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu --disable-software-rasterizer" \
|
||||||
|
DBUS_SESSION_BUS_ADDRESS=/dev/null
|
||||||
|
|
||||||
# ==============================================================================
|
# Metadata labels.
|
||||||
# Python virtual environment for additional Python tools (WeasyPrint, OpenCV, etc.)
|
LABEL org.opencontainers.image.title="Stirling-PDF Fat" \
|
||||||
# ==============================================================================
|
org.opencontainers.image.description="Fat version with extra fonts for air-gapped environments, includes Calibre, LibreOffice, Tesseract, OCRmyPDF" \
|
||||||
RUN python3 -m venv /opt/venv --system-site-packages \
|
org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" \
|
||||||
&& /opt/venv/bin/pip install --no-cache-dir weasyprint pdf2image opencv-python-headless \
|
org.opencontainers.image.licenses="MIT" \
|
||||||
&& /opt/venv/bin/python -c "import cv2; print('OpenCV version:', cv2.__version__)"
|
org.opencontainers.image.vendor="Stirling-Tools" \
|
||||||
|
org.opencontainers.image.url="https://www.stirlingpdf.com" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.stirlingpdf.com" \
|
||||||
|
maintainer="Stirling-Tools" \
|
||||||
|
org.opencontainers.image.authors="Stirling-Tools" \
|
||||||
|
org.opencontainers.image.version="${VERSION_TAG}" \
|
||||||
|
org.opencontainers.image.keywords="PDF, manipulation, fat, air-gapped, API, Spring Boot, React"
|
||||||
|
|
||||||
# Separate venv for unoserver (keeps it isolated)
|
|
||||||
ARG UNOSERVER_VERSION=3.6
|
|
||||||
RUN python3 -m venv /opt/unoserver-venv --system-site-packages \
|
|
||||||
&& /opt/unoserver-venv/bin/pip install --no-cache-dir "unoserver==${UNOSERVER_VERSION}"
|
|
||||||
|
|
||||||
# Make unoserver tools available in main venv PATH
|
|
||||||
RUN ln -sf /opt/unoserver-venv/bin/unoconvert /opt/venv/bin/unoconvert \
|
|
||||||
&& ln -sf /opt/unoserver-venv/bin/unoserver /opt/venv/bin/unoserver
|
|
||||||
|
|
||||||
# Extend PATH to include both virtual environments
|
|
||||||
ENV PATH="/opt/venv/bin:/opt/unoserver-venv/bin:${PATH}"
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# Final permissions, directories and font cache
|
|
||||||
# ==============================================================================
|
|
||||||
RUN set -eux; \
|
|
||||||
chmod +x /scripts/*; \
|
|
||||||
mkdir -p /configs /configs/heap_dumps /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf; \
|
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup \
|
|
||||||
/home/stirlingpdfuser /configs /logs /customFiles /pipeline /tmp/stirling-pdf \
|
|
||||||
/app.jar /restart-helper.jar /usr/share/fonts/truetype /scripts; \
|
|
||||||
chmod -R 755 /tmp/stirling-pdf
|
|
||||||
|
|
||||||
# Rebuild font cache
|
|
||||||
RUN fc-cache -f -v
|
|
||||||
|
|
||||||
# Force Qt/WebEngine to run headlessly (required for Calibre in Docker)
|
|
||||||
ENV QT_QPA_PLATFORM=offscreen \
|
|
||||||
QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu --disable-dev-shm-usage"
|
|
||||||
|
|
||||||
# Expose web UI port
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
STOPSIGNAL SIGTERM
|
STOPSIGNAL SIGTERM
|
||||||
|
|
||||||
# Use tini as init (handles signals and zombies correctly)
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
CMD curl -f http://localhost:8080/api/v1/info/status || exit 1
|
||||||
|
|
||||||
# CMD is empty – actual start command is defined in init.sh
|
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||||
CMD []
|
CMD []
|
||||||
|
|||||||
@@ -2,40 +2,50 @@
|
|||||||
# Single JAR contains both frontend and backend with minimal dependencies
|
# Single JAR contains both frontend and backend with minimal dependencies
|
||||||
|
|
||||||
# Stage 1: Build application with embedded frontend
|
# Stage 1: Build application with embedded frontend
|
||||||
FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build
|
FROM gradle:9.3.1-jdk25 AS build
|
||||||
|
|
||||||
# Install Node.js and npm for frontend build
|
# Install Node.js and npm for frontend build
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl \
|
curl \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
&& apt-get install -y nodejs \
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
&& npm --version \
|
&& npm --version \
|
||||||
&& node --version \
|
&& node --version \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy gradle files for dependency resolution
|
|
||||||
COPY build.gradle .
|
|
||||||
COPY settings.gradle .
|
|
||||||
COPY gradlew .
|
|
||||||
COPY gradle gradle/
|
|
||||||
COPY app/core/build.gradle core/.
|
|
||||||
COPY app/common/build.gradle common/.
|
|
||||||
COPY app/proprietary/build.gradle proprietary/.
|
|
||||||
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy gradle files for dependency resolution
|
||||||
|
COPY build.gradle settings.gradle gradlew ./
|
||||||
|
COPY gradle/ gradle/
|
||||||
|
COPY app/core/build.gradle app/core/
|
||||||
|
COPY app/common/build.gradle app/common/
|
||||||
|
COPY app/proprietary/build.gradle app/proprietary/
|
||||||
|
|
||||||
|
# JDK 25+: --add-exports is no longer accepted via JAVA_TOOL_OPTIONS; use JDK_JAVA_OPTIONS instead
|
||||||
|
ENV JDK_JAVA_OPTIONS="--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||||
|
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
|
||||||
|
./gradlew dependencies --no-daemon || true
|
||||||
|
|
||||||
# Copy entire project
|
# Copy entire project
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build ultra-lite JAR with embedded frontend (minimal features)
|
# Build ultra-lite JAR with embedded frontend (minimal features)
|
||||||
RUN DISABLE_ADDITIONAL_FEATURES=true \
|
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
|
||||||
./gradlew clean build -PbuildWithFrontend=true -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
DISABLE_ADDITIONAL_FEATURES=true \
|
||||||
|
./gradlew clean build \
|
||||||
|
-PbuildWithFrontend=true \
|
||||||
|
-x spotlessApply -x spotlessCheck -x test -x sonarqube \
|
||||||
|
--no-daemon
|
||||||
|
|
||||||
# Stage 2: Runtime image
|
# Stage 2: Runtime image
|
||||||
FROM alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
|
FROM eclipse-temurin:25-jre-alpine
|
||||||
|
|
||||||
ENV LANG=C.UTF-8 \
|
ENV LANG=C.UTF-8 \
|
||||||
LC_ALL=C.UTF-8
|
LC_ALL=C.UTF-8
|
||||||
@@ -43,30 +53,37 @@ ENV LANG=C.UTF-8 \
|
|||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
# Labels
|
# Labels
|
||||||
LABEL org.opencontainers.image.title="Stirling-PDF Ultra-Lite"
|
LABEL org.opencontainers.image.title="Stirling-PDF Ultra-Lite" \
|
||||||
LABEL org.opencontainers.image.description="Stirling-PDF with embedded frontend - Ultra-lite version with minimal dependencies"
|
org.opencontainers.image.description="Stirling-PDF with embedded frontend - Ultra-lite version with minimal dependencies" \
|
||||||
LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
|
org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" \
|
||||||
LABEL org.opencontainers.image.licenses="MIT"
|
org.opencontainers.image.licenses="MIT" \
|
||||||
LABEL org.opencontainers.image.vendor="Stirling-Tools"
|
org.opencontainers.image.vendor="Stirling-Tools" \
|
||||||
LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
|
org.opencontainers.image.url="https://www.stirlingpdf.com" \
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
|
org.opencontainers.image.documentation="https://docs.stirlingpdf.com" \
|
||||||
LABEL maintainer="Stirling-Tools"
|
maintainer="Stirling-Tools" \
|
||||||
LABEL org.opencontainers.image.authors="Stirling-Tools"
|
org.opencontainers.image.authors="Stirling-Tools" \
|
||||||
LABEL org.opencontainers.image.version="${VERSION_TAG}"
|
org.opencontainers.image.version="${VERSION_TAG}" \
|
||||||
LABEL org.opencontainers.image.keywords="PDF, manipulation, ultra-lite, API, Spring Boot, React"
|
org.opencontainers.image.keywords="PDF, manipulation, ultra-lite, API, Spring Boot, React"
|
||||||
|
|
||||||
# Copy scripts
|
# Copy scripts
|
||||||
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||||
COPY scripts/installFonts.sh /scripts/installFonts.sh
|
COPY scripts/installFonts.sh /scripts/installFonts.sh
|
||||||
COPY scripts/stirling-diagnostics.sh /scripts/stirling-diagnostics.sh
|
COPY scripts/stirling-diagnostics.sh /scripts/stirling-diagnostics.sh
|
||||||
|
|
||||||
# Copy built JAR from build stage
|
# Copy built JARs from build stage
|
||||||
COPY --from=build /app/app/core/build/libs/*.jar /app.jar
|
# Use numeric UID:GID (1000:1000) since the named user doesn't exist yet at COPY time
|
||||||
COPY --from=build /app/build/libs/restart-helper.jar /restart-helper.jar
|
COPY --from=build --chown=1000:1000 \
|
||||||
|
/app/app/core/build/libs/*.jar /app.jar
|
||||||
|
COPY --from=build --chown=1000:1000 \
|
||||||
|
/app/build/libs/restart-helper.jar /restart-helper.jar
|
||||||
|
|
||||||
# Environment Variables
|
# Environment Variables
|
||||||
|
# NOTE: Memory flags (InitialRAMPercentage, MaxRAMPercentage, MaxMetaspaceSize)
|
||||||
|
# are computed dynamically by init-without-ocr.sh based on container memory limits.
|
||||||
ENV VERSION_TAG=$VERSION_TAG \
|
ENV VERSION_TAG=$VERSION_TAG \
|
||||||
JAVA_BASE_OPTS="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
|
STIRLING_JVM_PROFILE="balanced" \
|
||||||
|
_JVM_OPTS_BALANCED="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/stirling-pdf/heap_dumps -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4m -XX:G1PeriodicGCInterval=60000 -XX:+UseStringDeduplication -XX:+UseCompactObjectHeaders -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \
|
||||||
|
_JVM_OPTS_PERFORMANCE="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/stirling-pdf/heap_dumps -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \
|
||||||
JAVA_CUSTOM_OPTS="" \
|
JAVA_CUSTOM_OPTS="" \
|
||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
PUID=1000 \
|
PUID=1000 \
|
||||||
@@ -79,6 +96,7 @@ ENV VERSION_TAG=$VERSION_TAG \
|
|||||||
ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||||
|
|
||||||
# Install minimal dependencies
|
# Install minimal dependencies
|
||||||
|
# --chown on COPY above removes need to chown JARs here
|
||||||
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
@@ -90,18 +108,16 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
shadow \
|
shadow \
|
||||||
su-exec \
|
su-exec && \
|
||||||
openjdk21-jre && \
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /tmp/stirling-pdf/heap_dumps && \
|
||||||
mkdir -p $HOME /configs /configs/heap_dumps /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \
|
|
||||||
mkdir -p /usr/share/fonts/opentype/noto && \
|
mkdir -p /usr/share/fonts/opentype/noto && \
|
||||||
chmod +x /scripts/*.sh && \
|
chmod +x /scripts/*.sh && \
|
||||||
# User permissions
|
# User permissions
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline /tmp/stirling-pdf
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar
|
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
# Set user and run command
|
# Set user and run command
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
|
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
|
||||||
CMD ["java", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=/tmp/stirling-pdf", "-jar", "/app.jar"]
|
CMD []
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
|
# STIRLING_JVM_PROFILE: "balanced" # Options: balanced (G1GC, default), performance (Shenandoah)
|
||||||
|
# JAVA_CUSTOM_OPTS: "" # Additional JVM flags appended to the active profile
|
||||||
PUID: 1002
|
PUID: 1002
|
||||||
PGID: 1002
|
PGID: 1002
|
||||||
UMASK: "022"
|
UMASK: "022"
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
|
# STIRLING_JVM_PROFILE: "balanced" # Options: balanced (G1GC, default), performance (Shenandoah)
|
||||||
|
# JAVA_CUSTOM_OPTS: "" # Additional JVM flags appended to the active profile
|
||||||
PUID: 1002
|
PUID: 1002
|
||||||
PGID: 1002
|
PGID: 1002
|
||||||
UMASK: "022"
|
UMASK: "022"
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ services:
|
|||||||
PROCESS_EXECUTOR_UNO_SERVER_ENDPOINTS_1_PROTOCOL: "http"
|
PROCESS_EXECUTOR_UNO_SERVER_ENDPOINTS_1_PROTOCOL: "http"
|
||||||
# Session limit should match endpoint count for optimal concurrency
|
# Session limit should match endpoint count for optimal concurrency
|
||||||
PROCESS_EXECUTOR_SESSION_LIMIT_LIBRE_OFFICE_SESSION_LIMIT: "2"
|
PROCESS_EXECUTOR_SESSION_LIMIT_LIBRE_OFFICE_SESSION_LIMIT: "2"
|
||||||
|
# STIRLING_JVM_PROFILE: "balanced" # Options: balanced (G1GC, default), performance (Shenandoah)
|
||||||
|
# JAVA_CUSTOM_OPTS: "" # Additional JVM flags appended to the active profile
|
||||||
PUID: 1002
|
PUID: 1002
|
||||||
PGID: 1002
|
PGID: 1002
|
||||||
UMASK: "022"
|
UMASK: "022"
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ services:
|
|||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
PROCESS_EXECUTOR_AUTO_UNO_SERVER: "true"
|
PROCESS_EXECUTOR_AUTO_UNO_SERVER: "true"
|
||||||
PROCESS_EXECUTOR_SESSION_LIMIT_LIBRE_OFFICE_SESSION_LIMIT: "1"
|
PROCESS_EXECUTOR_SESSION_LIMIT_LIBRE_OFFICE_SESSION_LIMIT: "1"
|
||||||
|
# STIRLING_JVM_PROFILE: "balanced" # Options: balanced (G1GC, default), performance (Shenandoah)
|
||||||
|
# JAVA_CUSTOM_OPTS: "" # Additional JVM flags appended to the active profile
|
||||||
PUID: 1002
|
PUID: 1002
|
||||||
PGID: 1002
|
PGID: 1002
|
||||||
UMASK: "022"
|
UMASK: "022"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user