初始化(可选)

零配置启动(默认行为)

// 无需任何初始化——直接使用即可。
// 首次操作时自动以本地文件系统启动,根目录为 JAR 所在目录下的 vkfiles/。
Vostok.File.write("config/app.conf", "key=value");
String text = Vostok.File.read("config/app.conf");

显式初始化

Vostok.init(cfg -> cfg
    .fileConfig(new VKFileConfig()
        .baseDir("./data/files")
        .charset(StandardCharsets.UTF_8)
        .watchRecursiveDefault(false)
        .datePartitionPattern("yyyy/MM/dd")
        .datePartitionZoneId("Asia/Shanghai")
    )
);
零配置启动
File 模块无需显式初始化。首次调用任意文件操作时,若尚未初始化,将自动以本地文件系统后端启动: 根目录为 JAR 所在目录下的 vkfiles/(IDE/解压部署时为 class 目录下的 vkfiles/), 编码 UTF-8,其余参数取默认值。 如需自定义根目录、编码或其他参数,在首次调用前调用 init(VKFileConfig) 即可。 初始化是线程安全且幂等的,重复调用 init() 会关闭并重建默认后端。

文本读写

// 创建文件(文件已存在时行为由后端决定,通常抛异常)
Vostok.File.create("config/app.conf", "key=value");

// 覆盖写入(文件不存在时自动创建)
Vostok.File.write("data/report.txt", content);

// 更新已有文件内容(文件不存在时抛 FK-404)
Vostok.File.update("config/app.conf", newContent);

// 追加文本
Vostok.File.append("logs/access.log", "2024-01-01 GET /\n");

// 读取全部文本
String text = Vostok.File.read("config/app.conf");

// 按行读取
List<String> lines = Vostok.File.readLines("config/app.conf");

// 按行写入(覆盖)
Vostok.File.writeLines("config/app.conf", List.of("k1=v1", "k2=v2"));

字节读写

// 写入字节(覆盖)
Vostok.File.writeBytes("assets/logo.png", pngBytes);

// 追加字节
Vostok.File.appendBytes("data/chunk.bin", chunk);

// 读取全部字节
byte[] raw = Vostok.File.readBytes("assets/logo.png");

// 部分读取:从 offset 开始读取 length 字节
byte[] slice = Vostok.File.readRange("data/large.bin", 1024, 512);

流式读写

// 从 InputStream 写入文件,返回写入字节数
long written = Vostok.File.writeFrom("uploads/avatar.jpg", inputStream);

// 写入时控制是否覆盖已有文件
long written = Vostok.File.writeFrom("uploads/avatar.jpg", inputStream, false);

// 从 InputStream 追加到文件,返回追加字节数
long appended = Vostok.File.appendFrom("data/stream.log", inputStream);

// 将文件全部内容写出到 OutputStream,返回字节数
long bytes = Vostok.File.readTo("assets/logo.png", outputStream);

// 部分读取到 OutputStream:从 offset 开始读取 length 字节
long bytes = Vostok.File.readRangeTo("data/large.bin", 0L, 4096L, outputStream);

目录操作

// 创建单层目录(父目录必须已存在)
Vostok.File.mkdir("uploads/2024");

// 递归创建目录(含所有父目录,等同 mkdirs)
Vostok.File.mkdirs("uploads/2024/01/photos");

// 列出目录直接子项(返回 VKFileInfo 列表)
List<VKFileInfo> items = Vostok.File.list("uploads");

// 递归列出目录所有子项
List<VKFileInfo> all = Vostok.File.list("uploads", true);

// 递归遍历并过滤(只要普通文件)
List<VKFileInfo> files = Vostok.File.walk("uploads", true,
    info -> !info.directory());

// 存在性与类型检查
boolean exists = Vostok.File.exists("config/app.conf");
boolean isDir  = Vostok.File.isDirectory("uploads");
boolean isFile = Vostok.File.isFile("config/app.conf");

// 获取文件大小(字节)
long bytes = Vostok.File.size("data/report.txt");

// 递归统计目录总大小(字节)
long total = Vostok.File.totalSize("uploads");

// 获取最后修改时间
Instant ts = Vostok.File.lastModified("data/report.txt");

// touch:不存在则创建空文件,已存在则更新修改时间
Vostok.File.touch("logs/app.log");

VKFileInfo 包含以下字段:

