mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-23 13:45:21 +00:00
feat:centralise temp-file management & cleanup across app/docker (#3797)
# Description of Changes Introduces TempFileManager, registry, and scheduled cleanup service; aligns all Docker images and runtime scripts to use a dedicated /tmp/stirling-pdf directory; updates controllers, utilities, and tests to use the new API; adds configurable system.tempFileManagement section. Closes #(issue_number) --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: a <a>
This commit is contained in:
parent
32aa568196
commit
bc9c127819
12
.claude/settings.local.json
Normal file
12
.claude/settings.local.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(chmod:*)",
|
||||||
|
"Bash(mkdir:*)",
|
||||||
|
"Bash(./gradlew:*)",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(cat:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
|
}
|
12
Dockerfile
12
Dockerfile
@ -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"]
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"]
|
||||||
|
@ -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"]
|
||||||
|
@ -8,22 +8,22 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut for a POST endpoint that is executed through the Stirling "auto‑job" framework.
|
* Shortcut for a POST endpoint that is executed through the Stirling "auto‑job" framework.
|
||||||
* <p>
|
|
||||||
* Behaviour notes:
|
|
||||||
* <ul>
|
|
||||||
* <li>The endpoint is registered with {@code POST} and, by default, consumes
|
|
||||||
* {@code multipart/form-data} unless you override {@link #consumes()}.</li>
|
|
||||||
* <li>When the client supplies {@code ?async=true} the call is handed to
|
|
||||||
* {@link stirling.software.common.service.JobExecutorService JobExecutorService} where it may
|
|
||||||
* be queued, retried, tracked and subject to time‑outs. For synchronous (default)
|
|
||||||
* invocations these advanced options are ignored.</li>
|
|
||||||
* <li>Progress information (see {@link #trackProgress()}) is stored in
|
|
||||||
* {@link stirling.software.common.service.TaskManager TaskManager} and can be
|
|
||||||
* polled via <code>GET /api/v1/general/job/{id}</code>.</li>
|
|
||||||
* </ul>
|
|
||||||
* </p>
|
|
||||||
*
|
*
|
||||||
* <p>Unless stated otherwise an attribute only affects <em>async</em> execution.</p>
|
* <p>Behaviour notes:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>The endpoint is registered with {@code POST} and, by default, consumes {@code
|
||||||
|
* multipart/form-data} unless you override {@link #consumes()}.
|
||||||
|
* <li>When the client supplies {@code ?async=true} the call is handed to {@link
|
||||||
|
* stirling.software.common.service.JobExecutorService JobExecutorService} where it may be
|
||||||
|
* queued, retried, tracked and subject to time‑outs. For synchronous (default) invocations
|
||||||
|
* these advanced options are ignored.
|
||||||
|
* <li>Progress information (see {@link #trackProgress()}) is stored in {@link
|
||||||
|
* stirling.software.common.service.TaskManager TaskManager} and can be polled via <code>
|
||||||
|
* GET /api/v1/general/job/{id}</code>.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Unless stated otherwise an attribute only affects <em>async</em> execution.
|
||||||
*/
|
*/
|
||||||
@Target(ElementType.METHOD)
|
@Target(ElementType.METHOD)
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@ -31,42 +31,42 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
|||||||
@RequestMapping(method = RequestMethod.POST)
|
@RequestMapping(method = RequestMethod.POST)
|
||||||
public @interface AutoJobPostMapping {
|
public @interface AutoJobPostMapping {
|
||||||
|
|
||||||
/**
|
/** Alias for {@link RequestMapping#value} – the path mapping of the endpoint. */
|
||||||
* Alias for {@link RequestMapping#value} – the path mapping of the endpoint.
|
|
||||||
*/
|
|
||||||
@AliasFor(annotation = RequestMapping.class, attribute = "value")
|
@AliasFor(annotation = RequestMapping.class, attribute = "value")
|
||||||
String[] value() default {};
|
String[] value() default {};
|
||||||
|
|
||||||
/**
|
/** MIME types this endpoint accepts. Defaults to {@code multipart/form-data}. */
|
||||||
* MIME types this endpoint accepts. Defaults to {@code multipart/form-data}.
|
|
||||||
*/
|
|
||||||
@AliasFor(annotation = RequestMapping.class, attribute = "consumes")
|
@AliasFor(annotation = RequestMapping.class, attribute = "consumes")
|
||||||
String[] consumes() default {"multipart/form-data"};
|
String[] consumes() default {"multipart/form-data"};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum execution time in milliseconds before the job is aborted.
|
* Maximum execution time in milliseconds before the job is aborted. A negative value means "use
|
||||||
* A negative value means "use the application default".
|
* the application default".
|
||||||
* <p>Only honoured when {@code async=true}.</p>
|
*
|
||||||
|
* <p>Only honoured when {@code async=true}.
|
||||||
*/
|
*/
|
||||||
long timeout() default -1;
|
long timeout() default -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Total number of attempts (initial + retries). Must be at least 1.
|
* Total number of attempts (initial + retries). Must be at least 1. Retries are executed
|
||||||
* Retries are executed with exponential back‑off.
|
* with exponential back‑off.
|
||||||
* <p>Only honoured when {@code async=true}.</p>
|
*
|
||||||
|
* <p>Only honoured when {@code async=true}.
|
||||||
*/
|
*/
|
||||||
int retryCount() default 1;
|
int retryCount() default 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record percentage / note updates so they can be retrieved via the REST status endpoint.
|
* Record percentage / note updates so they can be retrieved via the REST status endpoint.
|
||||||
* <p>Only honoured when {@code async=true}.</p>
|
*
|
||||||
|
* <p>Only honoured when {@code async=true}.
|
||||||
*/
|
*/
|
||||||
boolean trackProgress() default true;
|
boolean trackProgress() default true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If {@code true} the job may be placed in a queue instead of being rejected when resources
|
* If {@code true} the job may be placed in a queue instead of being rejected when resources are
|
||||||
* are scarce.
|
* scarce.
|
||||||
* <p>Only honoured when {@code async=true}.</p>
|
*
|
||||||
|
* <p>Only honoured when {@code async=true}.
|
||||||
*/
|
*/
|
||||||
boolean queueable() default false;
|
boolean queueable() default false;
|
||||||
|
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
package stirling.software.common.config;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.common.util.TempFileRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for the temporary file management system. Sets up the necessary beans and
|
||||||
|
* configures system properties.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TempFileConfiguration {
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the TempFileRegistry bean.
|
||||||
|
*
|
||||||
|
* @return A new TempFileRegistry instance
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public TempFileRegistry tempFileRegistry() {
|
||||||
|
return new TempFileRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initTempFileConfig() {
|
||||||
|
try {
|
||||||
|
ApplicationProperties.TempFileManagement tempFiles =
|
||||||
|
applicationProperties.getSystem().getTempFileManagement();
|
||||||
|
String customTempDirectory = tempFiles.getBaseTmpDir();
|
||||||
|
|
||||||
|
// Create the temp directory if it doesn't exist
|
||||||
|
Path tempDir = Path.of(customTempDirectory);
|
||||||
|
if (!Files.exists(tempDir)) {
|
||||||
|
Files.createDirectories(tempDir);
|
||||||
|
log.info("Created temporary directory: {}", tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Temporary file configuration initialized");
|
||||||
|
log.debug("Using temp directory: {}", customTempDirectory);
|
||||||
|
log.debug("Temp file prefix: {}", tempFiles.getPrefix());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to initialize temporary file configuration", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package stirling.software.common.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.DisposableBean;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
import stirling.software.common.util.TempFileRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles cleanup of temporary files on application shutdown. Implements Spring's DisposableBean
|
||||||
|
* interface to ensure cleanup happens during normal application shutdown.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class TempFileShutdownHook implements DisposableBean {
|
||||||
|
|
||||||
|
private final TempFileRegistry registry;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public TempFileShutdownHook(TempFileRegistry registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
|
||||||
|
// Register a JVM shutdown hook as a backup in case Spring's
|
||||||
|
// DisposableBean mechanism doesn't trigger (e.g., during a crash)
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(this::cleanupTempFiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Spring's DisposableBean interface method. Called during normal application shutdown. */
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
log.info("Application shutting down, cleaning up temporary files");
|
||||||
|
cleanupTempFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clean up all registered temporary files and directories. */
|
||||||
|
private void cleanupTempFiles() {
|
||||||
|
try {
|
||||||
|
// Clean up all registered files
|
||||||
|
Set<Path> files = registry.getAllRegisteredFiles();
|
||||||
|
int deletedCount = 0;
|
||||||
|
|
||||||
|
for (Path file : files) {
|
||||||
|
try {
|
||||||
|
if (Files.exists(file)) {
|
||||||
|
Files.deleteIfExists(file);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to delete temp file during shutdown: {}", file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up all registered directories
|
||||||
|
Set<Path> directories = registry.getTempDirectories();
|
||||||
|
for (Path dir : directories) {
|
||||||
|
try {
|
||||||
|
if (Files.exists(dir)) {
|
||||||
|
GeneralUtils.deleteDirectory(dir);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to delete temp directory during shutdown: {}", dir, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Shutdown cleanup complete. Deleted {} temporary files/directories",
|
||||||
|
deletedCount);
|
||||||
|
|
||||||
|
// Clear the registry
|
||||||
|
registry.clear();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error during shutdown cleanup", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -23,6 +23,9 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
|
import stirling.software.common.util.ApplicationContextProvider;
|
||||||
|
import stirling.software.common.util.TempFileManager;
|
||||||
|
import stirling.software.common.util.TempFileRegistry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adaptive PDF document factory that optimizes memory usage based on file size and available system
|
* Adaptive PDF document factory that optimizes memory usage based on file size and available system
|
||||||
@ -402,10 +405,37 @@ public class CustomPDFDocumentFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temp file handling with enhanced logging
|
// Temp file handling with enhanced logging and registry integration
|
||||||
private Path createTempFile(String prefix) throws IOException {
|
private Path createTempFile(String prefix) throws IOException {
|
||||||
|
// Check if TempFileManager is available in the application context
|
||||||
|
try {
|
||||||
|
TempFileManager tempFileManager =
|
||||||
|
ApplicationContextProvider.getBean(TempFileManager.class);
|
||||||
|
if (tempFileManager != null) {
|
||||||
|
// Use TempFileManager to create and register the temp file
|
||||||
|
File file = tempFileManager.createTempFile(".tmp");
|
||||||
|
log.debug("Created and registered temp file via TempFileManager: {}", file);
|
||||||
|
return file.toPath();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("TempFileManager not available, falling back to standard temp file creation");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to standard temp file creation
|
||||||
Path file = Files.createTempFile(prefix + tempCounter.incrementAndGet() + "-", ".tmp");
|
Path file = Files.createTempFile(prefix + tempCounter.incrementAndGet() + "-", ".tmp");
|
||||||
log.debug("Created temp file: {}", file);
|
log.debug("Created temp file: {}", file);
|
||||||
|
|
||||||
|
// Try to register the file with a static registry if possible
|
||||||
|
try {
|
||||||
|
TempFileRegistry registry = ApplicationContextProvider.getBean(TempFileRegistry.class);
|
||||||
|
if (registry != null) {
|
||||||
|
registry.register(file);
|
||||||
|
log.debug("Registered fallback temp file with registry: {}", file);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Could not register fallback temp file with registry: {}", file);
|
||||||
|
}
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,7 +173,9 @@ public class ResourceMonitor {
|
|||||||
log.info("System resource status changed from {} to {}", oldStatus, newStatus);
|
log.info("System resource status changed from {} to {}", oldStatus, newStatus);
|
||||||
log.info(
|
log.info(
|
||||||
"Current metrics - CPU: {}%, Memory: {}%, Free Memory: {} MB",
|
"Current metrics - CPU: {}%, Memory: {}%, Free Memory: {} MB",
|
||||||
String.format("%.1f", cpuUsage * 100), String.format("%.1f", memoryUsage * 100), freeMemory / (1024 * 1024));
|
String.format("%.1f", cpuUsage * 100),
|
||||||
|
String.format("%.1f", memoryUsage * 100),
|
||||||
|
freeMemory / (1024 * 1024));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error updating resource metrics: {}", e.getMessage(), e);
|
log.error("Error updating resource metrics: {}", e.getMessage(), e);
|
||||||
|
@ -0,0 +1,447 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
import stirling.software.common.util.TempFileManager;
|
||||||
|
import stirling.software.common.util.TempFileRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to periodically clean up temporary files. Runs scheduled tasks to delete old temp files
|
||||||
|
* and directories.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TempFileCleanupService {
|
||||||
|
|
||||||
|
private final TempFileRegistry registry;
|
||||||
|
private final TempFileManager tempFileManager;
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("machineType")
|
||||||
|
private String machineType;
|
||||||
|
|
||||||
|
// Maximum recursion depth for directory traversal
|
||||||
|
private static final int MAX_RECURSION_DEPTH = 5;
|
||||||
|
|
||||||
|
// File patterns that identify our temp files
|
||||||
|
private static final Predicate<String> IS_OUR_TEMP_FILE =
|
||||||
|
fileName ->
|
||||||
|
fileName.startsWith("stirling-pdf-")
|
||||||
|
|| fileName.startsWith("output_")
|
||||||
|
|| fileName.startsWith("compressedPDF")
|
||||||
|
|| fileName.startsWith("pdf-save-")
|
||||||
|
|| fileName.startsWith("pdf-stream-")
|
||||||
|
|| fileName.startsWith("PDFBox")
|
||||||
|
|| fileName.startsWith("input_")
|
||||||
|
|| fileName.startsWith("overlay-");
|
||||||
|
|
||||||
|
// File patterns that identify common system temp files
|
||||||
|
private static final Predicate<String> IS_SYSTEM_TEMP_FILE =
|
||||||
|
fileName ->
|
||||||
|
fileName.matches("lu\\d+[a-z0-9]*\\.tmp")
|
||||||
|
|| fileName.matches("ocr_process\\d+")
|
||||||
|
|| (fileName.startsWith("tmp") && !fileName.contains("jetty"))
|
||||||
|
|| fileName.startsWith("OSL_PIPE_")
|
||||||
|
|| (fileName.endsWith(".tmp") && !fileName.contains("jetty"));
|
||||||
|
|
||||||
|
// File patterns that should be excluded from cleanup
|
||||||
|
private static final Predicate<String> SHOULD_SKIP =
|
||||||
|
fileName ->
|
||||||
|
fileName.contains("jetty")
|
||||||
|
|| fileName.startsWith("jetty-")
|
||||||
|
|| fileName.equals("proc")
|
||||||
|
|| fileName.equals("sys")
|
||||||
|
|| fileName.equals("dev")
|
||||||
|
|| fileName.equals("hsperfdata_stirlingpdfuser")
|
||||||
|
|| fileName.startsWith("hsperfdata_")
|
||||||
|
|| fileName.equals(".pdfbox.cache");
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
// Create necessary directories
|
||||||
|
ensureDirectoriesExist();
|
||||||
|
|
||||||
|
// Perform startup cleanup if enabled
|
||||||
|
if (applicationProperties.getSystem().getTempFileManagement().isStartupCleanup()) {
|
||||||
|
runStartupCleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ensure that all required temp directories exist */
|
||||||
|
private void ensureDirectoriesExist() {
|
||||||
|
try {
|
||||||
|
ApplicationProperties.TempFileManagement tempFiles =
|
||||||
|
applicationProperties.getSystem().getTempFileManagement();
|
||||||
|
|
||||||
|
// Create the main temp directory
|
||||||
|
String customTempDirectory = tempFiles.getBaseTmpDir();
|
||||||
|
if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
|
||||||
|
Path tempDir = Path.of(customTempDirectory);
|
||||||
|
if (!Files.exists(tempDir)) {
|
||||||
|
Files.createDirectories(tempDir);
|
||||||
|
log.info("Created temp directory: {}", tempDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create LibreOffice temp directory
|
||||||
|
String libreOfficeTempDir = tempFiles.getLibreofficeDir();
|
||||||
|
if (libreOfficeTempDir != null && !libreOfficeTempDir.isEmpty()) {
|
||||||
|
Path loTempDir = Path.of(libreOfficeTempDir);
|
||||||
|
if (!Files.exists(loTempDir)) {
|
||||||
|
Files.createDirectories(loTempDir);
|
||||||
|
log.info("Created LibreOffice temp directory: {}", loTempDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error creating temp directories", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Scheduled task to clean up old temporary files. Runs at the configured interval. */
|
||||||
|
@Scheduled(
|
||||||
|
fixedDelayString =
|
||||||
|
"#{applicationProperties.system.tempFileManagement.cleanupIntervalMinutes}",
|
||||||
|
timeUnit = TimeUnit.MINUTES)
|
||||||
|
public void scheduledCleanup() {
|
||||||
|
log.info("Running scheduled temporary file cleanup");
|
||||||
|
long maxAgeMillis = tempFileManager.getMaxAgeMillis();
|
||||||
|
|
||||||
|
// Clean up registered temp files (managed by TempFileRegistry)
|
||||||
|
int registeredDeletedCount = tempFileManager.cleanupOldTempFiles(maxAgeMillis);
|
||||||
|
log.info("Cleaned up {} registered temporary files", registeredDeletedCount);
|
||||||
|
|
||||||
|
// Clean up registered temp directories
|
||||||
|
int directoriesDeletedCount = 0;
|
||||||
|
for (Path directory : registry.getTempDirectories()) {
|
||||||
|
try {
|
||||||
|
if (Files.exists(directory)) {
|
||||||
|
GeneralUtils.deleteDirectory(directory);
|
||||||
|
directoriesDeletedCount++;
|
||||||
|
log.debug("Cleaned up temporary directory: {}", directory);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to clean up temporary directory: {}", directory, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up PDFBox cache file
|
||||||
|
cleanupPDFBoxCache();
|
||||||
|
|
||||||
|
// Clean up unregistered temp files based on our cleanup strategy
|
||||||
|
boolean containerMode = isContainerMode();
|
||||||
|
int unregisteredDeletedCount = cleanupUnregisteredFiles(containerMode, true, maxAgeMillis);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Scheduled cleanup complete. Deleted {} registered files, {} unregistered files, {} directories",
|
||||||
|
registeredDeletedCount,
|
||||||
|
unregisteredDeletedCount,
|
||||||
|
directoriesDeletedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform startup cleanup of stale temporary files from previous runs. This is especially
|
||||||
|
* important in Docker environments where temp files persist between container restarts.
|
||||||
|
*/
|
||||||
|
private void runStartupCleanup() {
|
||||||
|
log.info("Running startup temporary file cleanup");
|
||||||
|
boolean containerMode = isContainerMode();
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Running in {} mode, using {} cleanup strategy",
|
||||||
|
machineType,
|
||||||
|
containerMode ? "aggressive" : "conservative");
|
||||||
|
|
||||||
|
// For startup cleanup, we use a longer timeout for non-container environments
|
||||||
|
long maxAgeMillis = containerMode ? 0 : 24 * 60 * 60 * 1000; // 0 or 24 hours
|
||||||
|
|
||||||
|
int totalDeletedCount = cleanupUnregisteredFiles(containerMode, false, maxAgeMillis);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Startup cleanup complete. Deleted {} temporary files/directories",
|
||||||
|
totalDeletedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up unregistered temporary files across all configured temp directories.
|
||||||
|
*
|
||||||
|
* @param containerMode Whether we're in container mode (more aggressive cleanup)
|
||||||
|
* @param isScheduled Whether this is a scheduled cleanup or startup cleanup
|
||||||
|
* @param maxAgeMillis Maximum age of files to clean in milliseconds
|
||||||
|
* @return Number of files deleted
|
||||||
|
*/
|
||||||
|
private int cleanupUnregisteredFiles(
|
||||||
|
boolean containerMode, boolean isScheduled, long maxAgeMillis) {
|
||||||
|
AtomicInteger totalDeletedCount = new AtomicInteger(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ApplicationProperties.TempFileManagement tempFiles =
|
||||||
|
applicationProperties.getSystem().getTempFileManagement();
|
||||||
|
Path[] dirsToScan;
|
||||||
|
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
|
||||||
|
Arrays.stream(dirsToScan)
|
||||||
|
.filter(Files::exists)
|
||||||
|
.forEach(
|
||||||
|
tempDir -> {
|
||||||
|
try {
|
||||||
|
String phase = isScheduled ? "scheduled" : "startup";
|
||||||
|
log.info(
|
||||||
|
"Scanning directory for {} cleanup: {}",
|
||||||
|
phase,
|
||||||
|
tempDir);
|
||||||
|
|
||||||
|
AtomicInteger dirDeletedCount = new AtomicInteger(0);
|
||||||
|
cleanupDirectoryStreaming(
|
||||||
|
tempDir,
|
||||||
|
containerMode,
|
||||||
|
0,
|
||||||
|
maxAgeMillis,
|
||||||
|
isScheduled,
|
||||||
|
path -> {
|
||||||
|
dirDeletedCount.incrementAndGet();
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug(
|
||||||
|
"Deleted temp file during {} cleanup: {}",
|
||||||
|
phase,
|
||||||
|
path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
int count = dirDeletedCount.get();
|
||||||
|
totalDeletedCount.addAndGet(count);
|
||||||
|
if (count > 0) {
|
||||||
|
log.info(
|
||||||
|
"Cleaned up {} files/directories in {}",
|
||||||
|
count,
|
||||||
|
tempDir);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error during cleanup of directory: {}", tempDir, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error during cleanup of unregistered files", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalDeletedCount.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the system temp directory path based on configuration or system property. */
|
||||||
|
private Path getSystemTempPath() {
|
||||||
|
String systemTempDir =
|
||||||
|
applicationProperties.getSystem().getTempFileManagement().getSystemTempDir();
|
||||||
|
if (systemTempDir != null && !systemTempDir.isEmpty()) {
|
||||||
|
return Path.of(systemTempDir);
|
||||||
|
} else {
|
||||||
|
return Path.of(System.getProperty("java.io.tmpdir"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Determine if we're running in a container environment. */
|
||||||
|
private boolean isContainerMode() {
|
||||||
|
return "Docker".equals(machineType) || "Kubernetes".equals(machineType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively clean up a directory using a streaming approach to reduce memory usage.
|
||||||
|
*
|
||||||
|
* @param directory The directory to clean
|
||||||
|
* @param containerMode Whether we're in container mode (more aggressive cleanup)
|
||||||
|
* @param depth Current recursion depth
|
||||||
|
* @param maxAgeMillis Maximum age of files to delete
|
||||||
|
* @param isScheduled Whether this is a scheduled cleanup (vs startup)
|
||||||
|
* @param onDeleteCallback Callback function when a file is deleted
|
||||||
|
* @throws IOException If an I/O error occurs
|
||||||
|
*/
|
||||||
|
private void cleanupDirectoryStreaming(
|
||||||
|
Path directory,
|
||||||
|
boolean containerMode,
|
||||||
|
int depth,
|
||||||
|
long maxAgeMillis,
|
||||||
|
boolean isScheduled,
|
||||||
|
Consumer<Path> onDeleteCallback)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
if (depth > MAX_RECURSION_DEPTH) {
|
||||||
|
log.debug("Maximum directory recursion depth reached for: {}", directory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.List<Path> subdirectories = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
try (Stream<Path> pathStream = Files.list(directory)) {
|
||||||
|
pathStream.forEach(
|
||||||
|
path -> {
|
||||||
|
try {
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
|
if (SHOULD_SKIP.test(fileName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.isDirectory(path)) {
|
||||||
|
subdirectories.add(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registry.contains(path.toFile())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldDeleteFile(path, fileName, containerMode, maxAgeMillis)) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
onDeleteCallback.accept(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (e.getMessage() != null
|
||||||
|
&& e.getMessage()
|
||||||
|
.contains("being used by another process")) {
|
||||||
|
log.debug("File locked, skipping delete: {}", path);
|
||||||
|
} else {
|
||||||
|
log.warn("Failed to delete temp file: {}", path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error processing path: {}", path, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Path subdirectory : subdirectories) {
|
||||||
|
try {
|
||||||
|
cleanupDirectoryStreaming(
|
||||||
|
subdirectory,
|
||||||
|
containerMode,
|
||||||
|
depth + 1,
|
||||||
|
maxAgeMillis,
|
||||||
|
isScheduled,
|
||||||
|
onDeleteCallback);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Error processing subdirectory: {}", subdirectory, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Determine if a file should be deleted based on its name, age, and other criteria. */
|
||||||
|
private boolean shouldDeleteFile(
|
||||||
|
Path path, String fileName, boolean containerMode, long maxAgeMillis) {
|
||||||
|
// First check if it matches our known temp file patterns
|
||||||
|
boolean isOurTempFile = IS_OUR_TEMP_FILE.test(fileName);
|
||||||
|
boolean isSystemTempFile = IS_SYSTEM_TEMP_FILE.test(fileName);
|
||||||
|
|
||||||
|
// Normal operation - check against temp file patterns
|
||||||
|
boolean shouldDelete = isOurTempFile || (containerMode && isSystemTempFile);
|
||||||
|
|
||||||
|
// Get file info for age checks
|
||||||
|
long lastModified = 0;
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
boolean isEmptyFile = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
lastModified = Files.getLastModifiedTime(path).toMillis();
|
||||||
|
// Special case for zero-byte files - these are often corrupted temp files
|
||||||
|
if (Files.size(path) == 0) {
|
||||||
|
isEmptyFile = true;
|
||||||
|
// For empty files, use a shorter timeout (5 minutes)
|
||||||
|
// Delete empty files older than 5 minutes
|
||||||
|
if ((currentTime - lastModified) > 5 * 60 * 1000) {
|
||||||
|
shouldDelete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.debug("Could not check file info, skipping: {}", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file age against maxAgeMillis only if it's not an empty file that we've already
|
||||||
|
// decided to delete
|
||||||
|
if (!isEmptyFile && shouldDelete && maxAgeMillis > 0) {
|
||||||
|
// In normal mode, check age against maxAgeMillis
|
||||||
|
shouldDelete = (currentTime - lastModified) > maxAgeMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldDelete;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clean up LibreOffice temporary files. This method is called after LibreOffice operations. */
|
||||||
|
public void cleanupLibreOfficeTempFiles() {
|
||||||
|
// Cleanup known LibreOffice temp directories
|
||||||
|
try {
|
||||||
|
Set<Path> directories = registry.getTempDirectories();
|
||||||
|
for (Path dir : directories) {
|
||||||
|
if (dir.getFileName().toString().contains("libreoffice") && Files.exists(dir)) {
|
||||||
|
// For directories containing "libreoffice", delete all contents
|
||||||
|
// but keep the directory itself for future use
|
||||||
|
cleanupDirectoryStreaming(
|
||||||
|
dir,
|
||||||
|
isContainerMode(),
|
||||||
|
0,
|
||||||
|
0, // age doesn't matter for LibreOffice cleanup
|
||||||
|
false,
|
||||||
|
path -> log.debug("Cleaned up LibreOffice temp file: {}", path));
|
||||||
|
log.debug("Cleaned up LibreOffice temp directory contents: {}", dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class that provides access to the ApplicationContext. Useful for getting beans in classes
|
||||||
|
* that are not managed by Spring.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ApplicationContextProvider implements ApplicationContextAware {
|
||||||
|
|
||||||
|
private static ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext context) throws BeansException {
|
||||||
|
applicationContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a bean by class type.
|
||||||
|
*
|
||||||
|
* @param <T> The type of the bean
|
||||||
|
* @param beanClass The class of the bean
|
||||||
|
* @return The bean instance, or null if not found
|
||||||
|
*/
|
||||||
|
public static <T> T getBean(Class<T> beanClass) {
|
||||||
|
if (applicationContext == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return applicationContext.getBean(beanClass);
|
||||||
|
} catch (BeansException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a bean by name and class type.
|
||||||
|
*
|
||||||
|
* @param <T> The type of the bean
|
||||||
|
* @param name The name of the bean
|
||||||
|
* @param beanClass The class of the bean
|
||||||
|
* @return The bean instance, or null if not found
|
||||||
|
*/
|
||||||
|
public static <T> T getBean(String name, Class<T> beanClass) {
|
||||||
|
if (applicationContext == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return applicationContext.getBean(name, beanClass);
|
||||||
|
} catch (BeansException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a bean of the specified type exists.
|
||||||
|
*
|
||||||
|
* @param beanClass The class of the bean
|
||||||
|
* @return true if the bean exists, false otherwise
|
||||||
|
*/
|
||||||
|
public static boolean containsBean(Class<?> beanClass) {
|
||||||
|
if (applicationContext == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
applicationContext.getBean(beanClass);
|
||||||
|
return true;
|
||||||
|
} catch (BeansException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -134,7 +134,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);
|
||||||
@ -153,7 +154,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)) {
|
||||||
@ -194,7 +196,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 {
|
||||||
|
|
||||||
HTMLToPdfRequest htmlRequest = createHtmlRequest(request);
|
HTMLToPdfRequest htmlRequest = createHtmlRequest(request);
|
||||||
@ -205,7 +208,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);
|
||||||
@ -214,7 +218,8 @@ public class EmlToPdf {
|
|||||||
htmlRequest,
|
htmlRequest,
|
||||||
simplifiedHtml.getBytes(StandardCharsets.UTF_8),
|
simplifiedHtml.getBytes(StandardCharsets.UTF_8),
|
||||||
"email.html",
|
"email.html",
|
||||||
disableSanitize);
|
disableSanitize,
|
||||||
|
tempFileManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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)) {
|
||||||
|
|
||||||
|
@ -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() + "}";
|
||||||
|
}
|
||||||
|
}
|
@ -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() + "}";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,249 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for managing temporary files in Stirling-PDF. Provides methods for creating, tracking,
|
||||||
|
* and cleaning up temporary files.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TempFileManager {
|
||||||
|
|
||||||
|
private final TempFileRegistry registry;
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a temporary file with the Stirling-PDF prefix. The file is automatically registered
|
||||||
|
* with the registry.
|
||||||
|
*
|
||||||
|
* @param suffix The suffix for the temporary file
|
||||||
|
* @return The created temporary file
|
||||||
|
* @throws IOException If an I/O error occurs
|
||||||
|
*/
|
||||||
|
public File createTempFile(String suffix) throws IOException {
|
||||||
|
ApplicationProperties.TempFileManagement tempFiles =
|
||||||
|
applicationProperties.getSystem().getTempFileManagement();
|
||||||
|
Path tempFilePath;
|
||||||
|
String customTempDirectory = tempFiles.getBaseTmpDir();
|
||||||
|
if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
|
||||||
|
Path tempDir = Path.of(customTempDirectory);
|
||||||
|
if (!Files.exists(tempDir)) {
|
||||||
|
Files.createDirectories(tempDir);
|
||||||
|
}
|
||||||
|
tempFilePath = Files.createTempFile(tempDir, tempFiles.getPrefix(), suffix);
|
||||||
|
} else {
|
||||||
|
tempFilePath = Files.createTempFile(tempFiles.getPrefix(), suffix);
|
||||||
|
}
|
||||||
|
File tempFile = tempFilePath.toFile();
|
||||||
|
return registry.register(tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a temporary directory with the Stirling-PDF prefix. The directory is automatically
|
||||||
|
* registered with the registry.
|
||||||
|
*
|
||||||
|
* @return The created temporary directory
|
||||||
|
* @throws IOException If an I/O error occurs
|
||||||
|
*/
|
||||||
|
public Path createTempDirectory() throws IOException {
|
||||||
|
ApplicationProperties.TempFileManagement tempFiles =
|
||||||
|
applicationProperties.getSystem().getTempFileManagement();
|
||||||
|
Path tempDirPath;
|
||||||
|
String customTempDirectory = tempFiles.getBaseTmpDir();
|
||||||
|
if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
|
||||||
|
Path tempDir = Path.of(customTempDirectory);
|
||||||
|
if (!Files.exists(tempDir)) {
|
||||||
|
Files.createDirectories(tempDir);
|
||||||
|
}
|
||||||
|
tempDirPath = Files.createTempDirectory(tempDir, tempFiles.getPrefix());
|
||||||
|
} else {
|
||||||
|
tempDirPath = Files.createTempDirectory(tempFiles.getPrefix());
|
||||||
|
}
|
||||||
|
return registry.registerDirectory(tempDirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a MultipartFile to a temporary File and register it. This is a wrapper around
|
||||||
|
* GeneralUtils.convertMultipartFileToFile that ensures the created temp file is registered.
|
||||||
|
*
|
||||||
|
* @param multipartFile The MultipartFile to convert
|
||||||
|
* @return The created temporary file
|
||||||
|
* @throws IOException If an I/O error occurs
|
||||||
|
*/
|
||||||
|
public File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
|
||||||
|
File tempFile = GeneralUtils.convertMultipartFileToFile(multipartFile);
|
||||||
|
return registry.register(tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a temporary file and unregister it from the registry.
|
||||||
|
*
|
||||||
|
* @param file The file to delete
|
||||||
|
* @return true if the file was deleted successfully, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean deleteTempFile(File file) {
|
||||||
|
if (file != null && file.exists()) {
|
||||||
|
boolean deleted = file.delete();
|
||||||
|
if (deleted) {
|
||||||
|
registry.unregister(file);
|
||||||
|
log.debug("Deleted temp file: {}", file.getAbsolutePath());
|
||||||
|
} else {
|
||||||
|
log.warn("Failed to delete temp file: {}", file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a temporary file and unregister it from the registry.
|
||||||
|
*
|
||||||
|
* @param path The path to delete
|
||||||
|
* @return true if the file was deleted successfully, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean deleteTempFile(Path path) {
|
||||||
|
if (path != null) {
|
||||||
|
try {
|
||||||
|
boolean deleted = Files.deleteIfExists(path);
|
||||||
|
if (deleted) {
|
||||||
|
registry.unregister(path);
|
||||||
|
log.debug("Deleted temp file: {}", path.toString());
|
||||||
|
} else {
|
||||||
|
log.debug("Temp file already deleted or does not exist: {}", path.toString());
|
||||||
|
}
|
||||||
|
return deleted;
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to delete temp file: {}", path.toString(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a temporary directory and all its contents.
|
||||||
|
*
|
||||||
|
* @param directory The directory to delete
|
||||||
|
*/
|
||||||
|
public void deleteTempDirectory(Path directory) {
|
||||||
|
if (directory != null && Files.isDirectory(directory)) {
|
||||||
|
try {
|
||||||
|
GeneralUtils.deleteDirectory(directory);
|
||||||
|
log.debug("Deleted temp directory: {}", directory.toString());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to delete temp directory: {}", directory.toString(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an existing file with the registry.
|
||||||
|
*
|
||||||
|
* @param file The file to register
|
||||||
|
* @return The same file for method chaining
|
||||||
|
*/
|
||||||
|
public File register(File file) {
|
||||||
|
if (file != null && file.exists()) {
|
||||||
|
return registry.register(file);
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old temporary files based on age.
|
||||||
|
*
|
||||||
|
* @param maxAgeMillis Maximum age in milliseconds for temp files
|
||||||
|
* @return Number of files deleted
|
||||||
|
*/
|
||||||
|
public int cleanupOldTempFiles(long maxAgeMillis) {
|
||||||
|
int deletedCount = 0;
|
||||||
|
|
||||||
|
// Get files older than max age
|
||||||
|
Set<Path> oldFiles = registry.getFilesOlderThan(maxAgeMillis);
|
||||||
|
|
||||||
|
// Delete each old file
|
||||||
|
for (Path file : oldFiles) {
|
||||||
|
if (deleteTempFile(file)) {
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Cleaned up {} old temporary files", deletedCount);
|
||||||
|
return deletedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum age for temporary files in milliseconds.
|
||||||
|
*
|
||||||
|
* @return Maximum age in milliseconds
|
||||||
|
*/
|
||||||
|
public long getMaxAgeMillis() {
|
||||||
|
long maxAgeHours =
|
||||||
|
applicationProperties.getSystem().getTempFileManagement().getMaxAgeHours();
|
||||||
|
return Duration.ofHours(maxAgeHours).toMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique temporary file name with the Stirling-PDF prefix.
|
||||||
|
*
|
||||||
|
* @param type Type identifier for the temp file
|
||||||
|
* @param extension File extension (without the dot)
|
||||||
|
* @return A unique temporary file name
|
||||||
|
*/
|
||||||
|
public String generateTempFileName(String type, String extension) {
|
||||||
|
String tempFilePrefix =
|
||||||
|
applicationProperties.getSystem().getTempFileManagement().getPrefix();
|
||||||
|
String uuid = UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
return tempFilePrefix + type + "-" + uuid + "." + extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a known LibreOffice temporary directory. This is used when integrating with
|
||||||
|
* LibreOffice for file conversions.
|
||||||
|
*
|
||||||
|
* @return The LibreOffice temp directory
|
||||||
|
* @throws IOException If directory creation fails
|
||||||
|
*/
|
||||||
|
public Path registerLibreOfficeTempDir() throws IOException {
|
||||||
|
ApplicationProperties.TempFileManagement tempFiles =
|
||||||
|
applicationProperties.getSystem().getTempFileManagement();
|
||||||
|
Path loTempDir;
|
||||||
|
String libreOfficeTempDir = tempFiles.getLibreofficeDir();
|
||||||
|
String customTempDirectory = tempFiles.getBaseTmpDir();
|
||||||
|
|
||||||
|
// First check if explicitly configured
|
||||||
|
if (libreOfficeTempDir != null && !libreOfficeTempDir.isEmpty()) {
|
||||||
|
loTempDir = Path.of(libreOfficeTempDir);
|
||||||
|
}
|
||||||
|
// Next check if we have a custom temp directory
|
||||||
|
else if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
|
||||||
|
loTempDir = Path.of(customTempDirectory, "libreoffice");
|
||||||
|
}
|
||||||
|
// Fall back to system temp dir with our application prefix
|
||||||
|
else {
|
||||||
|
loTempDir = Path.of(System.getProperty("java.io.tmpdir"), "stirling-pdf-libreoffice");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.exists(loTempDir)) {
|
||||||
|
Files.createDirectories(loTempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
return registry.registerDirectory(loTempDir);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,176 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.concurrent.ConcurrentSkipListSet;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central registry for tracking temporary files created by Stirling-PDF. Maintains a thread-safe
|
||||||
|
* collection of paths with their creation timestamps.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class TempFileRegistry {
|
||||||
|
|
||||||
|
private final ConcurrentMap<Path, Instant> registeredFiles = new ConcurrentHashMap<>();
|
||||||
|
private final Set<Path> thirdPartyTempFiles =
|
||||||
|
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
private final Set<Path> tempDirectories =
|
||||||
|
Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a temporary file with the registry.
|
||||||
|
*
|
||||||
|
* @param file The temporary file to track
|
||||||
|
* @return The same file for method chaining
|
||||||
|
*/
|
||||||
|
public File register(File file) {
|
||||||
|
if (file != null) {
|
||||||
|
registeredFiles.put(file.toPath(), Instant.now());
|
||||||
|
log.debug("Registered temp file: {}", file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a temporary path with the registry.
|
||||||
|
*
|
||||||
|
* @param path The temporary path to track
|
||||||
|
* @return The same path for method chaining
|
||||||
|
*/
|
||||||
|
public Path register(Path path) {
|
||||||
|
if (path != null) {
|
||||||
|
registeredFiles.put(path, Instant.now());
|
||||||
|
log.debug("Registered temp path: {}", path.toString());
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a temporary directory to be cleaned up.
|
||||||
|
*
|
||||||
|
* @param directory Directory to register
|
||||||
|
* @return The same directory for method chaining
|
||||||
|
*/
|
||||||
|
public Path registerDirectory(Path directory) {
|
||||||
|
if (directory != null && Files.isDirectory(directory)) {
|
||||||
|
tempDirectories.add(directory);
|
||||||
|
log.debug("Registered temp directory: {}", directory.toString());
|
||||||
|
}
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a third-party temporary file that requires special handling.
|
||||||
|
*
|
||||||
|
* @param file The third-party temp file
|
||||||
|
* @return The same file for method chaining
|
||||||
|
*/
|
||||||
|
public File registerThirdParty(File file) {
|
||||||
|
if (file != null) {
|
||||||
|
thirdPartyTempFiles.add(file.toPath());
|
||||||
|
log.debug("Registered third-party temp file: {}", file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister a file from the registry.
|
||||||
|
*
|
||||||
|
* @param file The file to unregister
|
||||||
|
*/
|
||||||
|
public void unregister(File file) {
|
||||||
|
if (file != null) {
|
||||||
|
registeredFiles.remove(file.toPath());
|
||||||
|
thirdPartyTempFiles.remove(file.toPath());
|
||||||
|
log.debug("Unregistered temp file: {}", file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister a path from the registry.
|
||||||
|
*
|
||||||
|
* @param path The path to unregister
|
||||||
|
*/
|
||||||
|
public void unregister(Path path) {
|
||||||
|
if (path != null) {
|
||||||
|
registeredFiles.remove(path);
|
||||||
|
thirdPartyTempFiles.remove(path);
|
||||||
|
log.debug("Unregistered temp path: {}", path.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered temporary files.
|
||||||
|
*
|
||||||
|
* @return Set of registered file paths
|
||||||
|
*/
|
||||||
|
public Set<Path> getAllRegisteredFiles() {
|
||||||
|
return registeredFiles.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get temporary files older than the specified duration in milliseconds.
|
||||||
|
*
|
||||||
|
* @param maxAgeMillis Maximum age in milliseconds
|
||||||
|
* @return Set of paths older than the specified age
|
||||||
|
*/
|
||||||
|
public Set<Path> getFilesOlderThan(long maxAgeMillis) {
|
||||||
|
Instant cutoffTime = Instant.now().minusMillis(maxAgeMillis);
|
||||||
|
return registeredFiles.entrySet().stream()
|
||||||
|
.filter(entry -> entry.getValue().isBefore(cutoffTime))
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered third-party temporary files.
|
||||||
|
*
|
||||||
|
* @return Set of third-party file paths
|
||||||
|
*/
|
||||||
|
public Set<Path> getThirdPartyTempFiles() {
|
||||||
|
return thirdPartyTempFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered temporary directories.
|
||||||
|
*
|
||||||
|
* @return Set of temporary directory paths
|
||||||
|
*/
|
||||||
|
public Set<Path> getTempDirectories() {
|
||||||
|
return tempDirectories;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is registered in the registry.
|
||||||
|
*
|
||||||
|
* @param file The file to check
|
||||||
|
* @return True if the file is registered, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean contains(File file) {
|
||||||
|
if (file == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Path path = file.toPath();
|
||||||
|
return registeredFiles.containsKey(path) || thirdPartyTempFiles.contains(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear all registry data. */
|
||||||
|
public void clear() {
|
||||||
|
registeredFiles.clear();
|
||||||
|
thirdPartyTempFiles.clear();
|
||||||
|
tempDirectories.clear();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for handling temporary files with proper cleanup. Provides helper methods and
|
||||||
|
* wrappers to ensure temp files are properly cleaned up.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class TempFileUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of temporary files that implements AutoCloseable. All files in the collection
|
||||||
|
* are cleaned up when close() is called.
|
||||||
|
*/
|
||||||
|
public static class TempFileCollection implements AutoCloseable {
|
||||||
|
private final TempFileManager manager;
|
||||||
|
private final List<File> tempFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
public TempFileCollection(TempFileManager manager) {
|
||||||
|
this.manager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File addTempFile(String suffix) throws IOException {
|
||||||
|
File file = manager.createTempFile(suffix);
|
||||||
|
tempFiles.add(file);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<File> getFiles() {
|
||||||
|
return new ArrayList<>(tempFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
for (File file : tempFiles) {
|
||||||
|
manager.deleteTempFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a function with a temporary file, ensuring cleanup in a finally block.
|
||||||
|
*
|
||||||
|
* @param <R> The return type of the function
|
||||||
|
* @param tempFileManager The temp file manager
|
||||||
|
* @param suffix File suffix (e.g., ".pdf")
|
||||||
|
* @param function The function to execute with the temp file
|
||||||
|
* @return The result of the function
|
||||||
|
* @throws IOException If an I/O error occurs
|
||||||
|
*/
|
||||||
|
public static <R> R withTempFile(
|
||||||
|
TempFileManager tempFileManager, String suffix, Function<File, R> function)
|
||||||
|
throws IOException {
|
||||||
|
File tempFile = tempFileManager.createTempFile(suffix);
|
||||||
|
try {
|
||||||
|
return function.apply(tempFile);
|
||||||
|
} finally {
|
||||||
|
tempFileManager.deleteTempFile(tempFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a function with multiple temporary files, ensuring cleanup in a finally block.
|
||||||
|
*
|
||||||
|
* @param <R> The return type of the function
|
||||||
|
* @param tempFileManager The temp file manager
|
||||||
|
* @param count Number of temp files to create
|
||||||
|
* @param suffix File suffix (e.g., ".pdf")
|
||||||
|
* @param function The function to execute with the temp files
|
||||||
|
* @return The result of the function
|
||||||
|
* @throws IOException If an I/O error occurs
|
||||||
|
*/
|
||||||
|
public static <R> R withMultipleTempFiles(
|
||||||
|
TempFileManager tempFileManager,
|
||||||
|
int count,
|
||||||
|
String suffix,
|
||||||
|
Function<List<File>, R> function)
|
||||||
|
throws IOException {
|
||||||
|
List<File> tempFiles = new ArrayList<>(count);
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
tempFiles.add(tempFileManager.createTempFile(suffix));
|
||||||
|
}
|
||||||
|
return function.apply(tempFiles);
|
||||||
|
} finally {
|
||||||
|
for (File file : tempFiles) {
|
||||||
|
tempFileManager.deleteTempFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely delete a list of temporary files, logging any errors.
|
||||||
|
*
|
||||||
|
* @param files The list of files to delete
|
||||||
|
*/
|
||||||
|
public static void safeDeleteFiles(List<Path> files) {
|
||||||
|
if (files == null) return;
|
||||||
|
|
||||||
|
for (Path file : files) {
|
||||||
|
if (file == null) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(file);
|
||||||
|
log.debug("Deleted temp file: {}", file);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to delete temp file: {}", file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an already created temp file with the registry. Use this for files created outside
|
||||||
|
* of TempFileManager.
|
||||||
|
*
|
||||||
|
* @param tempFileManager The temp file manager
|
||||||
|
* @param file The file to register
|
||||||
|
* @return The registered file
|
||||||
|
*/
|
||||||
|
public static File registerExistingTempFile(TempFileManager tempFileManager, File file) {
|
||||||
|
if (tempFileManager != null && file != null && file.exists()) {
|
||||||
|
return tempFileManager.register(file);
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,464 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.common.util.TempFileManager;
|
||||||
|
import stirling.software.common.util.TempFileRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for the TempFileCleanupService, focusing on its pattern-matching and cleanup logic.
|
||||||
|
*/
|
||||||
|
public class TempFileCleanupServiceTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TempFileRegistry registry;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TempFileManager tempFileManager;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApplicationProperties.System system;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApplicationProperties.TempFileManagement tempFileManagement;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private TempFileCleanupService cleanupService;
|
||||||
|
|
||||||
|
private Path systemTempDir;
|
||||||
|
private Path customTempDir;
|
||||||
|
private Path libreOfficeTempDir;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() throws IOException {
|
||||||
|
MockitoAnnotations.openMocks(this);
|
||||||
|
|
||||||
|
// Create test directories
|
||||||
|
systemTempDir = tempDir.resolve("systemTemp");
|
||||||
|
customTempDir = tempDir.resolve("customTemp");
|
||||||
|
libreOfficeTempDir = tempDir.resolve("libreOfficeTemp");
|
||||||
|
|
||||||
|
Files.createDirectories(systemTempDir);
|
||||||
|
Files.createDirectories(customTempDir);
|
||||||
|
Files.createDirectories(libreOfficeTempDir);
|
||||||
|
|
||||||
|
// Configure ApplicationProperties mocks
|
||||||
|
when(applicationProperties.getSystem()).thenReturn(system);
|
||||||
|
when(system.getTempFileManagement()).thenReturn(tempFileManagement);
|
||||||
|
when(tempFileManagement.getBaseTmpDir()).thenReturn(customTempDir.toString());
|
||||||
|
when(tempFileManagement.getLibreofficeDir()).thenReturn(libreOfficeTempDir.toString());
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testScheduledCleanup_RegisteredFiles() {
|
||||||
|
// Arrange
|
||||||
|
when(tempFileManager.cleanupOldTempFiles(anyLong())).thenReturn(5); // 5 files deleted
|
||||||
|
Set<Path> registeredDirs = new HashSet<>();
|
||||||
|
registeredDirs.add(tempDir.resolve("registeredDir"));
|
||||||
|
when(registry.getTempDirectories()).thenReturn(registeredDirs);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
cleanupService.scheduledCleanup();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(tempFileManager).cleanupOldTempFiles(anyLong());
|
||||||
|
verify(registry, times(1)).getTempDirectories();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCleanupTempFilePatterns() throws IOException {
|
||||||
|
// Arrange - Create various temp files
|
||||||
|
Path ourTempFile1 = Files.createFile(systemTempDir.resolve("output_123.pdf"));
|
||||||
|
Path ourTempFile2 = Files.createFile(systemTempDir.resolve("compressedPDF456.pdf"));
|
||||||
|
Path ourTempFile3 = Files.createFile(customTempDir.resolve("stirling-pdf-789.tmp"));
|
||||||
|
Path ourTempFile4 = Files.createFile(customTempDir.resolve("pdf-save-123-456.tmp"));
|
||||||
|
Path ourTempFile5 = Files.createFile(libreOfficeTempDir.resolve("input_file.pdf"));
|
||||||
|
|
||||||
|
// Old temporary files
|
||||||
|
Path oldTempFile = Files.createFile(systemTempDir.resolve("output_old.pdf"));
|
||||||
|
|
||||||
|
// System temp files that should be cleaned in container mode
|
||||||
|
Path sysTempFile1 = Files.createFile(systemTempDir.resolve("lu123abc.tmp"));
|
||||||
|
Path sysTempFile2 = Files.createFile(customTempDir.resolve("ocr_process123"));
|
||||||
|
Path sysTempFile3 = Files.createFile(customTempDir.resolve("tmp_upload.tmp"));
|
||||||
|
|
||||||
|
// Files that should be preserved
|
||||||
|
Path jettyFile1 = Files.createFile(systemTempDir.resolve("jetty-123.tmp"));
|
||||||
|
Path jettyFile2 = Files.createFile(systemTempDir.resolve("something-with-jetty-inside.tmp"));
|
||||||
|
Path regularFile = Files.createFile(systemTempDir.resolve("important.txt"));
|
||||||
|
|
||||||
|
// Create a nested directory with temp files
|
||||||
|
Path nestedDir = Files.createDirectories(systemTempDir.resolve("nested"));
|
||||||
|
Path nestedTempFile = Files.createFile(nestedDir.resolve("output_nested.pdf"));
|
||||||
|
|
||||||
|
// Empty file (special case)
|
||||||
|
Path emptyFile = Files.createFile(systemTempDir.resolve("empty.tmp"));
|
||||||
|
|
||||||
|
// Configure mock registry to say these files aren't registered
|
||||||
|
when(registry.contains(any(File.class))).thenReturn(false);
|
||||||
|
|
||||||
|
// The set of files that will be deleted in our test
|
||||||
|
Set<Path> deletedFiles = new HashSet<>();
|
||||||
|
|
||||||
|
// Use MockedStatic to mock Files operations
|
||||||
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Mock Files.list for each directory we'll process
|
||||||
|
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||||
|
.thenReturn(Stream.of(
|
||||||
|
ourTempFile1, ourTempFile2, oldTempFile, sysTempFile1,
|
||||||
|
jettyFile1, jettyFile2, regularFile, emptyFile, nestedDir));
|
||||||
|
|
||||||
|
mockedFiles.when(() -> Files.list(eq(customTempDir)))
|
||||||
|
.thenReturn(Stream.of(ourTempFile3, ourTempFile4, sysTempFile2, sysTempFile3));
|
||||||
|
|
||||||
|
mockedFiles.when(() -> Files.list(eq(libreOfficeTempDir)))
|
||||||
|
.thenReturn(Stream.of(ourTempFile5));
|
||||||
|
|
||||||
|
mockedFiles.when(() -> Files.list(eq(nestedDir)))
|
||||||
|
.thenReturn(Stream.of(nestedTempFile));
|
||||||
|
|
||||||
|
// Configure Files.isDirectory for each path
|
||||||
|
mockedFiles.when(() -> Files.isDirectory(eq(nestedDir))).thenReturn(true);
|
||||||
|
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||||
|
|
||||||
|
// Configure Files.exists to return true for all paths
|
||||||
|
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||||
|
|
||||||
|
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||||
|
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
|
// For files with "old" in the name, return a timestamp older than maxAgeMillis
|
||||||
|
if (fileName.contains("old")) {
|
||||||
|
return FileTime.fromMillis(System.currentTimeMillis() - 5000000);
|
||||||
|
}
|
||||||
|
// For empty.tmp file, return a timestamp older than 5 minutes (for empty file test)
|
||||||
|
else if (fileName.equals("empty.tmp")) {
|
||||||
|
return FileTime.fromMillis(System.currentTimeMillis() - 6 * 60 * 1000);
|
||||||
|
}
|
||||||
|
// For all other files, return a recent timestamp
|
||||||
|
else {
|
||||||
|
return FileTime.fromMillis(System.currentTimeMillis() - 60000); // 1 minute ago
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure Files.size to return different sizes based on file names
|
||||||
|
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
|
// Return 0 bytes for the empty file
|
||||||
|
if (fileName.equals("empty.tmp")) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
// Return normal size for all other files
|
||||||
|
else {
|
||||||
|
return 1024L; // 1 KB
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// For deleteIfExists, track which files would be deleted
|
||||||
|
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
deletedFiles.add(path);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act - set containerMode to false for this test
|
||||||
|
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||||
|
invokeCleanupDirectoryStreaming(customTempDir, false, 0, 3600000);
|
||||||
|
invokeCleanupDirectoryStreaming(libreOfficeTempDir, false, 0, 3600000);
|
||||||
|
|
||||||
|
// Assert - Only old temp files and empty files should be deleted
|
||||||
|
assertTrue(deletedFiles.contains(oldTempFile), "Old temp file should be deleted");
|
||||||
|
assertTrue(deletedFiles.contains(emptyFile), "Empty file should be deleted");
|
||||||
|
|
||||||
|
// Regular temp files should not be deleted because they're too new
|
||||||
|
assertFalse(deletedFiles.contains(ourTempFile1), "Recent temp file should be preserved");
|
||||||
|
assertFalse(deletedFiles.contains(ourTempFile2), "Recent temp file should be preserved");
|
||||||
|
assertFalse(deletedFiles.contains(ourTempFile3), "Recent temp file should be preserved");
|
||||||
|
assertFalse(deletedFiles.contains(ourTempFile4), "Recent temp file should be preserved");
|
||||||
|
assertFalse(deletedFiles.contains(ourTempFile5), "Recent temp file should be preserved");
|
||||||
|
|
||||||
|
// System temp files should not be deleted in non-container mode
|
||||||
|
assertFalse(deletedFiles.contains(sysTempFile1), "System temp file should be preserved in non-container mode");
|
||||||
|
assertFalse(deletedFiles.contains(sysTempFile2), "System temp file should be preserved in non-container mode");
|
||||||
|
assertFalse(deletedFiles.contains(sysTempFile3), "System temp file should be preserved in non-container mode");
|
||||||
|
|
||||||
|
// Jetty files and regular files should never be deleted
|
||||||
|
assertFalse(deletedFiles.contains(jettyFile1), "Jetty file should be preserved");
|
||||||
|
assertFalse(deletedFiles.contains(jettyFile2), "File with jetty in name should be preserved");
|
||||||
|
assertFalse(deletedFiles.contains(regularFile), "Regular file should be preserved");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testContainerModeCleanup() throws IOException {
|
||||||
|
// Arrange - Create various temp files
|
||||||
|
Path ourTempFile = Files.createFile(systemTempDir.resolve("output_123.pdf"));
|
||||||
|
Path sysTempFile = Files.createFile(systemTempDir.resolve("lu123abc.tmp"));
|
||||||
|
Path regularFile = Files.createFile(systemTempDir.resolve("important.txt"));
|
||||||
|
|
||||||
|
// Configure mock registry to say these files aren't registered
|
||||||
|
when(registry.contains(any(File.class))).thenReturn(false);
|
||||||
|
|
||||||
|
// The set of files that will be deleted in our test
|
||||||
|
Set<Path> deletedFiles = new HashSet<>();
|
||||||
|
|
||||||
|
// Use MockedStatic to mock Files operations
|
||||||
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Mock Files.list for systemTempDir
|
||||||
|
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||||
|
.thenReturn(Stream.of(ourTempFile, sysTempFile, regularFile));
|
||||||
|
|
||||||
|
// Configure Files.isDirectory
|
||||||
|
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||||
|
|
||||||
|
// Configure Files.exists
|
||||||
|
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||||
|
|
||||||
|
// Configure Files.getLastModifiedTime to return recent timestamps
|
||||||
|
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
|
.thenReturn(FileTime.fromMillis(System.currentTimeMillis() - 60000)); // 1 minute ago
|
||||||
|
|
||||||
|
// Configure Files.size to return normal size
|
||||||
|
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||||
|
.thenReturn(1024L); // 1 KB
|
||||||
|
|
||||||
|
// For deleteIfExists, track which files would be deleted
|
||||||
|
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
deletedFiles.add(path);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act - set containerMode to true and maxAgeMillis to 0 for container startup cleanup
|
||||||
|
invokeCleanupDirectoryStreaming(systemTempDir, true, 0, 0);
|
||||||
|
|
||||||
|
// Assert - In container mode, both our temp files and system temp files should be deleted
|
||||||
|
// regardless of age (when maxAgeMillis is 0)
|
||||||
|
assertTrue(deletedFiles.contains(ourTempFile), "Our temp file should be deleted in container mode");
|
||||||
|
assertTrue(deletedFiles.contains(sysTempFile), "System temp file should be deleted in container mode");
|
||||||
|
assertFalse(deletedFiles.contains(regularFile), "Regular file should be preserved");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmptyFileHandling() throws IOException {
|
||||||
|
// Arrange - Create an empty file
|
||||||
|
Path emptyFile = Files.createFile(systemTempDir.resolve("empty.tmp"));
|
||||||
|
Path recentEmptyFile = Files.createFile(systemTempDir.resolve("recent_empty.tmp"));
|
||||||
|
|
||||||
|
// Configure mock registry to say these files aren't registered
|
||||||
|
when(registry.contains(any(File.class))).thenReturn(false);
|
||||||
|
|
||||||
|
// The set of files that will be deleted in our test
|
||||||
|
Set<Path> deletedFiles = new HashSet<>();
|
||||||
|
|
||||||
|
// Use MockedStatic to mock Files operations
|
||||||
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Mock Files.list for systemTempDir
|
||||||
|
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||||
|
.thenReturn(Stream.of(emptyFile, recentEmptyFile));
|
||||||
|
|
||||||
|
// Configure Files.isDirectory
|
||||||
|
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||||
|
|
||||||
|
// Configure Files.exists
|
||||||
|
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||||
|
|
||||||
|
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||||
|
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
|
if (fileName.equals("empty.tmp")) {
|
||||||
|
// More than 5 minutes old
|
||||||
|
return FileTime.fromMillis(System.currentTimeMillis() - 6 * 60 * 1000);
|
||||||
|
} else {
|
||||||
|
// Less than 5 minutes old
|
||||||
|
return FileTime.fromMillis(System.currentTimeMillis() - 2 * 60 * 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure Files.size to return 0 for empty files
|
||||||
|
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||||
|
.thenReturn(0L);
|
||||||
|
|
||||||
|
// For deleteIfExists, track which files would be deleted
|
||||||
|
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
deletedFiles.add(path);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(deletedFiles.contains(emptyFile),
|
||||||
|
"Empty file older than 5 minutes should be deleted");
|
||||||
|
assertFalse(deletedFiles.contains(recentEmptyFile),
|
||||||
|
"Empty file newer than 5 minutes should not be deleted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecursiveDirectoryCleaning() throws IOException {
|
||||||
|
// Arrange - Create a nested directory structure with temp files
|
||||||
|
Path dir1 = Files.createDirectories(systemTempDir.resolve("dir1"));
|
||||||
|
Path dir2 = Files.createDirectories(dir1.resolve("dir2"));
|
||||||
|
Path dir3 = Files.createDirectories(dir2.resolve("dir3"));
|
||||||
|
|
||||||
|
Path tempFile1 = Files.createFile(dir1.resolve("output_1.pdf"));
|
||||||
|
Path tempFile2 = Files.createFile(dir2.resolve("output_2.pdf"));
|
||||||
|
Path tempFile3 = Files.createFile(dir3.resolve("output_old_3.pdf"));
|
||||||
|
|
||||||
|
// Configure mock registry to say these files aren't registered
|
||||||
|
when(registry.contains(any(File.class))).thenReturn(false);
|
||||||
|
|
||||||
|
// The set of files that will be deleted in our test
|
||||||
|
Set<Path> deletedFiles = new HashSet<>();
|
||||||
|
|
||||||
|
// Use MockedStatic to mock Files operations
|
||||||
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Mock Files.list for each directory
|
||||||
|
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||||
|
.thenReturn(Stream.of(dir1));
|
||||||
|
|
||||||
|
mockedFiles.when(() -> Files.list(eq(dir1)))
|
||||||
|
.thenReturn(Stream.of(tempFile1, dir2));
|
||||||
|
|
||||||
|
mockedFiles.when(() -> Files.list(eq(dir2)))
|
||||||
|
.thenReturn(Stream.of(tempFile2, dir3));
|
||||||
|
|
||||||
|
mockedFiles.when(() -> Files.list(eq(dir3)))
|
||||||
|
.thenReturn(Stream.of(tempFile3));
|
||||||
|
|
||||||
|
// Configure Files.isDirectory for each path
|
||||||
|
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
||||||
|
mockedFiles.when(() -> Files.isDirectory(eq(dir2))).thenReturn(true);
|
||||||
|
mockedFiles.when(() -> Files.isDirectory(eq(dir3))).thenReturn(true);
|
||||||
|
mockedFiles.when(() -> Files.isDirectory(eq(tempFile1))).thenReturn(false);
|
||||||
|
mockedFiles.when(() -> Files.isDirectory(eq(tempFile2))).thenReturn(false);
|
||||||
|
mockedFiles.when(() -> Files.isDirectory(eq(tempFile3))).thenReturn(false);
|
||||||
|
|
||||||
|
// Configure Files.exists to return true for all paths
|
||||||
|
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||||
|
|
||||||
|
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||||
|
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
|
if (fileName.contains("old")) {
|
||||||
|
// Old file
|
||||||
|
return FileTime.fromMillis(System.currentTimeMillis() - 5000000);
|
||||||
|
} else {
|
||||||
|
// Recent file
|
||||||
|
return FileTime.fromMillis(System.currentTimeMillis() - 60000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure Files.size to return normal size
|
||||||
|
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||||
|
.thenReturn(1024L);
|
||||||
|
|
||||||
|
// For deleteIfExists, track which files would be deleted
|
||||||
|
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||||
|
.thenAnswer(invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
deletedFiles.add(path);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||||
|
|
||||||
|
// Debug - print what was deleted
|
||||||
|
System.out.println("Deleted files: " + deletedFiles);
|
||||||
|
System.out.println("Looking for: " + tempFile3);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertFalse(deletedFiles.contains(tempFile1), "Recent temp file should be preserved");
|
||||||
|
assertFalse(deletedFiles.contains(tempFile2), "Recent temp file should be preserved");
|
||||||
|
assertTrue(deletedFiles.contains(tempFile3), "Old temp file in nested directory should be deleted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to invoke the private cleanupDirectoryStreaming method using reflection
|
||||||
|
*/
|
||||||
|
private void invokeCleanupDirectoryStreaming(Path directory, boolean containerMode, int depth, long maxAgeMillis)
|
||||||
|
throws IOException {
|
||||||
|
try {
|
||||||
|
// Create a consumer that tracks deleted files
|
||||||
|
AtomicInteger deleteCount = new AtomicInteger(0);
|
||||||
|
Consumer<Path> deleteCallback = path -> deleteCount.incrementAndGet();
|
||||||
|
|
||||||
|
// Get the method with updated signature
|
||||||
|
var method = TempFileCleanupService.class.getDeclaredMethod(
|
||||||
|
"cleanupDirectoryStreaming",
|
||||||
|
Path.class, boolean.class, int.class, long.class, boolean.class, Consumer.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
|
||||||
|
// Invoke the method with appropriate parameters
|
||||||
|
method.invoke(cleanupService, directory, containerMode, depth, maxAgeMillis, false, deleteCallback);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error invoking cleanupDirectoryStreaming", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matcher for exact path equality
|
||||||
|
private static Path eq(Path path) {
|
||||||
|
return argThat(arg -> arg != null && arg.equals(path));
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 "$@"
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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("[.][^.]+$", "")
|
||||||
|
@ -2,7 +2,6 @@ package stirling.software.SPDF.controller.api.misc;
|
|||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
@ -23,7 +22,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.BoundedLineReader;
|
|
||||||
import io.github.pixee.security.Filenames;
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@ -34,6 +32,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.common.util.TempFileManager;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/misc")
|
@RequestMapping("/api/v1/misc")
|
||||||
@ -43,8 +44,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
|||||||
public class OCRController {
|
public class OCRController {
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
/** Gets the list of available Tesseract languages from the tessdata directory */
|
/** Gets the list of available Tesseract languages from the tessdata directory */
|
||||||
public List<String> getAvailableTesseractLanguages() {
|
public List<String> getAvailableTesseractLanguages() {
|
||||||
@ -73,93 +74,117 @@ public class OCRController {
|
|||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
List<String> languages = request.getLanguages();
|
List<String> languages = request.getLanguages();
|
||||||
String ocrType = request.getOcrType();
|
String ocrType = request.getOcrType();
|
||||||
Path tempDir = Files.createTempDirectory("ocr_process");
|
|
||||||
Path tempInputFile = tempDir.resolve("input.pdf");
|
// Create a temp directory using TempFileManager directly
|
||||||
Path tempOutputDir = tempDir.resolve("output");
|
Path tempDirPath = tempFileManager.createTempDirectory();
|
||||||
Path tempImagesDir = tempDir.resolve("images");
|
File tempDir = tempDirPath.toFile();
|
||||||
Path finalOutputFile = tempDir.resolve("final_output.pdf");
|
|
||||||
Files.createDirectories(tempOutputDir);
|
|
||||||
Files.createDirectories(tempImagesDir);
|
|
||||||
Process process = null;
|
|
||||||
try {
|
try {
|
||||||
|
File tempInputFile = new File(tempDir, "input.pdf");
|
||||||
|
File tempOutputDir = new File(tempDir, "output");
|
||||||
|
File tempImagesDir = new File(tempDir, "images");
|
||||||
|
File finalOutputFile = new File(tempDir, "final_output.pdf");
|
||||||
|
|
||||||
|
// Create directories
|
||||||
|
tempOutputDir.mkdirs();
|
||||||
|
tempImagesDir.mkdirs();
|
||||||
|
|
||||||
// Save input file
|
// Save input file
|
||||||
inputFile.transferTo(tempInputFile.toFile());
|
inputFile.transferTo(tempInputFile);
|
||||||
|
|
||||||
PDFMergerUtility merger = new PDFMergerUtility();
|
PDFMergerUtility merger = new PDFMergerUtility();
|
||||||
merger.setDestinationFileName(finalOutputFile.toString());
|
merger.setDestinationFileName(finalOutputFile.toString());
|
||||||
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) {
|
|
||||||
|
try (PDDocument document = pdfDocumentFactory.load(tempInputFile)) {
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
int pageCount = document.getNumberOfPages();
|
int pageCount = document.getNumberOfPages();
|
||||||
|
|
||||||
for (int pageNum = 0; pageNum < pageCount; pageNum++) {
|
for (int pageNum = 0; pageNum < pageCount; pageNum++) {
|
||||||
PDPage page = document.getPage(pageNum);
|
PDPage page = document.getPage(pageNum);
|
||||||
boolean hasText = false;
|
boolean hasText = false;
|
||||||
|
|
||||||
// Check for existing text
|
// Check for existing text
|
||||||
try (PDDocument tempDoc = new PDDocument()) {
|
try (PDDocument tempDoc = new PDDocument()) {
|
||||||
tempDoc.addPage(page);
|
tempDoc.addPage(page);
|
||||||
PDFTextStripper stripper = new PDFTextStripper();
|
PDFTextStripper stripper = new PDFTextStripper();
|
||||||
hasText = !stripper.getText(tempDoc).trim().isEmpty();
|
hasText = !stripper.getText(tempDoc).trim().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean shouldOcr =
|
boolean shouldOcr =
|
||||||
switch (ocrType) {
|
switch (ocrType) {
|
||||||
case "skip-text" -> !hasText;
|
case "skip-text" -> !hasText;
|
||||||
case "force-ocr" -> true;
|
case "force-ocr" -> true;
|
||||||
default -> true;
|
default -> true;
|
||||||
};
|
};
|
||||||
Path pageOutputPath =
|
|
||||||
tempOutputDir.resolve(String.format("page_%d.pdf", pageNum));
|
File pageOutputPath =
|
||||||
|
new File(tempOutputDir, String.format("page_%d.pdf", pageNum));
|
||||||
|
|
||||||
if (shouldOcr) {
|
if (shouldOcr) {
|
||||||
// Convert page to image
|
// Convert page to image
|
||||||
BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300);
|
BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300);
|
||||||
Path imagePath =
|
File imagePath =
|
||||||
tempImagesDir.resolve(String.format("page_%d.png", pageNum));
|
new File(tempImagesDir, String.format("page_%d.png", pageNum));
|
||||||
ImageIO.write(image, "png", imagePath.toFile());
|
ImageIO.write(image, "png", imagePath);
|
||||||
|
|
||||||
// Build OCR command
|
// Build OCR command
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add("tesseract");
|
command.add("tesseract");
|
||||||
command.add(imagePath.toString());
|
command.add(imagePath.toString());
|
||||||
command.add(
|
command.add(
|
||||||
tempOutputDir
|
new File(tempOutputDir, String.format("page_%d", pageNum))
|
||||||
.resolve(String.format("page_%d", pageNum))
|
|
||||||
.toString());
|
.toString());
|
||||||
command.add("-l");
|
command.add("-l");
|
||||||
command.add(String.join("+", languages));
|
command.add(String.join("+", languages));
|
||||||
// Always output PDF
|
// Always output PDF
|
||||||
command.add("pdf");
|
command.add("pdf");
|
||||||
ProcessBuilder pb = new ProcessBuilder(command);
|
|
||||||
process = pb.start();
|
// Use ProcessExecutor to run tesseract command
|
||||||
// Capture any error output
|
try {
|
||||||
try (BufferedReader reader =
|
ProcessExecutorResult result =
|
||||||
new BufferedReader(
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.TESSERACT)
|
||||||
new InputStreamReader(process.getErrorStream()))) {
|
.runCommandWithOutputHandling(command);
|
||||||
String line;
|
|
||||||
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) {
|
log.debug(
|
||||||
log.debug("Tesseract: {}", line);
|
"Tesseract OCR completed for page {} with exit code {}",
|
||||||
|
pageNum,
|
||||||
|
result.getRc());
|
||||||
|
|
||||||
|
// Add OCR'd PDF to merger
|
||||||
|
merger.addSource(pageOutputPath);
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
log.error(
|
||||||
|
"Error processing page {} with tesseract: {}",
|
||||||
|
pageNum,
|
||||||
|
e.getMessage());
|
||||||
|
// If OCR fails, fall back to the original page
|
||||||
|
try (PDDocument pageDoc = new PDDocument()) {
|
||||||
|
pageDoc.addPage(page);
|
||||||
|
pageDoc.save(pageOutputPath);
|
||||||
|
merger.addSource(pageOutputPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int exitCode = process.waitFor();
|
|
||||||
if (exitCode != 0) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Tesseract failed with exit code: " + exitCode);
|
|
||||||
}
|
|
||||||
// Add OCR'd PDF to merger
|
|
||||||
merger.addSource(pageOutputPath.toFile());
|
|
||||||
} else {
|
} else {
|
||||||
// Save original page without OCR
|
// Save original page without OCR
|
||||||
try (PDDocument pageDoc = new PDDocument()) {
|
try (PDDocument pageDoc = new PDDocument()) {
|
||||||
pageDoc.addPage(page);
|
pageDoc.addPage(page);
|
||||||
pageDoc.save(pageOutputPath.toFile());
|
pageDoc.save(pageOutputPath);
|
||||||
merger.addSource(pageOutputPath.toFile());
|
merger.addSource(pageOutputPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge all pages into final PDF
|
// Merge all pages into final PDF
|
||||||
merger.mergeDocuments(null);
|
merger.mergeDocuments(null);
|
||||||
|
|
||||||
// Read the final PDF file
|
// Read the final PDF file
|
||||||
byte[] pdfContent = Files.readAllBytes(finalOutputFile);
|
byte[] pdfContent = java.nio.file.Files.readAllBytes(finalOutputFile.toPath());
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_OCR.pdf";
|
+ "_OCR.pdf";
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(
|
.header(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
@ -167,11 +192,8 @@ public class OCRController {
|
|||||||
.contentType(MediaType.APPLICATION_PDF)
|
.contentType(MediaType.APPLICATION_PDF)
|
||||||
.body(pdfContent);
|
.body(pdfContent);
|
||||||
} finally {
|
} finally {
|
||||||
if (process != null) {
|
// Clean up the temp directory and all its contents
|
||||||
process.destroy();
|
tempFileManager.deleteTempDirectory(tempDirPath);
|
||||||
}
|
|
||||||
// Clean up temporary files
|
|
||||||
deleteDirectory(tempDir);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,21 +214,4 @@ public class OCRController {
|
|||||||
zipOut.closeEntry();
|
zipOut.closeEntry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteDirectory(Path directory) {
|
|
||||||
try {
|
|
||||||
Files.walk(directory)
|
|
||||||
.sorted(Comparator.reverseOrder())
|
|
||||||
.forEach(
|
|
||||||
path -> {
|
|
||||||
try {
|
|
||||||
Files.delete(path);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Error deleting {}: {}", path, e.getMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Error walking directory {}: {}", directory, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -23,6 +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.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -32,6 +32,7 @@ import stirling.software.common.util.WebResponseUtils;
|
|||||||
public class RepairController {
|
public class RepairController {
|
||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/repair")
|
@PostMapping(consumes = "multipart/form-data", value = "/repair")
|
||||||
@Operation(
|
@Operation(
|
||||||
@ -43,25 +44,25 @@ public class RepairController {
|
|||||||
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile file)
|
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile file)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
MultipartFile inputFile = file.getFileInput();
|
MultipartFile inputFile = file.getFileInput();
|
||||||
// Save the uploaded file to a temporary location
|
|
||||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
// Use TempFile with try-with-resources for automatic cleanup
|
||||||
byte[] pdfBytes = null;
|
try (TempFile tempFile = new TempFile(tempFileManager, ".pdf")) {
|
||||||
inputFile.transferTo(tempInputFile.toFile());
|
// Save the uploaded file to the temporary location
|
||||||
try {
|
inputFile.transferTo(tempFile.getFile());
|
||||||
|
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add("qpdf");
|
command.add("qpdf");
|
||||||
command.add("--replace-input"); // Automatically fixes problems it can
|
command.add("--replace-input"); // Automatically fixes problems it can
|
||||||
command.add("--qdf"); // Linearizes and normalizes PDF structure
|
command.add("--qdf"); // Linearizes and normalizes PDF structure
|
||||||
command.add("--object-streams=disable"); // Can help with some corruptions
|
command.add("--object-streams=disable"); // Can help with some corruptions
|
||||||
command.add(tempInputFile.toString());
|
command.add(tempFile.getFile().getAbsolutePath());
|
||||||
|
|
||||||
ProcessExecutorResult returnCode =
|
ProcessExecutorResult returnCode =
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
|
||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
// Read the optimized PDF file
|
// Read the optimized PDF file
|
||||||
pdfBytes = pdfDocumentFactory.loadToBytes(tempInputFile.toFile());
|
byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempFile.getFile());
|
||||||
|
|
||||||
// Return the optimized PDF as a response
|
// Return the optimized PDF as a response
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
@ -69,9 +70,6 @@ public class RepairController {
|
|||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_repaired.pdf";
|
+ "_repaired.pdf";
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
} finally {
|
|
||||||
// Clean up the temporary files
|
|
||||||
Files.deleteIfExists(tempInputFile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import java.io.File;
|
|||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
@ -40,6 +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.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -49,6 +50,7 @@ import stirling.software.common.util.WebResponseUtils;
|
|||||||
public class StampController {
|
public class StampController {
|
||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-stamp")
|
@PostMapping(consumes = "multipart/form-data", value = "/add-stamp")
|
||||||
@Operation(
|
@Operation(
|
||||||
@ -188,14 +190,14 @@ public class StampController {
|
|||||||
if (!"".equals(resourceDir)) {
|
if (!"".equals(resourceDir)) {
|
||||||
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
|
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
|
||||||
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
|
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
|
||||||
File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
|
|
||||||
try (InputStream is = classPathResource.getInputStream();
|
// Use TempFile with try-with-resources for automatic cleanup
|
||||||
FileOutputStream os = new FileOutputStream(tempFile)) {
|
try (TempFile tempFileWrapper = new TempFile(tempFileManager, fileExtension)) {
|
||||||
IOUtils.copy(is, os);
|
File tempFile = tempFileWrapper.getFile();
|
||||||
font = PDType0Font.load(document, tempFile);
|
try (InputStream is = classPathResource.getInputStream();
|
||||||
} finally {
|
FileOutputStream os = new FileOutputStream(tempFile)) {
|
||||||
if (tempFile != null) {
|
IOUtils.copy(is, os);
|
||||||
Files.deleteIfExists(tempFile.toPath());
|
font = PDType0Font.load(document, tempFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,4 +44,7 @@ springdoc.swagger-ui.path=/index.html
|
|||||||
posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
|
posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
|
||||||
posthog.host=https://eu.i.posthog.com
|
posthog.host=https://eu.i.posthog.com
|
||||||
|
|
||||||
spring.main.allow-bean-definition-overriding=true
|
spring.main.allow-bean-definition-overriding=true
|
||||||
|
|
||||||
|
# Set up a consistent temporary directory location
|
||||||
|
java.io.tmpdir=${stirling.tempfiles.directory:${java.io.tmpdir}/stirling-pdf}
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user