- storage_instance 表 + secret 用途 storage_credential;令牌 / 密码 / 路径密码只写不读 - /storage-instances CRUD 与测试连接(版本、用户、根路径、容量);被库根引用不能删 - 远端路径 /@alist/<id>/<路径>,vfs 统一本地 / Alist 的 stat、列目录、建目录、读写、移动、删除 - 整理矩阵中涉及 Alist 的传输(storage/transfer.ts)与远端 ffprobe(直链只读头) - 媒体库目录可登记在 Alist 上(不写哨兵,目录可访问即在线);健康检查列出存储实例 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
122 lines
4.5 KiB
TypeScript
122 lines
4.5 KiB
TypeScript
import { z } from "zod";
|
|
import { EntityBase, Id, JsonObject, Timestamp, WriteOnlySecret } from "./common.ts";
|
|
import { InstanceHealth } from "./instances.ts";
|
|
|
|
// Alist / OpenList 存储实例(docs/design/storage-alist.md §3)。凭据(令牌 / 密码)与路径密码只写不读。
|
|
|
|
export const StorageKind = z.enum(["alist"]);
|
|
export type StorageKind = z.infer<typeof StorageKind>;
|
|
/** 只用于展示与版本差异;接口相同(MP 的 AlistGo 是同实现的子类) */
|
|
export const StorageFlavor = z.enum(["alist", "openlist"]);
|
|
export type StorageFlavor = z.infer<typeof StorageFlavor>;
|
|
/** token 永久令牌(Alist 后台「设置 → 其他 → 令牌」);password 账号密码换临时令牌;guest 访客 */
|
|
export const StorageAuthKind = z.enum(["token", "password", "guest"]);
|
|
export type StorageAuthKind = z.infer<typeof StorageAuthKind>;
|
|
|
|
/** 实例高级选项;不填用缺省(括号内) */
|
|
export const StorageOptions = z
|
|
.object({
|
|
/** 每秒请求数(5);0 不限 */
|
|
qps: z.number().int().min(0).max(100),
|
|
/** 并发(4);0 不限 */
|
|
concurrency: z.number().int().min(0).max(32),
|
|
/** 普通请求超时秒(30) */
|
|
timeoutSec: z.number().int().min(5).max(600),
|
|
/** 上传 / 下载无进度空闲超时秒(120) */
|
|
idleTimeoutSec: z.number().int().min(10).max(3600),
|
|
/** 等待 Alist 复制 / 移动 / 上传任务的上限分钟(360) */
|
|
taskTimeoutMin: z.number().int().min(1).max(10080),
|
|
/** 可重试错误的重试次数(3) */
|
|
retries: z.number().int().min(0).max(10),
|
|
/** 上传用 As-Task(false,同 MP) */
|
|
uploadAsTask: z.boolean(),
|
|
/** 目录监控最大深度(5,同 MP) */
|
|
snapshotMaxDepth: z.number().int().min(1).max(20),
|
|
/** 目录监控按目录修改时间跳过未变子树(true) */
|
|
snapshotCheckFolderModtime: z.boolean(),
|
|
})
|
|
.partial();
|
|
export type StorageOptions = z.infer<typeof StorageOptions>;
|
|
|
|
export const STORAGE_OPTION_DEFAULTS: Required<StorageOptions> = {
|
|
qps: 5,
|
|
concurrency: 4,
|
|
timeoutSec: 30,
|
|
idleTimeoutSec: 120,
|
|
taskTimeoutMin: 360,
|
|
retries: 3,
|
|
uploadAsTask: false,
|
|
snapshotMaxDepth: 5,
|
|
snapshotCheckFolderModtime: true,
|
|
};
|
|
|
|
/** Alist 内路径:以 / 开头,不含 .. 段 */
|
|
export const AlistPath = z
|
|
.string()
|
|
.max(4096)
|
|
.refine((p) => p.startsWith("/") && !p.includes("\0") && !p.split("/").some((s) => s === ".." || s === "."), {
|
|
message: "Alist 路径必须以 / 开头,不能含 . 或 ..",
|
|
});
|
|
|
|
export const StorageInstance = EntityBase.extend({
|
|
name: z.string().min(1).max(100),
|
|
kind: StorageKind,
|
|
flavor: StorageFlavor,
|
|
baseUrl: z.url(),
|
|
/** 本系统可浏览 / 使用的 Alist 根路径(缺省 /) */
|
|
rootPath: AlistPath,
|
|
authKind: StorageAuthKind,
|
|
username: z.string().nullable(),
|
|
secretRef: Id.nullable(),
|
|
pathPasswordRef: Id.nullable(),
|
|
enabled: z.boolean(),
|
|
options: StorageOptions,
|
|
healthStatus: InstanceHealth,
|
|
lastHealthAt: Timestamp.nullable(),
|
|
healthDetail: JsonObject,
|
|
versionInfo: JsonObject,
|
|
});
|
|
export type StorageInstance = z.infer<typeof StorageInstance>;
|
|
|
|
export const StorageInstanceCreate = StorageInstance.pick({
|
|
name: true,
|
|
flavor: true,
|
|
baseUrl: true,
|
|
rootPath: true,
|
|
authKind: true,
|
|
username: true,
|
|
enabled: true,
|
|
options: true,
|
|
})
|
|
.partial({ flavor: true, rootPath: true, authKind: true, username: true, enabled: true, options: true })
|
|
.extend({
|
|
/** 令牌(authKind=token)或密码(authKind=password);只写 */
|
|
credential: WriteOnlySecret(4096).optional(),
|
|
/** 路径密码;只写 */
|
|
pathPassword: WriteOnlySecret(1024).optional(),
|
|
});
|
|
export type StorageInstanceCreate = z.infer<typeof StorageInstanceCreate>;
|
|
/** 更新:credential / pathPassword 省略或空串 = 不变,null = 清除 */
|
|
export const StorageInstanceUpdate = StorageInstanceCreate.partial().extend({
|
|
credential: WriteOnlySecret(4096).nullable().optional(),
|
|
pathPassword: WriteOnlySecret(1024).nullable().optional(),
|
|
});
|
|
export type StorageInstanceUpdate = z.infer<typeof StorageInstanceUpdate>;
|
|
|
|
export const StorageUsage = z.object({ total: z.number(), free: z.number(), used: z.number() });
|
|
export type StorageUsage = z.infer<typeof StorageUsage>;
|
|
|
|
export const StorageTestResult = z.object({
|
|
ok: z.boolean(),
|
|
healthStatus: InstanceHealth,
|
|
version: z.string().nullable(),
|
|
/** 登录用户(访客为 guest) */
|
|
user: z.string().nullable(),
|
|
/** 根路径存在且是目录 */
|
|
rootOk: z.boolean(),
|
|
/** 容量;驱动不给或令牌不是管理员时为 null(显示「未知」) */
|
|
usage: StorageUsage.nullable(),
|
|
message: z.string().nullable(),
|
|
});
|
|
export type StorageTestResult = z.infer<typeof StorageTestResult>;
|