方法类型说明
path()String相对于存储根目录的路径
directory()boolean是否为目录
size()long文件大小(字节),目录为 0
lastModified()Instant最后修改时间

文件与目录的复制、移动、重命名、删除

// 重命名(在同目录下改名)
Vostok.File.rename("temp/draft.txt", "draft_v2.txt");

// 复制文件(默认覆盖目标)
Vostok.File.copy("templates/base.html", "pages/index.html");

// 复制文件,指定是否覆盖
Vostok.File.copy("templates/base.html", "pages/about.html", false);

// 移动/重命名文件(默认覆盖目标)
Vostok.File.move("temp/draft.txt", "published/article.txt");

// 移动文件,指定是否覆盖
Vostok.File.move("temp/draft.txt", "archive/article.txt", false);

// 复制目录(指定冲突策略)
Vostok.File.copyDir("src/assets", "dist/assets", VKFileConflictStrategy.OVERWRITE);

// 移动目录(指定冲突策略)
Vostok.File.moveDir("staging/release", "production/release", VKFileConflictStrategy.SKIP);

// 删除文件(文件不存在时抛 FK-404)
Vostok.File.delete("temp/draft.txt");

// 删除文件(文件不存在时静默返回 false,存在且删除成功返回 true)
boolean deleted = Vostok.File.deleteIfExists("temp/maybe.txt");

// 递归删除目录及其所有内容
boolean ok = Vostok.File.deleteRecursively("temp");

VKFileConflictStrategy 枚举值:

行为
FAIL目标已存在时抛出异常(默认)
SKIP目标已存在时跳过,不覆盖
OVERWRITE目标已存在时覆盖

日期分区路径

按当前时间自动插入日期目录,路径格式由 datePartitionPattern 配置(默认 yyyy/MM/dd)。

// 生成日期分区路径(不写入文件)
// 例如今天 2024-01-15,relativePath="report.csv"
// 返回 "2024/01/15/report.csv"
String path = Vostok.File.suggestDatePath("report.csv");

// 指定时刻生成路径
String path = Vostok.File.suggestDatePath("report.csv", Instant.parse("2024-06-01T00:00:00Z"));

// 按日期分区写入文本,返回实际写入路径
String writtenPath = Vostok.File.writeByDatePath("report.csv", csvContent);

// 按日期分区写入字节,返回实际写入路径
String writtenPath = Vostok.File.writeBytesByDatePath("photo.jpg", jpegBytes);

// 按日期分区从流写入,返回实际写入路径
String writtenPath = Vostok.File.writeFromByDatePath("upload.bin", inputStream);

临时文件

// 在 tmp/ 目录下创建临时文件,返回相对路径
String tmpPath = Vostok.File.createTemp("upload_", ".tmp");

// 在指定子目录下创建临时文件
String tmpPath = Vostok.File.createTemp("processing", "chunk_", ".bin");

压缩与解压

// ZIP 压缩(文件或目录)
Vostok.File.zip("data/reports", "archives/reports.zip");

// ZIP 解压(使用配置中的安全限制)
Vostok.File.unzip("archives/data.zip", "output/");

// ZIP 解压,指定是否覆盖已有文件
Vostok.File.unzip("archives/data.zip", "output/", false);

// ZIP 解压,自定义安全选项
VKUnzipOptions opts = VKUnzipOptions.builder()
    .replaceExisting(true)
    .maxEntries(500)
    .maxTotalUncompressedBytes(100L * 1024 * 1024)  // 100 MB
    .maxEntryUncompressedBytes(50L * 1024 * 1024)   // 50 MB
    .build();
Vostok.File.unzip("archives/data.zip", "output/", opts);

// GZip 压缩
Vostok.File.gzip("data/large.log", "data/large.log.gz");

// GZip 解压(方法名为 gunzip,非 ungzip)
Vostok.File.gunzip("data/large.log.gz", "data/large.log");
安全
ZIP 解压内置 Zip Slip 路径穿越防护和 Zip Bomb 检测。安全上限通过 VKUnzipOptionsVKFileConfig 中的 unzipMaxEntriesunzipMaxTotalUncompressedBytesunzipMaxEntryUncompressedBytes 控制;默认值 -1 表示不限制,生产环境建议显式配置。
说明
Excel(.xlsx)能力已迁移到 Vostok.Office,请参考 Office 模块文档

图片缩略图

