cleanup changes

This commit is contained in:
Anthony Stirling 2025-07-21 12:55:51 +01:00
parent 774c1f6552
commit 82900b9db1
8 changed files with 229 additions and 9 deletions

View File

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

View File

@ -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;
@ -46,6 +50,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 =
fileName ->
@ -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
) {}
}

View File

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

View File

@ -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) {

View File

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

View File

@ -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")

View File

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

View File

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