feat(storage): 存储实例与 Alist 虚拟路径(迁移 0090、设置接口、注册表、vfs、健康检查)
- 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
This commit is contained in:
1 parent
5ff24110f8
commit
c2bf8a071a
20 files changed
+1625
-8
No files matched your search
@@ -0,0 +1,62 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
|
||||
|
||||
// Alist / OpenList 存储实例(docs/design/storage-alist.md §3):远端路径用虚拟前缀 /@alist/<id>/<路径> 表示,
|
||||
// 库根 local_path、整理记录、资产位置里都直接存这个路径,不需要新列。凭据走 secret 表(purpose storage_credential)。
|
||||
|
||||
const PURPOSE_OLD = [
|
||||
"site_cookie",
|
||||
"site_passkey",
|
||||
"site_api_key",
|
||||
"site_password",
|
||||
"feed_url",
|
||||
"downloader_password",
|
||||
"media_server_api_key",
|
||||
"metadata_api_key",
|
||||
"llm_api_key",
|
||||
"other",
|
||||
];
|
||||
const PURPOSE_NEW = [...PURPOSE_OLD.slice(0, -1), "storage_credential", "other"];
|
||||
const HEALTH = ["unknown", "healthy", "degraded", "offline"];
|
||||
|
||||
const swapPurpose = (values: readonly string[]) => `
|
||||
alter table secret drop constraint if exists secret_purpose_check;
|
||||
alter table secret add constraint secret_purpose_check ${oneOf("purpose", values)};
|
||||
`;
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(db, swapPurpose(PURPOSE_NEW));
|
||||
await execDdl(
|
||||
db,
|
||||
entityTable(
|
||||
"storage_instance",
|
||||
`
|
||||
name text not null unique,
|
||||
kind text not null default 'alist' ${oneOf("kind", ["alist"])},
|
||||
flavor text not null default 'openlist' ${oneOf("flavor", ["alist", "openlist"])},
|
||||
base_url text not null,
|
||||
root_path text not null default '/' check (root_path like '/%'),
|
||||
auth_kind text not null default 'token' ${oneOf("auth_kind", ["token", "password", "guest"])},
|
||||
username text,
|
||||
secret_ref uuid references secret(id) on delete set null,
|
||||
path_password_ref uuid references secret(id) on delete set null,
|
||||
enabled boolean not null default true,
|
||||
options jsonb not null default '{}'::jsonb,
|
||||
health_status text not null default 'unknown' ${oneOf("health_status", HEALTH)},
|
||||
last_health_at timestamptz,
|
||||
health_detail jsonb not null default '{}'::jsonb,
|
||||
version_info jsonb not null default '{}'::jsonb`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`drop table if exists storage_instance cascade;
|
||||
delete from secret where purpose = 'storage_credential';
|
||||
${swapPurpose(PURPOSE_OLD)}`,
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user