Files
UHD_Media_Tool/packages/adapters/src/qbittorrent/safe-delete.ts
T

55 lines
2.7 KiB
TypeScript

import { posix } from "node:path";
import { DownloaderError } from "./errors.ts";
/**
* 删种连带删数据(deleteFiles=true)前的检查输入(REQ §2.1 原则 5:清理按明确授权)。
* contentPath 与 libraryRoots 必须是同一路径视图(都用下载器视角,或都用本机视角),由调用方经 path_mapping 换算。
*/
export interface SafeDeleteCheck {
/** qB 的 content_path(单文件种子为文件,多文件为根目录) */
contentPath: string;
/** 全部媒体库根(不只是本次入库的目标库);为空视为无法确认,拒绝 */
libraryRoots: readonly string[];
/** 下载内容已完成入库并校验(asset_location 已指向库内副本);只有 true 才允许删缓存 */
importVerified: boolean;
}
/** 规范化为可比较的 POSIX 路径:反斜杠转斜杠、折叠 . / ..、去掉末尾斜杠;Windows 盘符统一小写 */
function normalize(p: string): string | null {
const s = p.trim().replace(/\\/g, "/");
if (!s) return null;
const drive = /^[A-Za-z]:\//.test(s);
if (!drive && !s.startsWith("/")) return null;
let n = posix.normalize(s);
if (n.length > 1 && n.endsWith("/")) n = n.slice(0, -1);
return drive ? n[0]!.toLowerCase() + n.slice(1) : n;
}
const isRootLike = (n: string) => n === "/" || /^[a-z]:\/?$/.test(n);
const within = (child: string, parent: string) =>
child === parent || child.startsWith(parent === "/" ? "/" : `${parent}/`);
/**
* 删数据前的硬检查,不通过抛 `DownloaderError('unsafe_delete')`:
* 1. importVerified 必须为 true;
* 2. contentPath 必须是绝对路径,且不能是根目录;
* 3. contentPath 不能等于或位于任何库根之下,也不能是任何库根的上级目录(删上级会连带删掉整个库)。
* 引用检查(asset_location 是否仍指向该路径)属于业务层,调用方在 importVerified 里体现。
*/
export function assertSafeToDeleteData(check: SafeDeleteCheck, instanceId = ""): void {
const fail = (why: string): never => {
throw new DownloaderError("unsafe_delete", `[qb:${instanceId}] 拒绝删除数据: ${why}`, { instanceId });
};
if (check.importVerified !== true) fail("尚未验证入库");
const content = normalize(check.contentPath);
if (!content) throw fail("contentPath 不是绝对路径");
if (isRootLike(content)) fail("contentPath 是根目录");
if (check.libraryRoots.length === 0) fail("未提供媒体库根,无法确认不在库内");
for (const raw of check.libraryRoots) {
const root = normalize(raw);
if (!root) throw fail(`媒体库根不是绝对路径: ${raw}`);
if (within(content, root)) fail(`contentPath 位于媒体库 ${root} 内`);
if (within(root, content)) fail(`contentPath 包含媒体库 ${root}`);
}
}