tmp file clenaup

This commit is contained in:
Anthony Stirling 2025-05-29 12:38:44 +01:00
parent 2217cfb95d
commit ca1100125d
18 changed files with 2096 additions and 29 deletions

View File

@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(chmod:*)",
"Bash(mkdir:*)"
],
"deny": []
}
}

View File

@ -0,0 +1,96 @@
package stirling.software.common.config;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.util.TempFileRegistry;
/**
* Configuration for the temporary file management system. Sets up the necessary beans and
* configures system properties.
*/
@Slf4j
@Configuration
public class TempFileConfiguration {
@Value("${stirling.tempfiles.directory:}")
private String customTempDirectory;
@Autowired
@Qualifier("machineType")
private String machineType;
@Value("${stirling.tempfiles.prefix:stirling-pdf-}")
private String tempFilePrefix;
/**
* Create the TempFileRegistry bean.
*
* @return A new TempFileRegistry instance
*/
@Bean
public TempFileRegistry tempFileRegistry() {
return new TempFileRegistry();
}
@PostConstruct
public void initTempFileConfig() {
try {
// If a custom temp directory is specified in the config, use it
if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
Path tempDir = Path.of(customTempDirectory);
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
log.info("Created custom temporary directory: {}", tempDir);
}
// Set Java temp directory system property if in Docker/Kubernetes mode
if ("Docker".equals(machineType) || "Kubernetes".equals(machineType)) {
System.setProperty("java.io.tmpdir", customTempDirectory);
log.info(
"Set system temp directory to: {} for environment: {}",
customTempDirectory,
machineType);
}
} else {
// No custom directory specified, use java.io.tmpdir + application subfolder
String defaultTempDir;
if ("Docker".equals(machineType) || "Kubernetes".equals(machineType)) {
// Container environments should continue to use /tmp/stirling-pdf
defaultTempDir = "/tmp/stirling-pdf";
} else {
// Use system temp directory (java.io.tmpdir) with our application subfolder
// This automatically handles Windows (AppData\Local\Temp), macOS, and Linux systems
defaultTempDir = System.getProperty("java.io.tmpdir") + File.separator + "stirling-pdf";
}
customTempDirectory = defaultTempDir;
// Create the default temp directory
Path tempDir = Path.of(customTempDirectory);
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
log.info("Created default OS-specific temporary directory: {}", tempDir);
}
}
log.info("Temporary file configuration initialized");
log.info("Using temp directory: {}", customTempDirectory);
log.info("Temp file prefix: {}", tempFilePrefix);
} catch (Exception e) {
log.error("Failed to initialize temporary file configuration", e);
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,453 @@
package stirling.software.common.service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
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
public class TempFileCleanupService {
private final TempFileRegistry registry;
private final TempFileManager tempFileManager;
@Value("${stirling.tempfiles.cleanup-interval-minutes:30}")
private long cleanupIntervalMinutes;
@Value("${stirling.tempfiles.startup-cleanup:true}")
private boolean performStartupCleanup;
@Autowired
@Qualifier("machineType")
private String machineType;
@Value("${stirling.tempfiles.system-temp-dir:/tmp}")
private String systemTempDir;
@Value("${stirling.tempfiles.directory:/tmp/stirling-pdf}")
private String customTempDirectory;
@Value("${stirling.tempfiles.libreoffice-dir:/tmp/stirling-pdf/libreoffice}")
private String libreOfficeTempDir;
@Autowired
public TempFileCleanupService(TempFileRegistry registry, TempFileManager tempFileManager) {
this.registry = registry;
this.tempFileManager = tempFileManager;
// Create necessary directories
ensureDirectoriesExist();
// Perform startup cleanup if enabled
if (performStartupCleanup) {
runStartupCleanup();
}
}
/** Ensure that all required temp directories exist */
private void ensureDirectoriesExist() {
try {
// Create the main temp directory if specified
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 if specified
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 = "${stirling.tempfiles.cleanup-interval-minutes:60}",
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);
}
}
int unregisteredDeletedCount = 0;
try {
// Get all directories we need to clean
Path systemTempPath;
if (systemTempDir != null && !systemTempDir.isEmpty()) {
systemTempPath = Path.of(systemTempDir);
} else {
systemTempPath = Path.of(System.getProperty("java.io.tmpdir"));
}
Path[] dirsToScan = {
systemTempPath, Path.of(customTempDirectory), Path.of(libreOfficeTempDir)
};
boolean containerMode =
"Docker".equals(machineType) || "Kubernetes".equals(machineType);
// Process each directory
for (Path tempDir : dirsToScan) {
if (!Files.exists(tempDir)) {
continue;
}
int dirDeletedCount = cleanupDirectory(tempDir, containerMode, 0, maxAgeMillis);
unregisteredDeletedCount += dirDeletedCount;
if (dirDeletedCount > 0) {
log.info(
"Cleaned up {} unregistered files/directories in {}",
dirDeletedCount,
tempDir);
}
}
} catch (IOException e) {
log.error("Error during scheduled cleanup of unregistered files", e);
}
log.info(
"Scheduled cleanup complete. Deleted {} registered files, {} unregistered files, {} directories",
registeredDeletedCount,
unregisteredDeletedCount,
directoriesDeletedCount);
}
/** Overload of cleanupDirectory that uses the specified max age for files */
private int cleanupDirectory(
Path directory, boolean containerMode, int depth, long maxAgeMillis)
throws IOException {
if (depth > 5) {
log.warn("Maximum directory recursion depth reached for: {}", directory);
return 0;
}
int deletedCount = 0;
try (Stream<Path> paths = Files.list(directory)) {
for (Path path : paths.toList()) {
String fileName = path.getFileName().toString();
// Skip registered files - these are handled by TempFileManager
if (registry.contains(path.toFile())) {
continue;
}
// Skip Jetty-related directories and files
if (fileName.contains("jetty") || fileName.startsWith("jetty-")) {
continue;
}
// Check if this is a directory we should recursively scan
if (Files.isDirectory(path)) {
// Don't recurse into certain system directories
if (!fileName.equals("proc")
&& !fileName.equals("sys")
&& !fileName.equals("dev")) {
deletedCount +=
cleanupDirectory(path, containerMode, depth + 1, maxAgeMillis);
}
continue;
}
// Determine if this file matches our temp file patterns
boolean isOurTempFile =
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-");
// Avoid touching Jetty files
boolean isSystemTempFile =
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"));
boolean shouldDelete = isOurTempFile || (containerMode && isSystemTempFile);
// Special case for zero-byte files - these are often corrupted temp files
try {
if (Files.size(path) == 0) {
// For empty files, use a shorter timeout (5 minutes)
long lastModified = Files.getLastModifiedTime(path).toMillis();
long currentTime = System.currentTimeMillis();
// Delete empty files older than 5 minutes
if ((currentTime - lastModified) > 5 * 60 * 1000) {
shouldDelete = true;
}
}
} catch (IOException e) {
log.debug("Could not check file size, skipping: {}", path);
}
// Check file age against maxAgeMillis
if (shouldDelete) {
try {
long lastModified = Files.getLastModifiedTime(path).toMillis();
long currentTime = System.currentTimeMillis();
shouldDelete = (currentTime - lastModified) > maxAgeMillis;
} catch (IOException e) {
log.debug("Could not check file age, skipping: {}", path);
shouldDelete = false;
}
}
if (shouldDelete) {
try {
Files.deleteIfExists(path);
deletedCount++;
log.debug(
"Deleted unregistered temp file during scheduled cleanup: {}",
path);
} catch (IOException e) {
// Handle locked files more gracefully - just log at debug level
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 during scheduled cleanup: {}",
path,
e);
}
}
}
}
}
return deletedCount;
}
/**
* 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");
try {
// Get all directories we need to clean
Path systemTempPath;
if (systemTempDir != null && !systemTempDir.isEmpty()) {
systemTempPath = Path.of(systemTempDir);
} else {
systemTempPath = Path.of(System.getProperty("java.io.tmpdir"));
}
Path[] dirsToScan = {
systemTempPath, Path.of(customTempDirectory), Path.of(libreOfficeTempDir)
};
int totalDeletedCount = 0;
boolean containerMode =
"Docker".equals(machineType) || "Kubernetes".equals(machineType);
log.info(
"Running in {} mode, using {} cleanup strategy",
machineType,
containerMode ? "aggressive" : "conservative");
// Process each directory
for (Path tempDir : dirsToScan) {
if (!Files.exists(tempDir)) {
log.warn("Temporary directory does not exist: {}", tempDir);
continue;
}
log.info("Scanning directory for cleanup: {}", tempDir);
int dirDeletedCount = cleanupDirectory(tempDir, containerMode, 0);
totalDeletedCount += dirDeletedCount;
log.info("Cleaned up {} files/directories in {}", dirDeletedCount, tempDir);
}
log.info(
"Startup cleanup complete. Deleted {} temporary files/directories",
totalDeletedCount);
} catch (IOException e) {
log.error("Error during startup cleanup", e);
}
}
/**
* Recursively clean up a directory for temporary files.
*
* @param directory The directory to clean
* @param containerMode Whether we're in container mode (more aggressive cleanup)
* @param depth Current recursion depth (to prevent excessive recursion)
* @return Number of files deleted
*/
private int cleanupDirectory(Path directory, boolean containerMode, int depth)
throws IOException {
if (depth > 5) {
log.warn("Maximum directory recursion depth reached for: {}", directory);
return 0;
}
int deletedCount = 0;
try (Stream<Path> paths = Files.list(directory)) {
for (Path path : paths.toList()) {
String fileName = path.getFileName().toString();
// Skip Jetty-related directories and files
if (fileName.contains("jetty") || fileName.startsWith("jetty-")) {
continue;
}
// Check if this is a directory we should recursively scan
if (Files.isDirectory(path)) {
// Don't recurse into certain system directories
if (!fileName.equals("proc")
&& !fileName.equals("sys")
&& !fileName.equals("dev")) {
deletedCount += cleanupDirectory(path, containerMode, depth + 1);
}
continue;
}
// Determine if this file matches our temp file patterns
boolean isOurTempFile =
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-");
// Avoid touching Jetty files
boolean isSystemTempFile =
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"));
boolean shouldDelete = isOurTempFile || (containerMode && isSystemTempFile);
// Special case for zero-byte files - these are often corrupted temp files
boolean isEmptyFile = false;
try {
if (!Files.isDirectory(path) && Files.size(path) == 0) {
isEmptyFile = true;
// For empty files, use a shorter timeout (5 minutes)
long lastModified = Files.getLastModifiedTime(path).toMillis();
long currentTime = System.currentTimeMillis();
// Delete empty files older than 5 minutes
if ((currentTime - lastModified) > 5 * 60 * 1000) {
shouldDelete = true;
}
}
} catch (IOException e) {
log.debug("Could not check file size, skipping: {}", path);
}
// For non-container mode, check file age before deleting
if (!containerMode && (isOurTempFile || isSystemTempFile) && !isEmptyFile) {
try {
long lastModified = Files.getLastModifiedTime(path).toMillis();
long currentTime = System.currentTimeMillis();
// Only delete files older than 24 hours in non-container mode
shouldDelete = (currentTime - lastModified) > 24 * 60 * 60 * 1000;
} catch (IOException e) {
log.debug("Could not check file age, skipping: {}", path);
shouldDelete = false;
}
}
if (shouldDelete) {
try {
if (Files.isDirectory(path)) {
GeneralUtils.deleteDirectory(path);
} else {
Files.deleteIfExists(path);
}
deletedCount++;
log.debug("Deleted temp file during startup cleanup: {}", path);
} catch (IOException e) {
log.warn("Failed to delete temp file during startup cleanup: {}", path, e);
}
}
}
}
return deletedCount;
}
/** 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")) {
// For directories containing "libreoffice", delete all contents
// but keep the directory itself for future use
try (Stream<Path> files = Files.list(dir)) {
for (Path file : files.toList()) {
if (Files.isDirectory(file)) {
GeneralUtils.deleteDirectory(file);
} else {
Files.deleteIfExists(file);
}
}
}
log.debug("Cleaned up LibreOffice temp directory contents: {}", dir);
}
}
} catch (IOException e) {
log.warn("Failed to clean up LibreOffice temp files", e);
}
}
}

View File

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

View File

@ -0,0 +1,250 @@
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.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import lombok.extern.slf4j.Slf4j;
/**
* Service for managing temporary files in Stirling-PDF. Provides methods for creating, tracking,
* and cleaning up temporary files.
*/
@Slf4j
@Service
public class TempFileManager {
private final TempFileRegistry registry;
@Value("${stirling.tempfiles.prefix:stirling-pdf-}")
private String tempFilePrefix;
@Value("${stirling.tempfiles.directory:}")
private String customTempDirectory;
@Value("${stirling.tempfiles.libreoffice-dir:}")
private String libreOfficeTempDir;
@Value("${stirling.tempfiles.max-age-hours:24}")
private long maxAgeHours;
@Autowired
public TempFileManager(TempFileRegistry registry) {
this.registry = registry;
}
/**
* Create a temporary file with the Stirling-PDF prefix. The file is automatically registered
* 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 {
Path tempFilePath;
if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
Path tempDir = Path.of(customTempDirectory);
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
}
tempFilePath = Files.createTempFile(tempDir, tempFilePrefix, suffix);
} else {
tempFilePath = Files.createTempFile(tempFilePrefix, 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 {
Path tempDirPath;
if (customTempDirectory != null && !customTempDirectory.isEmpty()) {
Path tempDir = Path.of(customTempDirectory);
if (!Files.exists(tempDir)) {
Files.createDirectories(tempDir);
}
tempDirPath = Files.createTempDirectory(tempDir, tempFilePrefix);
} else {
tempDirPath = Files.createTempDirectory(tempFilePrefix);
}
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() {
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 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 {
Path loTempDir;
// 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);
}
}

View File

@ -0,0 +1,82 @@
package stirling.software.common.util;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
/**
* Utility class for generating consistent temporary file names. Provides methods to create
* standardized, identifiable temp file names.
*/
public class TempFileNamingConvention {
private static final String DEFAULT_PREFIX = "stirling-pdf-";
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
/**
* Create a temporary file name for a specific operation type.
*
* @param operationType The type of operation (e.g., "merge", "split", "watermark")
* @param extension File extension without the dot
* @return A formatted temporary file name
*/
public static String forOperation(String operationType, String extension) {
String timestamp = LocalDateTime.now().format(DATE_FORMATTER);
String uuid = UUID.randomUUID().toString().substring(0, 8);
return DEFAULT_PREFIX + operationType + "-" + timestamp + "-" + uuid + "." + extension;
}
/**
* Create a temporary file name for intermediate processing.
*
* @param operationType The type of operation
* @param step The processing step number or identifier
* @param extension File extension without the dot
* @return A formatted temporary file name for intermediate processing
*/
public static String forProcessingStep(String operationType, String step, String extension) {
String uuid = UUID.randomUUID().toString().substring(0, 8);
return DEFAULT_PREFIX + operationType + "-" + step + "-" + uuid + "." + extension;
}
/**
* Create a temporary file name for a LibreOffice operation.
*
* @param sourceFilename The original filename
* @param extension File extension without the dot
* @return A formatted temporary file name for LibreOffice operations
*/
public static String forLibreOffice(String sourceFilename, String extension) {
// Extract base filename without extension
String baseName = sourceFilename;
int lastDot = sourceFilename.lastIndexOf('.');
if (lastDot > 0) {
baseName = sourceFilename.substring(0, lastDot);
}
// Sanitize the base name
baseName = baseName.replaceAll("[^a-zA-Z0-9]", "_");
// Limit the length of the base name
if (baseName.length() > 20) {
baseName = baseName.substring(0, 20);
}
String uuid = UUID.randomUUID().toString().substring(0, 8);
return DEFAULT_PREFIX + "lo-" + baseName + "-" + uuid + "." + extension;
}
/**
* Create a temporary directory name.
*
* @param purpose The purpose of the directory
* @return A formatted temporary directory name
*/
public static String forTempDirectory(String purpose) {
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String uuid = UUID.randomUUID().toString().substring(0, 8);
return DEFAULT_PREFIX + purpose + "-" + timestamp + "-" + uuid;
}
}

View File

@ -0,0 +1,177 @@
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.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
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 {
// Track temp files with creation timestamps
private final Map<Path, Instant> registeredFiles = new ConcurrentHashMap<>();
// Separately track third-party temp files that need special handling
private final Set<Path> thirdPartyTempFiles = new ConcurrentSkipListSet<>();
// Track temp directories
private final Set<Path> tempDirectories = new ConcurrentSkipListSet<>();
/**
* 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();
}
}

View File

@ -0,0 +1,162 @@
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 wrapper class for a temporary file that implements AutoCloseable. Can be used with
* try-with-resources for automatic cleanup.
*/
public static class TempFile implements AutoCloseable {
private final TempFileManager manager;
private final File file;
public TempFile(TempFileManager manager, String suffix) throws IOException {
this.manager = manager;
this.file = manager.createTempFile(suffix);
}
public File getFile() {
return file;
}
public Path getPath() {
return file.toPath();
}
@Override
public void close() {
manager.deleteTempFile(file);
}
}
/**
* A collection of temporary files that implements AutoCloseable. All files in the collection
* 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;
}
}

View File

@ -0,0 +1,197 @@
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.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.test.util.ReflectionTestUtils;
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;
@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 service with our test directories
ReflectionTestUtils.setField(cleanupService, "systemTempDir", systemTempDir.toString());
ReflectionTestUtils.setField(cleanupService, "customTempDirectory", customTempDir.toString());
ReflectionTestUtils.setField(cleanupService, "libreOfficeTempDir", libreOfficeTempDir.toString());
ReflectionTestUtils.setField(cleanupService, "machineType", "Docker"); // Test in container mode
ReflectionTestUtils.setField(cleanupService, "performStartupCleanup", false); // Disable auto-startup cleanup
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"));
// 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);
// Create a file older than threshold
Path oldFile = Files.createFile(systemTempDir.resolve("output_old.pdf"));
Files.setLastModifiedTime(oldFile, FileTime.from( Files.getLastModifiedTime(oldFile).toMillis() - 5000000, TimeUnit.MILLISECONDS));
// Act
invokeCleanupDirectory(systemTempDir, true, 0, 3600000);
invokeCleanupDirectory(customTempDir, true, 0, 3600000);
invokeCleanupDirectory(libreOfficeTempDir, true, 0, 3600000);
// Assert - Our temp files and system temp files should be deleted (if old enough)
assertFalse(Files.exists(oldFile), "Old temp file should be deleted");
assertTrue(Files.exists(ourTempFile1), "Recent temp file should be preserved");
assertTrue(Files.exists(sysTempFile1), "Recent system temp file should be preserved");
// Jetty files and regular files should never be deleted
assertTrue(Files.exists(jettyFile1), "Jetty file should be preserved");
assertTrue(Files.exists(jettyFile2), "File with jetty in name should be preserved");
assertTrue(Files.exists(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"));
// Make it "old enough" to be deleted (>5 minutes)
Files.setLastModifiedTime(emptyFile, FileTime.from( Files.getLastModifiedTime(emptyFile).toMillis() - 6 * 60 * 1000, TimeUnit.MILLISECONDS));
// Configure mock registry to say this file isn't registered
when(registry.contains(any(File.class))).thenReturn(false);
// Act
invokeCleanupDirectory(systemTempDir, true, 0, 3600000);
// Assert
assertFalse(Files.exists(emptyFile), "Empty file older than 5 minutes should 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_3.pdf"));
// Make the deepest file old enough to be deleted
Files.setLastModifiedTime(tempFile3, FileTime.from( Files.getLastModifiedTime(tempFile3).toMillis() - 5000000, TimeUnit.MILLISECONDS));
// Configure mock registry to say these files aren't registered
when(registry.contains(any(File.class))).thenReturn(false);
// Act
invokeCleanupDirectory(systemTempDir, true, 0, 3600000);
// Assert
assertTrue(Files.exists(tempFile1), "Recent temp file should be preserved");
assertTrue(Files.exists(tempFile2), "Recent temp file should be preserved");
assertFalse(Files.exists(tempFile3), "Old temp file in nested directory should be deleted");
}
/**
* Helper method to invoke the private cleanupDirectory method using reflection
*/
private int invokeCleanupDirectory(Path directory, boolean containerMode, int depth, long maxAgeMillis)
throws IOException {
try {
var method = TempFileCleanupService.class.getDeclaredMethod(
"cleanupDirectory", Path.class, boolean.class, int.class, long.class);
method.setAccessible(true);
return (int) method.invoke(cleanupService, directory, containerMode, depth, maxAgeMillis);
} catch (Exception e) {
throw new RuntimeException("Error invoking cleanupDirectory", e);
}
}
}

View File

@ -0,0 +1,152 @@
package stirling.software.SPDF;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import io.github.pixee.security.SystemCommand;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.service.TempFileCleanupService;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.TempFileManager;
@Slf4j
@Component
public class UnoconvServer {
private static final long ACTIVITY_TIMEOUT = 20L * 60 * 1000; // 20 minutes
private static UnoconvServer INSTANCE;
private static final int LISTENER_PORT = 2002;
private ExecutorService executorService;
private long lastActivityTime;
private Process process;
private Path tempDir;
private final TempFileManager tempFileManager;
private final TempFileCleanupService cleanupService;
@Autowired
public UnoconvServer(TempFileManager tempFileManager, TempFileCleanupService cleanupService) {
this.tempFileManager = tempFileManager;
this.cleanupService = cleanupService;
INSTANCE = this;
}
public static UnoconvServer getInstance() {
// If INSTANCE is not set through Spring, try to get it from the ApplicationContext
if (INSTANCE == null) {
INSTANCE = ApplicationContextProvider.getBean(UnoconvServer.class);
if (INSTANCE == null) {
log.warn("Creating UnoconvServer without Spring context");
INSTANCE = new UnoconvServer(null, null);
}
}
return INSTANCE;
}
private boolean isServerRunning() {
log.info("Checking if unoconv server is running");
try (Socket socket = new Socket()) {
socket.connect(
new InetSocketAddress("localhost", LISTENER_PORT),
1000); // Timeout after 1 second
return true;
} catch (Exception e) {
return false;
}
}
public void start() throws IOException {
// Check if the server is already running
if (process != null && process.isAlive()) {
return;
}
// Create and register a temp directory for unoconv if TempFileManager is available
if (tempFileManager != null) {
tempDir = tempFileManager.registerLibreOfficeTempDir();
log.info("Created unoconv temp directory: {}", tempDir);
}
String command;
if (tempDir != null) {
command = "unoconv-server --user-profile " + tempDir.toString();
} else {
command = "unoconv-server";
}
// Start the server process
process = SystemCommand.runCommand(Runtime.getRuntime(), command);
lastActivityTime = System.currentTimeMillis();
// Start a background thread to monitor the activity timeout
executorService = Executors.newSingleThreadExecutor();
executorService.submit(
() -> {
while (true) {
long idleTime = System.currentTimeMillis() - lastActivityTime;
if (idleTime >= ACTIVITY_TIMEOUT) {
process.destroy();
if (cleanupService != null) {
cleanupService.cleanupLibreOfficeTempFiles();
}
break;
}
try {
Thread.sleep(5000); // Check for inactivity every 5 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
// Wait for the server to start up
long startTime = System.currentTimeMillis();
long timeout = 30000; // Timeout after 30 seconds
while (System.currentTimeMillis() - startTime < timeout) {
if (isServerRunning()) {
lastActivityTime = System.currentTimeMillis();
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Error waiting for server to start", e);
} // Check every 1 second
}
}
public synchronized void stop() {
// Stop the activity timeout monitor thread
if (executorService != null) {
executorService.shutdownNow();
}
// Stop the server process
if (process != null && process.isAlive()) {
process.destroy();
}
if (cleanupService != null) {
cleanupService.cleanupLibreOfficeTempFiles();
}
}
/**
* Notify that unoconv is being used, to reset the inactivity timer.
*/
public void notifyActivity() {
lastActivityTime = System.currentTimeMillis();
}
}

View File

@ -1,8 +1,7 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.io.File;
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,15 +22,22 @@ 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.TempFileManager;
import stirling.software.common.util.TempFileUtil;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
@RequiredArgsConstructor
public class RepairController { public class RepairController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
public RepairController(CustomPDFDocumentFactory pdfDocumentFactory, TempFileManager tempFileManager) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.tempFileManager = tempFileManager;
}
@PostMapping(consumes = "multipart/form-data", value = "/repair") @PostMapping(consumes = "multipart/form-data", value = "/repair")
@Operation( @Operation(
@ -43,25 +49,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 TempFileUtil.TempFile with try-with-resources for automatic cleanup
byte[] pdfBytes = null; try (TempFileUtil.TempFile tempFile = new TempFileUtil.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 +75,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);
} }
} }
} }

View File

@ -40,6 +40,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.TempFileManager;
import stirling.software.common.util.TempFileUtil;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -49,6 +51,7 @@ import stirling.software.common.util.WebResponseUtils;
public class StampController { public class StampController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDFDocumentFactory pdfDocumentFactory;
private final stirling.software.common.util.TempFileManager tempFileManager;
@PostMapping(consumes = "multipart/form-data", value = "/add-stamp") @PostMapping(consumes = "multipart/form-data", value = "/add-stamp")
@Operation( @Operation(
@ -188,14 +191,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 TempFileUtil.TempFile with try-with-resources for automatic cleanup
FileOutputStream os = new FileOutputStream(tempFile)) { try (TempFileUtil.TempFile tempFileWrapper = new TempFileUtil.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);
} }
} }
} }

