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; /** 只用于展示与版本差异;接口相同(MP 的 AlistGo 是同实现的子类) */ export const StorageFlavor = z.enum(["alist", "openlist"]); export type StorageFlavor = z.infer; /** token 永久令牌(Alist 后台「设置 → 其他 → 令牌」);password 账号密码换临时令牌;guest 访客 */ export const StorageAuthKind = z.enum(["token", "password", "guest"]); export type StorageAuthKind = z.infer; /** 实例高级选项;不填用缺省(括号内) */ 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; export const STORAGE_OPTION_DEFAULTS: Required = { 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; 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; /** 更新:credential / pathPassword 省略或空串 = 不变,null = 清除 */ export const StorageInstanceUpdate = StorageInstanceCreate.partial().extend({ credential: WriteOnlySecret(4096).nullable().optional(), pathPassword: WriteOnlySecret(1024).nullable().optional(), }); export type StorageInstanceUpdate = z.infer; export const StorageUsage = z.object({ total: z.number(), free: z.number(), used: z.number() }); export type StorageUsage = z.infer; 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;