feat(server): W5-A file manager module (/files)
Roots/list/stat with inline business status, path guard (realpath within registered roots, symlink escape, offline root 503), mkdir/rename syncing asset_location and path_binding revisions, two-phase move/copy/organize (planId + source facts), trash with reference checks and purgeTrash task, identify/probe/scrape, audit + idempotency keys. Runbook docs/runbook/files.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
09f6e3a3e5
commit
29cd10b27e
20 files changed
+4380
-1
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { defineTask } from "@umt/tasks";
|
||||
import { z } from "zod";
|
||||
import type { AoOverrides } from "../acquisition-organize/index.ts";
|
||||
import { createPorts } from "../acquisition-organize/ports.ts";
|
||||
import { timeSlot } from "../acquisition-organize/services/common.ts";
|
||||
import type { ServerContext, ServerModule } from "../types.ts";
|
||||
import { defaultOptions, errMessage, type FmDeps, type FmOptions } from "./common.ts";
|
||||
import { registerFileRoutes } from "./routes.ts";
|
||||
import { purgeTrash } from "./trash.ts";
|
||||
|
||||
/**
|
||||
* W5-A 文件管理(/files,docs/plan/06-file-manager-spec.md):只浏览已登记根(库根 + 下载器路径映射),
|
||||
* 行内业务状态、改名同步资产与路径绑定修订、移动/复制两段式 planId、整理走 planImport、回收站 + 引用检查、识别/探测/刮削。
|
||||
* 复用 acquisition-organize 的服务导出(planImport / 路径守卫 / Move 授权 / H&R),不改其文件。
|
||||
* 持久任务 purgeTrash:每日清理过期回收站项(FILES_TRASH_RETENTION_DAYS,默认 7 天)。
|
||||
*/
|
||||
export interface FmOverrides {
|
||||
ao?: AoOverrides;
|
||||
options?: Partial<FmOptions>;
|
||||
}
|
||||
|
||||
export function createFileManagerModule(overrides: FmOverrides = {}): ServerModule & { deps(): FmDeps } {
|
||||
let deps: FmDeps | null = null;
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
const lazy = (ctx: ServerContext): FmDeps => {
|
||||
if (!deps) {
|
||||
const { options: aoOptions, ...aoRest } = overrides.ao ?? {};
|
||||
deps = {
|
||||
ctx,
|
||||
ao: { ctx, ports: createPorts(ctx, { ...aoRest, ...(aoOptions ? { options: aoOptions } : {}) }) },
|
||||
options: { ...defaultOptions(ctx), ...overrides.options },
|
||||
};
|
||||
}
|
||||
return deps;
|
||||
};
|
||||
const enqueuePurge = async (d: FmDeps) => {
|
||||
try {
|
||||
await d.ctx.engine.enqueue(
|
||||
"purgeTrash",
|
||||
{},
|
||||
{
|
||||
idempotencyKey: `purgeTrash:${timeSlot(d.ao.ports.now(), d.options.purgeIntervalMs)}`,
|
||||
traceId: randomUUID(),
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
d.ctx.logger.warn({ err: { message: errMessage(err) } }, "回收站清理入队失败");
|
||||
}
|
||||
};
|
||||
return {
|
||||
name: "file-manager",
|
||||
deps() {
|
||||
if (!deps) throw new Error("file-manager 尚未装配");
|
||||
return deps;
|
||||
},
|
||||
registerRoutes(app, ctx) {
|
||||
registerFileRoutes(app, lazy(ctx));
|
||||
},
|
||||
registerTasks(registry, ctx) {
|
||||
registry.register(
|
||||
defineTask({
|
||||
name: "purgeTrash",
|
||||
version: 1,
|
||||
inputSchema: z.object({}).passthrough(),
|
||||
retryPolicy: { maximumAttempts: 3, initialInterval: "5s", backoffCoefficient: 2, maximumInterval: "60s" },
|
||||
handler: async (t) =>
|
||||
t.step.run("purge", async () => {
|
||||
const r = await purgeTrash(lazy(ctx), undefined, {});
|
||||
return { purged: r.purged.length, failed: r.failed.length };
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
start(ctx) {
|
||||
const d = lazy(ctx);
|
||||
if (!d.options.backgroundLoops || d.options.purgeIntervalMs <= 0) return;
|
||||
void enqueuePurge(d);
|
||||
timer = setInterval(() => void enqueuePurge(d), Math.min(d.options.purgeIntervalMs, 3600_000));
|
||||
},
|
||||
stop() {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const fileManagerModule = createFileManagerModule();
|
||||
Reference in new issue
Block a user