View File

@ -120,6 +120,14 @@ 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".
tempfiles:
prefix: stirling-pdf- # Prefix for all temporary files created by the application
directory: '' # If empty, defaults to java.io.tmpdir/stirling-pdf (AppData\Local\Temp\stirling-pdf on Windows)
libreoffice-dir: '' # If empty, defaults to java.io.tmpdir/stirling-pdf-libreoffice
max-age-hours: 4 # How long to keep temporary files
cleanup-interval-minutes: 30 # How often to run the cleanup process
startup-cleanup: true # Whether to clean temporary files on application startup
system-temp-dir: '' # If empty, defaults to java.io.tmpdir, e.g., /tmp on Linux or AppData\Local\Temp on Windows
ui: ui:
appName: '' # application's visible name appName: '' # application's visible name

View File

@ -6,7 +6,6 @@
# ___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _| # # ___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _| #
# |____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_| # # |____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_| #
# # # #
# Custom setting.yml file with all endpoints disabled to only be used for testing purposes #
# Do not comment out any entry, it will be removed on next startup # # Do not comment out any entry, it will be removed on next startup #
# If you want to override with environment parameter follow parameter naming SECURITY_INITIALLOGIN_USERNAME # # If you want to override with environment parameter follow parameter naming SECURITY_INITIALLOGIN_USERNAME #
############################################################################################################# #############################################################################################################
@ -65,12 +64,13 @@ premium:
key: 00000000-0000-0000-0000-000000000000 key: 00000000-0000-0000-0000-000000000000
enabled: false # Enable license key checks for pro/enterprise features enabled: false # Enable license key checks for pro/enterprise features
proFeatures: proFeatures:
database: true # Enable database features
SSOAutoLogin: false SSOAutoLogin: false
CustomMetadata: CustomMetadata:
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values autoUpdateMetadata: false
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username author: username
creator: Stirling-PDF # supports text such as 'Company-PDF' creator: Stirling-PDF
producer: Stirling-PDF # supports text such as 'Company-PDF' producer: Stirling-PDF
googleDrive: googleDrive:
enabled: false enabled: false
clientId: '' clientId: ''
@ -120,6 +120,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".
tempfiles:
prefix: stirling-pdf- # Prefix for all temporary files created by the application
directory: '/tmp/stirling-pdf' # For testing, explicitly set the temp directory
libreoffice-dir: '/tmp/stirling-pdf/libreoffice' # For testing, explicitly set the LibreOffice directory
max-age-hours: 4 # How long to keep temporary files
cleanup-interval-minutes: 30 # How often to run the cleanup process
startup-cleanup: true # Whether to clean temporary files on application startup
system-temp-dir: '/tmp' # For testing, explicitly set the system temp directory
# Always using the registry for consistent temp file tracking
ui: ui:
appName: '' # application's visible name appName: '' # application's visible name
@ -127,7 +136,7 @@ ui:
appNameNavbar: '' # name displayed on the navigation bar appNameNavbar: '' # name displayed on the navigation bar
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled. languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
endpoints: # All the possible endpoints are disabled endpoints:
toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages']) toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice']) groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
@ -138,7 +147,7 @@ metrics:
AutomaticallyGenerated: AutomaticallyGenerated:
key: cbb81c0f-50b1-450c-a2b5-89ae527776eb key: cbb81c0f-50b1-450c-a2b5-89ae527776eb
UUID: 10dd4fba-01fa-4717-9b78-3dc4f54e398a UUID: 10dd4fba-01fa-4717-9b78-3dc4f54e398a
appVersion: 0.44.3 appVersion: 0.46.2
processExecutor: processExecutor:
sessionLimit: # Process executor instances limits sessionLimit: # Process executor instances limits

