/** 下载器实例看到的路径 ↔ 本服务宿主看到的路径(前缀替换,支持多实例) */ export interface PathMapping { instanceId: string; downloaderPath: string; hostPath: string; } export type MapResult = | { ok: true; path: string; mapping: PathMapping } | { ok: false; reason: "no_mapping" | "ambiguous" }; const norm = (p: string) => { const s = p.replace(/\\/g, "/").replace(/\/{2,}/g, "/"); return s.length > 1 ? s.replace(/\/+$/, "") : s; }; function mapPrefix( path: string, instanceId: string, mappings: readonly PathMapping[], from: "downloaderPath" | "hostPath", to: "downloaderPath" | "hostPath", ): MapResult { const p = norm(path); let best: PathMapping[] = []; let bestLen = -1; for (const m of mappings) { if (m.instanceId !== instanceId) continue; const prefix = norm(m[from]); const hit = p === prefix || p.startsWith(prefix === "/" ? "/" : `${prefix}/`); if (!hit) continue; if (prefix.length > bestLen) { best = [m]; bestLen = prefix.length; } else if (prefix.length === bestLen) best.push(m); } if (best.length === 0) return { ok: false, reason: "no_mapping" }; const targets = new Set(best.map((m) => norm(m[to]))); if (targets.size > 1) return { ok: false, reason: "ambiguous" }; const m = best[0]!; const rest = p.slice(bestLen).replace(/^\//, ""); const base = norm(m[to]); const out = rest ? (base === "/" ? `/${rest}` : `${base}/${rest}`) : base; return { ok: true, path: out, mapping: m }; } /** 最长前缀匹配、按路径段边界(/downloads 不匹配 /downloads2);找不到映射不猜测 */ export function toHostPath(path: string, instanceId: string, mappings: readonly PathMapping[]): MapResult { return mapPrefix(path, instanceId, mappings, "downloaderPath", "hostPath"); } export function toDownloaderPath(path: string, instanceId: string, mappings: readonly PathMapping[]): MapResult { return mapPrefix(path, instanceId, mappings, "hostPath", "downloaderPath"); } /** 同一实例同一前缀指向不同目标 → 配置错误 */ export function validatePathMappings(mappings: readonly PathMapping[]): string[] { const errors: string[] = []; const seen = new Map(); for (const m of mappings) { const k = `${m.instanceId}\u0000${norm(m.downloaderPath)}`; const prev = seen.get(k); if (prev !== undefined && prev !== norm(m.hostPath)) errors.push(`实例 ${m.instanceId} 的 ${m.downloaderPath} 映射到多个宿主路径`); seen.set(k, norm(m.hostPath)); } return errors; }