// 生成缩略图字节,返回 byte[]
VKThumbnailOptions opts = VKThumbnailOptions.builder(200, 200)
    .mode(VKThumbnailMode.FIT)      // FIT:等比缩放;FILL:裁剪填充
    .format("jpg")
    .quality(0.85f)
    .keepAspectRatio(true)
    .upscale(false)
    .stripMetadata(true)
    .build();
byte[] thumb = Vostok.File.thumbnail("uploads/photo.jpg", opts);

// 生成缩略图并写入指定路径
Vostok.File.thumbnailTo("uploads/photo.jpg", "thumbs/photo_200x200.jpg", opts);

文件哈希

// 计算文件哈希,算法名同 MessageDigest(MD5 / SHA-1 / SHA-256 等)
String md5    = Vostok.File.hash("data/package.zip", "MD5");
String sha256 = Vostok.File.hash("data/package.zip", "SHA-256");

文件加密与解密

委托 Vostok.Security 的 AES-256-GCM 实现,采用 vkf2 分块流式格式(每 1 MB 独立一个 GCM 分块),内存消耗恒定,支持任意大小的二进制文件。加密密钥通过 DEK/KEK 双层 Key Wrapping 保护,keyId 和 KEK 版本号写入文件头,解密时自动读取,无需调用方保存额外参数。使用前须先初始化 Vostok.Security 的 KeyStore。

// 前提:初始化 Security 模块的 KeyStore(与 encryptWithKeyId 共享同一 KeyStore)
Vostok.Security.initKeyStore(new VKKeyStoreConfig()
    .baseDir("./keys")
    .masterKey("my-master-secret"));

// 加密文件:sourcePath 明文 → targetPath vkf2 密文,keyId 对应 KeyStore 中的 KEK
Vostok.File.encryptFile("data/secret.bin", "data/secret.enc", "file-key-v1");

// 解密文件:sourcePath 密文 → targetPath 明文(keyId 和 KEK 版本自动从文件头读取)
// 解密失败(认证错误/密钥错误)时目标文件不写入任何字节
Vostok.File.decryptFile("data/secret.enc", "data/secret.bin");

文件监听

watch() 返回 VKFileWatchHandle(实现 AutoCloseable),调用 close() 停止监听。

// 监听目录变化(是否递归由 watchRecursiveDefault 配置决定,默认 false)
VKFileWatchHandle handle = Vostok.File.watch("config/", event -> {
    System.out.println(event.type() + ": " + event.path() + " at " + event.time());
});

// 监听目录变化,显式指定是否递归
VKFileWatchHandle handle = Vostok.File.watch("uploads/", true, event -> {
    if (event.type() == VKFileWatchEventType.CREATE) {
        processUpload(event.path());
    }
});

// 停止监听
handle.close();

// 使用 try-with-resources 自动停止
try (VKFileWatchHandle h = Vostok.File.watch("hot-reload/", false, this::onFileChanged)) {
    // 监听期间执行业务逻辑…
}

VKFileWatchEvent 字段:

方法类型说明
type()VKFileWatchEventType事件类型:CREATE / MODIFY / DELETE / OVERFLOW
path()String发生变化的文件相对路径
time()Instant事件发生时刻

目录迁移

将当前存储根目录下的所有文件迁移到新的目录,支持复制或移动模式、并行迁移、断点续传、进度回调。

// 简单迁移(COPY_ONLY,冲突时抛异常)
VKFileMigrateResult result = Vostok.File.migrateBaseDir("/new/storage");

// 高级迁移选项
VKFileMigrateOptions opts = new VKFileMigrateOptions()
    .mode(VKFileMigrateMode.MOVE)                       // COPY_ONLY 或 MOVE
    .conflictStrategy(VKFileConflictStrategy.OVERWRITE)
    .verifyHash(true)                                    // 迁移后校验哈希
    .dryRun(false)                                        // true 时只模拟不实际操作
    .parallelism(4)                                       // 并行线程数
    .maxRetries(3)                                        // 单文件失败最大重试次数
    .retryIntervalMs(500L)                               // 重试间隔毫秒
    .checkpointFile("migrate.checkpoint")               // 断点续传文件
    .progressListener(p -> {
        System.out.printf("[%s] %s (%d/%d)%n",
            p.status(), p.path(), p.migratedFiles(), p.totalFiles());
    });

VKFileMigrateResult result = Vostok.File.migrateBaseDir("/new/storage", opts);

