import fs from "node:fs"; import path from "node:path"; import { ARCHIVE_TEMP_EXTENSIONS, LINK_ARTIFACT_EXTENSIONS, RAR_SPLIT_RE, SAMPLE_DIR_NAMES, SAMPLE_TOKEN_RE, SAMPLE_VIDEO_EXTENSIONS } from "./constants"; async function yieldToLoop(): Promise { await new Promise((resolve) => { setTimeout(resolve, 0); }); } export function isArchiveOrTempFile(filePath: string): boolean { const lower = filePath.toLowerCase(); const ext = path.extname(lower); if (ARCHIVE_TEMP_EXTENSIONS.has(ext)) { return true; } if (lower.includes(".part") && lower.endsWith(".rar")) { return true; } return RAR_SPLIT_RE.test(lower); } export function cleanupCancelledPackageArtifacts(packageDir: string): number { if (!fs.existsSync(packageDir)) { return 0; } let removed = 0; const stack = [packageDir]; while (stack.length > 0) { const current = stack.pop() as string; for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const full = path.join(current, entry.name); if (entry.isDirectory()) { stack.push(full); } else if (entry.isFile() && isArchiveOrTempFile(full)) { try { fs.rmSync(full, { force: true }); removed += 1; } catch { // ignore } } } } return removed; } export async function cleanupCancelledPackageArtifactsAsync(packageDir: string): Promise { try { await fs.promises.access(packageDir, fs.constants.F_OK); } catch { return 0; } let removed = 0; let touched = 0; const stack = [packageDir]; while (stack.length > 0) { const current = stack.pop() as string; let entries: fs.Dirent[] = []; try { entries = await fs.promises.readdir(current, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { const full = path.join(current, entry.name); if (entry.isDirectory()) { stack.push(full); } else if (entry.isFile() && isArchiveOrTempFile(full)) { try { await fs.promises.rm(full, { force: true }); removed += 1; } catch { // ignore } } touched += 1; if (touched % 80 === 0) { await yieldToLoop(); } } } return removed; } export function removeDownloadLinkArtifacts(extractDir: string): number { if (!fs.existsSync(extractDir)) { return 0; } let removed = 0; const stack = [extractDir]; while (stack.length > 0) { const current = stack.pop() as string; for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const full = path.join(current, entry.name); if (entry.isDirectory()) { stack.push(full); continue; } if (!entry.isFile()) { continue; } const ext = path.extname(entry.name).toLowerCase(); const name = entry.name.toLowerCase(); let shouldDelete = LINK_ARTIFACT_EXTENSIONS.has(ext); if (!shouldDelete && [".txt", ".html", ".htm", ".nfo"].includes(ext)) { if (/[._\- ](links?|downloads?|urls?|dlc)([._\- ]|$)/i.test(name)) { try { const text = fs.readFileSync(full, "utf8"); shouldDelete = /https?:\/\//i.test(text); } catch { shouldDelete = false; } } } if (shouldDelete) { try { fs.rmSync(full, { force: true }); removed += 1; } catch { // ignore } } } } return removed; } export function removeSampleArtifacts(extractDir: string): { files: number; dirs: number } { if (!fs.existsSync(extractDir)) { return { files: 0, dirs: 0 }; } let removedFiles = 0; let removedDirs = 0; const allDirs: string[] = []; const stack = [extractDir]; while (stack.length > 0) { const current = stack.pop() as string; allDirs.push(current); for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const full = path.join(current, entry.name); if (entry.isDirectory()) { stack.push(full); continue; } if (!entry.isFile()) { continue; } const parent = path.basename(path.dirname(full)).toLowerCase(); const stem = path.parse(entry.name).name.toLowerCase(); const ext = path.extname(entry.name).toLowerCase(); const inSampleDir = SAMPLE_DIR_NAMES.has(parent); const isSampleVideo = SAMPLE_VIDEO_EXTENSIONS.has(ext) && SAMPLE_TOKEN_RE.test(stem); if (inSampleDir || isSampleVideo) { try { fs.rmSync(full, { force: true }); removedFiles += 1; } catch { // ignore } } } } allDirs.sort((a, b) => b.length - a.length); for (const dir of allDirs) { if (dir === extractDir) { continue; } const base = path.basename(dir).toLowerCase(); if (!SAMPLE_DIR_NAMES.has(base)) { continue; } try { fs.rmSync(dir, { recursive: true, force: true }); removedDirs += 1; } catch { // ignore } } return { files: removedFiles, dirs: removedDirs }; }