task-149: 临时文件时间元数据补缺(TempFileMetadata 创建/修改/访问时间与大小读取,mtime 回退不改变清理语义,IO 错误容忍)+ 8 条测试
This commit is contained in:
+2
-1
@@ -118,7 +118,8 @@ public class LocalTempCleanupService {
|
||||
}
|
||||
|
||||
private boolean isExpired(File file, Instant expireBefore) {
|
||||
return Instant.ofEpochMilli(file.lastModified()).isBefore(expireBefore);
|
||||
// 以 mtime 近似最后访问(TempFileMetadata 回退语义,与现状一致)
|
||||
return TempFileMetadata.lastAccessTime(file).isBefore(expireBefore);
|
||||
}
|
||||
|
||||
private boolean isDirectoryEmpty(File directory) {
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.nanri.aiimage.modules.file.service;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* 临时文件时间元数据读取(task-149)。
|
||||
*
|
||||
* 创建/最后修改/大小显式读取;最后访问时间无独立记录表,以 mtime 回退
|
||||
* (与既有 LocalTempCleanupService 清理语义一致,不改变文件语义)。
|
||||
* 属性读取失败或文件缺失时回退 mtime/0,不抛 IO 异常(清理路径不得被阻断)。
|
||||
*/
|
||||
public final class TempFileMetadata {
|
||||
|
||||
private TempFileMetadata() {
|
||||
}
|
||||
|
||||
/** 创建时间;属性不可读时回退到最后修改时间。 */
|
||||
public static Instant creationTime(File file) {
|
||||
if (file == null || !file.isFile()) {
|
||||
return lastModified(file);
|
||||
}
|
||||
try {
|
||||
BasicFileAttributes attrs = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
|
||||
return attrs.creationTime().toInstant();
|
||||
} catch (IOException ex) {
|
||||
return lastModified(file);
|
||||
}
|
||||
}
|
||||
|
||||
/** 最后修改时间(mtime)。 */
|
||||
public static Instant lastModified(File file) {
|
||||
if (file == null) {
|
||||
return Instant.EPOCH;
|
||||
}
|
||||
return Instant.ofEpochMilli(file.lastModified());
|
||||
}
|
||||
|
||||
/**
|
||||
* 最后访问时间:无独立访问记录表,以 mtime 回退
|
||||
* (清理判据与现状一致:mtime 近似最后访问)。
|
||||
*/
|
||||
public static Instant lastAccessTime(File file) {
|
||||
return lastModified(file);
|
||||
}
|
||||
|
||||
/** 文件大小(字节);文件缺失时为 0。 */
|
||||
public static long size(File file) {
|
||||
if (file == null || !file.isFile()) {
|
||||
return 0L;
|
||||
}
|
||||
return file.length();
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.nanri.aiimage.modules.file.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
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.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* task-149:临时文件创建/访问时间记录契约(plan 09)。
|
||||
* 创建时间/大小显式读取;最后访问时间以 mtime 回退(与清理语义一致);
|
||||
* 缺记录/IO 错误容忍,不改变文件语义。
|
||||
*/
|
||||
class TempFileMetadataTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private File writeTempFile(String name, String content) throws Exception {
|
||||
Path path = tempDir.resolve(name);
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||
return path.toFile();
|
||||
}
|
||||
|
||||
@Test
|
||||
void creationTimeRecorded() throws Exception {
|
||||
File file = writeTempFile("a.tmp", "hello");
|
||||
|
||||
Instant creation = TempFileMetadata.creationTime(file);
|
||||
|
||||
assertNotNull(creation);
|
||||
assertFalse(creation.isAfter(Instant.now().plus(1, ChronoUnit.MINUTES)),
|
||||
"创建时间不得晚于当前时间");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sizeRecorded() throws Exception {
|
||||
File file = writeTempFile("b.tmp", "0123456789");
|
||||
|
||||
assertEquals(10L, TempFileMetadata.size(file), "大小按字节记录");
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessTimeReadable() throws Exception {
|
||||
File file = writeTempFile("c.tmp", "x");
|
||||
|
||||
Instant access = TempFileMetadata.lastAccessTime(file);
|
||||
|
||||
assertNotNull(access);
|
||||
assertFalse(access.isAfter(Instant.now().plus(1, ChronoUnit.MINUTES)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void accessTimeFallsBackToMtime() throws Exception {
|
||||
File file = writeTempFile("d.tmp", "y");
|
||||
|
||||
assertEquals(TempFileMetadata.lastModified(file), TempFileMetadata.lastAccessTime(file),
|
||||
"无独立访问记录时以 mtime 回退(清理语义不变)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void modifiedTimeUpdatesOnWrite() throws Exception {
|
||||
File file = writeTempFile("e.tmp", "v1");
|
||||
Instant before = TempFileMetadata.lastModified(file);
|
||||
|
||||
Thread.sleep(20);
|
||||
Files.writeString(file.toPath(), "v2-longer-content", StandardCharsets.UTF_8);
|
||||
Instant after = TempFileMetadata.lastModified(file);
|
||||
|
||||
assertFalse(after.isBefore(before), "写入后 mtime 必须更新");
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingFileIsGraceful() {
|
||||
File missing = tempDir.resolve("nope.tmp").toFile();
|
||||
|
||||
assertEquals(0L, TempFileMetadata.size(missing), "缺失文件大小为 0");
|
||||
assertNotNull(TempFileMetadata.creationTime(missing), "缺失文件不回退抛错");
|
||||
assertNotNull(TempFileMetadata.lastAccessTime(missing));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullFileIsGraceful() {
|
||||
assertEquals(0L, TempFileMetadata.size(null));
|
||||
assertEquals(Instant.EPOCH, TempFileMetadata.lastModified(null), "null 文件回退 EPOCH");
|
||||
assertEquals(Instant.EPOCH, TempFileMetadata.lastAccessTime(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeOrderingConsistent() throws Exception {
|
||||
File file = writeTempFile("f.tmp", "z");
|
||||
Instant creation = TempFileMetadata.creationTime(file);
|
||||
Instant modified = TempFileMetadata.lastModified(file);
|
||||
Instant now = Instant.now().plus(1, ChronoUnit.MINUTES);
|
||||
|
||||
assertTrue(!creation.isAfter(modified.plus(1, ChronoUnit.SECONDS)),
|
||||
"创建时间 <= 修改时间(容忍文件系统时钟精度)");
|
||||
assertTrue(!modified.isAfter(now), "修改时间 <= 当前时间");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user