centralise temp-file handling and make path configurable

This commit is contained in:
Anthony Stirling 2025-06-24 23:50:35 +01:00
parent 13fb7bdf66
commit 3eda85d00e
26 changed files with 434 additions and 204 deletions

View File

@ -33,7 +33,11 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
UNO_PATH=/usr/lib/libreoffice/program \ UNO_PATH=/usr/lib/libreoffice/program \
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
PATH=$PATH:/opt/venv/bin PATH=$PATH:/opt/venv/bin \
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
TMPDIR=/tmp/stirling-pdf \
TEMP=/tmp/stirling-pdf \
TMP=/tmp/stirling-pdf
# JDK for app # JDK for app
@ -78,17 +82,17 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \ ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
mv /usr/share/tessdata /usr/share/tessdata-original && \ mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \ mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \
fc-cache -f -v && \ fc-cache -f -v && \
chmod +x /scripts/* && \ chmod +x /scripts/* && \
chmod +x /scripts/init.sh && \ chmod +x /scripts/init.sh && \
# User permissions # User permissions
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \ chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar chown stirlingpdfuser:stirlingpdfgroup /app.jar
EXPOSE 8080/tcp EXPOSE 8080/tcp
# Set user and run command # Set user and run command
ENTRYPOINT ["tini", "--", "/scripts/init.sh"] ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]

View File

@ -27,7 +27,11 @@ RUN apt-get update && apt-get install -y \
&& apt-get clean && rm -rf /var/lib/apt/lists/* && apt-get clean && rm -rf /var/lib/apt/lists/*
# Setze die Environment Variable für setuptools # Setze die Environment Variable für setuptools
ENV SETUPTOOLS_USE_DISTUTILS=local ENV SETUPTOOLS_USE_DISTUTILS=local \
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
TMPDIR=/tmp/stirling-pdf \
TEMP=/tmp/stirling-pdf \
TMP=/tmp/stirling-pdf
# Installation der benötigten Python-Pakete # Installation der benötigten Python-Pakete
RUN python3 -m venv --system-site-packages /opt/venv \ RUN python3 -m venv --system-site-packages /opt/venv \
@ -40,8 +44,9 @@ ENV PATH="/opt/venv/bin:$PATH"
COPY . /workspace COPY . /workspace
RUN adduser --disabled-password --gecos '' devuser \ RUN mkdir -p /tmp/stirling-pdf \
&& chown -R devuser:devuser /home/devuser /workspace && adduser --disabled-password --gecos '' devuser \
&& chown -R devuser:devuser /home/devuser /workspace /tmp/stirling-pdf
RUN echo "devuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devuser \ RUN echo "devuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devuser \
&& chmod 0440 /etc/sudoers.d/devuser && chmod 0440 /etc/sudoers.d/devuser

View File

@ -46,7 +46,11 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
UNO_PATH=/usr/lib/libreoffice/program \ UNO_PATH=/usr/lib/libreoffice/program \
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
PATH=$PATH:/opt/venv/bin PATH=$PATH:/opt/venv/bin \
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
TMPDIR=/tmp/stirling-pdf \
TEMP=/tmp/stirling-pdf \
TMP=/tmp/stirling-pdf
# JDK for app # JDK for app
@ -92,16 +96,16 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \ ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
mv /usr/share/tessdata /usr/share/tessdata-original && \ mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \ mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \
fc-cache -f -v && \ fc-cache -f -v && \
chmod +x /scripts/* && \ chmod +x /scripts/* && \
chmod +x /scripts/init.sh && \ chmod +x /scripts/init.sh && \
# User permissions # User permissions
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \ chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar chown stirlingpdfuser:stirlingpdfgroup /app.jar
EXPOSE 8080/tcp EXPOSE 8080/tcp
# Set user and run command # Set user and run command
ENTRYPOINT ["tini", "--", "/scripts/init.sh"] ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]

View File

@ -11,7 +11,11 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \
JAVA_CUSTOM_OPTS="" \ JAVA_CUSTOM_OPTS="" \
PUID=1000 \ PUID=1000 \
PGID=1000 \ PGID=1000 \
UMASK=022 UMASK=022 \
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
TMPDIR=/tmp/stirling-pdf \
TEMP=/tmp/stirling-pdf \
TMP=/tmp/stirling-pdf
# Copy necessary files # Copy necessary files
COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
@ -35,10 +39,10 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
su-exec \ su-exec \
openjdk21-jre && \ openjdk21-jre && \
# User permissions # User permissions
mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto && \ mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto /tmp/stirling-pdf && \
chmod +x /scripts/*.sh && \ chmod +x /scripts/*.sh && \
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \ chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline /tmp/stirling-pdf && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar chown stirlingpdfuser:stirlingpdfgroup /app.jar
# Set environment variables # Set environment variables
@ -48,4 +52,4 @@ EXPOSE 8080/tcp
# Run the application # Run the application
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"] ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"] CMD ["java", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=/tmp/stirling-pdf", "-jar", "/app.jar"]

View File

@ -3,16 +3,15 @@ package stirling.software.common.config;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.TempFileRegistry; import stirling.software.common.util.TempFileRegistry;
/** /**
@ -21,17 +20,10 @@ import stirling.software.common.util.TempFileRegistry;
*/ */
@Slf4j @Slf4j
@Configuration @Configuration
@RequiredArgsConstructor
public class TempFileConfiguration { public class TempFileConfiguration {
@Value("${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf}") private final ApplicationProperties applicationProperties;
private String customTempDirectory;
@Autowired
@Qualifier("machineType")
private String machineType;
@Value("${stirling.tempfiles.prefix:stirling-pdf-}")
private String tempFilePrefix;
/** /**
* Create the TempFileRegistry bean. * Create the TempFileRegistry bean.
@ -46,6 +38,10 @@ public class TempFileConfiguration {
@PostConstruct @PostConstruct
public void initTempFileConfig() { public void initTempFileConfig() {
try { try {
ApplicationProperties.TempFileManagement tempFiles =
applicationProperties.getSystem().getTempFileManagement();
String customTempDirectory = tempFiles.getBaseTmpDir();
// Create the temp directory if it doesn't exist // Create the temp directory if it doesn't exist
Path tempDir = Path.of(customTempDirectory); Path tempDir = Path.of(customTempDirectory);
if (!Files.exists(tempDir)) { if (!Files.exists(tempDir)) {
@ -55,7 +51,7 @@ public class TempFileConfiguration {
log.info("Temporary file configuration initialized"); log.info("Temporary file configuration initialized");
log.info("Using temp directory: {}", customTempDirectory); log.info("Using temp directory: {}", customTempDirectory);
log.info("Temp file prefix: {}", tempFilePrefix); log.info("Temp file prefix: {}", tempFiles.getPrefix());
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to initialize temporary file configuration", e); log.error("Failed to initialize temporary file configuration", e);
} }

View File

@ -292,6 +292,7 @@ public class ApplicationProperties {
private Boolean enableUrlToPDF; private Boolean enableUrlToPDF;
private CustomPaths customPaths = new CustomPaths(); private CustomPaths customPaths = new CustomPaths();
private String fileUploadLimit; private String fileUploadLimit;
private TempFileManagement tempFileManagement = new TempFileManagement();
public boolean isAnalyticsEnabled() { public boolean isAnalyticsEnabled() {
return this.getEnableAnalytics() != null && this.getEnableAnalytics(); return this.getEnableAnalytics() != null && this.getEnableAnalytics();
@ -317,6 +318,30 @@ public class ApplicationProperties {
} }
} }
@Data
public static class TempFileManagement {
private String baseTmpDir = "";
private String libreofficeDir = "";
private String systemTempDir = "";
private String prefix = "stirling-pdf-";
private long maxAgeHours = 24;
private long cleanupIntervalMinutes = 30;
private boolean startupCleanup = true;
private boolean cleanupSystemTemp = false;
public String getBaseTmpDir() {
return baseTmpDir != null && !baseTmpDir.isEmpty()
? baseTmpDir
: java.lang.System.getProperty("java.io.tmpdir") + "/stirling-pdf";
}
public String getLibreofficeDir() {
return libreofficeDir != null && !libreofficeDir.isEmpty()
? libreofficeDir
: getBaseTmpDir() + "/libreoffice";
}
}
@Data @Data
public static class Datasource { public static class Datasource {
private boolean enableCustomDatabase; private boolean enableCustomDatabase;

View File

@ -13,12 +13,15 @@ import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager; import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.TempFileRegistry; import stirling.software.common.util.TempFileRegistry;
@ -29,30 +32,17 @@ import stirling.software.common.util.TempFileRegistry;
*/ */
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor
public class TempFileCleanupService { public class TempFileCleanupService {
private final TempFileRegistry registry; private final TempFileRegistry registry;
private final TempFileManager tempFileManager; private final TempFileManager tempFileManager;
private final ApplicationProperties applicationProperties;
@Value("${stirling.tempfiles.cleanup-interval-minutes:30}")
private long cleanupIntervalMinutes;
@Value("${stirling.tempfiles.startup-cleanup:true}")
private boolean performStartupCleanup;
@Autowired @Autowired
@Qualifier("machineType") @Qualifier("machineType")
private String machineType; private String machineType;
@Value("${stirling.tempfiles.system-temp-dir:/tmp}")
private String systemTempDir;
@Value("${stirling.tempfiles.directory:/tmp/stirling-pdf}")
private String customTempDirectory;
@Value("${stirling.tempfiles.libreoffice-dir:/tmp/stirling-pdf/libreoffice}")
private String libreOfficeTempDir;
// Maximum recursion depth for directory traversal // Maximum recursion depth for directory traversal
private static final int MAX_RECURSION_DEPTH = 5; private static final int MAX_RECURSION_DEPTH = 5;
@ -84,18 +74,18 @@ public class TempFileCleanupService {
|| fileName.startsWith("jetty-") || fileName.startsWith("jetty-")
|| fileName.equals("proc") || fileName.equals("proc")
|| fileName.equals("sys") || fileName.equals("sys")
|| fileName.equals("dev"); || fileName.equals("dev")
|| fileName.equals("hsperfdata_stirlingpdfuser")
@Autowired || fileName.startsWith("hsperfdata_")
public TempFileCleanupService(TempFileRegistry registry, TempFileManager tempFileManager) { || fileName.equals(".pdfbox.cache");
this.registry = registry;
this.tempFileManager = tempFileManager;
@PostConstruct
public void init() {
// Create necessary directories // Create necessary directories
ensureDirectoriesExist(); ensureDirectoriesExist();
// Perform startup cleanup if enabled // Perform startup cleanup if enabled
if (performStartupCleanup) { if (applicationProperties.getSystem().getTempFileManagement().isStartupCleanup()) {
runStartupCleanup(); runStartupCleanup();
} }
} }
@ -103,7 +93,11 @@ public class TempFileCleanupService {
/** Ensure that all required temp directories exist */ /** Ensure that all required temp directories exist */
private void ensureDirectoriesExist() { private void ensureDirectoriesExist() {
try { try {
// Create the main temp directory if specified ApplicationProperties.TempFileManagement tempFiles =
applicationProperties.getSystem().getTempFileManagement();
// Create the main temp directory
String customTempDirectory = tempFiles.getBaseTmpDir();
if (customTempDirectory != null && !customTempDirectory.isEmpty()) { if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
Path tempDir = Path.of(customTempDirectory); Path tempDir = Path.of(customTempDirectory);
if (!Files.exists(tempDir)) { if (!Files.exists(tempDir)) {
@ -112,7 +106,8 @@ public class TempFileCleanupService {
} }
} }
// Create LibreOffice temp directory if specified // Create LibreOffice temp directory
String libreOfficeTempDir = tempFiles.getLibreofficeDir();
if (libreOfficeTempDir != null && !libreOfficeTempDir.isEmpty()) { if (libreOfficeTempDir != null && !libreOfficeTempDir.isEmpty()) {
Path loTempDir = Path.of(libreOfficeTempDir); Path loTempDir = Path.of(libreOfficeTempDir);
if (!Files.exists(loTempDir)) { if (!Files.exists(loTempDir)) {
@ -127,7 +122,8 @@ public class TempFileCleanupService {
/** Scheduled task to clean up old temporary files. Runs at the configured interval. */ /** Scheduled task to clean up old temporary files. Runs at the configured interval. */
@Scheduled( @Scheduled(
fixedDelayString = "${stirling.tempfiles.cleanup-interval-minutes:60}", fixedDelayString =
"#{applicationProperties.system.tempFileManagement.cleanupIntervalMinutes}",
timeUnit = TimeUnit.MINUTES) timeUnit = TimeUnit.MINUTES)
public void scheduledCleanup() { public void scheduledCleanup() {
log.info("Running scheduled temporary file cleanup"); log.info("Running scheduled temporary file cleanup");
@ -151,6 +147,9 @@ public class TempFileCleanupService {
} }
} }
// Clean up PDFBox cache file
cleanupPDFBoxCache();
// Clean up unregistered temp files based on our cleanup strategy // Clean up unregistered temp files based on our cleanup strategy
boolean containerMode = isContainerMode(); boolean containerMode = isContainerMode();
int unregisteredDeletedCount = cleanupUnregisteredFiles(containerMode, true, maxAgeMillis); int unregisteredDeletedCount = cleanupUnregisteredFiles(containerMode, true, maxAgeMillis);
@ -198,11 +197,26 @@ public class TempFileCleanupService {
AtomicInteger totalDeletedCount = new AtomicInteger(0); AtomicInteger totalDeletedCount = new AtomicInteger(0);
try { try {
// Get all directories we need to clean ApplicationProperties.TempFileManagement tempFiles =
Path systemTempPath = getSystemTempPath(); applicationProperties.getSystem().getTempFileManagement();
Path[] dirsToScan = { Path[] dirsToScan;
systemTempPath, Path.of(customTempDirectory), Path.of(libreOfficeTempDir) if (tempFiles.isCleanupSystemTemp()
}; && tempFiles.getSystemTempDir() != null
&& !tempFiles.getSystemTempDir().isEmpty()) {
Path systemTempPath = getSystemTempPath();
dirsToScan =
new Path[] {
systemTempPath,
Path.of(tempFiles.getBaseTmpDir()),
Path.of(tempFiles.getLibreofficeDir())
};
} else {
dirsToScan =
new Path[] {
Path.of(tempFiles.getBaseTmpDir()),
Path.of(tempFiles.getLibreofficeDir())
};
}
// Process each directory // Process each directory
Arrays.stream(dirsToScan) Arrays.stream(dirsToScan)
@ -254,6 +268,8 @@ public class TempFileCleanupService {
/** Get the system temp directory path based on configuration or system property. */ /** Get the system temp directory path based on configuration or system property. */
private Path getSystemTempPath() { private Path getSystemTempPath() {
String systemTempDir =
applicationProperties.getSystem().getTempFileManagement().getSystemTempDir();
if (systemTempDir != null && !systemTempDir.isEmpty()) { if (systemTempDir != null && !systemTempDir.isEmpty()) {
return Path.of(systemTempDir); return Path.of(systemTempDir);
} else { } else {
@ -412,4 +428,22 @@ public class TempFileCleanupService {
log.warn("Failed to clean up LibreOffice temp files", e); log.warn("Failed to clean up LibreOffice temp files", e);
} }
} }
/**
* Clean up PDFBox cache file from user home directory. This cache file can grow large and
* should be periodically cleaned.
*/
private void cleanupPDFBoxCache() {
try {
Path userHome = Path.of(System.getProperty("user.home"));
Path pdfboxCache = userHome.resolve(".pdfbox.cache");
if (Files.exists(pdfboxCache)) {
Files.deleteIfExists(pdfboxCache);
log.debug("Cleaned up PDFBox cache file: {}", pdfboxCache);
}
} catch (IOException e) {
log.warn("Failed to clean up PDFBox cache file", e);
}
}
} }

View File

@ -131,7 +131,8 @@ public class EmlToPdf {
byte[] emlBytes, byte[] emlBytes,
String fileName, String fileName,
boolean disableSanitize, boolean disableSanitize,
stirling.software.common.service.CustomPDFDocumentFactory pdfDocumentFactory) stirling.software.common.service.CustomPDFDocumentFactory pdfDocumentFactory,
TempFileManager tempFileManager)
throws IOException, InterruptedException { throws IOException, InterruptedException {
validateEmlInput(emlBytes); validateEmlInput(emlBytes);
@ -150,7 +151,8 @@ public class EmlToPdf {
// Convert HTML to PDF // Convert HTML to PDF
byte[] pdfBytes = byte[] pdfBytes =
convertHtmlToPdf(weasyprintPath, request, htmlContent, disableSanitize); convertHtmlToPdf(
weasyprintPath, request, htmlContent, disableSanitize, tempFileManager);
// Attach files if available and requested // Attach files if available and requested
if (shouldAttachFiles(emailContent, request)) { if (shouldAttachFiles(emailContent, request)) {
@ -191,7 +193,8 @@ public class EmlToPdf {
String weasyprintPath, String weasyprintPath,
EmlToPdfRequest request, EmlToPdfRequest request,
String htmlContent, String htmlContent,
boolean disableSanitize) boolean disableSanitize,
TempFileManager tempFileManager)
throws IOException, InterruptedException { throws IOException, InterruptedException {
stirling.software.common.model.api.converters.HTMLToPdfRequest htmlRequest = stirling.software.common.model.api.converters.HTMLToPdfRequest htmlRequest =
@ -203,7 +206,8 @@ public class EmlToPdf {
htmlRequest, htmlRequest,
htmlContent.getBytes(StandardCharsets.UTF_8), htmlContent.getBytes(StandardCharsets.UTF_8),
"email.html", "email.html",
disableSanitize); disableSanitize,
tempFileManager);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
log.warn("Initial HTML to PDF conversion failed, trying with simplified HTML"); log.warn("Initial HTML to PDF conversion failed, trying with simplified HTML");
String simplifiedHtml = simplifyHtmlContent(htmlContent); String simplifiedHtml = simplifyHtmlContent(htmlContent);
@ -212,7 +216,8 @@ public class EmlToPdf {
htmlRequest, htmlRequest,
simplifiedHtml.getBytes(StandardCharsets.UTF_8), simplifiedHtml.getBytes(StandardCharsets.UTF_8),
"email.html", "email.html",
disableSanitize); disableSanitize,
tempFileManager);
} }
} }

View File

@ -26,88 +26,92 @@ public class FileToPdf {
HTMLToPdfRequest request, HTMLToPdfRequest request,
byte[] fileBytes, byte[] fileBytes,
String fileName, String fileName,
boolean disableSanitize) boolean disableSanitize,
TempFileManager tempFileManager)
throws IOException, InterruptedException { throws IOException, InterruptedException {
Path tempOutputFile = Files.createTempFile("output_", ".pdf"); try (TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) {
Path tempInputFile = null; try (TempFile tempInputFile =
byte[] pdfBytes; new TempFile(tempFileManager, fileName.endsWith(".html") ? ".html" : ".zip")) {
try {
if (fileName.endsWith(".html")) {
tempInputFile = Files.createTempFile("input_", ".html");
String sanitizedHtml =
sanitizeHtmlContent(
new String(fileBytes, StandardCharsets.UTF_8), disableSanitize);
Files.write(tempInputFile, sanitizedHtml.getBytes(StandardCharsets.UTF_8));
} else if (fileName.endsWith(".zip")) {
tempInputFile = Files.createTempFile("input_", ".zip");
Files.write(tempInputFile, fileBytes);
sanitizeHtmlFilesInZip(tempInputFile, disableSanitize);
} else {
throw new IllegalArgumentException("Unsupported file format: " + fileName);
}
List<String> command = new ArrayList<>(); if (fileName.endsWith(".html")) {
command.add(weasyprintPath); String sanitizedHtml =
command.add("-e"); sanitizeHtmlContent(
command.add("utf-8"); new String(fileBytes, StandardCharsets.UTF_8), disableSanitize);
command.add("-v"); Files.write(
command.add("--pdf-forms"); tempInputFile.getPath(),
command.add(tempInputFile.toString()); sanitizedHtml.getBytes(StandardCharsets.UTF_8));
command.add(tempOutputFile.toString()); } else if (fileName.endsWith(".zip")) {
Files.write(tempInputFile.getPath(), fileBytes);
sanitizeHtmlFilesInZip(
tempInputFile.getPath(), disableSanitize, tempFileManager);
} else {
throw new IllegalArgumentException("Unsupported file format: " + fileName);
}
ProcessExecutorResult returnCode = List<String> command = new ArrayList<>();
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) command.add(weasyprintPath);
.runCommandWithOutputHandling(command); command.add("-e");
command.add("utf-8");
command.add("-v");
command.add("--pdf-forms");
command.add(tempInputFile.getAbsolutePath());
command.add(tempOutputFile.getAbsolutePath());
pdfBytes = Files.readAllBytes(tempOutputFile); ProcessExecutorResult returnCode =
} catch (IOException e) { ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
pdfBytes = Files.readAllBytes(tempOutputFile); .runCommandWithOutputHandling(command);
if (pdfBytes.length < 1) {
throw e;
}
} finally {
Files.deleteIfExists(tempOutputFile);
Files.deleteIfExists(tempInputFile);
}
return pdfBytes; byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath());
try {
return pdfBytes;
} catch (Exception e) {
pdfBytes = Files.readAllBytes(tempOutputFile.getPath());
if (pdfBytes.length < 1) {
throw e;
}
return pdfBytes;
}
} // tempInputFile auto-closed
} // tempOutputFile auto-closed
} }
private static String sanitizeHtmlContent(String htmlContent, boolean disableSanitize) { private static String sanitizeHtmlContent(String htmlContent, boolean disableSanitize) {
return (!disableSanitize) ? CustomHtmlSanitizer.sanitize(htmlContent) : htmlContent; return (!disableSanitize) ? CustomHtmlSanitizer.sanitize(htmlContent) : htmlContent;
} }
private static void sanitizeHtmlFilesInZip(Path zipFilePath, boolean disableSanitize) private static void sanitizeHtmlFilesInZip(
Path zipFilePath, boolean disableSanitize, TempFileManager tempFileManager)
throws IOException { throws IOException {
Path tempUnzippedDir = Files.createTempDirectory("unzipped_"); try (TempDirectory tempUnzippedDir = new TempDirectory(tempFileManager)) {
try (ZipInputStream zipIn = try (ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream( ZipSecurity.createHardenedInputStream(
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) { new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
ZipEntry entry = zipIn.getNextEntry(); ZipEntry entry = zipIn.getNextEntry();
while (entry != null) { while (entry != null) {
Path filePath = tempUnzippedDir.resolve(sanitizeZipFilename(entry.getName())); Path filePath =
if (!entry.isDirectory()) { tempUnzippedDir.getPath().resolve(sanitizeZipFilename(entry.getName()));
Files.createDirectories(filePath.getParent()); if (!entry.isDirectory()) {
if (entry.getName().toLowerCase().endsWith(".html") Files.createDirectories(filePath.getParent());
|| entry.getName().toLowerCase().endsWith(".htm")) { if (entry.getName().toLowerCase().endsWith(".html")
String content = new String(zipIn.readAllBytes(), StandardCharsets.UTF_8); || entry.getName().toLowerCase().endsWith(".htm")) {
String sanitizedContent = sanitizeHtmlContent(content, disableSanitize); String content =
Files.write(filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8)); new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
} else { String sanitizedContent = sanitizeHtmlContent(content, disableSanitize);
Files.copy(zipIn, filePath); Files.write(
filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8));
} else {
Files.copy(zipIn, filePath);
}
} }
zipIn.closeEntry();
entry = zipIn.getNextEntry();
} }
zipIn.closeEntry();
entry = zipIn.getNextEntry();
} }
}
// Repack the sanitized files // Repack the sanitized files
zipDirectory(tempUnzippedDir, zipFilePath); zipDirectory(tempUnzippedDir.getPath(), zipFilePath);
} // tempUnzippedDir auto-cleaned
// Clean up
deleteDirectory(tempUnzippedDir);
} }
private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException { private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException {

View File

@ -34,7 +34,27 @@ import stirling.software.common.configuration.InstallationPathConfig;
public class GeneralUtils { public class GeneralUtils {
public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException { public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
File tempFile = Files.createTempFile("temp", null).toFile(); String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
if (customTempDir == null || customTempDir.isEmpty()) {
customTempDir = System.getProperty("stirling.tempfiles.directory");
}
File tempFile;
if (customTempDir != null && !customTempDir.isEmpty()) {
Path tempDir = Path.of(customTempDir);
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
}
tempFile = Files.createTempFile(tempDir, "stirling-pdf-", null).toFile();
} else {
Path tempDir = Path.of(System.getProperty("java.io.tmpdir"), "stirling-pdf");
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
}
tempFile = Files.createTempFile(tempDir, "stirling-pdf-", null).toFile();
}
try (InputStream inputStream = multipartFile.getInputStream(); try (InputStream inputStream = multipartFile.getInputStream();
FileOutputStream outputStream = new FileOutputStream(tempFile)) { FileOutputStream outputStream = new FileOutputStream(tempFile)) {

View File

@ -0,0 +1,44 @@
package stirling.software.common.util;
import java.io.IOException;
import java.nio.file.Path;
import lombok.extern.slf4j.Slf4j;
/**
* A wrapper class for a temporary directory that implements AutoCloseable. Can be used with
* try-with-resources for automatic cleanup.
*/
@Slf4j
public class TempDirectory implements AutoCloseable {
private final TempFileManager manager;
private final Path directory;
public TempDirectory(TempFileManager manager) throws IOException {
this.manager = manager;
this.directory = manager.createTempDirectory();
}
public Path getPath() {
return directory;
}
public String getAbsolutePath() {
return directory.toAbsolutePath().toString();
}
public boolean exists() {
return java.nio.file.Files.exists(directory);
}
@Override
public void close() {
manager.deleteTempDirectory(directory);
}
@Override
public String toString() {
return "TempDirectory{" + directory.toAbsolutePath() + "}";
}
}

View File

@ -0,0 +1,49 @@
package stirling.software.common.util;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import lombok.extern.slf4j.Slf4j;
/**
* A wrapper class for a temporary file that implements AutoCloseable. Can be used with
* try-with-resources for automatic cleanup.
*/
@Slf4j
public class TempFile implements AutoCloseable {
private final TempFileManager manager;
private final File file;
public TempFile(TempFileManager manager, String suffix) throws IOException {
this.manager = manager;
this.file = manager.createTempFile(suffix);
}
public File getFile() {
return file;
}
public Path getPath() {
return file.toPath();
}
public String getAbsolutePath() {
return file.getAbsolutePath();
}
public boolean exists() {
return file.exists();
}
@Override
public void close() {
manager.deleteTempFile(file);
}
@Override
public String toString() {
return "TempFile{" + file.getAbsolutePath() + "}";
}
}

View File

@ -8,39 +8,25 @@ import java.time.Duration;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
/** /**
* Service for managing temporary files in Stirling-PDF. Provides methods for creating, tracking, * Service for managing temporary files in Stirling-PDF. Provides methods for creating, tracking,
* and cleaning up temporary files. * and cleaning up temporary files.
*/ */
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor
public class TempFileManager { public class TempFileManager {
private final TempFileRegistry registry; private final TempFileRegistry registry;
private final ApplicationProperties applicationProperties;
@Value("${stirling.tempfiles.prefix:stirling-pdf-}")
private String tempFilePrefix;
@Value("${stirling.tempfiles.directory:}")
private String customTempDirectory;
@Value("${stirling.tempfiles.libreoffice-dir:}")
private String libreOfficeTempDir;
@Value("${stirling.tempfiles.max-age-hours:24}")
private long maxAgeHours;
@Autowired
public TempFileManager(TempFileRegistry registry) {
this.registry = registry;
}
/** /**
* Create a temporary file with the Stirling-PDF prefix. The file is automatically registered * Create a temporary file with the Stirling-PDF prefix. The file is automatically registered
@ -51,15 +37,18 @@ public class TempFileManager {
* @throws IOException If an I/O error occurs * @throws IOException If an I/O error occurs
*/ */
public File createTempFile(String suffix) throws IOException { public File createTempFile(String suffix) throws IOException {
ApplicationProperties.TempFileManagement tempFiles =
applicationProperties.getSystem().getTempFileManagement();
Path tempFilePath; Path tempFilePath;
String customTempDirectory = tempFiles.getBaseTmpDir();
if (customTempDirectory != null && !customTempDirectory.isEmpty()) { if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
Path tempDir = Path.of(customTempDirectory); Path tempDir = Path.of(customTempDirectory);
if (!Files.exists(tempDir)) { if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir); Files.createDirectories(tempDir);
} }
tempFilePath = Files.createTempFile(tempDir, tempFilePrefix, suffix); tempFilePath = Files.createTempFile(tempDir, tempFiles.getPrefix(), suffix);
} else { } else {
tempFilePath = Files.createTempFile(tempFilePrefix, suffix); tempFilePath = Files.createTempFile(tempFiles.getPrefix(), suffix);
} }
File tempFile = tempFilePath.toFile(); File tempFile = tempFilePath.toFile();
return registry.register(tempFile); return registry.register(tempFile);
@ -73,15 +62,18 @@ public class TempFileManager {
* @throws IOException If an I/O error occurs * @throws IOException If an I/O error occurs
*/ */
public Path createTempDirectory() throws IOException { public Path createTempDirectory() throws IOException {
ApplicationProperties.TempFileManagement tempFiles =
applicationProperties.getSystem().getTempFileManagement();
Path tempDirPath; Path tempDirPath;
String customTempDirectory = tempFiles.getBaseTmpDir();
if (customTempDirectory != null && !customTempDirectory.isEmpty()) { if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
Path tempDir = Path.of(customTempDirectory); Path tempDir = Path.of(customTempDirectory);
if (!Files.exists(tempDir)) { if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir); Files.createDirectories(tempDir);
} }
tempDirPath = Files.createTempDirectory(tempDir, tempFilePrefix); tempDirPath = Files.createTempDirectory(tempDir, tempFiles.getPrefix());
} else { } else {
tempDirPath = Files.createTempDirectory(tempFilePrefix); tempDirPath = Files.createTempDirectory(tempFiles.getPrefix());
} }
return registry.registerDirectory(tempDirPath); return registry.registerDirectory(tempDirPath);
} }
@ -202,6 +194,8 @@ public class TempFileManager {
* @return Maximum age in milliseconds * @return Maximum age in milliseconds
*/ */
public long getMaxAgeMillis() { public long getMaxAgeMillis() {
long maxAgeHours =
applicationProperties.getSystem().getTempFileManagement().getMaxAgeHours();
return Duration.ofHours(maxAgeHours).toMillis(); return Duration.ofHours(maxAgeHours).toMillis();
} }
@ -213,6 +207,8 @@ public class TempFileManager {
* @return A unique temporary file name * @return A unique temporary file name
*/ */
public String generateTempFileName(String type, String extension) { public String generateTempFileName(String type, String extension) {
String tempFilePrefix =
applicationProperties.getSystem().getTempFileManagement().getPrefix();
String uuid = UUID.randomUUID().toString().substring(0, 8); String uuid = UUID.randomUUID().toString().substring(0, 8);
return tempFilePrefix + type + "-" + uuid + "." + extension; return tempFilePrefix + type + "-" + uuid + "." + extension;
} }
@ -225,7 +221,11 @@ public class TempFileManager {
* @throws IOException If directory creation fails * @throws IOException If directory creation fails
*/ */
public Path registerLibreOfficeTempDir() throws IOException { public Path registerLibreOfficeTempDir() throws IOException {
ApplicationProperties.TempFileManagement tempFiles =
applicationProperties.getSystem().getTempFileManagement();
Path loTempDir; Path loTempDir;
String libreOfficeTempDir = tempFiles.getLibreofficeDir();
String customTempDirectory = tempFiles.getBaseTmpDir();
// First check if explicitly configured // First check if explicitly configured
if (libreOfficeTempDir != null && !libreOfficeTempDir.isEmpty()) { if (libreOfficeTempDir != null && !libreOfficeTempDir.isEmpty()) {

View File

@ -17,33 +17,6 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class TempFileUtil { public class TempFileUtil {
/**
* A wrapper class for a temporary file that implements AutoCloseable. Can be used with
* try-with-resources for automatic cleanup.
*/
public static class TempFile implements AutoCloseable {
private final TempFileManager manager;
private final File file;
public TempFile(TempFileManager manager, String suffix) throws IOException {
this.manager = manager;
this.file = manager.createTempFile(suffix);
}
public File getFile() {
return file;
}
public Path getPath() {
return file.toPath();
}
@Override
public void close() {
manager.deleteTempFile(file);
}
}
/** /**
* A collection of temporary files that implements AutoCloseable. All files in the collection * A collection of temporary files that implements AutoCloseable. All files in the collection
* are cleaned up when close() is called. * are cleaned up when close() is called.

View File

@ -26,6 +26,7 @@ import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.TempFileManager; import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.TempFileRegistry; import stirling.software.common.util.TempFileRegistry;
@ -43,6 +44,15 @@ public class TempFileCleanupServiceTest {
@Mock @Mock
private TempFileManager tempFileManager; private TempFileManager tempFileManager;
@Mock
private ApplicationProperties applicationProperties;
@Mock
private ApplicationProperties.System system;
@Mock
private ApplicationProperties.TempFileManagement tempFileManagement;
@InjectMocks @InjectMocks
private TempFileCleanupService cleanupService; private TempFileCleanupService cleanupService;
@ -63,12 +73,18 @@ public class TempFileCleanupServiceTest {
Files.createDirectories(customTempDir); Files.createDirectories(customTempDir);
Files.createDirectories(libreOfficeTempDir); Files.createDirectories(libreOfficeTempDir);
// Configure service with our test directories // Configure ApplicationProperties mocks
ReflectionTestUtils.setField(cleanupService, "systemTempDir", systemTempDir.toString()); when(applicationProperties.getSystem()).thenReturn(system);
ReflectionTestUtils.setField(cleanupService, "customTempDirectory", customTempDir.toString()); when(system.getTempFileManagement()).thenReturn(tempFileManagement);
ReflectionTestUtils.setField(cleanupService, "libreOfficeTempDir", libreOfficeTempDir.toString()); when(tempFileManagement.getBaseTmpDir()).thenReturn(customTempDir.toString());
ReflectionTestUtils.setField(cleanupService, "machineType", "Standard"); // Regular mode when(tempFileManagement.getLibreofficeDir()).thenReturn(libreOfficeTempDir.toString());
ReflectionTestUtils.setField(cleanupService, "performStartupCleanup", false); // Disable auto-startup cleanup when(tempFileManagement.getSystemTempDir()).thenReturn(systemTempDir.toString());
when(tempFileManagement.isStartupCleanup()).thenReturn(false);
when(tempFileManagement.isCleanupSystemTemp()).thenReturn(false);
when(tempFileManagement.getCleanupIntervalMinutes()).thenReturn(30L);
// Set machineType using reflection (still needed for this field)
ReflectionTestUtils.setField(cleanupService, "machineType", "Standard");
when(tempFileManager.getMaxAgeMillis()).thenReturn(3600000L); // 1 hour when(tempFileManager.getMaxAgeMillis()).thenReturn(3600000L); // 1 hour
} }

View File

@ -3,7 +3,11 @@ package stirling.software.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.anyString;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -22,14 +26,24 @@ public class FileToPdfTest {
byte[] fileBytes = new byte[0]; // Sample file bytes (empty input) byte[] fileBytes = new byte[0]; // Sample file bytes (empty input)
String fileName = "test.html"; // Sample file name indicating an HTML file String fileName = "test.html"; // Sample file name indicating an HTML file
boolean disableSanitize = false; // Flag to control sanitization boolean disableSanitize = false; // Flag to control sanitization
TempFileManager tempFileManager = mock(TempFileManager.class); // Mock TempFileManager
// Mock the temp file creation to return real temp files
try {
when(tempFileManager.createTempFile(anyString()))
.thenReturn(File.createTempFile("test", ".pdf"))
.thenReturn(File.createTempFile("test", ".html"));
} catch (IOException e) {
throw new RuntimeException(e);
}
// Expect an IOException to be thrown due to empty input // Expect an IOException to be thrown due to empty input or invalid weasyprint path
Throwable thrown = Throwable thrown =
assertThrows( assertThrows(
IOException.class, Exception.class,
() -> () ->
FileToPdf.convertHtmlToPdf( FileToPdf.convertHtmlToPdf(
"/path/", request, fileBytes, fileName, disableSanitize)); "/path/", request, fileBytes, fileName, disableSanitize, tempFileManager));
assertNotNull(thrown); assertNotNull(thrown);
} }

View File

@ -65,6 +65,9 @@ public class KeygenLicenseVerifier {
} }
public License verifyLicense(String licenseKeyOrCert) { public License verifyLicense(String licenseKeyOrCert) {
if (!applicationProperties.getPremium().isEnabled()) {
return License.NORMAL;
}
License license; License license;
LicenseContext context = new LicenseContext(); LicenseContext context = new LicenseContext();

View File

@ -28,9 +28,11 @@ if [[ -n "$LANGS" ]]; then
fi fi
echo "Setting permissions and ownership for necessary directories..." echo "Setting permissions and ownership for necessary directories..."
# Ensure temp directory exists and has correct permissions
mkdir -p /tmp/stirling-pdf || true
# Attempt to change ownership of directories and files # Attempt to change ownership of directories and files
if chown -R stirlingpdfuser:stirlingpdfgroup $HOME /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar; then if chown -R stirlingpdfuser:stirlingpdfgroup $HOME /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf /app.jar; then
chmod -R 755 /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar || true chmod -R 755 /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf /app.jar || true
# If chown succeeds, execute the command as stirlingpdfuser # If chown succeeds, execute the command as stirlingpdfuser
exec su-exec stirlingpdfuser "$@" exec su-exec stirlingpdfuser "$@"
else else

View File

@ -28,4 +28,9 @@ if [[ -n "$TESSERACT_LANGS" ]]; then
done done
fi fi
# Ensure temp directory exists with correct permissions before running main init
mkdir -p /tmp/stirling-pdf || true
chown -R stirlingpdfuser:stirlingpdfgroup /tmp/stirling-pdf || true
chmod -R 755 /tmp/stirling-pdf || true
/scripts/init-without-ocr.sh "$@" /scripts/init-without-ocr.sh "$@"

View File

@ -24,6 +24,7 @@ import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.api.converters.EmlToPdfRequest; import stirling.software.common.model.api.converters.EmlToPdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.EmlToPdf; import stirling.software.common.util.EmlToPdf;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -35,6 +36,7 @@ public class ConvertEmlToPDF {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDFDocumentFactory pdfDocumentFactory;
private final RuntimePathConfig runtimePathConfig; private final RuntimePathConfig runtimePathConfig;
private final TempFileManager tempFileManager;
@PostMapping(consumes = "multipart/form-data", value = "/eml/pdf") @PostMapping(consumes = "multipart/form-data", value = "/eml/pdf")
@Operation( @Operation(
@ -102,7 +104,8 @@ public class ConvertEmlToPDF {
fileBytes, fileBytes,
originalFilename, originalFilename,
false, false,
pdfDocumentFactory); pdfDocumentFactory,
tempFileManager);
if (pdfBytes == null || pdfBytes.length == 0) { if (pdfBytes == null || pdfBytes.length == 0) {
log.error("PDF conversion failed - empty output for {}", originalFilename); log.error("PDF conversion failed - empty output for {}", originalFilename);

View File

@ -18,6 +18,7 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.api.converters.HTMLToPdfRequest; import stirling.software.common.model.api.converters.HTMLToPdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.FileToPdf; import stirling.software.common.util.FileToPdf;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -32,6 +33,8 @@ public class ConvertHtmlToPDF {
private final RuntimePathConfig runtimePathConfig; private final RuntimePathConfig runtimePathConfig;
private final TempFileManager tempFileManager;
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf") @PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
@Operation( @Operation(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
@ -62,7 +65,8 @@ public class ConvertHtmlToPDF {
request, request,
fileInput.getBytes(), fileInput.getBytes(),
originalFilename, originalFilename,
disableSanitize); disableSanitize,
tempFileManager);
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes); pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);

View File

@ -28,6 +28,7 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.api.GeneralFile; import stirling.software.common.model.api.GeneralFile;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.FileToPdf; import stirling.software.common.util.FileToPdf;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -41,6 +42,8 @@ public class ConvertMarkdownToPdf {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final RuntimePathConfig runtimePathConfig; private final RuntimePathConfig runtimePathConfig;
private final TempFileManager tempFileManager;
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf") @PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
@Operation( @Operation(
summary = "Convert a Markdown file to PDF", summary = "Convert a Markdown file to PDF",
@ -82,7 +85,8 @@ public class ConvertMarkdownToPdf {
null, null,
htmlContent.getBytes(), htmlContent.getBytes(),
"converted.html", "converted.html",
disableSanitize); disableSanitize,
tempFileManager);
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes); pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
String outputFilename = String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "") originalFilename.replaceFirst("[.][^.]+$", "")

View File

@ -21,8 +21,8 @@ import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager; import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.TempFileUtil;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -45,8 +45,8 @@ public class RepairController {
throws IOException, InterruptedException { throws IOException, InterruptedException {
MultipartFile inputFile = file.getFileInput(); MultipartFile inputFile = file.getFileInput();
// Use TempFileUtil.TempFile with try-with-resources for automatic cleanup // Use TempFile with try-with-resources for automatic cleanup
try (TempFileUtil.TempFile tempFile = new TempFileUtil.TempFile(tempFileManager, ".pdf")) { try (TempFile tempFile = new TempFile(tempFileManager, ".pdf")) {
// Save the uploaded file to the temporary location // Save the uploaded file to the temporary location
inputFile.transferTo(tempFile.getFile()); inputFile.transferTo(tempFile.getFile());

View File

@ -39,8 +39,8 @@ import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.api.misc.AddStampRequest; import stirling.software.SPDF.model.api.misc.AddStampRequest;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager; import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.TempFileUtil;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -191,9 +191,8 @@ public class StampController {
ClassPathResource classPathResource = new ClassPathResource(resourceDir); ClassPathResource classPathResource = new ClassPathResource(resourceDir);
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
// Use TempFileUtil.TempFile with try-with-resources for automatic cleanup // Use TempFile with try-with-resources for automatic cleanup
try (TempFileUtil.TempFile tempFileWrapper = try (TempFile tempFileWrapper = new TempFile(tempFileManager, fileExtension)) {
new TempFileUtil.TempFile(tempFileManager, fileExtension)) {
File tempFile = tempFileWrapper.getFile(); File tempFile = tempFileWrapper.getFile();
try (InputStream is = classPathResource.getInputStream(); try (InputStream is = classPathResource.getInputStream();
FileOutputStream os = new FileOutputStream(tempFile)) { FileOutputStream os = new FileOutputStream(tempFile)) {

View File

@ -125,6 +125,15 @@ system:
weasyprint: '' #Defaults to /opt/venv/bin/weasyprint weasyprint: '' #Defaults to /opt/venv/bin/weasyprint
unoconvert: '' #Defaults to /opt/venv/bin/unoconvert unoconvert: '' #Defaults to /opt/venv/bin/unoconvert
fileUploadLimit: '' # Defaults to "". No limit when string is empty. Set a number, between 0 and 999, followed by one of the following strings to set a limit. "KB", "MB", "GB". fileUploadLimit: '' # Defaults to "". No limit when string is empty. Set a number, between 0 and 999, followed by one of the following strings to set a limit. "KB", "MB", "GB".
tempFileManagement:
baseTmpDir: '' # Defaults to java.io.tmpdir/stirling-pdf
libreofficeDir: '' # Defaults to tempFileManagement.baseTmpDir/libreoffice
systemTempDir: '' # Only used if cleanupSystemTemp is true
prefix: stirling-pdf- # Prefix for temp file names
maxAgeHours: 24 # Maximum age in hours before temp files are cleaned up
cleanupIntervalMinutes: 30 # How often to run cleanup (in minutes)
startupCleanup: true # Clean up old temp files on startup
cleanupSystemTemp: false # Whether to clean broader system temp directory
ui: ui:
appName: '' # application's visible name appName: '' # application's visible name

View File

@ -55,10 +55,12 @@ capture_file_list() {
-not -path '/config/*' \ -not -path '/config/*' \
-not -path '/logs/*' \ -not -path '/logs/*' \
-not -path '*/home/stirlingpdfuser/.config/libreoffice/*' \ -not -path '*/home/stirlingpdfuser/.config/libreoffice/*' \
-not -path '*/tmp/PDFBox*' \ -not -path '*/home/stirlingpdfuser/.pdfbox.cache' \
-not -path '*/tmp/stirling-pdf/PDFBox*' \
-not -path '*/tmp/stirling-pdf/hsperfdata_stirlingpdfuser/*' \
-not -path '*/tmp/hsperfdata_stirlingpdfuser/*' \ -not -path '*/tmp/hsperfdata_stirlingpdfuser/*' \
-not -path '*/tmp/lu*' \ -not -path '*/tmp/stirling-pdf/lu*' \
-not -path '*/tmp/tmp*' \ -not -path '*/tmp/stirling-pdf/tmp*' \
2>/dev/null | xargs -I{} sh -c 'stat -c \"%n %s %Y\" \"{}\" 2>/dev/null || true' | sort" > "$output_file" 2>/dev/null | xargs -I{} sh -c 'stat -c \"%n %s %Y\" \"{}\" 2>/dev/null || true' | sort" > "$output_file"
# Check if the output file has content # Check if the output file has content
@ -74,8 +76,10 @@ capture_file_list() {
-not -path '/config/*' \ -not -path '/config/*' \
-not -path '/logs/*' \ -not -path '/logs/*' \
-not -path '*/home/stirlingpdfuser/.config/libreoffice/*' \ -not -path '*/home/stirlingpdfuser/.config/libreoffice/*' \
-not -path '*/home/stirlingpdfuser/.pdfbox.cache' \
-not -path '*/tmp/PDFBox*' \ -not -path '*/tmp/PDFBox*' \
-not -path '*/tmp/hsperfdata_stirlingpdfuser/*' \ -not -path '*/tmp/hsperfdata_stirlingpdfuser/*' \
-not -path '*/tmp/stirling-pdf/hsperfdata_stirlingpdfuser/*' \
-not -path '*/tmp/lu*' \ -not -path '*/tmp/lu*' \
-not -path '*/tmp/tmp*' \ -not -path '*/tmp/tmp*' \
2>/dev/null | sort" > "$output_file" 2>/dev/null | sort" > "$output_file"