mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-21 23:15:03 +00:00
tmp file clenaup
This commit is contained in:
parent
2217cfb95d
commit
ca1100125d
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(chmod:*)",
|
||||
"Bash(mkdir:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,6 +23,9 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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
|
||||
@ -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 {
|
||||
// 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");
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
152
src/main/java/stirling/software/SPDF/UnoconvServer.java
Normal file
152
src/main/java/stirling/software/SPDF/UnoconvServer.java
Normal 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();
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
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;
|
||||
|
||||
@ -23,15 +22,22 @@ import stirling.software.common.model.api.PDFFile;
|
||||
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;
|
||||
import stirling.software.common.util.TempFileUtil;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class RepairController {
|
||||
|
||||
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")
|
||||
@Operation(
|
||||
@ -43,25 +49,25 @@ public class RepairController {
|
||||
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile file)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = file.getFileInput();
|
||||
// Save the uploaded file to a temporary location
|
||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
byte[] pdfBytes = null;
|
||||
inputFile.transferTo(tempInputFile.toFile());
|
||||
try {
|
||||
|
||||
// Use TempFileUtil.TempFile with try-with-resources for automatic cleanup
|
||||
try (TempFileUtil.TempFile tempFile = new TempFileUtil.TempFile(tempFileManager, ".pdf")) {
|
||||
// Save the uploaded file to the temporary location
|
||||
inputFile.transferTo(tempFile.getFile());
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("qpdf");
|
||||
command.add("--replace-input"); // Automatically fixes problems it can
|
||||
command.add("--qdf"); // Linearizes and normalizes PDF structure
|
||||
command.add("--object-streams=disable"); // Can help with some corruptions
|
||||
command.add(tempInputFile.toString());
|
||||
command.add(tempFile.getFile().getAbsolutePath());
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Read the optimized PDF file
|
||||
pdfBytes = pdfDocumentFactory.loadToBytes(tempInputFile.toFile());
|
||||
byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempFile.getFile());
|
||||
|
||||
// Return the optimized PDF as a response
|
||||
String outputFilename =
|
||||
@ -69,9 +75,6 @@ public class RepairController {
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_repaired.pdf";
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,8 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.AddStampRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.TempFileUtil;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@ -49,6 +51,7 @@ import stirling.software.common.util.WebResponseUtils;
|
||||
public class StampController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final stirling.software.common.util.TempFileManager tempFileManager;
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/add-stamp")
|
||||
@Operation(
|
||||
@ -188,14 +191,14 @@ public class StampController {
|
||||
if (!"".equals(resourceDir)) {
|
||||
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
|
||||
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
|
||||
File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
|
||||
try (InputStream is = classPathResource.getInputStream();
|
||||
FileOutputStream os = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copy(is, os);
|
||||
font = PDType0Font.load(document, tempFile);
|
||||
} finally {
|
||||
if (tempFile != null) {
|
||||
Files.deleteIfExists(tempFile.toPath());
|
||||
|
||||
// Use TempFileUtil.TempFile with try-with-resources for automatic cleanup
|
||||
try (TempFileUtil.TempFile tempFileWrapper = new TempFileUtil.TempFile(tempFileManager, fileExtension)) {
|
||||
File tempFile = tempFileWrapper.getFile();
|
||||
try (InputStream is = classPathResource.getInputStream();
|
||||
FileOutputStream os = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copy(is, os);
|
||||
font = PDType0Font.load(document, tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -120,6 +120,14 @@ system:
|
||||
weasyprint: '' # Defaults to /opt/venv/bin/weasyprint
|
||||
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".
|
||||
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:
|
||||
appName: '' # application's visible name
|
||||
|
@ -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 #
|
||||
# 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
|
||||
enabled: false # Enable license key checks for pro/enterprise features
|
||||
proFeatures:
|
||||
database: true # Enable database features
|
||||
SSOAutoLogin: false
|
||||
CustomMetadata:
|
||||
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
|
||||
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
|
||||
creator: Stirling-PDF # supports text such as 'Company-PDF'
|
||||
producer: Stirling-PDF # supports text such as 'Company-PDF'
|
||||
autoUpdateMetadata: false
|
||||
author: username
|
||||
creator: Stirling-PDF
|
||||
producer: Stirling-PDF
|
||||
googleDrive:
|
||||
enabled: false
|
||||
clientId: ''
|
||||
@ -120,6 +120,15 @@ system:
|
||||
weasyprint: '' # Defaults to /opt/venv/bin/weasyprint
|
||||
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".
|
||||
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:
|
||||
appName: '' # application's visible name
|
||||
@ -127,7 +136,7 @@ ui:
|
||||
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.
|
||||
|
||||
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'])
|
||||
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
|
||||
|
||||
@ -138,7 +147,7 @@ metrics:
|
||||
AutomaticallyGenerated:
|
||||
key: cbb81c0f-50b1-450c-a2b5-89ae527776eb
|
||||
UUID: 10dd4fba-01fa-4717-9b78-3dc4f54e398a
|
||||
appVersion: 0.44.3
|
||||
appVersion: 0.46.2
|
||||
|
||||
processExecutor:
|
||||
sessionLimit: # Process executor instances limits
|
||||
|
256
testing/test_temp_files.sh
Normal file
256
testing/test_temp_files.sh
Normal 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
|
20
testing/testdriver/temp_file_test.yml
Normal file
20
testing/testdriver/temp_file_test.yml
Normal 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
|
Loading…
x
Reference in New Issue
Block a user