Replace 7zip-bin with WinRAR for archive extraction v1.3.0
- Remove 7zip-bin dependency and asarUnpack config - Use WinRAR/UnRAR.exe from standard install paths with auto-detection - Add resolver with probing to cache the found extractor command - Add -y flag for auto-confirm and WinRAR.exe command support - Update tests for WinRAR argument format Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d867c55e37
commit
73c8b6e670
6
package-lock.json
generated
6
package-lock.json
generated
@ -1,15 +1,14 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.1.29",
|
||||
"version": "1.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.1.29",
|
||||
"version": "1.3.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"7zip-bin": "^5.2.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@ -2231,6 +2230,7 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
|
||||
"integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.1.29",
|
||||
"version": "1.3.0",
|
||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
@ -22,12 +22,11 @@
|
||||
"adm-zip": "^0.5.16",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"7zip-bin": "^5.2.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.13",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/node": "^24.0.13",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
@ -55,9 +54,6 @@
|
||||
"build/renderer/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/7zip-bin/**/*"
|
||||
],
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis",
|
||||
|
||||
@ -2,17 +2,15 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
import AdmZip from "adm-zip";
|
||||
import { path7za } from "7zip-bin";
|
||||
import { CleanupMode, ConflictMode } from "../shared/types";
|
||||
import { logger } from "./logger";
|
||||
import { removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
||||
|
||||
const DEFAULT_ARCHIVE_PASSWORDS = ["", "serienfans.org", "serienjunkies.org"];
|
||||
const FALLBACK_COMMANDS = ["7z", "C:\\Program Files\\7-Zip\\7z.exe", "C:\\Program Files (x86)\\7-Zip\\7z.exe", "unrar"];
|
||||
const NO_EXTRACTOR_MESSAGE = "WinRAR/UnRAR nicht gefunden. Bitte WinRAR installieren.";
|
||||
|
||||
let preferredExtractorCommand: string | null = null;
|
||||
let extractorUnavailable = false;
|
||||
let extractorUnavailableReason = "";
|
||||
let resolvedExtractorCommand: string | null = null;
|
||||
let resolveFailureReason = "";
|
||||
|
||||
export interface ExtractOptions {
|
||||
packageDir: string;
|
||||
@ -51,12 +49,6 @@ function cleanErrorText(text: string): string {
|
||||
return String(text || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
||||
}
|
||||
|
||||
function normalizeBundledExtractorPath(filePath: string): string {
|
||||
return filePath.includes("app.asar")
|
||||
? filePath.replace("app.asar", "app.asar.unpacked")
|
||||
: filePath;
|
||||
}
|
||||
|
||||
function archivePasswords(): string[] {
|
||||
const custom = String(process.env.RD_ARCHIVE_PASSWORDS || "")
|
||||
.split(/[;,\n]/g)
|
||||
@ -65,11 +57,26 @@ function archivePasswords(): string[] {
|
||||
return Array.from(new Set([...DEFAULT_ARCHIVE_PASSWORDS, ...custom]));
|
||||
}
|
||||
|
||||
function extractorCandidates(): string[] {
|
||||
const bundled = normalizeBundledExtractorPath(path7za);
|
||||
const ordered = preferredExtractorCommand
|
||||
? [preferredExtractorCommand, bundled, ...FALLBACK_COMMANDS]
|
||||
: [bundled, ...FALLBACK_COMMANDS];
|
||||
function winRarCandidates(): string[] {
|
||||
const programFiles = process.env.ProgramFiles || "C:\\Program Files";
|
||||
const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
||||
const localAppData = process.env.LOCALAPPDATA || "";
|
||||
|
||||
const installed = [
|
||||
path.join(programFiles, "WinRAR", "UnRAR.exe"),
|
||||
path.join(programFiles, "WinRAR", "WinRAR.exe"),
|
||||
path.join(programFilesX86, "WinRAR", "UnRAR.exe"),
|
||||
path.join(programFilesX86, "WinRAR", "WinRAR.exe")
|
||||
];
|
||||
|
||||
if (localAppData) {
|
||||
installed.push(path.join(localAppData, "Programs", "WinRAR", "UnRAR.exe"));
|
||||
installed.push(path.join(localAppData, "Programs", "WinRAR", "WinRAR.exe"));
|
||||
}
|
||||
|
||||
const ordered = resolvedExtractorCommand
|
||||
? [resolvedExtractorCommand, ...installed, "UnRAR.exe", "WinRAR.exe", "unrar", "winrar"]
|
||||
: [...installed, "UnRAR.exe", "WinRAR.exe", "unrar", "winrar"];
|
||||
return Array.from(new Set(ordered.filter(Boolean)));
|
||||
}
|
||||
|
||||
@ -79,6 +86,10 @@ function isAbsoluteCommand(command: string): boolean {
|
||||
|| command.includes("/");
|
||||
}
|
||||
|
||||
function isNoExtractorError(errorText: string): boolean {
|
||||
return String(errorText || "").toLowerCase().includes("nicht gefunden");
|
||||
}
|
||||
|
||||
type ExtractSpawnResult = {
|
||||
ok: boolean;
|
||||
missingCommand: boolean;
|
||||
@ -139,10 +150,10 @@ export function buildExternalExtractArgs(
|
||||
): string[] {
|
||||
const mode = effectiveConflictMode(conflictMode);
|
||||
const lower = command.toLowerCase();
|
||||
if (lower.includes("unrar")) {
|
||||
if (lower.includes("unrar") || lower.includes("winrar")) {
|
||||
const overwrite = mode === "overwrite" ? "-o+" : mode === "rename" ? "-or" : "-o-";
|
||||
const pass = password ? `-p${password}` : "-p-";
|
||||
return ["x", overwrite, pass, archivePath, `${targetDir}${path.sep}`];
|
||||
return ["x", overwrite, pass, "-y", archivePath, `${targetDir}${path.sep}`];
|
||||
}
|
||||
|
||||
const overwrite = mode === "overwrite" ? "-aoa" : mode === "rename" ? "-aou" : "-aos";
|
||||
@ -150,50 +161,54 @@ export function buildExternalExtractArgs(
|
||||
return ["x", "-y", overwrite, pass, archivePath, `-o${targetDir}`];
|
||||
}
|
||||
|
||||
async function runExternalExtract(archivePath: string, targetDir: string, conflictMode: ConflictMode): Promise<void> {
|
||||
if (extractorUnavailable) {
|
||||
throw new Error(extractorUnavailableReason || "Kein Entpacker gefunden (7-Zip/unrar fehlt)");
|
||||
async function resolveExtractorCommand(): Promise<string> {
|
||||
if (resolvedExtractorCommand) {
|
||||
return resolvedExtractorCommand;
|
||||
}
|
||||
if (resolveFailureReason) {
|
||||
throw new Error(resolveFailureReason);
|
||||
}
|
||||
|
||||
const candidates = extractorCandidates();
|
||||
const candidates = winRarCandidates();
|
||||
for (const command of candidates) {
|
||||
if (isAbsoluteCommand(command) && !fs.existsSync(command)) {
|
||||
continue;
|
||||
}
|
||||
const probeArgs = command.toLowerCase().includes("winrar") ? ["-?"] : ["?"];
|
||||
const probe = await runExtractCommand(command, probeArgs);
|
||||
if (!probe.missingCommand) {
|
||||
resolvedExtractorCommand = command;
|
||||
resolveFailureReason = "";
|
||||
logger.info(`Entpacker erkannt: ${command}`);
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
resolveFailureReason = NO_EXTRACTOR_MESSAGE;
|
||||
throw new Error(resolveFailureReason);
|
||||
}
|
||||
|
||||
async function runExternalExtract(archivePath: string, targetDir: string, conflictMode: ConflictMode): Promise<void> {
|
||||
const command = await resolveExtractorCommand();
|
||||
const passwords = archivePasswords();
|
||||
let lastError = "";
|
||||
let sawExecutableCommand = false;
|
||||
let missingCommands = 0;
|
||||
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
|
||||
for (const command of candidates) {
|
||||
if (isAbsoluteCommand(command) && !fs.existsSync(command)) {
|
||||
missingCommands += 1;
|
||||
continue;
|
||||
for (const password of passwords) {
|
||||
const args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password);
|
||||
const result = await runExtractCommand(command, args);
|
||||
if (result.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const password of passwords) {
|
||||
const args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password);
|
||||
const result = await runExtractCommand(command, args);
|
||||
if (result.ok) {
|
||||
preferredExtractorCommand = command;
|
||||
extractorUnavailable = false;
|
||||
extractorUnavailableReason = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.missingCommand) {
|
||||
missingCommands += 1;
|
||||
lastError = result.errorText;
|
||||
break;
|
||||
}
|
||||
|
||||
sawExecutableCommand = true;
|
||||
lastError = result.errorText;
|
||||
if (result.missingCommand) {
|
||||
resolvedExtractorCommand = null;
|
||||
resolveFailureReason = NO_EXTRACTOR_MESSAGE;
|
||||
throw new Error(NO_EXTRACTOR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sawExecutableCommand && missingCommands >= candidates.length) {
|
||||
extractorUnavailable = true;
|
||||
extractorUnavailableReason = "Kein Entpacker gefunden (7-Zip/unrar fehlt oder konnte nicht gestartet werden)";
|
||||
throw new Error(extractorUnavailableReason);
|
||||
lastError = result.errorText;
|
||||
}
|
||||
|
||||
throw new Error(lastError || "Entpacken fehlgeschlagen");
|
||||
@ -272,6 +287,13 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
||||
const errorText = String(error);
|
||||
lastError = errorText;
|
||||
logger.error(`Entpack-Fehler ${path.basename(archivePath)}: ${errorText}`);
|
||||
if (isNoExtractorError(errorText)) {
|
||||
const remaining = candidates.length - (extracted + failed);
|
||||
if (remaining > 0) {
|
||||
failed += remaining;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,36 +15,29 @@ afterEach(() => {
|
||||
|
||||
describe("extractor", () => {
|
||||
it("maps external extractor args by conflict mode", () => {
|
||||
expect(buildExternalExtractArgs("7z", "archive.rar", "C:\\target", "overwrite")).toEqual([
|
||||
expect(buildExternalExtractArgs("WinRAR.exe", "archive.rar", "C:\\target", "overwrite")).toEqual([
|
||||
"x",
|
||||
"-o+",
|
||||
"-p-",
|
||||
"-y",
|
||||
"-aoa",
|
||||
"-p",
|
||||
"archive.rar",
|
||||
"-oC:\\target"
|
||||
"C:\\target\\"
|
||||
]);
|
||||
expect(buildExternalExtractArgs("7z", "archive.rar", "C:\\target", "ask")).toEqual([
|
||||
expect(buildExternalExtractArgs("WinRAR.exe", "archive.rar", "C:\\target", "ask", "serienfans.org")).toEqual([
|
||||
"x",
|
||||
"-y",
|
||||
"-aos",
|
||||
"-p",
|
||||
"archive.rar",
|
||||
"-oC:\\target"
|
||||
]);
|
||||
expect(buildExternalExtractArgs("7z", "archive.rar", "C:\\target", "ask", "serienfans.org")).toEqual([
|
||||
"x",
|
||||
"-y",
|
||||
"-aos",
|
||||
"-o-",
|
||||
"-pserienfans.org",
|
||||
"-y",
|
||||
"archive.rar",
|
||||
"-oC:\\target"
|
||||
"C:\\target\\"
|
||||
]);
|
||||
|
||||
const unrarRename = buildExternalExtractArgs("unrar", "archive.rar", "C:\\target", "rename");
|
||||
expect(unrarRename[0]).toBe("x");
|
||||
expect(unrarRename[1]).toBe("-or");
|
||||
expect(unrarRename[2]).toBe("-p-");
|
||||
expect(unrarRename[3]).toBe("archive.rar");
|
||||
expect(unrarRename[3]).toBe("-y");
|
||||
expect(unrarRename[4]).toBe("archive.rar");
|
||||
});
|
||||
|
||||
it("deletes only successfully extracted archives", async () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user