task-160: 超保留期仍被引用巡检(超期+引用检出供人工处理、未超期/未引用不报、只读、可重复)+ 8 条测试
This commit is contained in:
+49
@@ -83,4 +83,53 @@ public class TempOrphanInspector {
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* 超保留期仍被引用巡检(task-160)。
|
||||
*
|
||||
* 只读:临时文件超保留期但仍被任务引用的清单(供人工处理);绝不删除。
|
||||
* 与 {@link #inspectOrphanFiles} 互补(该处报"未引用",此处报"被引用")。
|
||||
*/
|
||||
public OrphanFileReport inspectOverRetentionReferenced(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("over-retention 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("over-retention inspect walk failed dir={} err={}", dir.getAbsolutePath(), ex.getMessage());
|
||||
}
|
||||
OrphanFileReport report = new OrphanFileReport(entries,
|
||||
entries.stream().mapToInt(entry -> (int) Math.min(Integer.MAX_VALUE, entry.sizeBytes())).sum());
|
||||
if (!report.isEmpty()) {
|
||||
log.info("temp over-retention referenced report: count={} dir={}",
|
||||
report.entries().size(), dir.getAbsolutePath());
|
||||
for (OrphanFileEntry entry : report.entries()) {
|
||||
log.info("temp over-retention referenced: path={} sizeBytes={} lastModifiedAt={}",
|
||||
entry.path(), entry.sizeBytes(), entry.lastModifiedAt());
|
||||
}
|
||||
}
|
||||
return report;
|
||||
}
|
||||
}
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package com.nanri.aiimage.modules.file.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* task-160:超保留期仍被引用巡检契约(plan 09)。
|
||||
* 临时文件超保留期但仍有引用 → 检出(供人工处理);未超期不报;
|
||||
* 超期未引用不报(归孤立文件报表);只读;可重复。
|
||||
*/
|
||||
class OverRetentionReferencedTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private final TempOrphanInspector inspector = new TempOrphanInspector();
|
||||
|
||||
private Path writeFile(String name, String content) throws Exception {
|
||||
Path path = tempDir.resolve(name);
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||
return path;
|
||||
}
|
||||
|
||||
@Test
|
||||
void overRetentionReferencedDetected() throws Exception {
|
||||
writeFile("kept.tmp", "x");
|
||||
|
||||
var report = inspector.inspectOverRetentionReferenced(
|
||||
tempDir.toFile(), Instant.now().plus(1, ChronoUnit.HOURS), name -> name.equals("kept.tmp"));
|
||||
|
||||
assertEquals(1, report.entries().size(), "超期且被引用必须检出");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inRetentionNotReported() throws Exception {
|
||||
writeFile("fresh.tmp", "x");
|
||||
|
||||
var report = inspector.inspectOverRetentionReferenced(
|
||||
tempDir.toFile(), Instant.now().minus(1, ChronoUnit.HOURS), name -> true);
|
||||
|
||||
assertTrue(report.isEmpty(), "未超保留期不报");
|
||||
}
|
||||
|
||||
@Test
|
||||
void overRetentionUnreferencedNotReportedHere() throws Exception {
|
||||
writeFile("orphan.tmp", "x");
|
||||
|
||||
var report = inspector.inspectOverRetentionReferenced(
|
||||
tempDir.toFile(), Instant.now().plus(1, ChronoUnit.HOURS), name -> false);
|
||||
|
||||
assertTrue(report.isEmpty(), "超期未引用归孤立文件报表,不在此处重复报");
|
||||
}
|
||||
|
||||
@Test
|
||||
void boundaryRetentionIsGraceful() throws Exception {
|
||||
writeFile("edge.tmp", "x");
|
||||
Instant now = Instant.now();
|
||||
|
||||
var report = inspector.inspectOverRetentionReferenced(tempDir.toFile(), now, name -> true);
|
||||
|
||||
// 边界(mtime 与边界接近):不抛错、报表确定
|
||||
assertTrue(report.isEmpty() || report.entries().size() == 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyReportWhenNothingReferenced() throws Exception {
|
||||
writeFile("a.tmp", "x");
|
||||
|
||||
var report = inspector.inspectOverRetentionReferenced(
|
||||
tempDir.toFile(), Instant.now().plus(1, ChronoUnit.HOURS), name -> false);
|
||||
|
||||
assertTrue(report.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readOnlyDoesNotDelete() throws Exception {
|
||||
Path file = writeFile("referenced.tmp", "keep");
|
||||
|
||||
inspector.inspectOverRetentionReferenced(
|
||||
tempDir.toFile(), Instant.now().plus(1, ChronoUnit.HOURS), name -> true);
|
||||
|
||||
assertTrue(Files.exists(file), "只读巡检不得删除");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportIncludesDetails() throws Exception {
|
||||
writeFile("detail.tmp", "0123456789");
|
||||
|
||||
var report = inspector.inspectOverRetentionReferenced(
|
||||
tempDir.toFile(), Instant.now().plus(1, ChronoUnit.HOURS), name -> true);
|
||||
|
||||
TempOrphanInspector.OrphanFileEntry entry = report.entries().getFirst();
|
||||
assertTrue(entry.path().contains("detail.tmp"));
|
||||
assertEquals(10L, entry.sizeBytes());
|
||||
assertTrue(entry.lastModifiedAt() != null && !entry.lastModifiedAt().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rerunIsSafeAndStable() throws Exception {
|
||||
writeFile("rerun.tmp", "x");
|
||||
|
||||
var first = inspector.inspectOverRetentionReferenced(
|
||||
tempDir.toFile(), Instant.now().plus(1, ChronoUnit.HOURS), name -> true);
|
||||
var second = inspector.inspectOverRetentionReferenced(
|
||||
tempDir.toFile(), Instant.now().plus(1, ChronoUnit.HOURS), name -> true);
|
||||
|
||||
assertEquals(first.entries().size(), second.entries().size());
|
||||
assertEquals(first.entries().getFirst().path(), second.entries().getFirst().path());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user