mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-02 13:48:15 +02:00
Merge 1ad96242f5
into 043db37dfb
This commit is contained in:
commit
a7d5a66553
@ -0,0 +1,83 @@
|
|||||||
|
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")
|
||||||
|
public Executor cleanupExecutor() {
|
||||||
|
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
|
||||||
|
exec.setCorePoolSize(1);
|
||||||
|
exec.setMaxPoolSize(1);
|
||||||
|
exec.setQueueCapacity(100);
|
||||||
|
exec.setThreadNamePrefix("cleanup-");
|
||||||
|
|
||||||
|
// Set custom rejection handler to log when queue is full
|
||||||
|
exec.setRejectedExecutionHandler(
|
||||||
|
new RejectedExecutionHandler() {
|
||||||
|
private volatile long lastRejectionTime = 0;
|
||||||
|
private volatile int rejectionCount = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
rejectionCount++;
|
||||||
|
|
||||||
|
// Rate-limit logging to avoid spam
|
||||||
|
if (currentTime - lastRejectionTime
|
||||||
|
> 60000) { // Log at most once per minute
|
||||||
|
log.warn(
|
||||||
|
"Cleanup task rejected #{} - queue full! Active: {}, Queue size: {}, Pool size: {}",
|
||||||
|
rejectionCount,
|
||||||
|
executor.getActiveCount(),
|
||||||
|
executor.getQueue().size(),
|
||||||
|
executor.getPoolSize());
|
||||||
|
lastRejectionTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to discard oldest task and add this one
|
||||||
|
if (executor.getQueue().poll() != null) {
|
||||||
|
log.debug("Discarded oldest queued cleanup task to make room");
|
||||||
|
try {
|
||||||
|
executor.execute(r);
|
||||||
|
return;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// If still rejected, fall back to caller-runs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: caller-runs with timeout protection
|
||||||
|
log.warn(
|
||||||
|
"Executing cleanup task #{} on scheduler thread as last resort",
|
||||||
|
rejectionCount);
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
r.run();
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
if (duration > 30000) { // Warn if cleanup blocks scheduler for >30s
|
||||||
|
log.warn(
|
||||||
|
"Cleanup task on scheduler thread took {}ms - consider tuning",
|
||||||
|
duration);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Cleanup task failed on scheduler thread", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
exec.initialize();
|
||||||
|
return exec;
|
||||||
|
}
|
||||||
|
}
|
@ -329,6 +329,8 @@ public class ApplicationProperties {
|
|||||||
private long cleanupIntervalMinutes = 30;
|
private long cleanupIntervalMinutes = 30;
|
||||||
private boolean startupCleanup = true;
|
private boolean startupCleanup = true;
|
||||||
private boolean cleanupSystemTemp = false;
|
private boolean cleanupSystemTemp = false;
|
||||||
|
private int batchSize = 1000;
|
||||||
|
private long pauseBetweenBatchesMs = 50;
|
||||||
|
|
||||||
public String getBaseTmpDir() {
|
public String getBaseTmpDir() {
|
||||||
return baseTmpDir != null && !baseTmpDir.isEmpty()
|
return baseTmpDir != null && !baseTmpDir.isEmpty()
|
||||||
|
@ -5,14 +5,18 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
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.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@ -46,6 +50,15 @@ public class TempFileCleanupService {
|
|||||||
// Maximum recursion depth for directory traversal
|
// Maximum recursion depth for directory traversal
|
||||||
private static final int MAX_RECURSION_DEPTH = 5;
|
private static final int MAX_RECURSION_DEPTH = 5;
|
||||||
|
|
||||||
|
// Maximum consecutive failures before aborting batch cleanup
|
||||||
|
private static final int MAX_CONSECUTIVE_FAILURES = 10;
|
||||||
|
|
||||||
|
// 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
|
// File patterns that identify our temp files
|
||||||
private static final Predicate<String> IS_OUR_TEMP_FILE =
|
private static final Predicate<String> IS_OUR_TEMP_FILE =
|
||||||
fileName ->
|
fileName ->
|
||||||
@ -121,12 +134,78 @@ public class TempFileCleanupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Scheduled task to clean up old temporary files. Runs at the configured interval. */
|
/** Scheduled task to clean up old temporary files. Runs at the configured interval. */
|
||||||
|
@Async("cleanupExecutor")
|
||||||
@Scheduled(
|
@Scheduled(
|
||||||
fixedDelayString =
|
fixedDelayString =
|
||||||
"#{applicationProperties.system.tempFileManagement.cleanupIntervalMinutes}",
|
"#{applicationProperties.system.tempFileManagement.cleanupIntervalMinutes}",
|
||||||
timeUnit = TimeUnit.MINUTES)
|
timeUnit = TimeUnit.MINUTES)
|
||||||
public void scheduledCleanup() {
|
public CompletableFuture<Void> scheduledCleanup() {
|
||||||
log.info("Running scheduled temporary file cleanup");
|
// 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;
|
||||||
|
|
||||||
|
CompletableFuture<Void> cleanupFuture =
|
||||||
|
CompletableFuture.runAsync(
|
||||||
|
() -> {
|
||||||
|
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);
|
||||||
|
} catch (Exception e) {
|
||||||
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
|
lastCleanupDuration.set(duration);
|
||||||
|
log.error(
|
||||||
|
"Cleanup #{} failed after {}ms",
|
||||||
|
cleanupNumber,
|
||||||
|
duration,
|
||||||
|
e);
|
||||||
|
} finally {
|
||||||
|
cleanupRunning.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cleanupFuture
|
||||||
|
.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();
|
long maxAgeMillis = tempFileManager.getMaxAgeMillis();
|
||||||
|
|
||||||
// Clean up registered temp files (managed by TempFileRegistry)
|
// Clean up registered temp files (managed by TempFileRegistry)
|
||||||
@ -310,44 +389,81 @@ public class TempFileCleanupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
java.util.List<Path> subdirectories = new java.util.ArrayList<>();
|
java.util.List<Path> subdirectories = new java.util.ArrayList<>();
|
||||||
|
int batchSize = applicationProperties.getSystem().getTempFileManagement().getBatchSize();
|
||||||
|
long pauseMs =
|
||||||
|
applicationProperties
|
||||||
|
.getSystem()
|
||||||
|
.getTempFileManagement()
|
||||||
|
.getPauseBetweenBatchesMs();
|
||||||
|
int processed = 0;
|
||||||
|
int consecutiveFailures = 0;
|
||||||
|
|
||||||
try (Stream<Path> pathStream = Files.list(directory)) {
|
try (java.nio.file.DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
|
||||||
pathStream.forEach(
|
for (Path path : stream) {
|
||||||
path -> {
|
try {
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
|
if (SHOULD_SKIP.test(fileName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.isDirectory(path)) {
|
||||||
|
subdirectories.add(path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registry.contains(path.toFile())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldDeleteFile(path, fileName, containerMode, maxAgeMillis)) {
|
||||||
try {
|
try {
|
||||||
String fileName = path.getFileName().toString();
|
Files.deleteIfExists(path);
|
||||||
|
onDeleteCallback.accept(path);
|
||||||
if (SHOULD_SKIP.test(fileName)) {
|
consecutiveFailures = 0; // Reset failure count on success
|
||||||
return;
|
} catch (IOException e) {
|
||||||
|
consecutiveFailures++;
|
||||||
|
if (e.getMessage() != null
|
||||||
|
&& e.getMessage().contains("being used by another process")) {
|
||||||
|
log.debug("File locked, skipping delete: {}", path);
|
||||||
|
} else {
|
||||||
|
log.warn("Failed to delete temp file: {}", path, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Files.isDirectory(path)) {
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
||||||
subdirectories.add(path);
|
log.error(
|
||||||
return;
|
"Aborting directory cleanup after {} consecutive failures in: {}",
|
||||||
|
consecutiveFailures,
|
||||||
|
directory);
|
||||||
|
return; // Early exit from cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
if (registry.contains(path.toFile())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldDeleteFile(path, fileName, containerMode, maxAgeMillis)) {
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(path);
|
|
||||||
onDeleteCallback.accept(path);
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (e.getMessage() != null
|
|
||||||
&& e.getMessage()
|
|
||||||
.contains("being used by another process")) {
|
|
||||||
log.debug("File locked, skipping delete: {}", path);
|
|
||||||
} else {
|
|
||||||
log.warn("Failed to delete temp file: {}", path, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Error processing path: {}", path, e);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
consecutiveFailures++;
|
||||||
|
log.warn("Error processing path: {}", path, e);
|
||||||
|
|
||||||
|
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
||||||
|
log.error(
|
||||||
|
"Aborting directory cleanup after {} consecutive failures in: {}",
|
||||||
|
consecutiveFailures,
|
||||||
|
directory);
|
||||||
|
return; // Early exit from cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processed++;
|
||||||
|
if (batchSize > 0 && processed >= batchSize) {
|
||||||
|
if (pauseMs > 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(pauseMs);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processed = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Path subdirectory : subdirectories) {
|
for (Path subdirectory : subdirectories) {
|
||||||
@ -446,4 +562,41 @@ public class TempFileCleanupService {
|
|||||||
log.warn("Failed to clean up PDFBox cache file", e);
|
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) {}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Stream;
|
import java.nio.file.DirectoryStream;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@ -142,20 +142,27 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
// Mock Files.list for each directory we'll process
|
// Mock Files.newDirectoryStream for each directory we'll process
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles.when(() -> Files.newDirectoryStream(eq(systemTempDir)))
|
||||||
.thenReturn(Stream.of(
|
.thenReturn(directoryStreamOf(
|
||||||
ourTempFile1, ourTempFile2, oldTempFile, sysTempFile1,
|
ourTempFile1,
|
||||||
jettyFile1, jettyFile2, regularFile, emptyFile, nestedDir));
|
ourTempFile2,
|
||||||
|
oldTempFile,
|
||||||
|
sysTempFile1,
|
||||||
|
jettyFile1,
|
||||||
|
jettyFile2,
|
||||||
|
regularFile,
|
||||||
|
emptyFile,
|
||||||
|
nestedDir));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(customTempDir)))
|
mockedFiles.when(() -> Files.newDirectoryStream(eq(customTempDir)))
|
||||||
.thenReturn(Stream.of(ourTempFile3, ourTempFile4, sysTempFile2, sysTempFile3));
|
.thenReturn(directoryStreamOf(ourTempFile3, ourTempFile4, sysTempFile2, sysTempFile3));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(libreOfficeTempDir)))
|
mockedFiles.when(() -> Files.newDirectoryStream(eq(libreOfficeTempDir)))
|
||||||
.thenReturn(Stream.of(ourTempFile5));
|
.thenReturn(directoryStreamOf(ourTempFile5));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(nestedDir)))
|
mockedFiles.when(() -> Files.newDirectoryStream(eq(nestedDir)))
|
||||||
.thenReturn(Stream.of(nestedTempFile));
|
.thenReturn(directoryStreamOf(nestedTempFile));
|
||||||
|
|
||||||
// Configure Files.isDirectory for each path
|
// Configure Files.isDirectory for each path
|
||||||
mockedFiles.when(() -> Files.isDirectory(eq(nestedDir))).thenReturn(true);
|
mockedFiles.when(() -> Files.isDirectory(eq(nestedDir))).thenReturn(true);
|
||||||
@ -175,7 +182,7 @@ public class TempFileCleanupServiceTest {
|
|||||||
return FileTime.fromMillis(System.currentTimeMillis() - 5000000);
|
return FileTime.fromMillis(System.currentTimeMillis() - 5000000);
|
||||||
}
|
}
|
||||||
// For empty.tmp file, return a timestamp older than 5 minutes (for empty file test)
|
// For empty.tmp file, return a timestamp older than 5 minutes (for empty file test)
|
||||||
else if (fileName.equals("empty.tmp")) {
|
else if ("empty.tmp".equals(fileName)) {
|
||||||
return FileTime.fromMillis(System.currentTimeMillis() - 6 * 60 * 1000);
|
return FileTime.fromMillis(System.currentTimeMillis() - 6 * 60 * 1000);
|
||||||
}
|
}
|
||||||
// For all other files, return a recent timestamp
|
// For all other files, return a recent timestamp
|
||||||
@ -191,7 +198,7 @@ public class TempFileCleanupServiceTest {
|
|||||||
String fileName = path.getFileName().toString();
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
// Return 0 bytes for the empty file
|
// Return 0 bytes for the empty file
|
||||||
if (fileName.equals("empty.tmp")) {
|
if ("empty.tmp".equals(fileName)) {
|
||||||
return 0L;
|
return 0L;
|
||||||
}
|
}
|
||||||
// Return normal size for all other files
|
// Return normal size for all other files
|
||||||
@ -251,9 +258,10 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
// Mock Files.list for systemTempDir
|
// Mock Files.newDirectoryStream for systemTempDir
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(ourTempFile, sysTempFile, regularFile));
|
.when(() -> Files.newDirectoryStream(eq(systemTempDir)))
|
||||||
|
.thenReturn(directoryStreamOf(ourTempFile, sysTempFile, regularFile));
|
||||||
|
|
||||||
// Configure Files.isDirectory
|
// Configure Files.isDirectory
|
||||||
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||||
@ -302,9 +310,10 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
// Mock Files.list for systemTempDir
|
// Mock Files.newDirectoryStream for systemTempDir
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(emptyFile, recentEmptyFile));
|
.when(() -> Files.newDirectoryStream(eq(systemTempDir)))
|
||||||
|
.thenReturn(directoryStreamOf(emptyFile, recentEmptyFile));
|
||||||
|
|
||||||
// Configure Files.isDirectory
|
// Configure Files.isDirectory
|
||||||
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||||
@ -318,7 +327,7 @@ public class TempFileCleanupServiceTest {
|
|||||||
Path path = invocation.getArgument(0);
|
Path path = invocation.getArgument(0);
|
||||||
String fileName = path.getFileName().toString();
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
if (fileName.equals("empty.tmp")) {
|
if ("empty.tmp".equals(fileName)) {
|
||||||
// More than 5 minutes old
|
// More than 5 minutes old
|
||||||
return FileTime.fromMillis(System.currentTimeMillis() - 6 * 60 * 1000);
|
return FileTime.fromMillis(System.currentTimeMillis() - 6 * 60 * 1000);
|
||||||
} else {
|
} else {
|
||||||
@ -369,18 +378,22 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
// Mock Files.list for each directory
|
// Mock Files.newDirectoryStream for each directory
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(dir1));
|
.when(() -> Files.newDirectoryStream(eq(systemTempDir)))
|
||||||
|
.thenReturn(directoryStreamOf(dir1));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir1)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(tempFile1, dir2));
|
.when(() -> Files.newDirectoryStream(eq(dir1)))
|
||||||
|
.thenReturn(directoryStreamOf(tempFile1, dir2));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir2)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(tempFile2, dir3));
|
.when(() -> Files.newDirectoryStream(eq(dir2)))
|
||||||
|
.thenReturn(directoryStreamOf(tempFile2, dir3));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir3)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(tempFile3));
|
.when(() -> Files.newDirectoryStream(eq(dir3)))
|
||||||
|
.thenReturn(directoryStreamOf(tempFile3));
|
||||||
|
|
||||||
// Configure Files.isDirectory for each path
|
// Configure Files.isDirectory for each path
|
||||||
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
||||||
@ -461,4 +474,16 @@ public class TempFileCleanupServiceTest {
|
|||||||
private static Path eq(Path path) {
|
private static Path eq(Path path) {
|
||||||
return argThat(arg -> arg != null && arg.equals(path));
|
return argThat(arg -> arg != null && arg.equals(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DirectoryStream<Path> directoryStreamOf(Path... paths) {
|
||||||
|
return new DirectoryStream<>() {
|
||||||
|
@Override
|
||||||
|
public java.util.Iterator<Path> iterator() {
|
||||||
|
return java.util.Arrays.asList(paths).iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,11 @@ import java.awt.TrayIcon;
|
|||||||
import java.awt.event.WindowEvent;
|
import java.awt.event.WindowEvent;
|
||||||
import java.awt.event.WindowStateListener;
|
import java.awt.event.WindowStateListener;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
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.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
@ -30,6 +34,7 @@ import org.cef.callback.CefDownloadItem;
|
|||||||
import org.cef.callback.CefDownloadItemCallback;
|
import org.cef.callback.CefDownloadItemCallback;
|
||||||
import org.cef.handler.CefDownloadHandlerAdapter;
|
import org.cef.handler.CefDownloadHandlerAdapter;
|
||||||
import org.cef.handler.CefLoadHandlerAdapter;
|
import org.cef.handler.CefLoadHandlerAdapter;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@ -62,7 +67,11 @@ public class DesktopBrowser implements WebBrowser {
|
|||||||
private static TrayIcon trayIcon;
|
private static TrayIcon trayIcon;
|
||||||
private static SystemTray systemTray;
|
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(
|
SwingUtilities.invokeLater(
|
||||||
() -> {
|
() -> {
|
||||||
loadingWindow = new LoadingWindow(null, "Initializing...");
|
loadingWindow = new LoadingWindow(null, "Initializing...");
|
||||||
@ -120,6 +129,10 @@ public class DesktopBrowser implements WebBrowser {
|
|||||||
CefSettings settings = builder.getCefSettings();
|
CefSettings settings = builder.getCefSettings();
|
||||||
String basePath = InstallationPathConfig.getClientWebUIPath();
|
String basePath = InstallationPathConfig.getClientWebUIPath();
|
||||||
log.info("basePath " + basePath);
|
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.cache_path = new File(basePath + "cache").getAbsolutePath();
|
||||||
settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath();
|
settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath();
|
||||||
// settings.browser_subprocess_path = new File(basePath +
|
// 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
|
@PreDestroy
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
if (browser != null) browser.close(true);
|
if (browser != null) browser.close(true);
|
||||||
|
@ -145,6 +145,8 @@ system:
|
|||||||
cleanupIntervalMinutes: 30 # How often to run cleanup (in minutes)
|
cleanupIntervalMinutes: 30 # How often to run cleanup (in minutes)
|
||||||
startupCleanup: true # Clean up old temp files on startup
|
startupCleanup: true # Clean up old temp files on startup
|
||||||
cleanupSystemTemp: false # Whether to clean broader system temp directory
|
cleanupSystemTemp: false # Whether to clean broader system temp directory
|
||||||
|
batchSize: 1000 # Number of entries processed before optional pause (0 = unlimited)
|
||||||
|
pauseBetweenBatchesMs: 50 # Pause duration in milliseconds between batches
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
appName: '' # application's visible name
|
appName: '' # application's visible name
|
||||||
|
Loading…
Reference in New Issue
Block a user