Archived
feat: add 30-minute auto-cleanup for generated cache files
This commit is contained in:
@@ -4,16 +4,26 @@ import java.io.IOException;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件缓存服务。
|
* 文件缓存服务。
|
||||||
* 生成的文件保存到本地磁盘,通过随机 key 索引,供下载接口使用。
|
* 生成的文件保存到本地磁盘,通过随机 key 索引,供下载接口使用。
|
||||||
|
* 文件生成 30 分钟后自动清理。
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class FileCacheService {
|
public class FileCacheService {
|
||||||
@@ -21,22 +31,39 @@ public class FileCacheService {
|
|||||||
private static final Logger log = LoggerFactory.getLogger(FileCacheService.class);
|
private static final Logger log = LoggerFactory.getLogger(FileCacheService.class);
|
||||||
|
|
||||||
private final Path cacheDir;
|
private final Path cacheDir;
|
||||||
|
private final Duration expiration;
|
||||||
|
|
||||||
public FileCacheService(@Value("${gmp.cache-dir:/tmp/gmp-cache}") String cacheDirPath) {
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(
|
||||||
|
r -> new Thread(r, "file-cache-cleaner"));
|
||||||
|
|
||||||
|
public FileCacheService(@Value("${gmp.cache-dir:/tmp/gmp-cache}") String cacheDirPath,
|
||||||
|
@Value("${gmp.cache-expiration:30m}") Duration expiration) {
|
||||||
this.cacheDir = Path.of(cacheDirPath);
|
this.cacheDir = Path.of(cacheDirPath);
|
||||||
|
this.expiration = expiration;
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(this.cacheDir);
|
Files.createDirectories(this.cacheDir);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("无法创建缓存目录: " + cacheDirPath, e);
|
throw new RuntimeException("无法创建缓存目录: " + cacheDirPath, e);
|
||||||
}
|
}
|
||||||
log.info("File cache directory: {}", this.cacheDir.toAbsolutePath());
|
log.info("File cache directory: {}, expiration: {}", this.cacheDir.toAbsolutePath(), this.expiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void startCleanup() {
|
||||||
|
// Initial cleanup on startup
|
||||||
|
cleanExpired();
|
||||||
|
// Schedule periodic cleanup every 5 minutes
|
||||||
|
scheduler.scheduleAtFixedRate(this::cleanExpired, 5, 5, TimeUnit.MINUTES);
|
||||||
|
log.info("Cache cleanup scheduled every 5 minutes");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void stopCleanup() {
|
||||||
|
scheduler.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成一个随机 key,并将数据写入缓存文件。
|
* 生成一个随机 key,并将数据写入缓存文件。
|
||||||
*
|
|
||||||
* @param data 文件字节数据
|
|
||||||
* @return 随机 key
|
|
||||||
*/
|
*/
|
||||||
public String store(byte[] data) {
|
public String store(byte[] data) {
|
||||||
String key = UUID.randomUUID().toString().replace("-", "");
|
String key = UUID.randomUUID().toString().replace("-", "");
|
||||||
@@ -52,9 +79,6 @@ public class FileCacheService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 key 读取缓存文件。
|
* 根据 key 读取缓存文件。
|
||||||
*
|
|
||||||
* @param key 随机 key
|
|
||||||
* @return 文件字节数据,或 null(key 不存在)
|
|
||||||
*/
|
*/
|
||||||
public byte[] retrieve(String key) {
|
public byte[] retrieve(String key) {
|
||||||
Path file = cacheDir.resolve(sanitize(key));
|
Path file = cacheDir.resolve(sanitize(key));
|
||||||
@@ -71,10 +95,6 @@ public class FileCacheService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 把缓存文件内容写入输出流。
|
* 把缓存文件内容写入输出流。
|
||||||
*
|
|
||||||
* @param key 随机 key
|
|
||||||
* @param out 输出流
|
|
||||||
* @return 是否写入成功
|
|
||||||
*/
|
*/
|
||||||
public boolean writeToStream(String key, OutputStream out) throws IOException {
|
public boolean writeToStream(String key, OutputStream out) throws IOException {
|
||||||
Path file = cacheDir.resolve(sanitize(key));
|
Path file = cacheDir.resolve(sanitize(key));
|
||||||
@@ -85,6 +105,30 @@ public class FileCacheService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期的缓存文件(超过 expiration 时间的文件)。
|
||||||
|
*/
|
||||||
|
private void cleanExpired() {
|
||||||
|
try {
|
||||||
|
Instant cutoff = Instant.now().minus(this.expiration);
|
||||||
|
Files.list(cacheDir).forEach(file -> {
|
||||||
|
try {
|
||||||
|
if (Files.isRegularFile(file)) {
|
||||||
|
BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);
|
||||||
|
if (attrs.creationTime().toInstant().isBefore(cutoff)) {
|
||||||
|
Files.delete(file);
|
||||||
|
log.debug("Deleted expired cache file: {}", file.getFileName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to clean cache file: {}", file.getFileName(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Cache cleanup failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String sanitize(String key) {
|
private String sanitize(String key) {
|
||||||
return key.replaceAll("[^a-zA-Z0-9\\-_]", "");
|
return key.replaceAll("[^a-zA-Z0-9\\-_]", "");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user