// 检查迁移结果
if (!result.success()) {
    result.failures().forEach(f ->
        System.out.println("FAILED: " + f.path() + " - " + f.message()));
}
System.out.printf("迁移完成:%d 个文件,%d 字节,耗时 %d ms%n",
    result.migratedFiles(), result.migratedBytes(), result.durationMs());

多存储后端

// 注册自定义存储实现(实现 VKFileStore 接口)
Vostok.File.registerStore("s3", new S3FileStore(s3Client, bucket));

// 切换全局默认后端
Vostok.File.setDefaultMode("s3");

// 查询当前默认后端名称
String mode = Vostok.File.defaultMode();

// 查询所有已注册后端名称
Set<String> modes = Vostok.File.modes();

// 在当前线程临时切换后端(Runnable 形式)
Vostok.File.withMode("s3", () -> {
    Vostok.File.write("remote/file.txt", data);
});

// 在当前线程临时切换后端(Supplier 形式,带返回值)
String content = Vostok.File.withMode("s3", () -> Vostok.File.read("remote/config.txt"));

// 查询当前线程实际使用的后端(含 withMode 覆盖)
String current = Vostok.File.currentMode();

只读模式

Vostok.File.setReadOnly(true);   // 开启只读,所有写操作抛 FK-560
Vostok.File.setReadOnly(false);  // 恢复可写
boolean ro = Vostok.File.isReadOnly();
提示
Vostok.File.close() 会自动将只读状态重置为 false,防止重新初始化后残留只读状态。

模块状态

// 检查模块是否已初始化
boolean ready = Vostok.File.started();

// 获取当前配置副本
VKFileConfig cfg = Vostok.File.config();

// 关闭模块,释放所有后端资源
Vostok.File.close();

配置参数

参数类型默认值说明
modeString"local"默认存储后端名称
baseDirStringuser.dir本地存储根目录(懒加载默认为 JAR 所在目录/vkfiles)
charsetCharsetStandardCharsets.UTF_8文本读写编码,需传入 Charset 对象
unzipMaxEntrieslong-1(不限)ZIP 解压最大条目数,-1 表示不限制
unzipMaxTotalUncompressedByteslong-1(不限)ZIP 解压总字节上限,-1 表示不限制
unzipMaxEntryUncompressedByteslong-1(不限)单条目解压字节上限,-1 表示不限制
watchRecursiveDefaultbooleanfalsewatch(path, listener) 是否默认递归监听
datePartitionPatternString"yyyy/MM/dd"日期分区目录格式,需符合 DateTimeFormatter 语法
datePartitionZoneIdString系统默认时区日期分区所用时区 ID,如 "Asia/Shanghai"

API 速查

