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
|
||||
node_modules
|
||||
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
|
||||
# Version control
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
# Build outputs
|
||||
build/
|
||||
*/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
|
||||
*.iws
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# Logs
|
||||
# Logs and temp files
|
||||
*.log
|
||||
logs
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS files
|
||||
*.tmp
|
||||
*.pid
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Java compiled files
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# Test reports
|
||||
test-results
|
||||
coverage
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
# Docker itself
|
||||
Dockerfile*
|
||||
.dockerignore
|
||||
|
||||
# Temporary files
|
||||
tmp
|
||||
temp
|
||||
*.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
|
||||
# CI / CD configs (not needed in build context)
|
||||
.github/
|
||||
.circleci/
|
||||
.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
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
- name: Set up JDK 21
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "21"
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
jdk-version: [17, 21]
|
||||
jdk-version: [21, 25]
|
||||
spring-security: [true, false]
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: "21"
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
@@ -209,10 +209,10 @@ jobs:
|
||||
- name: Checkout repository
|
||||
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
|
||||
with:
|
||||
java-version: "21"
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
@@ -221,7 +221,12 @@ jobs:
|
||||
gradle-version: 8.14
|
||||
|
||||
- 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:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
@@ -265,10 +270,10 @@ jobs:
|
||||
- name: Checkout Repository
|
||||
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
|
||||
with:
|
||||
java-version: "21"
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
@@ -355,10 +360,10 @@ jobs:
|
||||
docker system prune -af || true
|
||||
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
|
||||
with:
|
||||
java-version: "21"
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
|
||||
@@ -330,10 +330,10 @@ jobs:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
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
|
||||
with:
|
||||
java-version: "21"
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
@@ -344,7 +344,12 @@ jobs:
|
||||
- name: Check licenses and generate report
|
||||
id: license-check
|
||||
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:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
|
||||
@@ -44,10 +44,10 @@ jobs:
|
||||
|
||||
- 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
|
||||
with:
|
||||
java-version: "21"
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
@@ -117,10 +117,10 @@ jobs:
|
||||
|
||||
- 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
|
||||
with:
|
||||
java-version: "21"
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
@@ -197,10 +197,10 @@ jobs:
|
||||
toolchain: stable
|
||||
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
|
||||
with:
|
||||
java-version: "21"
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
|
||||
@@ -39,10 +39,10 @@ jobs:
|
||||
|
||||
- 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
|
||||
with:
|
||||
java-version: "21"
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
|
||||
@@ -33,10 +33,10 @@ jobs:
|
||||
|
||||
- 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
|
||||
with:
|
||||
java-version: "21"
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
|
||||
@@ -27,8 +27,8 @@ spotless {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||
api 'org.springframework.boot:spring-boot-starter-webmvc'
|
||||
api 'org.springframework.boot:spring-boot-starter-aspectj'
|
||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
api 'com.fathzer:javaluator:3.0.6'
|
||||
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 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
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)
|
||||
api 'org.simplejavamail:simple-java-mail:8.12.6'
|
||||
api 'org.simplejavamail:outlook-module:8.12.6' // MSG file support
|
||||
|
||||
+2
-2
@@ -91,9 +91,9 @@ public class RuntimePathConfig {
|
||||
|
||||
// Initialize Operation paths
|
||||
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 defaultOcrMyPdfPath = isDocker ? "/usr/bin/ocrmypdf" : "ocrmypdf";
|
||||
String defaultOcrMyPdfPath = isDocker ? "/opt/venv/bin/ocrmypdf" : "ocrmypdf";
|
||||
String defaultSOfficePath = isDocker ? "/usr/bin/soffice" : "soffice";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+607
-448
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@ public class JobExecutorService {
|
||||
private final HttpServletRequest request;
|
||||
private final ResourceMonitor resourceMonitor;
|
||||
private final JobQueue jobQueue;
|
||||
private final ExecutorService executor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
||||
private final ExecutorService executor = ExecutorFactory.newVirtualThreadExecutor();
|
||||
private final long effectiveTimeoutMs;
|
||||
|
||||
@Autowired(required = false)
|
||||
|
||||
@@ -44,8 +44,10 @@ public class JobQueue implements SmartLifecycle {
|
||||
|
||||
private volatile BlockingQueue<QueuedJob> jobQueue;
|
||||
private final Map<String, QueuedJob> jobMap = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ExecutorService jobExecutor = ExecutorFactory.newVirtualOrCachedThreadExecutor();
|
||||
private final ScheduledExecutorService scheduler =
|
||||
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 boolean shuttingDown = false;
|
||||
|
||||
@@ -43,7 +43,9 @@ public class ResourceMonitor {
|
||||
@Value("${stirling.resource.monitor.interval-ms:60000}")
|
||||
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 OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ public class TaskManager {
|
||||
|
||||
private final FileStorage fileStorage;
|
||||
private final ScheduledExecutorService cleanupExecutor =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
Executors.newSingleThreadScheduledExecutor(
|
||||
Thread.ofVirtual().name("task-cleanup-", 0).factory());
|
||||
|
||||
/** Initialize the task manager and start the cleanup scheduler */
|
||||
public TaskManager(FileStorage fileStorage) {
|
||||
|
||||
@@ -2,30 +2,28 @@ package stirling.software.common.util;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
/**
|
||||
* Factory for creating executors backed by virtual threads (Java 21+). Virtual threads are
|
||||
* 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 final class ExecutorFactory {
|
||||
|
||||
@Slf4j
|
||||
public class ExecutorFactory {
|
||||
private ExecutorFactory() {}
|
||||
|
||||
/** Creates an {@link ExecutorService} that starts a new virtual thread for each task. */
|
||||
public static ExecutorService newVirtualThreadExecutor() {
|
||||
return Executors.newVirtualThreadPerTaskExecutor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ExecutorService using virtual threads if available (Java 21+), or falls back to a
|
||||
* cached thread pool on older Java versions.
|
||||
* Creates a {@link ScheduledExecutorService} backed by a single virtual thread. Useful for
|
||||
* periodic/delayed tasks that should not pin a platform thread.
|
||||
*/
|
||||
public static ExecutorService newVirtualOrCachedThreadExecutor() {
|
||||
try {
|
||||
ExecutorService executor =
|
||||
(ExecutorService)
|
||||
Executors.class
|
||||
.getMethod("newVirtualThreadPerTaskExecutor")
|
||||
.invoke(null);
|
||||
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();
|
||||
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()) {
|
||||
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) {
|
||||
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 maxSize = Math.max(1000, totalPages * 3);
|
||||
for (String page : pages) {
|
||||
@@ -672,7 +673,7 @@ public class GeneralUtils {
|
||||
"Page list exceeds maximum allowed size of " + maxSize);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return new ArrayList<>(result);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -226,50 +226,54 @@ public class ProcessExecutor {
|
||||
List<String> outputLines = new ArrayList<>();
|
||||
|
||||
Thread errorReaderThread =
|
||||
new Thread(
|
||||
() -> {
|
||||
try (BufferedReader errorReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getErrorStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
errorReader, 5_000_000))
|
||||
!= null) {
|
||||
errorLines.add(line);
|
||||
if (liveUpdates) log.info(line);
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
log.warn("Error reader thread was interrupted due to timeout.");
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
});
|
||||
Thread.ofVirtual()
|
||||
.unstarted(
|
||||
() -> {
|
||||
try (BufferedReader errorReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getErrorStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
errorReader, 5_000_000))
|
||||
!= null) {
|
||||
errorLines.add(line);
|
||||
if (liveUpdates) log.info(line);
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
log.warn(
|
||||
"Error reader thread was interrupted due to timeout.");
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
});
|
||||
|
||||
Thread outputReaderThread =
|
||||
new Thread(
|
||||
() -> {
|
||||
try (BufferedReader outputReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getInputStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
outputReader, 5_000_000))
|
||||
!= null) {
|
||||
outputLines.add(line);
|
||||
if (liveUpdates) log.info(line);
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
log.warn("Error reader thread was interrupted due to timeout.");
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
});
|
||||
Thread.ofVirtual()
|
||||
.unstarted(
|
||||
() -> {
|
||||
try (BufferedReader outputReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getInputStream(),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
outputReader, 5_000_000))
|
||||
!= null) {
|
||||
outputLines.add(line);
|
||||
if (liveUpdates) log.info(line);
|
||||
}
|
||||
} catch (InterruptedIOException e) {
|
||||
log.warn(
|
||||
"Error reader thread was interrupted due to timeout.");
|
||||
} catch (IOException e) {
|
||||
log.error("exception", e);
|
||||
}
|
||||
});
|
||||
|
||||
errorReaderThread.start();
|
||||
outputReaderThread.start();
|
||||
@@ -370,7 +374,7 @@ public class ProcessExecutor {
|
||||
}
|
||||
// Match common unoconvert variants (but NOT soffice)
|
||||
String lowerBasename = basename.toLowerCase(java.util.Locale.ROOT);
|
||||
if (lowerBasename.contains("unoconvert") || lowerBasename.equals("unoconv")) {
|
||||
if (lowerBasename.contains("unoconvert") || "unoconv".equals(lowerBasename)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-6
@@ -4,18 +4,23 @@ import java.beans.PropertyEditorSupport;
|
||||
import java.util.ArrayList;
|
||||
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 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
|
||||
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
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
@@ -24,7 +29,6 @@ public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
|
||||
TypeReference<ArrayList<RedactionArea>> typeRef = new TypeReference<>() {};
|
||||
List<RedactionArea> list = objectMapper.readValue(text, typeRef);
|
||||
setValue(list);
|
||||
|
||||
+4
-3
@@ -4,12 +4,13 @@ import java.beans.PropertyEditorSupport;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
public class StringToMapPropertyEditor extends PropertyEditorSupport {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
|
||||
+4
-3
@@ -12,11 +12,12 @@ class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
||||
public StrategyType lastStrategyUsed;
|
||||
|
||||
public SpyPDFDocumentFactory(PdfMetadataService service) {
|
||||
super(service);
|
||||
super(service); // TempFileManager falls back to Files.createTempFile in tests
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamCacheCreateFunction getStreamCacheFunction(long contentSize) {
|
||||
protected StreamCacheCreateFunction getStreamCacheFunction(
|
||||
long contentSize, MemorySnapshot mem) {
|
||||
StrategyType type;
|
||||
if (contentSize < 10 * 1024 * 1024) {
|
||||
type = StrategyType.MEMORY_ONLY;
|
||||
@@ -26,6 +27,6 @@ class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
||||
type = StrategyType.TEMP_FILE;
|
||||
}
|
||||
this.lastStrategyUsed = type;
|
||||
return super.getStreamCacheFunction(contentSize); // delegate to real behavior
|
||||
return super.getStreamCacheFunction(contentSize, mem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,17 +121,19 @@ public class UnoServerPoolTest {
|
||||
|
||||
// Try to acquire a third endpoint in separate thread (should block)
|
||||
Thread blockingThread =
|
||||
new Thread(
|
||||
() -> {
|
||||
try {
|
||||
acquireLatch.countDown(); // Signal we're about to block
|
||||
UnoServerPool.UnoServerLease lease3 = pool.acquireEndpoint();
|
||||
acquired.incrementAndGet();
|
||||
lease3.close();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
Thread.ofVirtual()
|
||||
.unstarted(
|
||||
() -> {
|
||||
try {
|
||||
acquireLatch.countDown(); // Signal we're about to block
|
||||
UnoServerPool.UnoServerLease lease3 =
|
||||
pool.acquireEndpoint();
|
||||
acquired.incrementAndGet();
|
||||
lease3.close();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
|
||||
blockingThread.start();
|
||||
acquireLatch.await(); // Wait for thread to start
|
||||
|
||||
+14
-4
@@ -143,11 +143,21 @@ class StringToArrayListPropertyEditorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAsText_InvalidStructure() {
|
||||
// Arrange - this JSON doesn't match RedactionArea structure
|
||||
void testSetAsText_UnknownProperties() {
|
||||
// Arrange - this JSON contains properties not in RedactionArea
|
||||
// With FAIL_ON_UNKNOWN_PROPERTIES disabled, this should ignore the unknown properties
|
||||
String json = "[{\"invalid\":\"structure\"}]";
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(IllegalArgumentException.class, () -> editor.setAsText(json));
|
||||
// Act
|
||||
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 'org.springframework.boot:spring-boot-starter-jetty'
|
||||
implementation 'com.posthog.java:posthog:1.2.0'
|
||||
implementation 'org.telegram:telegrambots:6.9.7.1'
|
||||
implementation 'org.eclipse.jetty.http2:jetty-http2-server'
|
||||
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 "org.bouncycastle:bcprov-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 'org.apache.poi:poi-ooxml:5.5.1'
|
||||
|
||||
// Batik
|
||||
implementation 'org.apache.xmlgraphics:batik-all:1.19'
|
||||
// Batik only bridge module needed (transitively pulls anim, gvt, util, css, dom, svg-dom)
|
||||
// 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
|
||||
implementation 'de.rototor.pdfbox:graphics2d:3.0.5'
|
||||
|
||||
@@ -49,8 +49,8 @@ public class LibreOfficeListener {
|
||||
process = SystemCommand.runCommand(Runtime.getRuntime(), "unoconv --listener");
|
||||
lastActivityTime = System.currentTimeMillis();
|
||||
|
||||
// Start a background thread to monitor the activity timeout
|
||||
executorService = Executors.newSingleThreadExecutor();
|
||||
// Start a virtual thread to monitor the activity timeout
|
||||
executorService = Executors.newVirtualThreadPerTaskExecutor();
|
||||
executorService.submit(
|
||||
() -> {
|
||||
while (true) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
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.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
@@ -176,11 +176,14 @@ public class SPDFApplication {
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onWebServerInitialized(WebServerInitializedEvent event) {
|
||||
int actualPort = event.getWebServer().getPort();
|
||||
serverPortStatic = String.valueOf(actualPort);
|
||||
public void onApplicationReady(ApplicationReadyEvent event) {
|
||||
String port =
|
||||
event.getApplicationContext().getEnvironment().getProperty("local.server.port");
|
||||
if (port != null) {
|
||||
serverPortStatic = port;
|
||||
}
|
||||
// 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() {
|
||||
|
||||
@@ -51,9 +51,7 @@ public class ExternalAppDepConfig {
|
||||
*/
|
||||
private final Map<String, List<String>> commandToGroupMapping;
|
||||
|
||||
private final ExecutorService pool =
|
||||
Executors.newFixedThreadPool(
|
||||
Math.max(2, Runtime.getRuntime().availableProcessors() / 2));
|
||||
private final ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
public ExternalAppDepConfig(
|
||||
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
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.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
|
||||
@@ -54,8 +54,8 @@ public class TauriProcessMonitor {
|
||||
scheduler =
|
||||
Executors.newSingleThreadScheduledExecutor(
|
||||
r -> {
|
||||
Thread t = new Thread(r, "tauri-process-monitor");
|
||||
t.setDaemon(true);
|
||||
Thread t =
|
||||
Thread.ofVirtual().name("tauri-process-monitor").unstarted(r);
|
||||
return t;
|
||||
});
|
||||
|
||||
|
||||
@@ -154,8 +154,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
.maxAge(3600);
|
||||
} else {
|
||||
// Default to allowing all origins when nothing is configured
|
||||
logger.info(
|
||||
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); allowing all origins.");
|
||||
logger.debug(
|
||||
"No CORS allowed origins configured in settings.yml (system.corsAllowedOrigins); WebMvcConfig allowing all origins.");
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.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.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.Getter;
|
||||
@@ -32,6 +29,9 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
|
||||
@@ -16,9 +16,6 @@ import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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 lombok.Data;
|
||||
@@ -35,6 +32,9 @@ import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@UiDataApi
|
||||
public class UIDataController {
|
||||
@@ -44,18 +44,21 @@ public class UIDataController {
|
||||
private final UserServiceInterface userService;
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public UIDataController(
|
||||
ApplicationProperties applicationProperties,
|
||||
SharedSignatureService signatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
ResourceLoader resourceLoader,
|
||||
RuntimePathConfig runtimePathConfig) {
|
||||
RuntimePathConfig runtimePathConfig,
|
||||
ObjectMapper objectMapper) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.signatureService = signatureService;
|
||||
this.userService = userService;
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@GetMapping("/footer-info")
|
||||
@@ -93,9 +96,8 @@ public class UIDataController {
|
||||
|
||||
try (InputStream is = resource.getInputStream()) {
|
||||
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Map<String, List<Dependency>> licenseData =
|
||||
mapper.readValue(json, new TypeReference<>() {});
|
||||
objectMapper.readValue(json, new TypeReference<>() {});
|
||||
data.setDependencies(licenseData.get("dependencies"));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load licenses data", e);
|
||||
@@ -127,8 +129,8 @@ public class UIDataController {
|
||||
|
||||
for (String config : pipelineConfigs) {
|
||||
Map<String, Object> jsonContent =
|
||||
new ObjectMapper()
|
||||
.readValue(config, new TypeReference<Map<String, Object>>() {});
|
||||
objectMapper.readValue(
|
||||
config, new TypeReference<Map<String, Object>>() {});
|
||||
String name = (String) jsonContent.get("name");
|
||||
if (name == null || name.length() < 1) {
|
||||
String filename =
|
||||
|
||||
+36
-19
@@ -91,22 +91,32 @@ public class ConvertOfficeController {
|
||||
|
||||
Path libreOfficeProfile = null;
|
||||
try {
|
||||
ProcessExecutorResult result;
|
||||
// Run Unoconvert command
|
||||
if (isUnoconvertAvailable()) {
|
||||
// Unoconvert: schreibe direkt in outputPath innerhalb des workDir
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(runtimePathConfig.getUnoConvertPath());
|
||||
command.add("--convert-to");
|
||||
command.add("pdf");
|
||||
command.add(inputPath.toString());
|
||||
command.add(outputPath.toString());
|
||||
ProcessExecutorResult result = null;
|
||||
IOException unoconvertException = null;
|
||||
|
||||
result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
} // Run soffice command
|
||||
else {
|
||||
// Try unoconvert first if available
|
||||
if (isUnoconvertAvailable()) {
|
||||
try {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(runtimePathConfig.getUnoConvertPath());
|
||||
command.add("--convert-to");
|
||||
command.add("pdf");
|
||||
command.add(inputPath.toString());
|
||||
command.add(outputPath.toString());
|
||||
|
||||
result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
} catch (IOException e) {
|
||||
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_");
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(runtimePathConfig.getSOfficePath());
|
||||
@@ -114,14 +124,21 @@ public class ConvertOfficeController {
|
||||
command.add("--headless");
|
||||
command.add("--nologo");
|
||||
command.add("--convert-to");
|
||||
command.add("pdf:writer_pdf_Export");
|
||||
command.add("pdf");
|
||||
command.add("--outdir");
|
||||
command.add(workDir.toString());
|
||||
command.add(inputPath.toString());
|
||||
|
||||
result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
try {
|
||||
result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
} catch (IOException e) {
|
||||
if (unoconvertException != null) {
|
||||
e.addSuppressed(unoconvertException);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the result
|
||||
|
||||
+1
-4
@@ -70,10 +70,7 @@ public class ConvertPDFToExcelController {
|
||||
tables.size() == 1
|
||||
? String.format(Locale.ROOT, "Page %d", pageNum)
|
||||
: String.format(
|
||||
Locale.ROOT,
|
||||
"Page %d Table %d",
|
||||
pageNum,
|
||||
tableIdx + 1);
|
||||
Locale.ROOT, "Page %d Table %d", pageNum, tableIdx + 1);
|
||||
|
||||
sheetName = getUniqueSheetName(workbook, 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.ResponseBody;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.exception.CacheUnavailableException;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@ControllerAdvice(assignableTypes = ConvertPdfJsonController.class)
|
||||
@Slf4j
|
||||
@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.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.opencsv.CSVWriter;
|
||||
|
||||
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.WebResponseUtils;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/form")
|
||||
@Tag(
|
||||
|
||||
+16
-17
@@ -1,6 +1,5 @@
|
||||
package stirling.software.SPDF.controller.api.form;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -8,13 +7,14 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
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.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 {
|
||||
|
||||
private static final String KEY_FIELDS = "fields";
|
||||
@@ -32,8 +32,7 @@ final class FormPayloadParser {
|
||||
|
||||
private FormPayloadParser() {}
|
||||
|
||||
static Map<String, Object> parseValueMap(ObjectMapper objectMapper, String json)
|
||||
throws IOException {
|
||||
static Map<String, Object> parseValueMap(ObjectMapper objectMapper, String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return Map.of();
|
||||
}
|
||||
@@ -41,7 +40,7 @@ final class FormPayloadParser {
|
||||
JsonNode root;
|
||||
try {
|
||||
root = objectMapper.readTree(json);
|
||||
} catch (IOException e) {
|
||||
} catch (JacksonException e) {
|
||||
// Fallback to legacy direct map parse (will throw again if invalid)
|
||||
return objectMapper.readValue(json, MAP_TYPE);
|
||||
}
|
||||
@@ -88,14 +87,14 @@ final class FormPayloadParser {
|
||||
}
|
||||
|
||||
static List<FormUtils.ModifyFormFieldDefinition> parseModificationDefinitions(
|
||||
ObjectMapper objectMapper, String json) throws IOException {
|
||||
ObjectMapper objectMapper, String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
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()) {
|
||||
return List.of();
|
||||
}
|
||||
@@ -119,7 +118,7 @@ final class FormPayloadParser {
|
||||
}
|
||||
}
|
||||
} else if (root.isTextual()) {
|
||||
final String single = trimToNull(root.asText());
|
||||
final String single = trimToNull(root.asText(""));
|
||||
if (single != null) {
|
||||
names.add(single);
|
||||
}
|
||||
@@ -131,7 +130,7 @@ final class FormPayloadParser {
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(json, STRING_LIST_TYPE);
|
||||
} catch (IOException e) {
|
||||
} catch (JacksonException e) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidFormat",
|
||||
"Invalid {0} format: {1}",
|
||||
@@ -199,7 +198,7 @@ final class FormPayloadParser {
|
||||
return null;
|
||||
}
|
||||
if (node.isTextual()) {
|
||||
return trimToEmpty(node.asText());
|
||||
return trimToEmpty(node.asText(""));
|
||||
}
|
||||
if (node.isNumber()) {
|
||||
return node.numberValue().toString();
|
||||
@@ -208,7 +207,7 @@ final class FormPayloadParser {
|
||||
return Boolean.toString(node.booleanValue());
|
||||
}
|
||||
// Fallback for other scalar-like nodes
|
||||
return trimToEmpty(node.asText());
|
||||
return trimToEmpty(node.asText(""));
|
||||
}
|
||||
|
||||
private static void collectNames(JsonNode arrayNode, Set<String> sink) {
|
||||
@@ -229,7 +228,7 @@ final class FormPayloadParser {
|
||||
}
|
||||
|
||||
if (node.isTextual()) {
|
||||
return trimToNull(node.asText());
|
||||
return trimToNull(node.asText(""));
|
||||
}
|
||||
|
||||
if (node.isObject()) {
|
||||
@@ -264,8 +263,8 @@ final class FormPayloadParser {
|
||||
private static Map<String, Object> objectToLinkedMap(JsonNode objectNode) {
|
||||
final Map<String, Object> result = new LinkedHashMap<>();
|
||||
objectNode
|
||||
.fieldNames()
|
||||
.forEachRemaining(
|
||||
.propertyNames()
|
||||
.forEach(
|
||||
key -> {
|
||||
final JsonNode v = objectNode.get(key);
|
||||
if (v == null || v.isNull()) {
|
||||
|
||||
+2
-3
@@ -82,9 +82,8 @@ public class ExtractImagesController {
|
||||
Set<byte[]> processedImages = new HashSet<>();
|
||||
|
||||
if (useMultithreading) {
|
||||
// Executor service to handle multithreading
|
||||
ExecutorService executor =
|
||||
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||
// Virtual thread executor — lightweight threads ideal for I/O-bound image extraction
|
||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||
Set<Future<Void>> futures = new HashSet<>();
|
||||
|
||||
// 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");
|
||||
}
|
||||
if (ocrType != null && !ocrType.isEmpty()) {
|
||||
if ("skip-text".equals(ocrType)) {
|
||||
command.add("--skip-text");
|
||||
} else if ("force-ocr".equals(ocrType)) {
|
||||
if ("force-ocr".equals(ocrType)) {
|
||||
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");
|
||||
@@ -378,11 +380,12 @@ public class OCRController {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("tesseract");
|
||||
command.add(imagePath.toString());
|
||||
command.add(
|
||||
String outputBase =
|
||||
new File(
|
||||
tempOutputDir,
|
||||
String.format(Locale.ROOT, "page_%d", pageNum))
|
||||
.toString());
|
||||
.toString();
|
||||
command.add(outputBase);
|
||||
command.add("-l");
|
||||
command.add(String.join("+", selectedLanguages));
|
||||
command.add("pdf"); // Always output PDF
|
||||
@@ -400,6 +403,18 @@ public class OCRController {
|
||||
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
|
||||
merger.addSource(pageOutputPath);
|
||||
} else {
|
||||
|
||||
+5
-5
@@ -14,10 +14,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
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 lombok.RequiredArgsConstructor;
|
||||
@@ -34,6 +30,10 @@ import stirling.software.common.service.PostHogService;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.DatabindException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@PipelineApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -54,7 +54,7 @@ public class PipelineController {
|
||||
+ "Users provide files and a JSON configuration defining the sequence of operations to perform. "
|
||||
+ "Input:PDF Output:PDF/ZIP Type:MIMO")
|
||||
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
||||
throws JsonMappingException, JsonProcessingException {
|
||||
throws DatabindException, JacksonException {
|
||||
MultipartFile[] files = request.getFileInput();
|
||||
String jsonString = request.getJson();
|
||||
if (files == null) {
|
||||
|
||||
+2
-2
@@ -28,8 +28,6 @@ import org.springframework.core.io.Resource;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.util.FileMonitor;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
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.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 lombok.RequiredArgsConstructor;
|
||||
@@ -66,6 +62,11 @@ import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
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
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -77,7 +78,7 @@ public class GetInfoOnPDF {
|
||||
private static final String PAGE_PREFIX = "Page ";
|
||||
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 VeraPDFService veraPDFService;
|
||||
|
||||
+20
-1
@@ -1,5 +1,7 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
@@ -54,10 +56,27 @@ public class MetricsController {
|
||||
}
|
||||
Map<String, String> status = new HashMap<>();
|
||||
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);
|
||||
}
|
||||
|
||||
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")
|
||||
@Operation(
|
||||
summary = "GET request count",
|
||||
|
||||
@@ -190,6 +190,31 @@ public class GlobalExceptionHandler {
|
||||
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
|
||||
* codes.
|
||||
@@ -1107,6 +1132,15 @@ public class GlobalExceptionHandler {
|
||||
public ResponseEntity<ProblemDetail> handleIOException(
|
||||
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
|
||||
IOException processedException =
|
||||
ExceptionUtils.handlePdfException(ex, request.getRequestURI());
|
||||
|
||||
@@ -3,10 +3,10 @@ package stirling.software.SPDF.model;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
|
||||
public class ApiEndpoint {
|
||||
private final String name;
|
||||
private Map<String, JsonNode> parameters;
|
||||
@@ -18,10 +18,10 @@ public class ApiEndpoint {
|
||||
postNode.path("parameters")
|
||||
.forEach(
|
||||
paramNode -> {
|
||||
String paramName = paramNode.path("name").asText();
|
||||
String paramName = paramNode.path("name").asText("");
|
||||
parameters.put(paramName, paramNode);
|
||||
});
|
||||
this.description = postNode.path("description").asText();
|
||||
this.description = postNode.path("description").asText("");
|
||||
}
|
||||
|
||||
public boolean areParametersValid(Map<String, Object> providedParams) {
|
||||
|
||||
@@ -15,9 +15,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
|
||||
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.util.RegexPatternUtils;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ApiDocService {
|
||||
@@ -36,12 +36,15 @@ public class ApiDocService {
|
||||
|
||||
private final ServletContext servletContext;
|
||||
private final UserServiceInterface userService;
|
||||
private final ObjectMapper objectMapper;
|
||||
Map<String, List<String>> outputToFileTypes = new HashMap<>();
|
||||
JsonNode apiDocsJsonRootNode;
|
||||
|
||||
public ApiDocService(
|
||||
ObjectMapper objectMapper,
|
||||
ServletContext servletContext,
|
||||
@Autowired(required = false) UserServiceInterface userService) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.servletContext = servletContext;
|
||||
this.userService = userService;
|
||||
}
|
||||
@@ -116,8 +119,7 @@ public class ApiDocService {
|
||||
ResponseEntity<String> response =
|
||||
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
|
||||
apiDocsJson = response.getBody();
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
|
||||
apiDocsJsonRootNode = objectMapper.readTree(apiDocsJson);
|
||||
JsonNode paths = apiDocsJsonRootNode.path("paths");
|
||||
paths.propertyStream()
|
||||
.forEach(
|
||||
|
||||
+2
-1
@@ -427,7 +427,8 @@ public class CertificateValidationService {
|
||||
try (InputStream certStream =
|
||||
getClass().getClassLoader().getResourceAsStream("certs/cacert.pem")) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,8 +90,6 @@ import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
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.TempFileManager;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -6834,7 +6834,8 @@ public class PdfJsonConversionService {
|
||||
|
||||
/** Schedules automatic cleanup of cached documents after 30 minutes. */
|
||||
private void scheduleDocumentCleanup(String jobId) {
|
||||
new Thread(
|
||||
Thread.ofVirtual()
|
||||
.start(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(TimeUnit.MINUTES.toMillis(30));
|
||||
@@ -6843,7 +6844,6 @@ public class PdfJsonConversionService {
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
})
|
||||
.start();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ import java.util.stream.Stream;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.common.configuration.InstallationPathConfig;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class SharedSignatureService {
|
||||
@@ -33,9 +33,9 @@ public class SharedSignatureService {
|
||||
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public SharedSignatureService() {
|
||||
public SharedSignatureService(ObjectMapper objectMapper) {
|
||||
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
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.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.util.ExceptionUtils;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Service for lazy loading PDF pages. Caches PDF documents and extracts pages on-demand to reduce
|
||||
* memory usage for large PDFs.
|
||||
@@ -255,7 +255,8 @@ public class PdfLazyLoadingService {
|
||||
|
||||
/** Schedules automatic cleanup of cached documents after 30 minutes. */
|
||||
private void scheduleDocumentCleanup(String jobId) {
|
||||
new Thread(
|
||||
Thread.ofVirtual()
|
||||
.start(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(TimeUnit.MINUTES.toMillis(30));
|
||||
@@ -264,8 +265,7 @@ public class PdfLazyLoadingService {
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
})
|
||||
.start();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+23
-9
@@ -18,14 +18,14 @@ import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.service.pdfjson.type3.Type3FontSignatureCalculator;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@@ -202,20 +202,34 @@ public class Type3FontLibrary {
|
||||
if (payload == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] data = null;
|
||||
String base64;
|
||||
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 {
|
||||
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) {
|
||||
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()) {
|
||||
data = loadResourceBytes(payload.resource);
|
||||
}
|
||||
if (data == null || data.length == 0) {
|
||||
byte[] data = loadResourceBytes(payload.resource);
|
||||
if (data == null || data.length == 0) {
|
||||
return null;
|
||||
}
|
||||
base64 = Base64.getEncoder().encodeToString(data);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
String base64 = Base64.getEncoder().encodeToString(data);
|
||||
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.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.Type3GlyphExtractor;
|
||||
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
|
||||
* 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 {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER =
|
||||
new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
||||
JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build();
|
||||
|
||||
private Type3SignatureTool() {}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ public class SvgToPdf {
|
||||
private GraphicsNode buildGvtWithTimeout(BridgeContext ctx, SVGDocument svgDoc)
|
||||
throws IOException {
|
||||
GVTBuilder builder = new GVTBuilder();
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||
|
||||
Callable<GraphicsNode> buildTask = () -> builder.build(ctx, svgDoc);
|
||||
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
|
||||
spring.jpa.open-in-view=false
|
||||
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.whitelabel.enabled=false
|
||||
server.error.include-stacktrace=always
|
||||
@@ -38,7 +50,7 @@ spring.devtools.livereload.enabled=true
|
||||
spring.devtools.restart.exclude=stirling.software.proprietary.security/**
|
||||
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
||||
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.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.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.model.api.EditTableOfContentsRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class EditTableOfContentsControllerTest {
|
||||
|
||||
|
||||
+18
-16
@@ -40,14 +40,15 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
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.service.VeraPDFService;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
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")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GetInfoOnPDFTest {
|
||||
@@ -64,7 +65,7 @@ class GetInfoOnPDFTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
objectMapper = JsonMapper.builder().build();
|
||||
}
|
||||
|
||||
/** Helper method to load a PDF file from test resources */
|
||||
@@ -216,8 +217,8 @@ class GetInfoOnPDFTest {
|
||||
Assertions.assertTrue(jsonNode.has("Permissions"));
|
||||
|
||||
JsonNode metadata = jsonNode.get("Metadata");
|
||||
Assertions.assertEquals("Test Title", metadata.get("Title").asText());
|
||||
Assertions.assertEquals("Test Author", metadata.get("Author").asText());
|
||||
Assertions.assertEquals("Test Title", metadata.get("Title").asText(""));
|
||||
Assertions.assertEquals("Test Author", metadata.get("Author").asText(""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,12 +316,12 @@ class GetInfoOnPDFTest {
|
||||
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
|
||||
JsonNode metadata = jsonNode.get("Metadata");
|
||||
|
||||
Assertions.assertEquals("Test Title", metadata.get("Title").asText());
|
||||
Assertions.assertEquals("Test Author", metadata.get("Author").asText());
|
||||
Assertions.assertEquals("Test Subject", metadata.get("Subject").asText());
|
||||
Assertions.assertEquals("test, pdf, metadata", metadata.get("Keywords").asText());
|
||||
Assertions.assertEquals("Test Creator", metadata.get("Creator").asText());
|
||||
Assertions.assertEquals("Test Producer", metadata.get("Producer").asText());
|
||||
Assertions.assertEquals("Test Title", metadata.get("Title").asText(""));
|
||||
Assertions.assertEquals("Test Author", metadata.get("Author").asText(""));
|
||||
Assertions.assertEquals("Test Subject", metadata.get("Subject").asText(""));
|
||||
Assertions.assertEquals("test, pdf, metadata", metadata.get("Keywords").asText(""));
|
||||
Assertions.assertEquals("Test Creator", metadata.get("Creator").asText(""));
|
||||
Assertions.assertEquals("Test Producer", metadata.get("Producer").asText(""));
|
||||
Assertions.assertTrue(metadata.has("CreationDate"));
|
||||
Assertions.assertTrue(metadata.has("ModificationDate"));
|
||||
|
||||
@@ -512,10 +513,10 @@ class GetInfoOnPDFTest {
|
||||
JsonNode page1 = perPageInfo.get("Page 1");
|
||||
Assertions.assertTrue(page1.has("Size"));
|
||||
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");
|
||||
Assertions.assertEquals("Letter", page2.get("Size").get("Standard Page").asText());
|
||||
Assertions.assertEquals("Letter", page2.get("Size").get("Standard Page").asText(""));
|
||||
|
||||
loadedDoc.close();
|
||||
}
|
||||
@@ -569,7 +570,8 @@ class GetInfoOnPDFTest {
|
||||
JsonNode jsonNode = objectMapper.readTree(jsonResponse);
|
||||
|
||||
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
|
||||
@@ -645,7 +647,7 @@ class GetInfoOnPDFTest {
|
||||
|
||||
Assertions.assertTrue(jsonNode.has("error"));
|
||||
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 com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
class ApiEndpointTest {
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final ObjectMapper mapper = JsonMapper.builder().build();
|
||||
|
||||
private JsonNode postNodeWithParams(String description, String... names) {
|
||||
ObjectNode post = mapper.createObjectNode();
|
||||
|
||||
@@ -12,14 +12,15 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
|
||||
import stirling.software.SPDF.model.ApiEndpoint;
|
||||
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)
|
||||
class ApiDocServiceTest {
|
||||
|
||||
@@ -27,11 +28,11 @@ class ApiDocServiceTest {
|
||||
@Mock UserServiceInterface userService;
|
||||
|
||||
ApiDocService apiDocService;
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectMapper mapper = JsonMapper.builder().build();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
apiDocService = new ApiDocService(servletContext, userService);
|
||||
apiDocService = new ApiDocService(mapper, servletContext, userService);
|
||||
}
|
||||
|
||||
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.common.configuration.InstallationPathConfig;
|
||||
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
class SignatureServiceTest {
|
||||
|
||||
@TempDir Path tempDir;
|
||||
@@ -53,7 +55,7 @@ class SignatureServiceTest {
|
||||
.thenReturn(tempDir.toString());
|
||||
|
||||
// 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-security'
|
||||
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-cache'
|
||||
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.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.TaskDecorator;
|
||||
import org.springframework.core.task.support.TaskExecutorAdapter;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
|
||||
/**
|
||||
* MDC context-propagating task decorator Copies MDC context from the caller thread to the async
|
||||
* executor thread
|
||||
* MDC context-propagating task decorator. Copies MDC context from the caller thread to the
|
||||
* virtual thread executing the task.
|
||||
*/
|
||||
static class MDCContextTaskDecorator implements TaskDecorator {
|
||||
@Override
|
||||
@@ -42,16 +43,9 @@ public class AsyncConfig {
|
||||
|
||||
@Bean(name = "auditExecutor")
|
||||
public Executor auditExecutor() {
|
||||
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
|
||||
exec.setCorePoolSize(2);
|
||||
exec.setMaxPoolSize(8);
|
||||
exec.setQueueCapacity(1_000);
|
||||
exec.setThreadNamePrefix("audit-");
|
||||
|
||||
// Set the task decorator to propagate MDC context
|
||||
exec.setTaskDecorator(new MDCContextTaskDecorator());
|
||||
|
||||
exec.initialize();
|
||||
return exec;
|
||||
TaskExecutorAdapter adapter =
|
||||
new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
|
||||
adapter.setTaskDecorator(new MDCContextTaskDecorator());
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-6
@@ -1,15 +1,12 @@
|
||||
package stirling.software.proprietary.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/** Configuration to enable scheduling for the audit system. */
|
||||
/** Configuration for audit system transaction management. */
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@EnableScheduling
|
||||
public class AuditJpaConfig {
|
||||
// This configuration enables scheduling for audit cleanup tasks
|
||||
// JPA repositories are now managed by DatabaseConfig to avoid conflicts
|
||||
// No additional beans or methods needed
|
||||
// Scheduling is enabled on SPDFApplication — no duplicate @EnableScheduling needed.
|
||||
// JPA repositories are now managed by DatabaseConfig to avoid conflicts.
|
||||
}
|
||||
|
||||
+6
-3
@@ -12,8 +12,6 @@ import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.util.SecretMasker;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Component
|
||||
@Primary
|
||||
@RequiredArgsConstructor
|
||||
@@ -68,7 +68,10 @@ public class CustomAuditEventRepository implements AuditEventRepository {
|
||||
.build();
|
||||
repo.save(ent);
|
||||
} 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.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.media.Schema;
|
||||
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.security.config.EnterpriseEndpoint;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/** REST endpoints for the audit dashboard. */
|
||||
@Slf4j
|
||||
@RestController
|
||||
@@ -266,7 +266,7 @@ public class AuditDashboardController {
|
||||
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.error("Error serializing audit events to JSON", e);
|
||||
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.RequestParam;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.security.config.EnterpriseEndpoint;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/** REST API controller for audit data used by React frontend. */
|
||||
@Slf4j
|
||||
@ProprietaryUiDataApi
|
||||
@@ -332,7 +332,7 @@ public class AuditRestController {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> parsed = objectMapper.readValue(event.getData(), Map.class);
|
||||
details = parsed;
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.warn("Failed to parse audit event data as JSON: {}", event.getData());
|
||||
details.put("rawData", event.getData());
|
||||
}
|
||||
@@ -380,7 +380,7 @@ public class AuditRestController {
|
||||
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.error("Error serializing audit events to JSON", e);
|
||||
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.PathVariable;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
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.service.UserLicenseSettingsService;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@ProprietaryUiDataApi
|
||||
public class ProprietaryUIDataController {
|
||||
@@ -414,7 +414,7 @@ public class ProprietaryUIDataController {
|
||||
String settingsJson;
|
||||
try {
|
||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException e) {
|
||||
log.error("Error converting settings map", e);
|
||||
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.RequestParam;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.security.config.EnterpriseEndpoint;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/** REST API controller for usage analytics data used by React frontend. */
|
||||
@Slf4j
|
||||
@ProprietaryUiDataApi
|
||||
@@ -135,7 +135,7 @@ public class UsageRestController {
|
||||
return normalizeEndpoint(requestUri.toString());
|
||||
}
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
} catch (JacksonException 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.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.boot.jdbc.DatabaseDriver;
|
||||
import org.springframework.boot.persistence.autoconfigure.EntityScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
@@ -75,10 +75,18 @@ public class DatabaseConfig {
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
dataSourceBuilder
|
||||
.url(DATASOURCE_DEFAULT_URL)
|
||||
.url(url)
|
||||
.driverClassName(DatabaseDriver.H2.getDriverClassName())
|
||||
.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.web.builders.HttpSecurity;
|
||||
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.HeadersConfigurer.FrameOptionsConfig;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
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.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml5AuthenticationRequestResolver;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
@@ -82,7 +84,7 @@ public class SecurityConfiguration {
|
||||
private final PersistentLoginRepository persistentLoginRepository;
|
||||
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
||||
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
||||
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||
private final OpenSaml5AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||
private final stirling.software.proprietary.service.UserLicenseSettingsService
|
||||
licenseSettingsService;
|
||||
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||
@@ -105,7 +107,7 @@ public class SecurityConfiguration {
|
||||
@Autowired(required = false)
|
||||
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
||||
@Autowired(required = false)
|
||||
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver,
|
||||
OpenSaml5AuthenticationRequestResolver saml2AuthenticationRequestResolver,
|
||||
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository,
|
||||
stirling.software.proprietary.service.UserLicenseSettingsService
|
||||
licenseSettingsService) {
|
||||
@@ -151,7 +153,8 @@ public class SecurityConfiguration {
|
||||
// Default to allowing all origins when nothing is configured
|
||||
cfg.setAllowedOriginPatterns(List.of("*"));
|
||||
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)
|
||||
@@ -225,7 +228,7 @@ public class SecurityConfiguration {
|
||||
http.cors(cors -> cors.configurationSource(corsSource));
|
||||
} else {
|
||||
// Explicitly disable CORS when no origins are configured
|
||||
http.cors(cors -> cors.disable());
|
||||
http.cors(CorsConfigurer::disable);
|
||||
}
|
||||
|
||||
http.csrf(CsrfConfigurer::disable);
|
||||
@@ -233,24 +236,24 @@ public class SecurityConfiguration {
|
||||
// Configure X-Frame-Options based on settings.yml configuration
|
||||
// When login is disabled, automatically disable X-Frame-Options to allow embedding
|
||||
if (!loginEnabledValue) {
|
||||
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()));
|
||||
http.headers(headers -> headers.frameOptions(FrameOptionsConfig::disable));
|
||||
} else {
|
||||
String xFrameOption = securityProperties.getXFrameOptions();
|
||||
if (xFrameOption != null) {
|
||||
http.headers(
|
||||
headers -> {
|
||||
if ("DISABLED".equalsIgnoreCase(xFrameOption)) {
|
||||
headers.frameOptions(frameOptions -> frameOptions.disable());
|
||||
headers.frameOptions(FrameOptionsConfig::disable);
|
||||
} else if ("SAMEORIGIN".equalsIgnoreCase(xFrameOption)) {
|
||||
headers.frameOptions(frameOptions -> frameOptions.sameOrigin());
|
||||
headers.frameOptions(FrameOptionsConfig::sameOrigin);
|
||||
} else {
|
||||
// Default to DENY
|
||||
headers.frameOptions(frameOptions -> frameOptions.deny());
|
||||
headers.frameOptions(FrameOptionsConfig::deny);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 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
|
||||
if (securityProperties.isSaml2Active() && runningProOrHigher) {
|
||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml4AuthenticationProvider();
|
||||
OpenSaml5AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml5AuthenticationProvider();
|
||||
authenticationProvider.setResponseAuthenticationConverter(
|
||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||
http.authenticationProvider(authenticationProvider)
|
||||
|
||||
+90
-87
@@ -12,11 +12,6 @@ import org.bouncycastle.crypto.signers.Ed25519Signer;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
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.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -24,6 +19,9 @@ import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -47,7 +45,7 @@ public class KeygenLicenseVerifier {
|
||||
|
||||
private static final String JWT_PREFIX = "key/";
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
// Shared HTTP client for connection pooling
|
||||
@@ -135,11 +133,11 @@ public class KeygenLicenseVerifier {
|
||||
String algorithm = "";
|
||||
|
||||
try {
|
||||
JSONObject attrs = new JSONObject(payload);
|
||||
encryptedData = (String) attrs.get("enc");
|
||||
encodedSignature = (String) attrs.get("sig");
|
||||
algorithm = (String) attrs.get("alg");
|
||||
} catch (JSONException e) {
|
||||
JsonNode attrs = objectMapper.readTree(payload);
|
||||
encryptedData = attrs.path("enc").asText("");
|
||||
encodedSignature = attrs.path("sig").asText("");
|
||||
algorithm = attrs.path("alg").asText("");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to parse license file: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
@@ -215,11 +213,17 @@ public class KeygenLicenseVerifier {
|
||||
|
||||
private boolean processCertificateData(String certData, LicenseContext context) {
|
||||
try {
|
||||
JSONObject licenseData = new JSONObject(certData);
|
||||
JSONObject metaObj = licenseData.optJSONObject("meta");
|
||||
if (metaObj != null) {
|
||||
String issuedStr = metaObj.optString("issued", null);
|
||||
String expiryStr = metaObj.optString("expiry", null);
|
||||
JsonNode licenseData = objectMapper.readTree(certData);
|
||||
JsonNode metaObj = licenseData.path("meta");
|
||||
if (!metaObj.isMissingNode() && metaObj.isObject()) {
|
||||
String issuedStr =
|
||||
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) {
|
||||
java.time.Instant issued = java.time.Instant.parse(issuedStr);
|
||||
@@ -244,31 +248,32 @@ public class KeygenLicenseVerifier {
|
||||
}
|
||||
|
||||
// Get the main license data
|
||||
JSONObject dataObj = licenseData.optJSONObject("data");
|
||||
if (dataObj == null) {
|
||||
JsonNode dataObj = licenseData.path("data");
|
||||
if (dataObj.isMissingNode() || !dataObj.isObject()) {
|
||||
log.error("No data object found in certificate");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract license or machine information
|
||||
JSONObject attributesObj = dataObj.optJSONObject("attributes");
|
||||
if (attributesObj != null) {
|
||||
JsonNode attributesObj = dataObj.path("attributes");
|
||||
if (!attributesObj.isMissingNode() && attributesObj.isObject()) {
|
||||
log.info("Found attributes in certificate data");
|
||||
|
||||
// Check for floating license
|
||||
context.isFloatingLicense = attributesObj.optBoolean("floating", false);
|
||||
context.maxMachines = attributesObj.optInt("maxMachines", 1);
|
||||
context.isFloatingLicense = attributesObj.path("floating").asBoolean(false);
|
||||
context.maxMachines = attributesObj.path("maxMachines").asInt(1);
|
||||
|
||||
// Extract metadata
|
||||
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
|
||||
if (metadataObj != null) {
|
||||
JsonNode metadataObj = attributesObj.path("metadata");
|
||||
if (!metadataObj.isMissingNode() && metadataObj.isObject()) {
|
||||
// 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
|
||||
// Old licenses: Only had isEnterprise flag
|
||||
// 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
|
||||
// ENTERPRISE license (isEnterprise=true, users>0) = limited seats
|
||||
@@ -282,7 +287,7 @@ public class KeygenLicenseVerifier {
|
||||
}
|
||||
|
||||
// Check license status if available
|
||||
String status = attributesObj.optString("status", null);
|
||||
String status = attributesObj.path("status").asText(null);
|
||||
if (status != null
|
||||
&& !"ACTIVE".equals(status)
|
||||
&& !"EXPIRING".equals(status)) { // Accept "EXPIRING" status as valid
|
||||
@@ -372,11 +377,11 @@ public class KeygenLicenseVerifier {
|
||||
try {
|
||||
log.info("Processing license payload: {}", payload);
|
||||
|
||||
JSONObject licenseData = new JSONObject(payload);
|
||||
JsonNode licenseData = objectMapper.readTree(payload);
|
||||
|
||||
JSONObject licenseObj = licenseData.optJSONObject("license");
|
||||
if (licenseObj == null) {
|
||||
String id = licenseData.optString("id", null);
|
||||
JsonNode licenseObj = licenseData.path("license");
|
||||
if (licenseObj.isMissingNode() || !licenseObj.isObject()) {
|
||||
String id = licenseData.path("id").asText(null);
|
||||
if (id != null) {
|
||||
log.info("Found license ID: {}", id);
|
||||
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);
|
||||
|
||||
// Check for floating license in license object
|
||||
context.isFloatingLicense = licenseObj.optBoolean("floating", false);
|
||||
context.maxMachines = licenseObj.optInt("maxMachines", 1);
|
||||
context.isFloatingLicense = licenseObj.path("floating").asBoolean(false);
|
||||
context.maxMachines = licenseObj.path("maxMachines").asInt(1);
|
||||
if (context.isFloatingLicense) {
|
||||
log.info("Detected floating license with max machines: {}", context.maxMachines);
|
||||
}
|
||||
|
||||
// Check expiry date
|
||||
String expiryStr = licenseObj.optString("expiry", null);
|
||||
String expiryStr = licenseObj.path("expiry").asText(null);
|
||||
if (expiryStr != null && !"null".equals(expiryStr)) {
|
||||
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
|
||||
java.time.Instant now = java.time.Instant.now();
|
||||
@@ -413,9 +418,9 @@ public class KeygenLicenseVerifier {
|
||||
}
|
||||
|
||||
// Extract account, product, policy info
|
||||
JSONObject accountObj = licenseData.optJSONObject("account");
|
||||
if (accountObj != null) {
|
||||
String accountId = accountObj.optString("id", "unknown");
|
||||
JsonNode accountObj = licenseData.path("account");
|
||||
if (!accountObj.isMissingNode() && accountObj.isObject()) {
|
||||
String accountId = accountObj.path("id").asText("unknown");
|
||||
log.info("License belongs to account: {}", accountId);
|
||||
|
||||
// Verify this matches your expected account ID
|
||||
@@ -426,14 +431,14 @@ public class KeygenLicenseVerifier {
|
||||
}
|
||||
|
||||
// Extract policy information if available
|
||||
JSONObject policyObj = licenseData.optJSONObject("policy");
|
||||
if (policyObj != null) {
|
||||
String policyId = policyObj.optString("id", "unknown");
|
||||
JsonNode policyObj = licenseData.path("policy");
|
||||
if (!policyObj.isMissingNode() && policyObj.isObject()) {
|
||||
String policyId = policyObj.path("id").asText("unknown");
|
||||
log.info("License uses policy: {}", policyId);
|
||||
|
||||
// Check for floating license in policy
|
||||
boolean policyFloating = policyObj.optBoolean("floating", false);
|
||||
int policyMaxMachines = policyObj.optInt("maxMachines", 1);
|
||||
boolean policyFloating = policyObj.path("floating").asBoolean(false);
|
||||
int policyMaxMachines = policyObj.path("maxMachines").asInt(1);
|
||||
|
||||
// Policy settings take precedence
|
||||
if (policyFloating) {
|
||||
@@ -445,16 +450,21 @@ public class KeygenLicenseVerifier {
|
||||
}
|
||||
|
||||
// Extract max users and isEnterprise from policy or metadata
|
||||
context.isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false);
|
||||
int users = policyObj.optInt("users", -1);
|
||||
context.isEnterpriseLicense = policyObj.path("isEnterprise").asBoolean(false);
|
||||
int users = policyObj.path("users").asInt(-1);
|
||||
|
||||
if (users == -1) {
|
||||
// Try to get users from metadata if not at policy level
|
||||
Object metadataObj = policyObj.opt("metadata");
|
||||
if (metadataObj instanceof JSONObject metadata) {
|
||||
JsonNode metadataObj = policyObj.path("metadata");
|
||||
if (!metadataObj.isMissingNode() && metadataObj.isObject()) {
|
||||
context.isEnterpriseLicense =
|
||||
metadata.optBoolean("isEnterprise", context.isEnterpriseLicense);
|
||||
users = metadata.optInt("users", context.isEnterpriseLicense ? 1 : 0);
|
||||
metadataObj
|
||||
.path("isEnterprise")
|
||||
.asBoolean(context.isEnterpriseLicense);
|
||||
users =
|
||||
metadataObj
|
||||
.path("users")
|
||||
.asInt(context.isEnterpriseLicense ? 1 : 0);
|
||||
} else {
|
||||
// Default based on license type
|
||||
users = context.isEnterpriseLicense ? 1 : 0;
|
||||
@@ -493,9 +503,9 @@ public class KeygenLicenseVerifier {
|
||||
validateLicense(licenseKey, machineFingerprint, context);
|
||||
if (validationResponse != null) {
|
||||
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) {
|
||||
String code = validationResponse.path("meta").path("code").asText();
|
||||
String code = validationResponse.path("meta").path("code").asText("");
|
||||
log.info(code);
|
||||
if ("NO_MACHINE".equals(code)
|
||||
|| "NO_MACHINES".equals(code)
|
||||
@@ -579,8 +589,8 @@ public class KeygenLicenseVerifier {
|
||||
JsonNode metaNode = jsonResponse.path("meta");
|
||||
boolean isValid = metaNode.path("valid").asBoolean();
|
||||
|
||||
String detail = metaNode.path("detail").asText();
|
||||
String code = metaNode.path("code").asText();
|
||||
String detail = metaNode.path("detail").asText("");
|
||||
String code = metaNode.path("code").asText("");
|
||||
|
||||
log.info("License validity: {}", isValid);
|
||||
log.info("Validation detail: {}", detail);
|
||||
@@ -604,7 +614,7 @@ public class KeygenLicenseVerifier {
|
||||
|
||||
if (includedNode.isArray()) {
|
||||
for (JsonNode node : includedNode) {
|
||||
if ("policies".equals(node.path("type").asText())) {
|
||||
if ("policies".equals(node.path("type").asText(""))) {
|
||||
policyNode = node;
|
||||
break;
|
||||
}
|
||||
@@ -690,9 +700,9 @@ public class KeygenLicenseVerifier {
|
||||
|
||||
for (JsonNode machine : machines) {
|
||||
if (machineFingerprint.equals(
|
||||
machine.path("attributes").path("fingerprint").asText())) {
|
||||
machine.path("attributes").path("fingerprint").asText(""))) {
|
||||
isCurrentMachineActivated = true;
|
||||
currentMachineId = machine.path("id").asText();
|
||||
currentMachineId = machine.path("id").asText("");
|
||||
log.info(
|
||||
"Current machine is already activated with ID: {}",
|
||||
currentMachineId);
|
||||
@@ -726,7 +736,7 @@ public class KeygenLicenseVerifier {
|
||||
java.time.Instant.parse(createdStr);
|
||||
if (oldestTime == null || createdTime.isBefore(oldestTime)) {
|
||||
oldestTime = createdTime;
|
||||
oldestMachineId = machine.path("id").asText();
|
||||
oldestMachineId = machine.path("id").asText("");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
@@ -740,7 +750,7 @@ public class KeygenLicenseVerifier {
|
||||
if (oldestMachineId == null) {
|
||||
log.warn(
|
||||
"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);
|
||||
@@ -770,35 +780,28 @@ public class KeygenLicenseVerifier {
|
||||
hostname = "Unknown";
|
||||
}
|
||||
|
||||
JSONObject body =
|
||||
new JSONObject()
|
||||
.put(
|
||||
"data",
|
||||
new JSONObject()
|
||||
.put("type", "machines")
|
||||
.put(
|
||||
"attributes",
|
||||
new JSONObject()
|
||||
.put("fingerprint", machineFingerprint)
|
||||
.put(
|
||||
"platform",
|
||||
System.getProperty("os.name"))
|
||||
.put("name", hostname))
|
||||
.put(
|
||||
"relationships",
|
||||
new JSONObject()
|
||||
.put(
|
||||
"license",
|
||||
new JSONObject()
|
||||
.put(
|
||||
"data",
|
||||
new JSONObject()
|
||||
.put(
|
||||
"type",
|
||||
"licenses")
|
||||
.put(
|
||||
"id",
|
||||
licenseId)))));
|
||||
tools.jackson.databind.node.ObjectNode attributes = objectMapper.createObjectNode();
|
||||
attributes.put("fingerprint", machineFingerprint);
|
||||
attributes.put("platform", System.getProperty("os.name"));
|
||||
attributes.put("name", hostname);
|
||||
|
||||
tools.jackson.databind.node.ObjectNode licenseRef = objectMapper.createObjectNode();
|
||||
licenseRef.put("type", "licenses");
|
||||
licenseRef.put("id", licenseId);
|
||||
|
||||
tools.jackson.databind.node.ObjectNode licenseRelation = objectMapper.createObjectNode();
|
||||
licenseRelation.set("data", licenseRef);
|
||||
|
||||
tools.jackson.databind.node.ObjectNode relationships = objectMapper.createObjectNode();
|
||||
relationships.set("license", licenseRelation);
|
||||
|
||||
tools.jackson.databind.node.ObjectNode data = objectMapper.createObjectNode();
|
||||
data.put("type", "machines");
|
||||
data.set("attributes", attributes);
|
||||
data.set("relationships", relationships);
|
||||
|
||||
tools.jackson.databind.node.ObjectNode body = objectMapper.createObjectNode();
|
||||
body.set("data", data);
|
||||
|
||||
HttpRequest request =
|
||||
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.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.responses.ApiResponse;
|
||||
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.UpdateSettingsRequest;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@AdminApi
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@@ -543,7 +543,8 @@ public class AdminSettingsController {
|
||||
pendingChanges.clear();
|
||||
|
||||
// Give the HTTP response time to complete, then exit
|
||||
new Thread(
|
||||
Thread.ofVirtual()
|
||||
.start(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
@@ -554,8 +555,7 @@ public class AdminSettingsController {
|
||||
log.error("Restart interrupted: {}", e.getMessage(), e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
})
|
||||
.start();
|
||||
});
|
||||
|
||||
return ResponseEntity.ok(
|
||||
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.RestController;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.Data;
|
||||
@@ -31,6 +28,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@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 final RuntimePathConfig runtimePathConfig;
|
||||
private final ObjectMapper objectMapper;
|
||||
private static volatile List<String> cachedRemoteTessdata = null;
|
||||
private static volatile long cachedRemoteTessdataExpiry = 0L;
|
||||
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()) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
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 =
|
||||
items.stream()
|
||||
.map(item -> (String) item.get("name"))
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private Object credentials;
|
||||
|
||||
public ApiKeyAuthenticationToken(String apiKey) {
|
||||
super(null);
|
||||
super((Collection<? extends GrantedAuthority>) null);
|
||||
this.principal = null;
|
||||
this.credentials = apiKey;
|
||||
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.core.convert.converter.Converter;
|
||||
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 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.RelyingPartyRegistrationRepository;
|
||||
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;
|
||||
|
||||
@@ -151,10 +151,10 @@ public class Saml2Configuration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
||||
public OpenSaml5AuthenticationRequestResolver authenticationRequestResolver(
|
||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||
OpenSaml4AuthenticationRequestResolver resolver =
|
||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||
OpenSaml5AuthenticationRequestResolver resolver =
|
||||
new OpenSaml5AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||
|
||||
resolver.setRelayStateResolver(
|
||||
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.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.ExpiredJwtException;
|
||||
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.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class JwtService implements JwtServiceInterface {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final KeyPersistenceServiceInterface keyPersistenceService;
|
||||
private final boolean v2Enabled;
|
||||
private final ApplicationProperties.Security securityProperties;
|
||||
|
||||
@Autowired
|
||||
public JwtService(
|
||||
ObjectMapper objectMapper,
|
||||
@Qualifier("v2Enabled") boolean v2Enabled,
|
||||
KeyPersistenceServiceInterface keyPersistenceService,
|
||||
ApplicationProperties applicationProperties) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.v2Enabled = v2Enabled;
|
||||
this.keyPersistenceService = keyPersistenceService;
|
||||
this.securityProperties = applicationProperties.getSecurity();
|
||||
@@ -374,14 +375,14 @@ public class JwtService implements JwtServiceInterface {
|
||||
|
||||
byte[] headerBytes = Base64.getUrlDecoder().decode(tokenParts[0]);
|
||||
Map<String, Object> header =
|
||||
OBJECT_MAPPER.readValue(
|
||||
objectMapper.readValue(
|
||||
headerBytes, new TypeReference<Map<String, Object>>() {});
|
||||
Object keyId = header.get("kid");
|
||||
return keyId instanceof String ? (String) keyId : null;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug("Failed to decode Base64 JWT header: {}", e.getMessage());
|
||||
return null;
|
||||
} catch (java.io.IOException e) {
|
||||
} catch (tools.jackson.core.JacksonException e) {
|
||||
log.debug("Failed to parse JWT header as JSON: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
+5
-4
@@ -15,8 +15,6 @@ import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.SavedSignatureResponse;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Service for managing user signatures with authentication and storage limits. This proprietary
|
||||
* 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 final String SIGNATURE_BASE_PATH;
|
||||
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// Storage limits per user
|
||||
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_TOTAL_USER_STORAGE_BYTES = 20_000_000; // 20MB total per user
|
||||
|
||||
public SignatureService() {
|
||||
public SignatureService(ObjectMapper objectMapper) {
|
||||
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.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
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.service.UserLicenseSettingsService;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ProprietaryUIDataControllerTest {
|
||||
|
||||
@@ -63,7 +64,7 @@ class ProprietaryUIDataControllerTest {
|
||||
applicationProperties.getSecurity().getSaml2().setEnabled(false);
|
||||
|
||||
auditConfig = new AuditConfigurationProperties(applicationProperties);
|
||||
objectMapper = new ObjectMapper();
|
||||
objectMapper = JsonMapper.builder().build();
|
||||
|
||||
controller =
|
||||
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.setup.MockMvcBuilders;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
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.UserService;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuthControllerLoginTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||
|
||||
private MockMvc mockMvc;
|
||||
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.setup.MockMvcBuilders;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
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.UserService;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuthControllerMfaTest {
|
||||
|
||||
private static final String USERNAME = "[email protected]";
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private Authentication authentication;
|
||||
|
||||
+15
-13
@@ -19,6 +19,8 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
class UIDataTessdataControllerTest {
|
||||
|
||||
@Test
|
||||
@@ -27,7 +29,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn("ignored/path");
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -49,7 +51,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -77,7 +79,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -100,7 +102,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng", "fra");
|
||||
@@ -139,7 +141,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -164,7 +166,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected boolean isWritableDirectory(Path dir) {
|
||||
return false;
|
||||
@@ -186,7 +188,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -217,7 +219,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -258,7 +260,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng", "fra");
|
||||
@@ -282,7 +284,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -307,7 +309,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -330,7 +332,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(missingDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected List<String> getRemoteTessdataLanguages() {
|
||||
return List.of("eng");
|
||||
@@ -352,7 +354,7 @@ class UIDataTessdataControllerTest {
|
||||
Mockito.when(runtimePathConfig.getTessDataPath()).thenReturn(tempDir.toString());
|
||||
|
||||
UIDataTessdataController controller =
|
||||
new UIDataTessdataController(runtimePathConfig) {
|
||||
new UIDataTessdataController(runtimePathConfig, JsonMapper.builder().build()) {
|
||||
@Override
|
||||
protected boolean isWritableDirectory(Path dir) {
|
||||
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.setup.MockMvcBuilders;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
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.service.UserLicenseSettingsService;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserControllerTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||
|
||||
@Mock private UserService userService;
|
||||
@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.exception.AuthenticationFailureException;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class JwtServiceTest {
|
||||
|
||||
@@ -66,7 +69,8 @@ class JwtServiceTest {
|
||||
testVerificationKey = new JwtVerificationKey("test-key-id", encodedPublicKey);
|
||||
|
||||
ApplicationProperties applicationProperties = new ApplicationProperties();
|
||||
jwtService = new JwtService(true, keystoreService, applicationProperties);
|
||||
ObjectMapper objectMapper = JsonMapper.builder().build();
|
||||
jwtService = new JwtService(objectMapper, true, keystoreService, applicationProperties);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+51
-16
@@ -2,7 +2,7 @@ plugins {
|
||||
id "java"
|
||||
id "jacoco"
|
||||
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 "io.swagger.swaggerhub" version "1.3.2"
|
||||
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.JsonSlurper
|
||||
import groovy.xml.XmlSlurper
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.tasks.testing.Test
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
ext {
|
||||
springBootVersion = "3.5.9"
|
||||
springBootVersion = "4.0.3"
|
||||
pdfboxVersion = "3.0.6"
|
||||
imageioVersion = "3.13.0"
|
||||
lombokVersion = "1.18.42"
|
||||
bouncycastleVersion = "1.83"
|
||||
springSecuritySamlVersion = "6.5.6"
|
||||
springSecuritySamlVersion = "7.0.2"
|
||||
openSamlVersion = "4.3.2"
|
||||
commonmarkVersion = "0.27.0"
|
||||
googleJavaFormatVersion = "1.28.0"
|
||||
logback = "1.5.25"
|
||||
commonmarkVersion = "0.27.1"
|
||||
googleJavaFormatVersion = "1.34.1"
|
||||
logback = "1.5.28"
|
||||
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 = { ->
|
||||
@@ -70,7 +81,6 @@ allprojects {
|
||||
version = '2.5.3'
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||
}
|
||||
}
|
||||
@@ -138,8 +148,11 @@ subprojects {
|
||||
apply plugin: 'jacoco'
|
||||
|
||||
java {
|
||||
// 17 is lowest but we support and recommend 21
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
if (project.name != "stirling-pdf") {
|
||||
@@ -167,12 +180,14 @@ subprojects {
|
||||
}
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
||||
// Exclude vulnerable BouncyCastle version used in tableau
|
||||
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
|
||||
exclude group: 'org.bouncycastle', module: 'bcutil-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 {
|
||||
@@ -191,7 +206,11 @@ subprojects {
|
||||
compileOnly "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-webmvc-test'
|
||||
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
|
||||
|
||||
@@ -201,13 +220,15 @@ subprojects {
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = "UTF-8"
|
||||
options.release = rootProject.ext.modernJavaVersion
|
||||
if (!project.hasProperty("noSpotless")) {
|
||||
dependsOn "spotlessApply"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("compileJava", JavaCompile).configure {
|
||||
options.compilerArgs.add("-parameters")
|
||||
// options.compilerArgs.add("-Xlint:deprecation")
|
||||
// options.compilerArgs.add("-Xlint:unchecked")
|
||||
}
|
||||
|
||||
def jacocoReport = tasks.named("jacocoTestReport")
|
||||
@@ -387,6 +408,17 @@ subprojects {
|
||||
finalizedBy("copySwaggerDoc")
|
||||
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')
|
||||
classpath = files()
|
||||
destinationDirectory = file("${buildDir}/restart-helper-classes")
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
destinationDirectory = layout.buildDirectory.dir("restart-helper-classes")
|
||||
def restartMajorVersion = project.ext.modernJavaVersion
|
||||
def restartCompatibility = JavaVersion.toVersion(restartMajorVersion.toString())
|
||||
sourceCompatibility = restartCompatibility
|
||||
targetCompatibility = restartCompatibility
|
||||
options.release.set(restartMajorVersion)
|
||||
}
|
||||
|
||||
// Task to create restart-helper.jar
|
||||
@@ -544,9 +579,9 @@ tasks.register('buildRestartHelper', Jar) {
|
||||
description = 'Builds the restart-helper.jar'
|
||||
dependsOn 'compileRestartHelper'
|
||||
|
||||
from "${buildDir}/restart-helper-classes"
|
||||
from layout.buildDirectory.dir("restart-helper-classes")
|
||||
archiveFileName = 'restart-helper.jar'
|
||||
destinationDirectory = file("${buildDir}/libs")
|
||||
destinationDirectory = layout.buildDirectory.dir("libs")
|
||||
|
||||
manifest {
|
||||
attributes 'Main-Class': 'RestartHelper'
|
||||
|
||||
@@ -24,6 +24,13 @@ COPY gradle gradle/
|
||||
COPY app/core/build.gradle core/.
|
||||
COPY app/common/build.gradle common/.
|
||||
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
|
||||
|
||||
WORKDIR /app
|
||||
@@ -54,7 +61,11 @@ LABEL org.opencontainers.image.keywords="PDF, manipulation, unified, API, Spring
|
||||
# Copy backend files
|
||||
COPY scripts /scripts
|
||||
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 --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
|
||||
|
||||
# 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 && \
|
||||
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 && \
|
||||
@@ -122,11 +134,14 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
||||
py3-pillow@testing \
|
||||
py3-pdf2image@testing && \
|
||||
python3 -m venv /opt/venv && \
|
||||
/opt/venv/bin/pip install --upgrade pip setuptools && \
|
||||
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
||||
/opt/venv/bin/pip install --no-cache-dir unoserver weasyprint && \
|
||||
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 /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 && \
|
||||
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 && \
|
||||
@@ -135,8 +150,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
||||
chmod +x /entrypoint.sh && \
|
||||
# User permissions
|
||||
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 stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||
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
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
|
||||
|
||||
@@ -24,6 +24,13 @@ COPY gradle gradle/
|
||||
COPY app/core/build.gradle core/.
|
||||
COPY app/common/build.gradle common/.
|
||||
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
|
||||
|
||||
WORKDIR /app
|
||||
@@ -53,7 +60,11 @@ LABEL org.opencontainers.image.keywords="PDF, manipulation, unified, ultra-lite,
|
||||
# Copy backend files
|
||||
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.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 --from=frontend-build /app/dist /usr/share/nginx/html
|
||||
@@ -80,6 +91,7 @@ ENV DISABLE_ADDITIONAL_FEATURES=false \
|
||||
ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||
|
||||
# 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 && \
|
||||
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 && \
|
||||
@@ -100,8 +112,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
||||
chmod +x /entrypoint.sh && \
|
||||
# User permissions
|
||||
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 stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /configs /customFiles /pipeline /tmp/stirling-pdf /var/lib/nginx /var/log/nginx /usr/share/nginx
|
||||
|
||||
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:
|
||||
backend:
|
||||
stirling-pdf:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: docker/backend/Dockerfile.fat
|
||||
container_name: stirling-pdf-backend-fat
|
||||
dockerfile: docker/embedded/Dockerfile.fat
|
||||
container_name: stirling-pdf-fat
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
@@ -15,9 +15,7 @@ services:
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- "8080:8080" # TODO: Remove in production - for debugging only
|
||||
expose:
|
||||
- "8080"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ../../stirling/latest/data:/usr/share/tessdata:rw
|
||||
- ../../stirling/latest/config:/configs:rw
|
||||
@@ -37,24 +35,6 @@ services:
|
||||
networks:
|
||||
- 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:
|
||||
stirling-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
services:
|
||||
backend:
|
||||
stirling-pdf:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: docker/backend/Dockerfile.ultra-lite
|
||||
container_name: stirling-pdf-backend-ultra-lite
|
||||
dockerfile: docker/embedded/Dockerfile.ultra-lite
|
||||
container_name: stirling-pdf-ultra-lite
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
@@ -15,9 +15,7 @@ services:
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- "8080:8080" # TODO: Remove in production - for debugging only
|
||||
expose:
|
||||
- "8080"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ../../stirling/latest/config:/configs:rw
|
||||
- ../../stirling/latest/logs:/logs:rw
|
||||
@@ -34,24 +32,6 @@ services:
|
||||
networks:
|
||||
- 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:
|
||||
stirling-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
services:
|
||||
backend:
|
||||
stirling-pdf:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: docker/backend/Dockerfile
|
||||
container_name: stirling-pdf-backend
|
||||
dockerfile: docker/embedded/Dockerfile
|
||||
container_name: stirling-pdf
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
|
||||
@@ -11,9 +11,7 @@ services:
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- "8080:8080" # TODO: Remove in production - for debugging only
|
||||
expose:
|
||||
- "8080"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ../../stirling/latest/data:/usr/share/tessdata:rw
|
||||
- ../../stirling/latest/config:/configs:rw
|
||||
@@ -32,24 +30,6 @@ services:
|
||||
networks:
|
||||
- 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:
|
||||
stirling-network:
|
||||
driver: bridge
|
||||
|
||||
+553
-174
@@ -1,213 +1,592 @@
|
||||
# Stirling-PDF Dockerfile - Full version with embedded frontend
|
||||
# Single JAR contains both frontend and backend
|
||||
# Stirling-PDF - Full version (embedded frontend)
|
||||
|
||||
# Stage 1: Build application with embedded frontend
|
||||
FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build
|
||||
FROM ubuntu:noble AS calibre-build
|
||||
|
||||
# Install Node.js and npm for frontend build
|
||||
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/*
|
||||
ARG CALIBRE_VERSION=9.3.1
|
||||
|
||||
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; \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends -t ${VERSION_CODENAME}-backports \
|
||||
libreoffice libreoffice-java-common; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
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/*; \
|
||||
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
|
||||
|
||||
# ==============================================================================
|
||||
# Create non-root user (stirlingpdfuser) with configurable UID/GID
|
||||
# ==============================================================================
|
||||
# Build the Java application and frontend.
|
||||
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 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; \
|
||||
groupadd -g "${PGID}" stirlingpdfgroup 2>/dev/null \
|
||||
|| groupadd 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; \
|
||||
useradd -m -u "${PUID}" -g stirlingpdfgroup \
|
||||
-d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser 2>/dev/null \
|
||||
|| useradd -m -g stirlingpdfgroup \
|
||||
-d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
||||
fi; \
|
||||
fi
|
||||
ln -sf /usr/sbin/gosu /usr/local/bin/su-exec
|
||||
|
||||
# Compatibility alias for older entrypoint scripts expecting su-exec
|
||||
RUN ln -sf /usr/sbin/gosu /usr/local/bin/su-exec
|
||||
# Application files.
|
||||
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=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 --from=app-build --chown=1000:1000 \
|
||||
/app/build/libs/restart-helper.jar /restart-helper.jar
|
||||
COPY --chown=1000:1000 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/
|
||||
|
||||
# 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
|
||||
|
||||
# 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 \
|
||||
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="" \
|
||||
HOME=/home/stirlingpdfuser \
|
||||
PUID=${PUID} \
|
||||
PGID=${PGID} \
|
||||
UMASK=022 \
|
||||
PATH="/opt/venv/bin:${PATH}" \
|
||||
UNO_PATH=/usr/lib/libreoffice/program \
|
||||
LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin \
|
||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||
TMPDIR=/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
|
||||
|
||||
# ==============================================================================
|
||||
# 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__)"
|
||||
# Metadata labels.
|
||||
LABEL org.opencontainers.image.title="Stirling-PDF" \
|
||||
org.opencontainers.image.description="Full version with Calibre, LibreOffice, Tesseract, OCRmyPDF" \
|
||||
org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
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
|
||||
|
||||
STOPSIGNAL SIGTERM
|
||||
|
||||
# Use tini as init (handles signals and zombies correctly)
|
||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
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 []
|
||||
|
||||
+558
-176
@@ -1,158 +1,563 @@
|
||||
# Stirling-PDF Dockerfile - Fat version with embedded frontend
|
||||
# Single JAR contains both frontend and backend with extra fonts for air-gapped environments
|
||||
# Stirling-PDF - Fat version (embedded frontend)
|
||||
# Extra fonts for air-gapped environments
|
||||
|
||||
# Stage 1: Build application with embedded frontend
|
||||
FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build
|
||||
FROM ubuntu:noble AS calibre-build
|
||||
|
||||
# Install Node.js and npm for frontend build
|
||||
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/*
|
||||
ARG CALIBRE_VERSION=9.3.1
|
||||
|
||||
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; \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends -t ${VERSION_CODENAME}-backports \
|
||||
libreoffice libreoffice-java-common; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
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/*; \
|
||||
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
|
||||
|
||||
# ==============================================================================
|
||||
# Create non-root user (stirlingpdfuser) with configurable UID/GID
|
||||
# ==============================================================================
|
||||
# Build the Java application and frontend.
|
||||
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 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; \
|
||||
groupadd -g "${PGID}" stirlingpdfgroup 2>/dev/null \
|
||||
|| groupadd 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; \
|
||||
useradd -m -u "${PUID}" -g stirlingpdfgroup \
|
||||
-d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser 2>/dev/null \
|
||||
|| useradd -m -g stirlingpdfgroup \
|
||||
-d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \
|
||||
fi; \
|
||||
fi
|
||||
ln -sf /usr/sbin/gosu /usr/local/bin/su-exec
|
||||
|
||||
# Compatibility alias for older entrypoint scripts expecting su-exec
|
||||
RUN ln -sf /usr/sbin/gosu /usr/local/bin/su-exec
|
||||
# Application files.
|
||||
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
|
||||
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/
|
||||
# Fonts go to system dir — root ownership is correct (world-readable)
|
||||
COPY --link 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
|
||||
|
||||
# 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 \
|
||||
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="" \
|
||||
HOME=/home/stirlingpdfuser \
|
||||
PUID=${PUID} \
|
||||
@@ -160,57 +565,34 @@ ENV VERSION_TAG=$VERSION_TAG \
|
||||
UMASK=022 \
|
||||
FAT_DOCKER=true \
|
||||
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
||||
PATH="/opt/venv/bin:${PATH}" \
|
||||
UNO_PATH=/usr/lib/libreoffice/program \
|
||||
LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin \
|
||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||
TMPDIR=/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
|
||||
|
||||
# ==============================================================================
|
||||
# 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__)"
|
||||
# Metadata labels.
|
||||
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" \
|
||||
org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
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
|
||||
|
||||
STOPSIGNAL SIGTERM
|
||||
|
||||
# Use tini as init (handles signals and zombies correctly)
|
||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
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 []
|
||||
|
||||
@@ -2,40 +2,50 @@
|
||||
# Single JAR contains both frontend and backend with minimal dependencies
|
||||
|
||||
# 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
|
||||
RUN apt-get update && apt-get install -y \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
&& 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 \
|
||||
&& 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 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 . .
|
||||
|
||||
# Build ultra-lite JAR with embedded frontend (minimal features)
|
||||
RUN DISABLE_ADDITIONAL_FEATURES=true \
|
||||
./gradlew clean build -PbuildWithFrontend=true -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
|
||||
DISABLE_ADDITIONAL_FEATURES=true \
|
||||
./gradlew clean build \
|
||||
-PbuildWithFrontend=true \
|
||||
-x spotlessApply -x spotlessCheck -x test -x sonarqube \
|
||||
--no-daemon
|
||||
|
||||
# Stage 2: Runtime image
|
||||
FROM alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
|
||||
FROM eclipse-temurin:25-jre-alpine
|
||||
|
||||
ENV LANG=C.UTF-8 \
|
||||
LC_ALL=C.UTF-8
|
||||
@@ -43,30 +53,37 @@ ENV LANG=C.UTF-8 \
|
||||
ARG VERSION_TAG
|
||||
|
||||
# Labels
|
||||
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"
|
||||
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, ultra-lite, API, Spring Boot, React"
|
||||
LABEL org.opencontainers.image.title="Stirling-PDF Ultra-Lite" \
|
||||
org.opencontainers.image.description="Stirling-PDF with embedded frontend - Ultra-lite version with minimal dependencies" \
|
||||
org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
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, ultra-lite, API, Spring Boot, React"
|
||||
|
||||
# Copy scripts
|
||||
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||
COPY scripts/installFonts.sh /scripts/installFonts.sh
|
||||
COPY scripts/stirling-diagnostics.sh /scripts/stirling-diagnostics.sh
|
||||
|
||||
# Copy built JAR from build stage
|
||||
COPY --from=build /app/app/core/build/libs/*.jar /app.jar
|
||||
COPY --from=build /app/build/libs/restart-helper.jar /restart-helper.jar
|
||||
# Copy built JARs from build stage
|
||||
# Use numeric UID:GID (1000:1000) since the named user doesn't exist yet at COPY time
|
||||
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
|
||||
# NOTE: Memory flags (InitialRAMPercentage, MaxRAMPercentage, MaxMetaspaceSize)
|
||||
# are computed dynamically by init-without-ocr.sh based on container memory limits.
|
||||
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="" \
|
||||
HOME=/home/stirlingpdfuser \
|
||||
PUID=1000 \
|
||||
@@ -79,6 +96,7 @@ ENV VERSION_TAG=$VERSION_TAG \
|
||||
ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||
|
||||
# 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 && \
|
||||
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 && \
|
||||
@@ -90,18 +108,16 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
||||
bash \
|
||||
curl \
|
||||
shadow \
|
||||
su-exec \
|
||||
openjdk21-jre && \
|
||||
mkdir -p $HOME /configs /configs/heap_dumps /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \
|
||||
su-exec && \
|
||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /tmp/stirling-pdf/heap_dumps && \
|
||||
mkdir -p /usr/share/fonts/opentype/noto && \
|
||||
chmod +x /scripts/*.sh && \
|
||||
# User permissions
|
||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar /restart-helper.jar
|
||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline /tmp/stirling-pdf
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
|
||||
# Set user and run command
|
||||
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:
|
||||
DISABLE_ADDITIONAL_FEATURES: "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
|
||||
PGID: 1002
|
||||
UMASK: "022"
|
||||
|
||||
@@ -20,6 +20,8 @@ services:
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "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
|
||||
PGID: 1002
|
||||
UMASK: "022"
|
||||
|
||||
@@ -33,6 +33,8 @@ services:
|
||||
PROCESS_EXECUTOR_UNO_SERVER_ENDPOINTS_1_PROTOCOL: "http"
|
||||
# Session limit should match endpoint count for optimal concurrency
|
||||
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
|
||||
PGID: 1002
|
||||
UMASK: "022"
|
||||
|
||||
@@ -21,6 +21,8 @@ services:
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
PROCESS_EXECUTOR_AUTO_UNO_SERVER: "true"
|
||||
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
|
||||
PGID: 1002
|
||||
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