mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
cleanup changes
This commit is contained in:
parent
774c1f6552
commit
82900b9db1
@ -1,14 +1,19 @@
|
||||
package stirling.software.common.config;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
@Slf4j
|
||||
public class CleanupAsyncConfig {
|
||||
|
||||
@Bean(name = "cleanupExecutor")
|
||||
@ -18,6 +23,23 @@ public class CleanupAsyncConfig {
|
||||
exec.setMaxPoolSize(1);
|
||||
exec.setQueueCapacity(100);
|
||||
exec.setThreadNamePrefix("cleanup-");
|
||||
|
||||
// Set custom rejection handler to log when queue is full
|
||||
exec.setRejectedExecutionHandler(new RejectedExecutionHandler() {
|
||||
@Override
|
||||
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
|
||||
log.warn("Cleanup task rejected - queue full! Active: {}, Queue size: {}, Pool size: {}",
|
||||
executor.getActiveCount(),
|
||||
executor.getQueue().size(),
|
||||
executor.getPoolSize());
|
||||
|
||||
// Use caller-runs policy as fallback - this will block the scheduler thread
|
||||
// but ensures the cleanup still happens
|
||||
log.warn("Executing cleanup task on scheduler thread as fallback");
|
||||
r.run();
|
||||
}
|
||||
});
|
||||
|
||||
exec.initialize();
|
||||
return exec;
|
||||
}
|
||||
|
@ -5,8 +5,12 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@ -45,6 +49,12 @@ public class TempFileCleanupService {
|
||||
|
||||
// Maximum recursion depth for directory traversal
|
||||
private static final int MAX_RECURSION_DEPTH = 5;
|
||||
|
||||
// Cleanup state management
|
||||
private final AtomicBoolean cleanupRunning = new AtomicBoolean(false);
|
||||
private final AtomicLong lastCleanupDuration = new AtomicLong(0);
|
||||
private final AtomicLong cleanupCount = new AtomicLong(0);
|
||||
private final AtomicLong lastCleanupTimestamp = new AtomicLong(0);
|
||||
|
||||
// File patterns that identify our temp files
|
||||
private static final Predicate<String> IS_OUR_TEMP_FILE =
|
||||
@ -126,8 +136,51 @@ public class TempFileCleanupService {
|
||||
fixedDelayString =
|
||||
"#{applicationProperties.system.tempFileManagement.cleanupIntervalMinutes}",
|
||||
timeUnit = TimeUnit.MINUTES)
|
||||
public void scheduledCleanup() {
|
||||
log.info("Running scheduled temporary file cleanup");
|
||||
public CompletableFuture<Void> scheduledCleanup() {
|
||||
// Check if cleanup is already running
|
||||
if (!cleanupRunning.compareAndSet(false, true)) {
|
||||
log.warn("Cleanup already in progress (running for {}ms), skipping this cycle",
|
||||
System.currentTimeMillis() - lastCleanupTimestamp.get());
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
// Calculate timeout as 2x cleanup interval
|
||||
long timeoutMinutes = applicationProperties.getSystem().getTempFileManagement().getCleanupIntervalMinutes() * 2;
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
long startTime = System.currentTimeMillis();
|
||||
lastCleanupTimestamp.set(startTime);
|
||||
long cleanupNumber = cleanupCount.incrementAndGet();
|
||||
|
||||
try {
|
||||
log.info("Starting cleanup #{} with {}min timeout", cleanupNumber, timeoutMinutes);
|
||||
doScheduledCleanup();
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
lastCleanupDuration.set(duration);
|
||||
log.info("Cleanup #{} completed successfully in {}ms", cleanupNumber, duration);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
lastCleanupDuration.set(duration);
|
||||
log.error("Cleanup #{} failed after {}ms", cleanupNumber, duration, e);
|
||||
return null;
|
||||
} finally {
|
||||
cleanupRunning.set(false);
|
||||
}
|
||||
}).orTimeout(timeoutMinutes, TimeUnit.MINUTES)
|
||||
.exceptionally(throwable -> {
|
||||
if (throwable.getCause() instanceof TimeoutException) {
|
||||
log.error("Cleanup #{} timed out after {}min - forcing cleanup state reset",
|
||||
cleanupCount.get(), timeoutMinutes);
|
||||
cleanupRunning.set(false);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/** Internal method that performs the actual cleanup work */
|
||||
private void doScheduledCleanup() {
|
||||
long maxAgeMillis = tempFileManager.getMaxAgeMillis();
|
||||
|
||||
// Clean up registered temp files (managed by TempFileRegistry)
|
||||
@ -464,4 +517,51 @@ public class TempFileCleanupService {
|
||||
log.warn("Failed to clean up PDFBox cache file", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cleanup status and metrics for monitoring
|
||||
*/
|
||||
public String getCleanupStatus() {
|
||||
if (cleanupRunning.get()) {
|
||||
long runningTime = System.currentTimeMillis() - lastCleanupTimestamp.get();
|
||||
return String.format("Running for %dms (cleanup #%d)", runningTime, cleanupCount.get());
|
||||
} else {
|
||||
long lastDuration = lastCleanupDuration.get();
|
||||
long lastTime = lastCleanupTimestamp.get();
|
||||
if (lastTime > 0) {
|
||||
long timeSinceLastRun = System.currentTimeMillis() - lastTime;
|
||||
return String.format("Last cleanup #%d: %dms duration, %dms ago",
|
||||
cleanupCount.get(), lastDuration, timeSinceLastRun);
|
||||
} else {
|
||||
return "No cleanup runs yet";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cleanup is currently running
|
||||
*/
|
||||
public boolean isCleanupRunning() {
|
||||
return cleanupRunning.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cleanup metrics
|
||||
*/
|
||||
public CleanupMetrics getMetrics() {
|
||||
return new CleanupMetrics(
|
||||
cleanupCount.get(),
|
||||
lastCleanupDuration.get(),
|
||||
lastCleanupTimestamp.get(),
|
||||
cleanupRunning.get()
|
||||
);
|
||||
}
|
||||
|
||||
/** Simple record for cleanup metrics */
|
||||
public record CleanupMetrics(
|
||||
long totalRuns,
|
||||
long lastDurationMs,
|
||||
long lastRunTimestamp,
|
||||
boolean currentlyRunning
|
||||
) {}
|
||||
}
|
||||
|
@ -11,7 +11,11 @@ import java.awt.TrayIcon;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowStateListener;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@ -30,6 +34,7 @@ import org.cef.callback.CefDownloadItem;
|
||||
import org.cef.callback.CefDownloadItemCallback;
|
||||
import org.cef.handler.CefDownloadHandlerAdapter;
|
||||
import org.cef.handler.CefLoadHandlerAdapter;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@ -62,7 +67,11 @@ public class DesktopBrowser implements WebBrowser {
|
||||
private static TrayIcon trayIcon;
|
||||
private static SystemTray systemTray;
|
||||
|
||||
public DesktopBrowser() {
|
||||
private final String appVersion;
|
||||
private static final String VERSION_FILE = "last_version.txt";
|
||||
|
||||
public DesktopBrowser(@Qualifier("appVersion") String appVersion) {
|
||||
this.appVersion = appVersion;
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
loadingWindow = new LoadingWindow(null, "Initializing...");
|
||||
@ -120,6 +129,10 @@ public class DesktopBrowser implements WebBrowser {
|
||||
CefSettings settings = builder.getCefSettings();
|
||||
String basePath = InstallationPathConfig.getClientWebUIPath();
|
||||
log.info("basePath " + basePath);
|
||||
|
||||
// Check if version has changed and reset cache if needed
|
||||
checkVersionAndResetCache(basePath);
|
||||
|
||||
settings.cache_path = new File(basePath + "cache").getAbsolutePath();
|
||||
settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath();
|
||||
// settings.browser_subprocess_path = new File(basePath +
|
||||
@ -424,6 +437,87 @@ public class DesktopBrowser implements WebBrowser {
|
||||
}
|
||||
}
|
||||
|
||||
private void checkVersionAndResetCache(String basePath) {
|
||||
try {
|
||||
Path versionFilePath = Paths.get(basePath, VERSION_FILE);
|
||||
String currentVersion = appVersion != null ? appVersion : "0.0.0";
|
||||
|
||||
// Read last stored version
|
||||
String lastVersion = "0.0.0";
|
||||
if (Files.exists(versionFilePath)) {
|
||||
lastVersion = new String(Files.readAllBytes(versionFilePath)).trim();
|
||||
}
|
||||
|
||||
log.info("Current version: {}, Last version: {}", currentVersion, lastVersion);
|
||||
|
||||
// Compare major and minor versions
|
||||
if (shouldResetCache(currentVersion, lastVersion)) {
|
||||
log.info("Version change detected, resetting cache");
|
||||
resetCache(basePath);
|
||||
|
||||
// Store current version
|
||||
Files.createDirectories(versionFilePath.getParent());
|
||||
Files.write(versionFilePath, currentVersion.getBytes());
|
||||
log.info("Version file updated to: {}", currentVersion);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error checking version and resetting cache", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldResetCache(String currentVersion, String lastVersion) {
|
||||
try {
|
||||
String[] currentParts = currentVersion.split("\\.");
|
||||
String[] lastParts = lastVersion.split("\\.");
|
||||
|
||||
if (currentParts.length < 2 || lastParts.length < 2) {
|
||||
return true; // Reset if version format is unexpected
|
||||
}
|
||||
|
||||
int currentMajor = Integer.parseInt(currentParts[0]);
|
||||
int currentMinor = Integer.parseInt(currentParts[1]);
|
||||
int lastMajor = Integer.parseInt(lastParts[0]);
|
||||
int lastMinor = Integer.parseInt(lastParts[1]);
|
||||
|
||||
return currentMajor != lastMajor || currentMinor != lastMinor;
|
||||
} catch (Exception e) {
|
||||
log.warn("Error comparing versions, will reset cache: {}", e.getMessage());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetCache(String basePath) {
|
||||
try {
|
||||
Path cachePath = Paths.get(basePath, "cache");
|
||||
Path rootCachePath = Paths.get(basePath, "root_cache");
|
||||
|
||||
if (Files.exists(cachePath)) {
|
||||
deleteDirectoryRecursively(cachePath);
|
||||
log.info("Deleted cache directory: {}", cachePath);
|
||||
}
|
||||
|
||||
if (Files.exists(rootCachePath)) {
|
||||
deleteDirectoryRecursively(rootCachePath);
|
||||
log.info("Deleted root cache directory: {}", rootCachePath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error resetting cache directories", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteDirectoryRecursively(Path path) throws IOException {
|
||||
Files.walk(path)
|
||||
.sorted((a, b) -> b.compareTo(a)) // Delete files before directories
|
||||
.forEach(
|
||||
p -> {
|
||||
try {
|
||||
Files.delete(p);
|
||||
} catch (IOException e) {
|
||||
log.warn("Could not delete: {}", p, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
if (browser != null) browser.close(true);
|
||||
|
@ -232,7 +232,8 @@ public class ConvertImgPDFController {
|
||||
PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType, pdfDocumentFactory);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
bytes,
|
||||
new File(file[0].getOriginalFilename()).getName().replaceFirst("[.][^.]+$", "") + "_converted.pdf");
|
||||
new File(file[0].getOriginalFilename()).getName().replaceFirst("[.][^.]+$", "")
|
||||
+ "_converted.pdf");
|
||||
}
|
||||
|
||||
private String getMediaType(String imageFormat) {
|
||||
|
@ -47,7 +47,8 @@ public class PrintFileController {
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
if (originalFilename != null && (originalFilename.contains("..") || Paths.get(originalFilename).isAbsolute())) {
|
||||
if (originalFilename != null
|
||||
&& (originalFilename.contains("..") || Paths.get(originalFilename).isAbsolute())) {
|
||||
throw new IOException("Invalid file path detected: " + originalFilename);
|
||||
}
|
||||
String printerName = request.getPrinterName();
|
||||
|
@ -42,7 +42,6 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import java.lang.IllegalArgumentException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@ -67,7 +66,7 @@ public class StampController {
|
||||
if (pdfFileName.contains("..") || pdfFileName.startsWith("/")) {
|
||||
throw new IllegalArgumentException("Invalid PDF file path");
|
||||
}
|
||||
|
||||
|
||||
String stampType = request.getStampType();
|
||||
String stampText = request.getStampText();
|
||||
MultipartFile stampImage = request.getStampImage();
|
||||
|
@ -331,7 +331,8 @@ public class PipelineProcessor {
|
||||
for (File file : files) {
|
||||
Path normalizedPath = Paths.get(file.getName()).normalize();
|
||||
if (normalizedPath.startsWith("..")) {
|
||||
throw new SecurityException("Potential path traversal attempt in file name: " + file.getName());
|
||||
throw new SecurityException(
|
||||
"Potential path traversal attempt in file name: " + file.getName());
|
||||
}
|
||||
Path path = Paths.get(file.getAbsolutePath());
|
||||
// debug statement
|
||||
|
@ -83,7 +83,9 @@ public class WatermarkController {
|
||||
MultipartFile watermarkImage = request.getWatermarkImage();
|
||||
if (watermarkImage != null) {
|
||||
String watermarkImageFileName = watermarkImage.getOriginalFilename();
|
||||
if (watermarkImageFileName != null && (watermarkImageFileName.contains("..") || watermarkImageFileName.startsWith("/"))) {
|
||||
if (watermarkImageFileName != null
|
||||
&& (watermarkImageFileName.contains("..")
|
||||
|| watermarkImageFileName.startsWith("/"))) {
|
||||
throw new SecurityException("Invalid file path in watermarkImage");
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user