方法说明
文本读写
create(path, content)创建新文件并写入内容
write(path, content)覆盖写入文本
update(path, content)更新已有文件内容(不存在抛 FK-404)
append(path, content)追加文本
read(path)读取全部文本
readLines(path)按行读取,返回 List<String>
writeLines(path, lines)按行覆盖写入
字节读写
writeBytes(path, bytes)覆盖写入字节
appendBytes(path, bytes)追加字节
readBytes(path)读取全部字节
readRange(path, offset, length)部分读取字节
流式读写
writeFrom(path, input)从 InputStream 写入,返回字节数
writeFrom(path, input, replace)从流写入,指定是否覆盖
appendFrom(path, input)从流追加,返回字节数
readTo(path, output)读取全部内容到 OutputStream
readRangeTo(path, offset, len, output)部分读取到 OutputStream
目录与文件管理
mkdir(path)创建单层目录
mkdirs(path)递归创建目录(含父目录)
list(path)列出直接子项,返回 List<VKFileInfo>
list(path, recursive)列出子项,可递归
walk(path, recursive, filter)遍历并过滤
exists(path)是否存在
isFile(path)是否为普通文件
isDirectory(path)是否为目录
size(path)文件大小(字节)
totalSize(dirPath)目录总大小(字节)
lastModified(path)最后修改时间(Instant)
touch(path)创建空文件或更新修改时间
rename(path, newName)重命名(同目录)
copy(src, dest)复制文件(默认覆盖)
copy(src, dest, replace)复制文件,指定是否覆盖
move(src, dest)移动文件(默认覆盖)
move(src, dest, replace)移动文件,指定是否覆盖
copyDir(src, dest, strategy)复制目录
moveDir(src, dest, strategy)移动目录
delete(path)删除文件(不存在抛 FK-404)
deleteIfExists(path)删除文件(不存在返回 false)
deleteRecursively(path)递归删除目录
日期分区
suggestDatePath(relativePath)生成当前时间的分区路径
suggestDatePath(relativePath, atTime)生成指定时刻的分区路径
writeByDatePath(relativePath, content)按日期分区写入文本,返回实际路径
writeBytesByDatePath(relativePath, bytes)按日期分区写入字节,返回实际路径
writeFromByDatePath(relativePath, input)按日期分区从流写入,返回实际路径
临时文件
createTemp(prefix, suffix)在 tmp/ 下创建临时文件,返回相对路径
createTemp(subDir, prefix, suffix)在指定子目录下创建临时文件
压缩与解压
zip(src, zipPath)ZIP 压缩文件或目录
zip(src, zipPath, includeBaseDir)ZIP 压缩并指定是否保留源目录名
unzip(zipPath, destDir)ZIP 解压(使用配置安全限制)
unzip(zipPath, destDir, replace)ZIP 解压,指定是否覆盖
unzip(zipPath, destDir, options)ZIP 解压,自定义安全选项
gzip(src, gzPath)GZip 压缩
gunzip(gzPath, dest)GZip 解压
图片与哈希
thumbnail(imagePath, options)生成缩略图字节
thumbnailTo(imagePath, targetPath, options)生成缩略图并写入文件
hash(path, algorithm)计算文件哈希(MD5/SHA-256 等)
加密与解密
encryptFile(src, dest, keyId)vkf2 分块流式 AES-256-GCM 加密文件(需先初始化 Security KeyStore)
decryptFile(src, dest)解密 vkf2/vkf1 格式文件(keyId 自动从文件头读取,失败时目标文件不写入任何字节)
文件监听
watch(path, listener)监听文件/目录变化,返回 VKFileWatchHandle
watch(path, recursive, listener)监听,显式指定是否递归
目录迁移
migrateBaseDir(targetBaseDir)迁移存储根目录(默认选项)
migrateBaseDir(targetBaseDir, options)迁移存储根目录(自定义选项)
多后端与状态
registerStore(mode, store)注册自定义存储后端
setDefaultMode(mode)切换全局默认后端
defaultMode()获取当前默认后端名称
modes()获取所有已注册后端名称
withMode(mode, action)当前线程临时切换后端(Runnable)
withMode(mode, supplier)当前线程临时切换后端(Supplier<T>)
currentMode()获取当前线程实际使用的后端名称
setReadOnly(flag)设置只读模式
isReadOnly()查询只读模式
started()查询模块是否已初始化
config()获取当前配置副本
close()关闭模块,释放所有后端资源

错误码

错误码常量触发场景
FK-400INVALID_ARGUMENT参数为 null 或空白
FK-401NOT_INITIALIZED模块未初始化(极少见,通常会触发懒加载)
FK-402CONFIG_ERROR配置错误(如无效的日期格式、未注册的后端)
FK-403STATE_ERROR后端未找到或状态异常
FK-404NOT_FOUND目标路径不存在
FK-410PATH_ERROR路径非法或路径穿越(Zip Slip)
FK-500IO_ERROR底层 IO 操作失败
FK-520SECURITY_ERROR安全校验失败
FK-530UNSUPPORTED当前后端不支持该操作
FK-540ZIP_BOMB_RISK解压超过安全限制(Zip Bomb 检测)
FK-550IMAGE_DECODE_ERROR图片解码失败
FK-551IMAGE_ENCODE_ERROR图片编码失败
FK-552IMAGE_LIMIT_EXCEEDED图片像素数超过安全上限
FK-553UNSUPPORTED_IMAGE_FORMAT不支持的图片格式
FK-560READ_ONLY_ERROR只读模式下执行写操作
FK-561ENCRYPT_ERROR文件加密或解密失败(含密钥错误)
FK-562GZIP_ERRORGZip 压缩或解压失败

异常处理

import yueyang.vostok.file.exception.VKFileException;

try {
    Vostok.File.read("missing.txt");
} catch (VKFileException e) {
    System.out.println(e.getCode() + ": " + e.getMessage());
    // e.g. FK-404: Path not found
    switch (e.getErrorCode()) {
        case NOT_FOUND       -> handleNotFound();
        case READ_ONLY_ERROR -> handleReadOnly();
        case IO_ERROR        -> handleIoError(e);
        default              -> throw e;
    }
}

完整错误码见 错误码参考