Fix split-archive cleanup after extraction and release v1.3.7

This commit is contained in:
Sucukdeluxe 2026-02-27 15:07:12 +01:00
parent 0a99d3c584
commit 75fc582299
3 changed files with 128 additions and 3 deletions

View File

@ -1,6 +1,6 @@
{
"name": "real-debrid-downloader",
"version": "1.3.6",
"version": "1.3.7",
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
"main": "build/main/main/main.js",
"author": "Sucukdeluxe",

View File

@ -245,11 +245,76 @@ function extractZipArchive(archivePath: string, targetDir: string, conflictMode:
}
}
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function collectArchiveCleanupTargets(sourceArchivePath: string): string[] {
const targets = new Set<string>([sourceArchivePath]);
const dir = path.dirname(sourceArchivePath);
const fileName = path.basename(sourceArchivePath);
let filesInDir: string[] = [];
try {
filesInDir = fs.readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name);
} catch {
return Array.from(targets);
}
const addMatching = (pattern: RegExp): void => {
for (const candidate of filesInDir) {
if (pattern.test(candidate)) {
targets.add(path.join(dir, candidate));
}
}
};
const multipartRar = fileName.match(/^(.*)\.part\d+\.rar$/i);
if (multipartRar) {
const prefix = escapeRegex(multipartRar[1]);
addMatching(new RegExp(`^${prefix}\\.part\\d+\\.rar$`, "i"));
return Array.from(targets);
}
if (/\.rar$/i.test(fileName)) {
const stem = escapeRegex(fileName.replace(/\.rar$/i, ""));
addMatching(new RegExp(`^${stem}\\.rar$`, "i"));
addMatching(new RegExp(`^${stem}\\.r\\d{2}$`, "i"));
return Array.from(targets);
}
if (/\.zip$/i.test(fileName)) {
const stem = escapeRegex(fileName.replace(/\.zip$/i, ""));
addMatching(new RegExp(`^${stem}\\.zip$`, "i"));
addMatching(new RegExp(`^${stem}\\.z\\d{2}$`, "i"));
return Array.from(targets);
}
if (/\.7z$/i.test(fileName)) {
const stem = escapeRegex(fileName.replace(/\.7z$/i, ""));
addMatching(new RegExp(`^${stem}\\.7z$`, "i"));
addMatching(new RegExp(`^${stem}\\.7z\\.\\d{3}$`, "i"));
return Array.from(targets);
}
return Array.from(targets);
}
function cleanupArchives(sourceFiles: string[], cleanupMode: CleanupMode): void {
if (cleanupMode === "none") {
return;
}
for (const filePath of sourceFiles) {
const targets = new Set<string>();
for (const sourceFile of sourceFiles) {
for (const target of collectArchiveCleanupTargets(sourceFile)) {
targets.add(target);
}
}
for (const filePath of targets) {
try {
fs.rmSync(filePath, { force: true });
} catch {

View File

@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import AdmZip from "adm-zip";
import { afterEach, describe, expect, it } from "vitest";
import { buildExternalExtractArgs, extractPackageArchives } from "../src/main/extractor";
import { buildExternalExtractArgs, collectArchiveCleanupTargets, extractPackageArchives } from "../src/main/extractor";
const tempDirs: string[] = [];
@ -71,6 +71,66 @@ describe("extractor", () => {
expect(fs.existsSync(path.join(targetDir, "release.txt"))).toBe(true);
});
it("collects companion rar parts for cleanup", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-"));
tempDirs.push(root);
const packageDir = path.join(root, "pkg");
fs.mkdirSync(packageDir, { recursive: true });
const part1 = path.join(packageDir, "show.s01e01.part01.rar");
const part2 = path.join(packageDir, "show.s01e01.part02.rar");
const part3 = path.join(packageDir, "show.s01e01.part03.rar");
const other = path.join(packageDir, "other.s01e01.part01.rar");
fs.writeFileSync(part1, "a", "utf8");
fs.writeFileSync(part2, "b", "utf8");
fs.writeFileSync(part3, "c", "utf8");
fs.writeFileSync(other, "x", "utf8");
const targets = new Set(collectArchiveCleanupTargets(part1));
expect(targets.has(part1)).toBe(true);
expect(targets.has(part2)).toBe(true);
expect(targets.has(part3)).toBe(true);
expect(targets.has(other)).toBe(false);
});
it("deletes split zip companion parts when cleanup is enabled", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-"));
tempDirs.push(root);
const packageDir = path.join(root, "pkg");
const targetDir = path.join(root, "out");
fs.mkdirSync(packageDir, { recursive: true });
const zipPath = path.join(packageDir, "season.zip");
const z01Path = path.join(packageDir, "season.z01");
const z02Path = path.join(packageDir, "season.z02");
const otherPath = path.join(packageDir, "other.z01");
const zip = new AdmZip();
zip.addFile("episode.txt", Buffer.from("ok"));
zip.writeZip(zipPath);
fs.writeFileSync(z01Path, "part1", "utf8");
fs.writeFileSync(z02Path, "part2", "utf8");
fs.writeFileSync(otherPath, "keep", "utf8");
const result = await extractPackageArchives({
packageDir,
targetDir,
cleanupMode: "delete",
conflictMode: "overwrite",
removeLinks: false,
removeSamples: false
});
expect(result.extracted).toBe(1);
expect(result.failed).toBe(0);
expect(fs.existsSync(zipPath)).toBe(false);
expect(fs.existsSync(z01Path)).toBe(false);
expect(fs.existsSync(z02Path)).toBe(false);
expect(fs.existsSync(otherPath)).toBe(true);
expect(fs.existsSync(path.join(targetDir, "episode.txt"))).toBe(true);
});
it("treats ask conflict mode as skip in zip extraction", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-"));
tempDirs.push(root);