test(storage): 真实 OpenList 往返测试与 alist-e2e 验收脚本
- packages/adapters alist.real.test.ts:上传 / 冲突 / 同驱动与跨驱动复制移动 / 下载比对(环境变量开启) - apps/server alist-organize.real.test.ts:本地 → OpenList(整理复制)→ 本地(移动)→ OpenList 内整理 - scripts/alist-e2e.ts:对任意地址 + 令牌或账号密码跑往返,退出码表示是否全部通过 - 写后目标暂不可见时刷新父目录列表(OpenList 的 get 不认 refresh) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
cf364435eb
commit
fed2202e74
4 files changed
+408
-2
No files matched your search
@@ -0,0 +1,158 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { type AlistAuth, AlistClient } from "@umt/adapters";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { executeRecord } from "../../src/modules/acquisition-organize/services/organize.ts";
|
||||
import { overrideStorage } from "../../src/storage/registry.ts";
|
||||
import { toVPath } from "../../src/storage/vpath.ts";
|
||||
import { createFmHarness, type FmHarness, key, pgAvailable, seedEntity } from "../file-manager/harness.ts";
|
||||
|
||||
// 真实 OpenList 上跑一次整理往返(环境变量开启,缺省跳过;变量同 packages/adapters/src/alist/alist.real.test.ts):
|
||||
// 本地下载目录 → 整理(复制)到 OpenList 上的媒体库 → 从 OpenList 整理(移动)回本地媒体库 → OpenList 内移动。
|
||||
|
||||
const URL_ = Deno.env.get("UMT_ALIST_REAL_URL");
|
||||
const ROOT = Deno.env.get("UMT_ALIST_REAL_ROOT");
|
||||
const auth: AlistAuth | null = Deno.env.get("UMT_ALIST_REAL_TOKEN")
|
||||
? { kind: "token", token: Deno.env.get("UMT_ALIST_REAL_TOKEN")! }
|
||||
: Deno.env.get("UMT_ALIST_REAL_USER")
|
||||
? {
|
||||
kind: "password",
|
||||
username: Deno.env.get("UMT_ALIST_REAL_USER")!,
|
||||
password: Deno.env.get("UMT_ALIST_REAL_PASSWORD") ?? "",
|
||||
}
|
||||
: null;
|
||||
const enabled = !!URL_ && !!ROOT && !!auth && (await pgAvailable());
|
||||
|
||||
describe.skipIf(!enabled)("真实 OpenList:整理往返 本地 → OpenList → 本地", () => {
|
||||
let h: FmHarness;
|
||||
let client: AlistClient;
|
||||
let storageId: string;
|
||||
let base: string;
|
||||
let libId: string;
|
||||
const body = `real-video-${randomBytes(64).toString("hex")}`;
|
||||
beforeAll(async () => {
|
||||
h = await createFmHarness("alist_real", {}, { runTransfers: false });
|
||||
client = new AlistClient({ baseUrl: URL_!, auth: auth! });
|
||||
base = `${ROOT!.replace(/\/+$/, "")}/umt-org-${randomBytes(3).toString("hex")}`;
|
||||
await client.mkdir(`${base}/library`);
|
||||
await client.mkdir(`${base}/incoming`);
|
||||
const row = await h.db
|
||||
.insertInto("storage_instance")
|
||||
.values({ name: "真实 OpenList", base_url: URL_!, root_path: base })
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
storageId = row.id;
|
||||
overrideStorage(storageId, { client, name: "真实 OpenList", rootPath: base });
|
||||
libId = (
|
||||
await h.db
|
||||
.insertInto("library_root")
|
||||
.values({
|
||||
name: "OpenList 媒体库",
|
||||
media_kind: "mixed",
|
||||
local_path: toVPath(storageId, `${base}/library`),
|
||||
management_mode: "managed",
|
||||
sentinel_file: null,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow()
|
||||
).id;
|
||||
}, 60_000);
|
||||
afterAll(async () => {
|
||||
await client?.removePath(base).catch(() => undefined);
|
||||
await h?.close();
|
||||
});
|
||||
|
||||
async function organize(items: unknown[], action: "copy" | "move", targetLibraryRootId: string) {
|
||||
const dry = await h.api("POST", "/files/organize", {
|
||||
idempotencyKey: key(),
|
||||
items,
|
||||
action,
|
||||
targetLibraryRootId,
|
||||
dryRun: true,
|
||||
});
|
||||
expect(dry.body.data.groups[0].error, JSON.stringify(dry.body)).toBeNull();
|
||||
const exec = await h.api("POST", "/files/organize", {
|
||||
idempotencyKey: key(),
|
||||
items,
|
||||
action,
|
||||
targetLibraryRootId,
|
||||
dryRun: false,
|
||||
planIds: [dry.body.data.groups[0].planId],
|
||||
});
|
||||
const recordId = exec.body.data.recordIds[0] as string;
|
||||
await executeRecord(h.module.deps().ao, recordId);
|
||||
const rec = await h.db
|
||||
.selectFrom("organization_record")
|
||||
.select(["status"])
|
||||
.where("id", "=", recordId)
|
||||
.executeTakeFirstOrThrow();
|
||||
const main = await h.db
|
||||
.selectFrom("organization_file_result")
|
||||
.select(["target_path", "checksum", "status"])
|
||||
.where("organization_record_id", "=", recordId)
|
||||
.where("role", "=", "main")
|
||||
.executeTakeFirstOrThrow();
|
||||
return { status: rec.status, main };
|
||||
}
|
||||
|
||||
it("本地 → OpenList(复制)→ 本地(移动)→ OpenList 内移动", async () => {
|
||||
const film = await seedEntity(h.db, { kind: "movie", title: "Real Round Trip", year: 2024 });
|
||||
const name = "Real.Round.Trip.2024.1080p.WEB-DL.mkv";
|
||||
await writeFile(join(h.dl, name), body);
|
||||
|
||||
const up = await organize([{ root: h.dlRoot, path: name, mediaEntityId: film }], "copy", libId);
|
||||
console.log(`[real] 本地 → OpenList 复制:${up.status},目标 ${up.main.target_path},${up.main.checksum}`);
|
||||
expect(up.status).toBe("succeeded");
|
||||
const remoteFile = `${base}/library/${up.main.target_path}`;
|
||||
expect((await client.get(remoteFile, { refresh: true })).size).toBe(body.length);
|
||||
|
||||
// 从 OpenList 移回本地媒体库(文件管理「移动到」:下载成功后删远端源,已登记位置跟随到本地)
|
||||
const rel = `library/${up.main.target_path}`;
|
||||
const src = { root: `st:${storageId}`, path: rel };
|
||||
const dryMv = await h.api("POST", "/files/move", {
|
||||
idempotencyKey: key(),
|
||||
items: [src],
|
||||
target: { root: h.libRoot, path: "" },
|
||||
mode: "move",
|
||||
dryRun: true,
|
||||
});
|
||||
expect(dryMv.status, JSON.stringify(dryMv.body)).toBe(200);
|
||||
const mv = await h.api("POST", "/files/move", {
|
||||
idempotencyKey: key(),
|
||||
items: [src],
|
||||
target: { root: h.libRoot, path: "" },
|
||||
mode: "move",
|
||||
dryRun: false,
|
||||
planId: dryMv.body.data.planId,
|
||||
});
|
||||
expect(mv.status, JSON.stringify(mv.body)).toBe(200);
|
||||
const recordId = mv.body.data.executed.recordId as string;
|
||||
await executeRecord(h.module.deps().ao, recordId);
|
||||
const back = await h.db
|
||||
.selectFrom("organization_record")
|
||||
.select("status")
|
||||
.where("id", "=", recordId)
|
||||
.executeTakeFirstOrThrow();
|
||||
const localName = up.main.target_path!.split("/").pop()!;
|
||||
console.log(`[real] OpenList → 本地 移动:${back.status},目标 ${localName}`);
|
||||
expect(back.status).toBe("succeeded");
|
||||
expect(await readFile(join(h.lib, localName), "utf8")).toBe(body);
|
||||
expect(await client.stat(remoteFile, { refresh: true })).toBeNull();
|
||||
|
||||
// OpenList 内整理(同实例 API move):另一部作品放在 OpenList 的 incoming
|
||||
const film2 = await seedEntity(h.db, { kind: "movie", title: "Real Inside Move", year: 2023 });
|
||||
const name2 = "Real.Inside.Move.2023.1080p.WEB-DL.mkv";
|
||||
await writeFile(join(h.dl, name2), body);
|
||||
await client.upload(join(h.dl, name2), `${base}/incoming/${name2}`);
|
||||
const inside = await organize(
|
||||
[{ root: `st:${storageId}`, path: `incoming/${name2}`, mediaEntityId: film2 }],
|
||||
"move",
|
||||
libId,
|
||||
);
|
||||
console.log(`[real] OpenList 内移动:${inside.status},目标 ${inside.main.target_path}`);
|
||||
expect(inside.status).toBe("succeeded");
|
||||
expect(await client.stat(`${base}/incoming/${name2}`, { refresh: true })).toBeNull();
|
||||
expect((await client.get(`${base}/library/${inside.main.target_path}`, { refresh: true })).size).toBe(body.length);
|
||||
}, 120_000);
|
||||
});
|
||||
Reference in new issue
Block a user