256
testing/test_temp_files.sh Normal file
View File

@ -0,0 +1,256 @@
#!/bin/bash
# This script tests the temporary file cleanup functionality in Stirling-PDF.
# It creates various temporary files inside a Docker container and verifies
# that they are properly cleaned up.
# Find project root by locating build.gradle
find_root() {
local dir="$PWD"
while [[ "$dir" != "/" ]]; do
if [[ -f "$dir/build.gradle" ]]; then
echo "$dir"
return 0
fi
dir="$(dirname "$dir")"
done
echo "Error: build.gradle not found" >&2
exit 1
}
PROJECT_ROOT=$(find_root)
CONTAINER_NAME="stirling-pdf-temp-file-test"
COMPOSE_FILE="$PROJECT_ROOT/testing/testdriver/temp_file_test.yml"
SNAPSHOT_DIR="$PROJECT_ROOT/testing/file_snapshots"
SUCCESS=true
# Create directories
mkdir -p "$SNAPSHOT_DIR"
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
# Function to check the health of the service
check_health() {
local service_name=$1
local end=$((SECONDS+60))
echo -n "Waiting for $service_name to become healthy..."
until [ "$(docker inspect --format='{{json .State.Health.Status}}' "$service_name")" == '"healthy"' ] || [ $SECONDS -ge $end ]; do
sleep 3
echo -n "."
if [ $SECONDS -ge $end ]; then
echo -e "\n$service_name health check timed out after 60 seconds."
echo "Printing logs for $service_name:"
docker logs "$service_name"
return 1
fi
done
echo -e "\n$service_name is healthy!"
return 0
}
# Function to capture all files in /tmp and its subdirectories
capture_temp_files() {
local output_file=$1
echo "Capturing temporary files list..."
docker exec $CONTAINER_NAME sh -c "find /tmp -type f | sort" > "$output_file"
# Count files
local count=$(wc -l < "$output_file")
echo "Found $count files in /tmp"
}
# Function to create test temporary files in the container
create_test_files() {
echo "Creating test temporary files..."
# Create files with various patterns in different directories
docker exec $CONTAINER_NAME sh -c '
# Create files in /tmp
touch /tmp/output_123.pdf
touch /tmp/compressedPDF456.pdf
touch /tmp/stirling-pdf-789.tmp
touch /tmp/pdf-save-123-456.tmp
touch /tmp/pdf-stream-789-012.tmp
touch /tmp/PDFBox123.tmp
touch /tmp/input_test.pdf
touch /tmp/overlay-test.pdf
# Create system-like temp files
touch /tmp/lu123abc.tmp
mkdir -p /tmp/ocr_process123
touch /tmp/tmp_upload.tmp
touch /tmp/OSL_PIPE_1000_stirling
touch /tmp/random.tmp
# Create Jetty files (should be preserved)
touch /tmp/jetty-123.tmp
touch /tmp/something-with-jetty-inside.tmp
# Create nested directories with temp files
mkdir -p /tmp/stirling-pdf
touch /tmp/stirling-pdf/nested_output.pdf
mkdir -p /tmp/webp_outputXYZ
touch /tmp/webp_outputXYZ/output_nested.pdf
# Create an empty file (special case)
touch /tmp/empty.tmp
# Create normal files (should be preserved)
touch /tmp/important.txt
echo "Test files created successfully"
'
}
# Function to trigger cleanup by modifying settings
trigger_cleanup() {
echo "Triggering temporary file cleanup..."
# Set aggressive cleanup settings and restart
docker exec $CONTAINER_NAME sh -c '
echo "stirling.tempfiles.max-age-hours=0.001" >> /app/application.properties
echo "stirling.tempfiles.cleanup-interval-minutes=0.1" >> /app/application.properties
touch /app/restart-trigger
'
# Wait for cleanup to run
echo "Waiting for cleanup to run (30 seconds)..."
sleep 30
}
# Function to verify cleanup results
verify_cleanup() {
local before_file=$1
local after_file=$2
local status=true
echo "Verifying cleanup results..."
# Files that should be cleaned
local should_be_cleaned=(
"/tmp/output_123.pdf"
"/tmp/compressedPDF456.pdf"
"/tmp/stirling-pdf-789.tmp"
"/tmp/pdf-save-123-456.tmp"
"/tmp/pdf-stream-789-012.tmp"
"/tmp/PDFBox123.tmp"
"/tmp/input_test.pdf"
"/tmp/overlay-test.pdf"
"/tmp/lu123abc.tmp"
"/tmp/ocr_process123"
"/tmp/tmp_upload.tmp"
"/tmp/OSL_PIPE_1000_stirling"
"/tmp/random.tmp"
"/tmp/empty.tmp"
"/tmp/stirling-pdf/nested_output.pdf"
"/tmp/webp_outputXYZ/output_nested.pdf"
)
# Files that should be preserved
local should_be_preserved=(
"/tmp/jetty-123.tmp"
"/tmp/something-with-jetty-inside.tmp"
"/tmp/important.txt"
)
# Check files that should be cleaned
for file in "${should_be_cleaned[@]}"; do
if grep -q "$file" "$after_file"; then
echo -e "${RED}FAIL: $file was not cleaned up${NC}"
status=false
else
echo -e "${GREEN}PASS: $file was properly cleaned up${NC}"
fi
done
# Check files that should be preserved
for file in "${should_be_preserved[@]}"; do
if grep -q "$file" "$after_file"; then
echo -e "${GREEN}PASS: $file was properly preserved${NC}"
else
echo -e "${RED}FAIL: $file was incorrectly cleaned up${NC}"
status=false
fi
done
return $status
}
# Main function
main() {
echo -e "${YELLOW}Starting temporary file cleanup test...${NC}"
# Create special test compose file
cat > "$COMPOSE_FILE" << EOL
version: '3.8'
services:
stirling-pdf:
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-ultra-lite
container_name: $CONTAINER_NAME
environment:
- DOCKER_ENABLE_SECURITY=false
- APP_FILESYSTEM_DIRECTORY_BASE=/app/customFiles
- STIRLING_MACHINE_TYPE=Docker
- STIRLING_TEMPFILES_STARTUP_CLEANUP=false
- STIRLING_TEMPFILES_CLEANUP_INTERVAL_MINUTES=5
- JAVA_OPTS=-Xmx500m
ports:
- 8080:8080
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/actuator/health || exit 1"]
interval: 10s
timeout: 5s
retries: 3
restart: unless-stopped
EOL
# Start the container
docker-compose -f "$COMPOSE_FILE" up -d
# Wait for container to be healthy
if ! check_health "$CONTAINER_NAME"; then
echo -e "${RED}Failed to start test container${NC}"
docker-compose -f "$COMPOSE_FILE" down
exit 1
fi
# Create temporary files
create_test_files
# Capture initial state
BEFORE_FILE="$SNAPSHOT_DIR/temp_files_before.txt"
capture_temp_files "$BEFORE_FILE"
# Trigger cleanup
trigger_cleanup
# Capture final state
AFTER_FILE="$SNAPSHOT_DIR/temp_files_after.txt"
capture_temp_files "$AFTER_FILE"
# Verify cleanup results
if verify_cleanup "$BEFORE_FILE" "$AFTER_FILE"; then
echo -e "${GREEN}Temporary file cleanup test PASSED${NC}"
else
echo -e "${RED}Temporary file cleanup test FAILED${NC}"
SUCCESS=false
fi
# Clean up
docker-compose -f "$COMPOSE_FILE" down
if $SUCCESS; then
exit 0
else
exit 1
fi
}
main

View File

@ -0,0 +1,20 @@
version: '3.8'
services:
stirling-pdf:
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-ultra-lite
container_name: stirling-pdf-temp-file-test
environment:
- DOCKER_ENABLE_SECURITY=false
- APP_FILESYSTEM_DIRECTORY_BASE=/app/customFiles
- STIRLING_MACHINE_TYPE=Docker
- STIRLING_TEMPFILES_STARTUP_CLEANUP=false
- STIRLING_TEMPFILES_CLEANUP_INTERVAL_MINUTES=5
- JAVA_OPTS=-Xmx500m
ports:
- 8080:8080
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/actuator/health || exit 1"]
interval: 10s
timeout: 5s
retries: 3
restart: unless-stopped