import { existsSync } from "node:fs"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { createSubtitlesModule } from "../../src/modules/subtitles/index.ts"; import * as lfs from "../../src/storage/local-fs.ts"; import { createHarness, type Harness, pgAvailable } from "../auth-settings/harness.ts"; import { DEV_KEY } from "../helpers.ts"; // server 侧本地写入口(整理 / 刮削 / 字幕 / 回收站 / 上传共用的 storage/local-fs.ts)对 Alist 虚拟路径一律拒绝, // 本机不出现 /@alist;补字幕的目标在 Alist 媒体库上时明确失败(不支持),不往本机写。 const V = "/@alist/00000000-0000-4000-8000-0000000000ee/s1/lib"; const code = async (p: Promise) => { try { await p; } catch (e) { return (e as { code?: string }).code; } return "no-error"; }; describe("local-fs 写入口守卫", () => { let dir: string; beforeAll(async () => { dir = await mkdtemp(join(tmpdir(), "umt-lfs-")); await lfs.writeFile(join(dir, "a.txt"), "x"); }); afterAll(async () => { await rm(dir, { recursive: true, force: true }); }); it("mkdir / writeFile / rename / copyFile / rm / rmdir / unlink / link / open(写)都拒绝虚拟路径", async () => { const R = "REMOTE_PATH_ON_LOCAL_FS"; expect(await code(lfs.mkdir(`${V}/d`, { recursive: true }))).toBe(R); expect(await code(lfs.writeFile(`${V}/a.nfo`, ""))).toBe(R); expect(await code(lfs.rename(join(dir, "a.txt"), `${V}/a.txt`))).toBe(R); expect(await code(lfs.copyFile(join(dir, "a.txt"), `${V}/a.txt`))).toBe(R); expect(await code(lfs.rm(`${V}/a.txt`, { force: true }))).toBe(R); expect(await code(lfs.rmdir(V))).toBe(R); expect(await code(lfs.unlink(`${V}/a.txt`))).toBe(R); expect(await code(lfs.link(join(dir, "a.txt"), `${V}/a.txt`))).toBe(R); expect(await code(lfs.open(`${V}/a.txt`, "w"))).toBe(R); expect(existsSync("/@alist")).toBe(false); expect(existsSync(join(dir, "a.txt"))).toBe(true); }); it("本地路径与只读打开照常", async () => { await lfs.mkdir(join(dir, "sub"), { recursive: true }); await lfs.rename(join(dir, "a.txt"), join(dir, "sub", "a.txt")); expect(await lfs.readFile(join(dir, "sub", "a.txt"), "utf8")).toBe("x"); expect(await code(lfs.open(`${V}/a.txt`, "r"))).toBe("ENOENT"); }); }); const hasPg = await pgAvailable(); describe.skipIf(!hasPg)("补字幕:目标在 Alist 媒体库上明确失败", () => { let h: Harness; let entityId: string; let locationId: string; beforeAll(async () => { h = await createHarness({ extraModules: [createSubtitlesModule()] }); const root = await h.db .insertInto("library_root") .values({ name: "Alist 剧集", media_kind: "mixed", local_path: V, management_mode: "managed", sentinel_file: null, }) .returning("id") .executeTakeFirstOrThrow(); entityId = ( await h.db .insertInto("media_entity") .values({ kind: "series", title: "远端剧", year: 2024 } as never) .returning("id") .executeTakeFirstOrThrow() ).id; const a = await h.db .insertInto("media_asset") .values({ media_entity_id: entityId, kind: "video", status: "active" }) .returning("id") .executeTakeFirstOrThrow(); locationId = ( await h.db .insertInto("asset_location") .values({ media_asset_id: a.id, location_kind: "library", library_root_id: root.id, relative_path: "远端剧 (2024)/Season 1/远端剧 - S01E01.mkv", status: "available", }) .returning("id") .executeTakeFirstOrThrow() ).id; }, 120_000); afterAll(async () => h?.close()); it("写入返回失败原因,不写任何文件、不建资产", async () => { const res = await h.app.request("/api/v1/subtitles/apply", { method: "POST", headers: { authorization: `Bearer ${DEV_KEY}`, "content-type": "application/json" }, body: JSON.stringify({ entityId, items: [ { name: "a.srt", ext: "srt", language: "zh-Hans", content: btoa("1\n00:00:01,000 --> 00:00:02,000\nhi\n"), targetLocationId: locationId, }, ], }), }); const body = await res.json(); expect(res.status, JSON.stringify(body)).toBe(200); expect(body.data.written).toEqual([]); expect(body.data.failed).toEqual([{ name: "a.srt", reason: "目标视频在 Alist 存储上,暂不支持补字幕" }]); expect(existsSync("/@alist")).toBe(false); const subs = await h.db.selectFrom("media_asset").select("id").where("kind", "=", "subtitle").execute(); expect(subs).toEqual([]); }); });