Fix long path extraction using subst drive mapping instead of \?\ prefix
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
WinRAR doesn't support \?\ prefix (interprets it as UNC network path). Replace with subst drive mapping: maps targetDir to a short drive letter (Z:, Y:, etc.) before extraction, then removes mapping after. This keeps total paths under 260 chars even when archives contain deep internal directory structures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2ae22f942e
commit
e013c63c59
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.5.16",
|
||||
"version": "1.5.17",
|
||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -1,30 +1,56 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { spawn } from "node:child_process";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import AdmZip from "adm-zip";
|
||||
import { CleanupMode, ConflictMode } from "../shared/types";
|
||||
import { logger } from "./logger";
|
||||
import { removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
||||
|
||||
const DEFAULT_ARCHIVE_PASSWORDS = ["", "serienfans.org", "serienjunkies.org"];
|
||||
|
||||
/**
|
||||
* On Windows, prefix an absolute path with \\?\ to bypass the 260-char MAX_PATH limit.
|
||||
* WinRAR 5.x+ and 7-Zip support this prefix for long output paths.
|
||||
*/
|
||||
function longPathForWindows(p: string): string {
|
||||
if (process.platform !== "win32") return p;
|
||||
const resolved = path.resolve(p);
|
||||
if (resolved.startsWith("\\\\?\\")) return resolved;
|
||||
if (resolved.startsWith("\\\\")) {
|
||||
// UNC path \\server\share -> \\?\UNC\server\share
|
||||
return "\\\\?\\UNC\\" + resolved.slice(2);
|
||||
}
|
||||
return "\\\\?\\" + resolved;
|
||||
}
|
||||
const NO_EXTRACTOR_MESSAGE = "WinRAR/UnRAR nicht gefunden. Bitte WinRAR installieren.";
|
||||
|
||||
// ── subst drive mapping for long paths on Windows ──
|
||||
const SUBST_THRESHOLD = 100;
|
||||
const activeSubstDrives = new Set<string>();
|
||||
|
||||
function findFreeSubstDrive(): string | null {
|
||||
if (process.platform !== "win32") return null;
|
||||
for (let code = 90; code >= 71; code--) { // Z to G
|
||||
const letter = String.fromCharCode(code);
|
||||
if (activeSubstDrives.has(letter)) continue;
|
||||
try {
|
||||
fs.accessSync(`${letter}:\\`);
|
||||
// Drive exists, skip
|
||||
} catch {
|
||||
return letter;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface SubstMapping { drive: string; original: string; }
|
||||
|
||||
function createSubstMapping(targetDir: string): SubstMapping | null {
|
||||
if (process.platform !== "win32" || targetDir.length < SUBST_THRESHOLD) return null;
|
||||
const drive = findFreeSubstDrive();
|
||||
if (!drive) return null;
|
||||
const result = spawnSync("subst", [`${drive}:`, targetDir], { stdio: "pipe", timeout: 5000 });
|
||||
if (result.status !== 0) {
|
||||
logger.warn(`subst ${drive}: fehlgeschlagen: ${String(result.stderr || "").trim()}`);
|
||||
return null;
|
||||
}
|
||||
activeSubstDrives.add(drive);
|
||||
logger.info(`subst ${drive}: -> ${targetDir}`);
|
||||
return { drive, original: targetDir };
|
||||
}
|
||||
|
||||
function removeSubstMapping(mapping: SubstMapping): void {
|
||||
spawnSync("subst", [`${mapping.drive}:`, "/d"], { stdio: "pipe", timeout: 5000 });
|
||||
activeSubstDrives.delete(mapping.drive);
|
||||
logger.info(`subst ${mapping.drive}: entfernt`);
|
||||
}
|
||||
|
||||
let resolvedExtractorCommand: string | null = null;
|
||||
let resolveFailureReason = "";
|
||||
let resolveFailureAt = 0;
|
||||
@ -621,14 +647,13 @@ export function buildExternalExtractArgs(
|
||||
const perfArgs = usePerformanceFlags && shouldUseExtractorPerformanceFlags()
|
||||
? ["-idc", extractorThreadSwitch()]
|
||||
: [];
|
||||
const longTarget = longPathForWindows(targetDir);
|
||||
return ["x", overwrite, pass, "-y", ...perfArgs, archivePath, `${longTarget}${path.sep}`];
|
||||
return ["x", overwrite, pass, "-y", ...perfArgs, archivePath, `${targetDir}${path.sep}`];
|
||||
}
|
||||
|
||||
const overwrite = mode === "overwrite" ? "-aoa" : mode === "rename" ? "-aou" : "-aos";
|
||||
// NOTE: Same password-in-args limitation as above applies to 7z as well.
|
||||
const pass = password ? `-p${password}` : "-p";
|
||||
return ["x", "-y", overwrite, pass, archivePath, `-o${longPathForWindows(targetDir)}`];
|
||||
return ["x", "-y", overwrite, pass, archivePath, `-o${targetDir}`];
|
||||
}
|
||||
|
||||
async function resolveExtractorCommandInternal(): Promise<string> {
|
||||
@ -699,6 +724,31 @@ async function runExternalExtract(
|
||||
|
||||
await fs.promises.mkdir(targetDir, { recursive: true });
|
||||
|
||||
// On Windows, long targetDir + archive internal paths can exceed MAX_PATH (260 chars).
|
||||
// Use "subst" to map the targetDir to a short drive letter for the extraction process.
|
||||
const subst = createSubstMapping(targetDir);
|
||||
const effectiveTargetDir = subst ? `${subst.drive}:` : targetDir;
|
||||
|
||||
try {
|
||||
return await runExternalExtractInner(command, archivePath, effectiveTargetDir, conflictMode, passwordCandidates, onArchiveProgress, signal, timeoutMs);
|
||||
} finally {
|
||||
if (subst) removeSubstMapping(subst);
|
||||
}
|
||||
}
|
||||
|
||||
async function runExternalExtractInner(
|
||||
command: string,
|
||||
archivePath: string,
|
||||
targetDir: string,
|
||||
conflictMode: ConflictMode,
|
||||
passwordCandidates: string[],
|
||||
onArchiveProgress: ((percent: number) => void) | undefined,
|
||||
signal: AbortSignal | undefined,
|
||||
timeoutMs: number
|
||||
): Promise<string> {
|
||||
const passwords = passwordCandidates;
|
||||
let lastError = "";
|
||||
|
||||
let announcedStart = false;
|
||||
let bestPercent = 0;
|
||||
let usePerformanceFlags = externalExtractorSupportsPerfFlags && shouldUseExtractorPerformanceFlags();
|
||||
|
||||
@ -20,15 +20,14 @@ describe("extractor", () => {
|
||||
expect(overwriteArgs).toContain("-idc");
|
||||
expect(overwriteArgs.some((value) => /^-mt\d+$/i.test(value))).toBe(true);
|
||||
expect(overwriteArgs[overwriteArgs.length - 2]).toBe("archive.rar");
|
||||
const winrarTarget = process.platform === "win32" ? "\\\\?\\C:\\target\\" : "C:\\target\\";
|
||||
expect(overwriteArgs[overwriteArgs.length - 1]).toBe(winrarTarget);
|
||||
expect(overwriteArgs[overwriteArgs.length - 1]).toBe("C:\\target\\");
|
||||
|
||||
const askArgs = buildExternalExtractArgs("WinRAR.exe", "archive.rar", "C:\\target", "ask", "serienfans.org");
|
||||
expect(askArgs.slice(0, 4)).toEqual(["x", "-o-", "-pserienfans.org", "-y"]);
|
||||
expect(askArgs).toContain("-idc");
|
||||
expect(askArgs.some((value) => /^-mt\d+$/i.test(value))).toBe(true);
|
||||
expect(askArgs[askArgs.length - 2]).toBe("archive.rar");
|
||||
expect(askArgs[askArgs.length - 1]).toBe(winrarTarget);
|
||||
expect(askArgs[askArgs.length - 1]).toBe("C:\\target\\");
|
||||
|
||||
const compatibilityArgs = buildExternalExtractArgs("WinRAR.exe", "archive.rar", "C:\\target", "overwrite", "", false);
|
||||
expect(compatibilityArgs).not.toContain("-idc");
|
||||
@ -456,8 +455,7 @@ describe("extractor", () => {
|
||||
expect(args7z).toContain("-aoa");
|
||||
expect(args7z).toContain("-p");
|
||||
expect(args7z).toContain("archive.7z");
|
||||
const sevenZTarget = process.platform === "win32" ? "-o\\\\?\\C:\\target" : "-oC:\\target";
|
||||
expect(args7z).toContain(sevenZTarget);
|
||||
expect(args7z).toContain("-oC:\\target");
|
||||
});
|
||||
|
||||
it("builds 7z args with skip conflict mode", () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user