task-155: 孤立文件巡检报表(只读巡检超保留期且无引用文件,报表含路径/大小/时间,绝不删除,可重复)+ 8 条测试

This commit is contained in:
2026-09-02 06:47:03 +08:00
parent 87a6d20f88
commit d64ef5284a
2 changed files with 207 additions and 0 deletions
@@ -0,0 +1,86 @@
package com.nanri.aiimage.modules.file.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* 临时目录孤立文件巡检(task-155)。
*
* 只读巡检:输出"无任何引用(或引用判定失败视为未引用候选)且超过保留期"的
* 文件清单报表(日志 + 返回对象);绝不删除文件。巡检可重复执行。
*/
@Slf4j
@Service
public class TempOrphanInspector {
public record OrphanFileEntry(String path, long sizeBytes, String lastModifiedAt) {
}
public record OrphanFileReport(List<OrphanFileEntry> entries, int totalBytes) {
public boolean isEmpty() {
return entries == null || entries.isEmpty();
}
}
/**
* 巡检目录内孤立文件。
*
* @param dir 临时根目录
* @param expireBefore 保留期边界(lastModified 早于该时刻视为过期)
* @param isReferenced fileKey/文件名 → 是否仍被引用(引用判定失败返回 false 时
* 文件进候选,由报表人工复核;判定抛异常时保守跳过该文件)
*/
public OrphanFileReport inspectOrphanFiles(File dir, Instant expireBefore, Predicate<String> isReferenced) {
List<OrphanFileEntry> entries = new ArrayList<>();
if (dir == null || !dir.isDirectory() || expireBefore == null) {
return new OrphanFileReport(entries, 0);
}
try (Stream<Path> paths = Files.walk(dir.toPath())) {
paths.filter(Files::isRegularFile).forEach(path -> {
File file = path.toFile();
Instant lastModified = TempFileMetadata.lastModified(file);
if (!lastModified.isBefore(expireBefore)) {
return;
}
boolean referenced;
try {
referenced = isReferenced != null && isReferenced.test(file.getName());
} catch (Exception ex) {
log.warn("orphan inspect reference check failed file={} err={}",
file.getName(), ex.getMessage());
return;
}
if (!referenced) {
entries.add(new OrphanFileEntry(
path.toString(),
file.length(),
lastModified.toString()));
}
});
} catch (IOException ex) {
log.warn("orphan inspect walk failed dir={} err={}", dir.getAbsolutePath(), ex.getMessage());
}
int totalBytes = entries.stream().mapToInt(entry -> (int) Math.min(Integer.MAX_VALUE, entry.sizeBytes())).sum();
OrphanFileReport report = new OrphanFileReport(entries, totalBytes);
if (!report.isEmpty()) {
log.info("temp orphan file report: count={} totalBytes={} dir={}",
report.entries().size(), totalBytes, dir.getAbsolutePath());
for (OrphanFileEntry entry : report.entries()) {
log.info("temp orphan file: path={} sizeBytes={} lastModifiedAt={}",
entry.path(), entry.sizeBytes(), entry.lastModifiedAt());
}
}
return report;
}
}