From da51e03cefc6d924ae629b988a11b2ce8e590973 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 27 Feb 2026 15:15:16 +0100 Subject: [PATCH] Backfill extracted archive cleanup on startup in v1.3.8 --- package.json | 2 +- src/main/download-manager.ts | 75 +++++++++++++++++++++++++++++++++- tests/download-manager.test.ts | 70 +++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2bc2b11..92968de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.3.7", + "version": "1.3.8", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 63d3ba1..c7a725c 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -7,7 +7,7 @@ import { AppSettings, DownloadItem, DownloadStats, DownloadSummary, DownloadStat import { REQUEST_RETRIES } from "./constants"; import { cleanupCancelledPackageArtifactsAsync } from "./cleanup"; import { DebridService, MegaWebUnrestrictor } from "./debrid"; -import { extractPackageArchives } from "./extractor"; +import { collectArchiveCleanupTargets, extractPackageArchives } from "./extractor"; import { validateFileAgainstManifest } from "./integrity"; import { logger } from "./logger"; import { StoragePaths, saveSession } from "./storage"; @@ -179,12 +179,14 @@ export class DownloadManager extends EventEmitter { this.normalizeSessionStatuses(); this.recoverPostProcessingOnStartup(); this.resolveExistingQueuedOpaqueFilenames(); + this.cleanupExistingExtractedArchives(); } public setSettings(next: AppSettings): void { this.settings = next; this.debridService.setSettings(next); this.resolveExistingQueuedOpaqueFilenames(); + this.cleanupExistingExtractedArchives(); this.emitState(); } @@ -580,6 +582,77 @@ export class DownloadManager extends EventEmitter { } } + private cleanupExistingExtractedArchives(): void { + if (this.settings.cleanupMode === "none") { + return; + } + + const cleanupTargetsByPackage = new Map>(); + for (const packageId of this.session.packageOrder) { + const pkg = this.session.packages[packageId]; + if (!pkg || pkg.cancelled || pkg.status !== "completed") { + continue; + } + + const items = pkg.itemIds + .map((itemId) => this.session.items[itemId]) + .filter(Boolean) as DownloadItem[]; + if (items.length === 0 || !items.every((item) => item.status === "completed")) { + continue; + } + + const extractedItems = items.filter((item) => item.fullStatus === "Entpackt"); + if (extractedItems.length === 0) { + continue; + } + + const packageTargets = cleanupTargetsByPackage.get(packageId) ?? new Set(); + for (const item of extractedItems) { + const targetPath = String(item.targetPath || "").trim(); + if (!targetPath) { + continue; + } + for (const cleanupTarget of collectArchiveCleanupTargets(targetPath)) { + packageTargets.add(cleanupTarget); + } + } + if (packageTargets.size > 0) { + cleanupTargetsByPackage.set(packageId, packageTargets); + } + } + + if (cleanupTargetsByPackage.size === 0) { + return; + } + + this.cleanupQueue = this.cleanupQueue + .then(async () => { + for (const [packageId, targets] of cleanupTargetsByPackage.entries()) { + const pkg = this.session.packages[packageId]; + if (!pkg) { + continue; + } + + let removed = 0; + for (const targetPath of targets) { + try { + await fs.promises.rm(targetPath, { force: true }); + removed += 1; + } catch { + // ignore + } + } + + if (removed > 0) { + logger.info(`Nachtraegliches Archive-Cleanup fuer ${pkg.name}: ${removed} Datei(en) geloescht`); + } + } + }) + .catch((error) => { + logger.warn(`Nachtraegliches Archive-Cleanup fehlgeschlagen: ${compactErrorText(error)}`); + }); + } + public cancelPackage(packageId: string): void { const pkg = this.session.packages[packageId]; if (!pkg) { diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index e4f9088..b193971 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -821,6 +821,76 @@ describe("download manager", () => { expect(snapshot.canStart).toBe(true); }); + it("cleans leftover split archives on startup for already extracted packages", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); + tempDirs.push(root); + + const packageDir = path.join(root, "downloads", "legacy"); + fs.mkdirSync(packageDir, { recursive: true }); + const part1 = path.join(packageDir, "legacy.release.part01.rar"); + const part2 = path.join(packageDir, "legacy.release.part02.rar"); + const part3 = path.join(packageDir, "legacy.release.part03.rar"); + const keep = path.join(packageDir, "keep.txt"); + fs.writeFileSync(part2, "part2", "utf8"); + fs.writeFileSync(part3, "part3", "utf8"); + fs.writeFileSync(keep, "keep", "utf8"); + + const session = emptySession(); + const packageId = "legacy-pkg"; + const itemId = "legacy-item"; + const createdAt = Date.now() - 20_000; + + session.packageOrder = [packageId]; + session.packages[packageId] = { + id: packageId, + name: "legacy", + outputDir: packageDir, + extractDir: path.join(root, "extract", "legacy"), + status: "completed", + itemIds: [itemId], + cancelled: false, + enabled: true, + createdAt, + updatedAt: createdAt + }; + session.items[itemId] = { + id: itemId, + packageId, + url: "https://dummy/legacy", + provider: "realdebrid", + status: "completed", + retries: 0, + speedBps: 0, + downloadedBytes: 123, + totalBytes: 123, + progressPercent: 100, + fileName: path.basename(part1), + targetPath: part1, + resumable: true, + attempts: 1, + lastError: "", + fullStatus: "Entpackt", + createdAt, + updatedAt: createdAt + }; + + new DownloadManager( + { + ...defaultSettings(), + token: "rd-token", + outputDir: path.join(root, "downloads"), + extractDir: path.join(root, "extract"), + autoExtract: true, + cleanupMode: "delete" + }, + session, + createStoragePaths(path.join(root, "state")) + ); + + await waitFor(() => !fs.existsSync(part2) && !fs.existsSync(part3), 5000); + expect(fs.existsSync(keep)).toBe(true); + }); + it("resets run counters and reconnect state on start", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); tempDirs.push(root);