diff --git a/_upload_release.mjs b/_upload_release.mjs deleted file mode 100644 index 8023018..0000000 --- a/_upload_release.mjs +++ /dev/null @@ -1,75 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { spawnSync } from "node:child_process"; - -const credResult = spawnSync("git", ["credential", "fill"], { - input: "protocol=https\nhost=codeberg.org\n\n", - encoding: "utf8", - stdio: ["pipe", "pipe", "pipe"] -}); -const creds = new Map(); -for (const line of credResult.stdout.split(/\r?\n/)) { - if (line.includes("=")) { - const [k, v] = line.split("=", 2); - creds.set(k, v); - } -} -const auth = "Basic " + Buffer.from(creds.get("username") + ":" + creds.get("password")).toString("base64"); -const owner = "Sucukdeluxe"; -const repo = "real-debrid-downloader"; -const tag = "v1.5.35"; -const baseApi = `https://codeberg.org/api/v1/repos/${owner}/${repo}`; - -async function main() { - await fetch(baseApi, { - method: "PATCH", - headers: { Authorization: auth, "Content-Type": "application/json" }, - body: JSON.stringify({ has_releases: true }) - }); - - const createRes = await fetch(`${baseApi}/releases`, { - method: "POST", - headers: { Authorization: auth, "Content-Type": "application/json", Accept: "application/json" }, - body: JSON.stringify({ - tag_name: tag, - target_commitish: "main", - name: tag, - body: "- Fix: Fortschritt zeigt jetzt kombinierten Wert (Download + Entpacken)\n- Fix: Pausieren zeigt nicht mehr 'Warte auf Daten'\n- Pixel-perfekte Dual-Layer Progress-Bar Texte (clip-path)", - draft: false, - prerelease: false - }) - }); - const release = await createRes.json(); - if (!createRes.ok) { - console.error("Create failed:", JSON.stringify(release)); - process.exit(1); - } - console.log("Release created:", release.id); - - const files = [ - "Real-Debrid-Downloader Setup 1.5.35.exe", - "Real-Debrid-Downloader 1.5.35.exe", - "latest.yml", - "Real-Debrid-Downloader Setup 1.5.35.exe.blockmap" - ]; - for (const f of files) { - const filePath = path.join("release", f); - const data = fs.readFileSync(filePath); - const uploadUrl = `${baseApi}/releases/${release.id}/assets?name=${encodeURIComponent(f)}`; - const res = await fetch(uploadUrl, { - method: "POST", - headers: { Authorization: auth, "Content-Type": "application/octet-stream" }, - body: data - }); - if (res.ok) { - console.log("Uploaded:", f); - } else if (res.status === 409 || res.status === 422) { - console.log("Skipped existing:", f); - } else { - console.error("Upload failed for", f, ":", res.status); - } - } - console.log(`Done! https://codeberg.org/${owner}/${repo}/releases/tag/${tag}`); -} - -main().catch(e => { console.error(e.message); process.exit(1); }); diff --git a/installer/RealDebridDownloader.iss b/installer/RealDebridDownloader.iss index b749627..54ee1a8 100644 --- a/installer/RealDebridDownloader.iss +++ b/installer/RealDebridDownloader.iss @@ -25,11 +25,11 @@ AppPublisher=Sucukdeluxe DefaultDirName={autopf}\{#MyAppName} DefaultGroupName={#MyAppName} OutputDir={#MyOutputDir} -OutputBaseFilename=Real-Debrid-Downloader-Setup-{#MyAppVersion} +OutputBaseFilename=Real-Debrid-Downloader Setup {#MyAppVersion} Compression=lzma SolidCompression=yes WizardStyle=modern -PrivilegesRequired=admin +PrivilegesRequired=lowest ArchitecturesInstallIn64BitMode=x64compatible UninstallDisplayIcon={app}\{#MyAppExeName} SetupIconFile={#MyIconFile} @@ -39,8 +39,8 @@ Name: "german"; MessagesFile: "compiler:Languages\German.isl" Name: "english"; MessagesFile: "compiler:Default.isl" [Files] -Source: "{#MySourceDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "{#MyIconFile}"; DestDir: "{app}"; DestName: "app_icon.ico"; Flags: ignoreversion +Source: "{#MySourceDir}\\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs +Source: "{#MyIconFile}"; DestDir: "{app}"; DestName: "app_icon.ico" [Icons] Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app_icon.ico" diff --git a/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java b/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java index 413b830..33f1fef 100644 --- a/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java +++ b/resources/extractor-jvm/src/com/sucukdeluxe/extractor/JBindExtractorMain.java @@ -26,6 +26,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; @@ -42,6 +43,8 @@ public final class JBindExtractorMain { private static final Pattern NUMBERED_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.zip\\.\\d{3}$"); private static final Pattern OLD_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.z\\d{2,3}$"); private static final Pattern SEVEN_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.7z\\.001$"); + private static final Pattern DIGIT_SUFFIX_RE = Pattern.compile("\\d{2,3}"); + private static final Pattern WINDOWS_SPECIAL_CHARS_RE = Pattern.compile("[:<>*?\"\\|]"); private static volatile boolean sevenZipInitialized = false; private JBindExtractorMain() { @@ -152,30 +155,35 @@ public final class JBindExtractorMain { } ensureDirectory(output.getParentFile()); + rejectSymlink(output); long[] remaining = new long[] { itemUnits }; + boolean extractionSuccess = false; try { InputStream in = zipFile.getInputStream(header); - OutputStream out = new FileOutputStream(output); try { - byte[] buffer = new byte[BUFFER_SIZE]; - while (true) { - int read = in.read(buffer); - if (read < 0) { - break; + OutputStream out = new FileOutputStream(output); + try { + byte[] buffer = new byte[BUFFER_SIZE]; + while (true) { + int read = in.read(buffer); + if (read < 0) { + break; + } + if (read == 0) { + continue; + } + out.write(buffer, 0, read); + long accounted = Math.min(remaining[0], (long) read); + remaining[0] -= accounted; + progress.advance(accounted); } - if (read == 0) { - continue; + } finally { + try { + out.close(); + } catch (Throwable ignored) { } - out.write(buffer, 0, read); - long accounted = Math.min(remaining[0], (long) read); - remaining[0] -= accounted; - progress.advance(accounted); } } finally { - try { - out.close(); - } catch (Throwable ignored) { - } try { in.close(); } catch (Throwable ignored) { @@ -188,11 +196,19 @@ public final class JBindExtractorMain { if (modified > 0) { output.setLastModified(modified); } + extractionSuccess = true; } catch (ZipException error) { if (isWrongPassword(error, encrypted)) { throw new WrongPasswordException(error); } throw error; + } finally { + if (!extractionSuccess && output.exists()) { + try { + output.delete(); + } catch (Throwable ignored) { + } + } } } @@ -221,6 +237,9 @@ public final class JBindExtractorMain { IInArchive archive = context.archive; ISimpleInArchive simple = archive.getSimpleInterface(); ISimpleInArchiveItem[] items = simple.getArchiveItems(); + if (items == null) { + throw new IOException("Archiv enthalt keine Eintrage oder konnte nicht gelesen werden: " + request.archiveFile.getAbsolutePath()); + } long totalUnits = 0; boolean encrypted = false; @@ -260,8 +279,10 @@ public final class JBindExtractorMain { } ensureDirectory(output.getParentFile()); + rejectSymlink(output); final FileOutputStream out = new FileOutputStream(output); final long[] remaining = new long[] { itemUnits }; + boolean extractionSuccess = false; try { ExtractOperationResult result = item.extractSlow(new ISequentialOutStream() { @Override @@ -291,6 +312,7 @@ public final class JBindExtractorMain { } throw new IOException("7z-Fehler: " + result.name()); } + extractionSuccess = true; } catch (SevenZipException error) { if (looksLikeWrongPassword(error, encrypted)) { throw new WrongPasswordException(error); @@ -301,6 +323,12 @@ public final class JBindExtractorMain { out.close(); } catch (Throwable ignored) { } + if (!extractionSuccess && output.exists()) { + try { + output.delete(); + } catch (Throwable ignored) { + } + } } try { @@ -328,14 +356,31 @@ public final class JBindExtractorMain { if (SEVEN_ZIP_SPLIT_RE.matcher(nameLower).matches()) { VolumedArchiveInStream volumed = new VolumedArchiveInStream(archiveFile.getName(), callback); - IInArchive archive = SevenZip.openInArchive(null, volumed, callback); - return new SevenZipArchiveContext(archive, null, volumed, callback); + try { + IInArchive archive = SevenZip.openInArchive(null, volumed, callback); + return new SevenZipArchiveContext(archive, null, volumed, callback); + } catch (Exception error) { + callback.close(); + throw error; + } } RandomAccessFile raf = new RandomAccessFile(archiveFile, "r"); RandomAccessFileInStream stream = new RandomAccessFileInStream(raf); - IInArchive archive = SevenZip.openInArchive(null, stream, callback); - return new SevenZipArchiveContext(archive, stream, null, callback); + try { + IInArchive archive = SevenZip.openInArchive(null, stream, callback); + return new SevenZipArchiveContext(archive, stream, null, callback); + } catch (Exception error) { + try { + stream.close(); + } catch (Throwable ignored) { + } + try { + raf.close(); + } catch (Throwable ignored) { + } + throw error; + } } private static boolean isWrongPassword(ZipException error, boolean encrypted) { @@ -396,7 +441,7 @@ public final class JBindExtractorMain { } if (siblingName.startsWith(prefix) && siblingName.length() >= prefix.length() + 2) { String suffix = siblingName.substring(prefix.length()); - if (suffix.matches("\\d{2,3}")) { + if (DIGIT_SUFFIX_RE.matcher(suffix).matches()) { return true; } } @@ -480,6 +525,12 @@ public final class JBindExtractorMain { } if (normalized.matches("^[a-zA-Z]:.*")) { normalized = normalized.substring(2); + while (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + while (normalized.startsWith("\\")) { + normalized = normalized.substring(1); + } } File targetCanonical = targetDir.getCanonicalFile(); File output = new File(targetCanonical, normalized); @@ -488,7 +539,8 @@ public final class JBindExtractorMain { String outputPath = outputCanonical.getPath(); String targetPathNorm = isWindows() ? targetPath.toLowerCase(Locale.ROOT) : targetPath; String outputPathNorm = isWindows() ? outputPath.toLowerCase(Locale.ROOT) : outputPath; - if (!outputPathNorm.equals(targetPathNorm) && !outputPathNorm.startsWith(targetPathNorm + File.separator)) { + String targetPrefix = targetPathNorm.endsWith(File.separator) ? targetPathNorm : targetPathNorm + File.separator; + if (!outputPathNorm.equals(targetPathNorm) && !outputPathNorm.startsWith(targetPrefix)) { throw new IOException("Path Traversal blockiert: " + entryName); } return outputCanonical; @@ -506,20 +558,50 @@ public final class JBindExtractorMain { if (entry.length() == 0) { return fallback; } + // Sanitize Windows special characters from each path segment + String[] segments = entry.split("/", -1); + StringBuilder sanitized = new StringBuilder(); + for (int i = 0; i < segments.length; i++) { + if (i > 0) { + sanitized.append('/'); + } + sanitized.append(WINDOWS_SPECIAL_CHARS_RE.matcher(segments[i]).replaceAll("_")); + } + entry = sanitized.toString(); + if (entry.length() == 0) { + return fallback; + } return entry; } private static long safeSize(Long value) { if (value == null) { - return 1; + return 0; } long size = value.longValue(); if (size <= 0) { - return 1; + return 0; } return size; } + private static void rejectSymlink(File file) throws IOException { + if (file == null) { + return; + } + if (Files.isSymbolicLink(file.toPath())) { + throw new IOException("Zieldatei ist ein Symlink, Schreiben verweigert: " + file.getAbsolutePath()); + } + // Also check parent directories for symlinks + File parent = file.getParentFile(); + while (parent != null) { + if (Files.isSymbolicLink(parent.toPath())) { + throw new IOException("Elternverzeichnis ist ein Symlink, Schreiben verweigert: " + parent.getAbsolutePath()); + } + parent = parent.getParentFile(); + } + } + private static void ensureDirectory(File dir) throws IOException { if (dir == null) { return; @@ -828,12 +910,11 @@ public final class JBindExtractorMain { if (filename == null || filename.trim().length() == 0) { return null; } - File direct = new File(filename); - if (direct.isAbsolute() && direct.exists()) { - return direct; - } + // Always resolve relative to the archive's parent directory. + // Never accept absolute paths to prevent path traversal. + String baseName = new File(filename).getName(); if (archiveDir != null) { - File relative = new File(archiveDir, filename); + File relative = new File(archiveDir, baseName); if (relative.exists()) { return relative; } @@ -843,13 +924,13 @@ public final class JBindExtractorMain { if (!sibling.isFile()) { continue; } - if (sibling.getName().equalsIgnoreCase(filename)) { + if (sibling.getName().equalsIgnoreCase(baseName)) { return sibling; } } } } - return direct.exists() ? direct : null; + return null; } @Override diff --git a/scripts/afterPack.cjs b/scripts/afterPack.cjs index f1e1e53..aa1a957 100644 --- a/scripts/afterPack.cjs +++ b/scripts/afterPack.cjs @@ -2,8 +2,17 @@ const path = require("path"); const { rcedit } = require("rcedit"); module.exports = async function afterPack(context) { - const exePath = path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.exe`); + const productFilename = context.packager?.appInfo?.productFilename; + if (!productFilename) { + console.warn(" • rcedit: skipped — productFilename not available"); + return; + } + const exePath = path.join(context.appOutDir, `${productFilename}.exe`); const iconPath = path.resolve(__dirname, "..", "assets", "app_icon.ico"); console.log(` • rcedit: patching icon → ${exePath}`); - await rcedit(exePath, { icon: iconPath }); + try { + await rcedit(exePath, { icon: iconPath }); + } catch (error) { + console.warn(` • rcedit: failed — ${String(error)}`); + } }; diff --git a/scripts/debrid_service_smoke.ts b/scripts/debrid_service_smoke.ts index 1e60515..3ceb955 100644 --- a/scripts/debrid_service_smoke.ts +++ b/scripts/debrid_service_smoke.ts @@ -31,18 +31,21 @@ async function main(): Promise { login: settings.megaLogin, password: settings.megaPassword })); - const service = new DebridService(settings, { - megaWebUnrestrict: (link) => megaWeb.unrestrict(link) - }); - for (const link of links) { - try { - const result = await service.unrestrictLink(link); - console.log(`[OK] ${result.providerLabel} -> ${result.fileName}`); - } catch (error) { - console.log(`[FAIL] ${String(error)}`); + try { + const service = new DebridService(settings, { + megaWebUnrestrict: (link) => megaWeb.unrestrict(link) + }); + for (const link of links) { + try { + const result = await service.unrestrictLink(link); + console.log(`[OK] ${result.providerLabel} -> ${result.fileName}`); + } catch (error) { + console.log(`[FAIL] ${String(error)}`); + } } + } finally { + megaWeb.dispose(); } - megaWeb.dispose(); } -void main(); +main().catch(e => { console.error(e); process.exit(1); }); diff --git a/scripts/mega_web_generate_download_test.mjs b/scripts/mega_web_generate_download_test.mjs index 0066daa..008b5e5 100644 --- a/scripts/mega_web_generate_download_test.mjs +++ b/scripts/mega_web_generate_download_test.mjs @@ -16,8 +16,8 @@ function sleep(ms) { } function cookieFrom(headers) { - const raw = headers.get("set-cookie") || ""; - return raw.split(",").map((x) => x.split(";")[0].trim()).filter(Boolean).join("; "); + const cookies = headers.getSetCookie(); + return cookies.map((x) => x.split(";")[0].trim()).filter(Boolean).join("; "); } function parseDebridCodes(html) { @@ -47,6 +47,9 @@ async function resolveCode(cookie, code) { }); const text = (await res.text()).trim(); if (text === "reload") { + if (attempt % 5 === 0) { + console.log(` [retry] code=${code} attempt=${attempt}/50 (waiting for server)`); + } await sleep(800); continue; } @@ -98,7 +101,13 @@ async function main() { redirect: "manual" }); + if (loginRes.status >= 400) { + throw new Error(`Login failed with HTTP ${loginRes.status}`); + } const cookie = cookieFrom(loginRes.headers); + if (!cookie) { + throw new Error("Login returned no session cookie"); + } console.log("login", loginRes.status, loginRes.headers.get("location") || ""); const debridRes = await fetch("https://www.mega-debrid.eu/index.php?form=debrid", { @@ -136,4 +145,4 @@ async function main() { } } -await main(); +await main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/scripts/provider_smoke_check.mjs b/scripts/provider_smoke_check.mjs index 3ce78a1..cf00611 100644 --- a/scripts/provider_smoke_check.mjs +++ b/scripts/provider_smoke_check.mjs @@ -66,6 +66,8 @@ async function callRealDebrid(link) { }; } +// megaCookie is intentionally cached at module scope so that multiple +// callMegaDebrid() invocations reuse the same session cookie. async function callMegaDebrid(link) { if (!megaCookie) { const loginRes = await fetch("https://www.mega-debrid.eu/index.php?form=login", { @@ -77,13 +79,15 @@ async function callMegaDebrid(link) { body: new URLSearchParams({ login: megaLogin, password: megaPassword, remember: "on" }), redirect: "manual" }); - megaCookie = (loginRes.headers.get("set-cookie") || "") - .split(",") + if (loginRes.status >= 400) { + return { ok: false, error: `Mega-Web login failed with HTTP ${loginRes.status}` }; + } + megaCookie = loginRes.headers.getSetCookie() .map((chunk) => chunk.split(";")[0].trim()) .filter(Boolean) .join("; "); if (!megaCookie) { - return { ok: false, error: "Mega-Web login failed" }; + return { ok: false, error: "Mega-Web login returned no session cookie" }; } } @@ -290,4 +294,4 @@ async function main() { } } -await main(); +await main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/scripts/release_gitea.mjs b/scripts/release_gitea.mjs index 834ea16..060ae4b 100644 --- a/scripts/release_gitea.mjs +++ b/scripts/release_gitea.mjs @@ -37,7 +37,8 @@ function runWithInput(command, args, input) { cwd: process.cwd(), encoding: "utf8", input, - stdio: ["pipe", "pipe", "pipe"] + stdio: ["pipe", "pipe", "pipe"], + timeout: 10000 }); if (result.status !== 0) { const stderr = String(result.stderr || "").trim(); @@ -95,15 +96,17 @@ function getGiteaRepo() { const preferredBase = normalizeBaseUrl(process.env.GITEA_BASE_URL || process.env.FORGEJO_BASE_URL || "https://git.24-music.de"); + const preferredProtocol = preferredBase ? new URL(preferredBase).protocol : "https:"; + for (const remote of remotes) { try { const remoteUrl = runCapture("git", ["remote", "get-url", remote]); const parsed = parseRemoteUrl(remoteUrl); const remoteBase = `https://${parsed.host}`.toLowerCase(); - if (preferredBase && remoteBase !== preferredBase.toLowerCase()) { + if (preferredBase && remoteBase !== preferredBase.toLowerCase().replace(/^http:/, "https:")) { continue; } - return { remote, ...parsed, baseUrl: `https://${parsed.host}` }; + return { remote, ...parsed, baseUrl: `${preferredProtocol}//${parsed.host}` }; } catch { // try next remote } @@ -179,7 +182,8 @@ function updatePackageVersion(rootDir, version) { const packagePath = path.join(rootDir, "package.json"); const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); if (String(packageJson.version || "") === version) { - throw new Error(`package.json is already at version ${version}`); + process.stdout.write(`package.json is already at version ${version}, skipping update.\n`); + return; } packageJson.version = version; fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8"); @@ -257,9 +261,31 @@ async function createOrGetRelease(baseApi, tag, authHeader, notes) { async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, files) { for (const fileName of files) { const filePath = path.join(releaseDir, fileName); - const fileData = fs.readFileSync(filePath); + const fileSize = fs.statSync(filePath).size; const uploadUrl = `${baseApi}/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`; - const response = await apiRequest("POST", uploadUrl, authHeader, fileData, "application/octet-stream"); + + // Stream large files instead of loading them entirely into memory + const fileStream = fs.createReadStream(filePath); + const response = await fetch(uploadUrl, { + method: "POST", + headers: { + Accept: "application/json", + Authorization: authHeader, + "Content-Type": "application/octet-stream", + "Content-Length": String(fileSize) + }, + body: fileStream, + duplex: "half" + }); + + const text = await response.text(); + let parsed; + try { + parsed = text ? JSON.parse(text) : null; + } catch { + parsed = text; + } + if (response.ok) { process.stdout.write(`Uploaded: ${fileName}\n`); continue; @@ -268,7 +294,7 @@ async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, f process.stdout.write(`Skipped existing asset: ${fileName}\n`); continue; } - throw new Error(`Asset upload failed for ${fileName} (${response.status}): ${JSON.stringify(response.body)}`); + throw new Error(`Asset upload failed for ${fileName} (${response.status}): ${JSON.stringify(parsed)}`); } } @@ -290,17 +316,18 @@ async function main() { ensureNoTrackedChanges(); ensureTagMissing(tag); + + if (args.dryRun) { + process.stdout.write(`Dry run: would release ${tag}. No changes made.\n`); + return; + } + updatePackageVersion(rootDir, version); process.stdout.write(`Building release artifacts for ${tag}...\n`); run(NPM_EXECUTABLE, ["run", "release:win"]); const assets = ensureAssetsExist(rootDir, version); - if (args.dryRun) { - process.stdout.write(`Dry run complete. Assets exist for ${tag}.\n`); - return; - } - run("git", ["add", "package.json"]); run("git", ["commit", "-m", `Release ${tag}`]); run("git", ["push", repo.remote, "main"]); diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index d3092e4..d0e3e43 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -285,7 +285,7 @@ export class AppController { public exportBackup(): string { const settings = { ...this.settings }; - const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaPassword", "bestToken", "allDebridToken", "ddownloadPassword"]; + const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword"]; for (const key of SENSITIVE_KEYS) { const val = settings[key]; if (typeof val === "string" && val.length > 0) { @@ -307,7 +307,7 @@ export class AppController { return { restored: false, message: "Kein gültiges Backup (settings/session fehlen)" }; } const importedSettings = parsed.settings as AppSettings; - const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaPassword", "bestToken", "allDebridToken", "ddownloadPassword"]; + const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword"]; for (const key of SENSITIVE_KEYS) { const val = (importedSettings as Record)[key]; if (typeof val === "string" && val.startsWith("***")) { diff --git a/src/main/container.ts b/src/main/container.ts index d5043ea..cbdd62c 100644 --- a/src/main/container.ts +++ b/src/main/container.ts @@ -164,7 +164,7 @@ async function decryptDlcLocal(filePath: string): Promise const dlcData = content.slice(0, -88); const rcUrl = DLC_SERVICE_URL.replace("{KEY}", encodeURIComponent(dlcKey)); - const rcResponse = await fetch(rcUrl, { method: "GET" }); + const rcResponse = await fetch(rcUrl, { method: "GET", signal: AbortSignal.timeout(30000) }); if (!rcResponse.ok) { return []; } @@ -217,7 +217,8 @@ async function tryDcryptUpload(fileContent: Buffer, fileName: string): Promise { const response = await fetch(DCRYPT_PASTE_URL, { method: "POST", - body: form + body: form, + signal: AbortSignal.timeout(30000) }); if (response.status === 413) { return null; diff --git a/src/main/debrid.ts b/src/main/debrid.ts index 65345df..73c16a2 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -1154,6 +1154,9 @@ export class DebridService { private options: DebridServiceOptions; + private cachedDdownloadClient: DdownloadClient | null = null; + private cachedDdownloadKey = ""; + public constructor(settings: AppSettings, options: DebridServiceOptions = {}) { this.settings = cloneSettings(settings); this.options = options; @@ -1163,6 +1166,16 @@ export class DebridService { this.settings = cloneSettings(next); } + private getDdownloadClient(login: string, password: string): DdownloadClient { + const key = `${login}\0${password}`; + if (this.cachedDdownloadClient && this.cachedDdownloadKey === key) { + return this.cachedDdownloadClient; + } + this.cachedDdownloadClient = new DdownloadClient(login, password); + this.cachedDdownloadKey = key; + return this.cachedDdownloadClient; + } + public async resolveFilenames( links: string[], onResolved?: (link: string, fileName: string) => void, @@ -1338,7 +1351,7 @@ export class DebridService { return new AllDebridClient(settings.allDebridToken).unrestrictLink(link, signal); } if (provider === "ddownload") { - return new DdownloadClient(settings.ddownloadLogin, settings.ddownloadPassword).unrestrictLink(link, signal); + return this.getDdownloadClient(settings.ddownloadLogin, settings.ddownloadPassword).unrestrictLink(link, signal); } return new BestDebridClient(settings.bestToken).unrestrictLink(link, signal); } diff --git a/src/main/debug-server.ts b/src/main/debug-server.ts index c754409..204ed7e 100644 --- a/src/main/debug-server.ts +++ b/src/main/debug-server.ts @@ -261,7 +261,7 @@ export function startDebugServer(mgr: DownloadManager, baseDir: string): void { const port = getPort(baseDir); server = http.createServer(handleRequest); - server.listen(port, "0.0.0.0", () => { + server.listen(port, "127.0.0.1", () => { logger.info(`Debug-Server gestartet auf Port ${port}`); }); server.on("error", (err) => { diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 3aa3b00..62dfc1d 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -20,6 +20,23 @@ import { UiSnapshot } from "../shared/types"; import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS, SPEED_WINDOW_SECONDS, WRITE_BUFFER_SIZE, WRITE_FLUSH_TIMEOUT_MS, ALLOCATION_UNIT_SIZE, STREAM_HIGH_WATER_MARK, DISK_BUSY_THRESHOLD_MS } from "./constants"; + +// Reference counter for NODE_TLS_REJECT_UNAUTHORIZED to avoid race conditions +// when multiple parallel downloads need TLS verification disabled (e.g. DDownload). +let tlsSkipRefCount = 0; +function acquireTlsSkip(): void { + tlsSkipRefCount += 1; + if (tlsSkipRefCount === 1) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + } +} +function releaseTlsSkip(): void { + tlsSkipRefCount -= 1; + if (tlsSkipRefCount <= 0) { + tlsSkipRefCount = 0; + delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; + } +} import { cleanupCancelledPackageArtifactsAsync } from "./cleanup"; import { DebridService, MegaWebUnrestrictor, checkRapidgatorOnline } from "./debrid"; import { clearExtractResumeState, collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates } from "./extractor"; @@ -3212,11 +3229,11 @@ export class DownloadManager extends EventEmitter { for (const item of Object.values(this.session.items)) { if (item.status !== "completed") continue; - const fs = item.fullStatus || ""; + const fullSt = item.fullStatus || ""; // Only relabel items with active extraction status (e.g. "Entpacken 45%", "Passwort prüfen") // Skip items that were merely waiting ("Entpacken - Ausstehend", "Entpacken - Warten auf Parts") // as they were never actively extracting and "abgebrochen" would be misleading. - if (/^Entpacken\b/i.test(fs) && !/Ausstehend/i.test(fs) && !/Warten/i.test(fs) && !isExtractedLabel(fs)) { + if (/^Entpacken\b/i.test(fullSt) && !/Ausstehend/i.test(fullSt) && !/Warten/i.test(fullSt) && !isExtractedLabel(fullSt)) { item.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)"; item.updatedAt = nowMs(); const pkg = this.session.packages[item.packageId]; @@ -3305,7 +3322,7 @@ export class DownloadManager extends EventEmitter { this.session.reconnectReason = ""; for (const item of Object.values(this.session.items)) { - if (item.provider !== "realdebrid" && item.provider !== "megadebrid" && item.provider !== "bestdebrid" && item.provider !== "alldebrid") { + if (item.provider !== "realdebrid" && item.provider !== "megadebrid" && item.provider !== "bestdebrid" && item.provider !== "alldebrid" && item.provider !== "ddownload") { item.provider = null; } if (item.status === "cancelled" && item.fullStatus === "Gestoppt") { @@ -5152,16 +5169,13 @@ export class DownloadManager extends EventEmitter { const connectTimeoutMs = getDownloadConnectTimeoutMs(); let connectTimer: NodeJS.Timeout | null = null; const connectAbortController = new AbortController(); - const prevTlsReject = process.env.NODE_TLS_REJECT_UNAUTHORIZED; + if (skipTlsVerify) acquireTlsSkip(); try { if (connectTimeoutMs > 0) { connectTimer = setTimeout(() => { connectAbortController.abort("connect_timeout"); }, connectTimeoutMs); } - if (skipTlsVerify) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - } response = await fetch(directUrl, { method: "GET", headers, @@ -5181,10 +5195,7 @@ export class DownloadManager extends EventEmitter { } throw error; } finally { - if (skipTlsVerify) { - if (prevTlsReject === undefined) delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; - else process.env.NODE_TLS_REJECT_UNAUTHORIZED = prevTlsReject; - } + if (skipTlsVerify) releaseTlsSkip(); if (connectTimer) { clearTimeout(connectTimer); } diff --git a/src/main/extractor.ts b/src/main/extractor.ts index 7e916e1..e5ba90c 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -62,6 +62,26 @@ function removeSubstMapping(mapping: SubstMapping): void { logger.info(`subst ${mapping.drive}: entfernt`); } +export function cleanupStaleSubstDrives(): void { + if (process.platform !== "win32") return; + try { + const result = spawnSync("subst", [], { stdio: "pipe", timeout: 5000 }); + const output = String(result.stdout || ""); + for (const line of output.split("\n")) { + const match = line.match(/^([A-Z]):\\: => (.+)/i); + if (!match) continue; + const drive = match[1].toUpperCase(); + const target = match[2].trim(); + if (/\\rd-extract-|\\Real-Debrid-Downloader/i.test(target)) { + spawnSync("subst", [`${drive}:`, "/d"], { stdio: "pipe", timeout: 5000 }); + logger.info(`Stale subst ${drive}: entfernt (${target})`); + } + } + } catch { + // ignore — subst cleanup is best-effort + } +} + let resolvedExtractorCommand: string | null = null; let resolveFailureReason = ""; let resolveFailureAt = 0; diff --git a/src/main/main.ts b/src/main/main.ts index b1b1a87..053f413 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -7,6 +7,7 @@ import { IPC_CHANNELS } from "../shared/ipc"; import { getLogFilePath, logger } from "./logger"; import { APP_NAME } from "./constants"; import { extractHttpLinksFromText } from "./utils"; +import { cleanupStaleSubstDrives } from "./extractor"; /* ── IPC validation helpers ────────────────────────────────────── */ function validateString(value: unknown, name: string): string { @@ -81,7 +82,7 @@ function createWindow(): BrowserWindow { responseHeaders: { ...details.responseHeaders, "Content-Security-Policy": [ - "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.real-debrid.com https://codeberg.org https://bestdebrid.com https://api.alldebrid.com https://www.mega-debrid.eu" + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.real-debrid.com https://codeberg.org https://bestdebrid.com https://api.alldebrid.com https://www.mega-debrid.eu https://git.24-music.de https://ddownload.com https://ddl.to" ] } }); @@ -188,7 +189,12 @@ function startClipboardWatcher(): void { } lastClipboardText = normalizeClipboardText(clipboard.readText()); clipboardTimer = setInterval(() => { - const text = normalizeClipboardText(clipboard.readText()); + let text: string; + try { + text = normalizeClipboardText(clipboard.readText()); + } catch { + return; + } if (text === lastClipboardText || !text.trim()) { return; } @@ -481,6 +487,7 @@ app.on("second-instance", () => { }); app.whenReady().then(() => { + cleanupStaleSubstDrives(); registerIpcHandlers(); mainWindow = createWindow(); bindMainWindowLifecycle(mainWindow); @@ -493,6 +500,9 @@ app.whenReady().then(() => { bindMainWindowLifecycle(mainWindow); } }); +}).catch((error) => { + console.error("App startup failed:", error); + app.quit(); }); app.on("window-all-closed", () => { diff --git a/src/main/mega-web-fallback.ts b/src/main/mega-web-fallback.ts index f9c1b04..faa38ac 100644 --- a/src/main/mega-web-fallback.ts +++ b/src/main/mega-web-fallback.ts @@ -228,22 +228,23 @@ export class MegaWebFallback { } public async unrestrict(link: string, signal?: AbortSignal): Promise { + const overallSignal = withTimeoutSignal(signal, 180000); return this.runExclusive(async () => { - throwIfAborted(signal); + throwIfAborted(overallSignal); const creds = this.getCredentials(); if (!creds.login.trim() || !creds.password.trim()) { return null; } if (!this.cookie || Date.now() - this.cookieSetAt > 20 * 60 * 1000) { - await this.login(creds.login, creds.password, signal); + await this.login(creds.login, creds.password, overallSignal); } - const generated = await this.generate(link, signal); + const generated = await this.generate(link, overallSignal); if (!generated) { this.cookie = ""; - await this.login(creds.login, creds.password, signal); - const retry = await this.generate(link, signal); + await this.login(creds.login, creds.password, overallSignal); + const retry = await this.generate(link, overallSignal); if (!retry) { return null; } @@ -261,7 +262,7 @@ export class MegaWebFallback { fileSize: null, retriesUsed: 0 }; - }, signal); + }, overallSignal); } public invalidateSession(): void { diff --git a/src/main/session-log.ts b/src/main/session-log.ts index f28c47a..d4c4b3b 100644 --- a/src/main/session-log.ts +++ b/src/main/session-log.ts @@ -76,7 +76,12 @@ async function cleanupOldSessionLogs(dir: string, maxAgeDays: number): Promise((resolve) => writeStream.once("drain", resolve)); + } downloadedBytes += buf.byteLength; resetIdleTimer(); emitDownloadProgress(false); } + } catch (error) { + writeStream.destroy(); + await fs.promises.rm(tempPath, { force: true }).catch(() => {}); + throw error; } finally { clearIdleTimer(); } + await new Promise((resolve, reject) => { + writeStream.end(() => resolve()); + writeStream.on("error", reject); + }); + if (idleTimedOut) { + await fs.promises.rm(tempPath, { force: true }).catch(() => {}); throw new Error(`Update Download Body Timeout nach ${Math.ceil(idleTimeoutMs / 1000)}s`); } - const fileBuffer = Buffer.concat(chunks); - if (totalBytes && fileBuffer.byteLength !== totalBytes) { - throw new Error(`Update Download unvollständig (${fileBuffer.byteLength} / ${totalBytes} Bytes)`); + if (totalBytes && downloadedBytes !== totalBytes) { + await fs.promises.rm(tempPath, { force: true }).catch(() => {}); + throw new Error(`Update Download unvollständig (${downloadedBytes} / ${totalBytes} Bytes)`); } - await fs.promises.writeFile(targetPath, fileBuffer); + await fs.promises.rename(tempPath, targetPath); emitDownloadProgress(true); - logger.info(`Update-Download abgeschlossen: ${targetPath} (${fileBuffer.byteLength} Bytes)`); + logger.info(`Update-Download abgeschlossen: ${targetPath} (${downloadedBytes} Bytes)`); return { expectedBytes: totalBytes }; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 52c18c4..2d832a4 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -61,7 +61,7 @@ const emptyStats = (): DownloadStats => ({ const emptySnapshot = (): UiSnapshot => ({ settings: { - token: "", megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "", + token: "", megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "", ddownloadLogin: "", ddownloadPassword: "", archivePasswordList: "", rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid", providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "", @@ -115,15 +115,6 @@ function extractHoster(url: string): string { } catch { return ""; } } -function formatHoster(item: DownloadItem): string { - const hoster = extractHoster(item.url); - const label = hoster || "-"; - if (item.provider) { - return `${label} via ${providerLabels[item.provider]}`; - } - return label; -} - const settingsSubTabs: { key: SettingsSubTab; label: string }[] = [ { key: "allgemein", label: "Allgemein" }, { key: "accounts", label: "Accounts" }, @@ -1878,10 +1869,12 @@ export function App(): ReactElement { const executeDeleteSelection = useCallback((ids: Set): void => { const current = snapshotRef.current; + const promises: Promise[] = []; for (const id of ids) { - if (current.session.items[id]) void window.rd.removeItem(id); - else if (current.session.packages[id]) void window.rd.cancelPackage(id); + if (current.session.items[id]) promises.push(window.rd.removeItem(id)); + else if (current.session.packages[id]) promises.push(window.rd.cancelPackage(id)); } + void Promise.all(promises).catch(() => {}); setSelectedIds(new Set()); }, []); @@ -1924,28 +1917,28 @@ export function App(): ReactElement { const onExportBackup = async (): Promise => { closeMenus(); - try { + await performQuickAction(async () => { const result = await window.rd.exportBackup(); if (result.saved) { showToast("Sicherung exportiert"); } - } catch (error) { + }, (error) => { showToast(`Sicherung fehlgeschlagen: ${String(error)}`, 2600); - } + }); }; const onImportBackup = async (): Promise => { closeMenus(); - try { + await performQuickAction(async () => { const result = await window.rd.importBackup(); if (result.restored) { showToast(result.message, 4000); } else if (result.message !== "Abgebrochen") { showToast(`Sicherung laden fehlgeschlagen: ${result.message}`, 3000); } - } catch (error) { + }, (error) => { showToast(`Sicherung laden fehlgeschlagen: ${String(error)}`, 2600); - } + }); }; const onMenuRestart = (): void => { @@ -2279,7 +2272,7 @@ export function App(): ReactElement { onClick={() => { if (snapshot.session.paused) { setSnapshot((prev) => ({ ...prev, session: { ...prev.session, paused: false } })); - void window.rd.togglePause(); + void window.rd.togglePause().catch(() => {}); } else { void onStartDownloads(); } @@ -2293,7 +2286,7 @@ export function App(): ReactElement { disabled={!snapshot.canPause || snapshot.session.paused} onClick={() => { setSnapshot((prev) => ({ ...prev, session: { ...prev.session, paused: true } })); - void window.rd.togglePause(); + void window.rd.togglePause().catch(() => {}); }} > @@ -2520,7 +2513,7 @@ export function App(): ReactElement { }}>Ausgewählte entfernen ({selectedHistoryIds.size}) )} {historyEntries.length > 0 && ( - + )} {historyEntries.length === 0 &&
Noch keine abgeschlossenen Pakete im Verlauf.
} @@ -2607,7 +2600,7 @@ export function App(): ReactElement { {entry.status === "completed" ? "Abgeschlossen" : "Gelöscht"}
- +
)} @@ -3052,8 +3045,8 @@ export function App(): ReactElement { )} @@ -3063,7 +3056,7 @@ export function App(): ReactElement {
{hasPackages && !contextMenu.itemId && ( )} @@ -3097,7 +3090,7 @@ export function App(): ReactElement { const itemIds = multi ? [...selectedIds].filter((id) => snapshot.session.items[id]) : [contextMenu.itemId!]; - void window.rd.resetItems(itemIds); + void window.rd.resetItems(itemIds).catch(() => {}); setContextMenu(null); }}>Zurücksetzen{multi ? ` (${[...selectedIds].filter((id) => snapshot.session.items[id]).length})` : ""} )} @@ -3129,7 +3122,7 @@ export function App(): ReactElement { const itemIds = [...selectedIds].filter((id) => snapshot.session.items[id]); const skippable = itemIds.filter((id) => { const it = snapshot.session.items[id]; return it && (it.status === "queued" || it.status === "reconnect_wait"); }); if (skippable.length === 0) return null; - return ; + return ; })()} {hasPackages && (
@@ -3228,8 +3221,8 @@ export function App(): ReactElement {
{linkPopup.links.map((link, i) => (
- { void navigator.clipboard.writeText(link.name); showToast("Name kopiert"); }}>{link.name} - { void navigator.clipboard.writeText(link.url); showToast("Link kopiert"); }}>{link.url} + { void navigator.clipboard.writeText(link.name).then(() => showToast("Name kopiert")).catch(() => showToast("Kopieren fehlgeschlagen")); }}>{link.name} + { void navigator.clipboard.writeText(link.url).then(() => showToast("Link kopiert")).catch(() => showToast("Kopieren fehlgeschlagen")); }}>{link.url}
))}
@@ -3237,15 +3230,13 @@ export function App(): ReactElement { {linkPopup.isPackage && ( )} {linkPopup.isPackage && ( )} diff --git a/src/renderer/styles.css b/src/renderer/styles.css index c56f9a3..5c18553 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -1639,6 +1639,7 @@ td { border-radius: 12px; padding: 10px 14px; box-shadow: 0 16px 30px rgba(0, 0, 0, 0.35); + z-index: 50; } .ctx-menu { diff --git a/tests/auto-rename.test.ts b/tests/auto-rename.test.ts index eeb7104..a67c793 100644 --- a/tests/auto-rename.test.ts +++ b/tests/auto-rename.test.ts @@ -269,6 +269,7 @@ describe("buildAutoRenameBaseName", () => { const result = buildAutoRenameBaseName("Show.S99.720p-4sf", "show.s99e999.720p.mkv"); // SCENE_EPISODE_RE allows up to 3-digit episodes and 2-digit seasons expect(result).not.toBeNull(); + expect(result!).toContain("S99E999"); }); // Real-world scene release patterns @@ -343,6 +344,7 @@ describe("buildAutoRenameBaseName", () => { const result = buildAutoRenameBaseName("Show.S01-4sf", "show.s01e01.mkv"); // "mkv" should not be treated as part of the filename match expect(result).not.toBeNull(); + expect(result!).toContain("S01E01"); }); it("does not match episode-like patterns in codec strings", () => { @@ -373,6 +375,7 @@ describe("buildAutoRenameBaseName", () => { // Extreme edge case - sanitizeFilename trims leading dots expect(result).not.toBeNull(); expect(result!).toContain("S01E01"); + expect(result!).toContain("-4sf"); expect(result!).not.toContain(".S01E01.S01E01"); // no duplication }); diff --git a/tests/debrid.test.ts b/tests/debrid.test.ts index 486ab79..7647be4 100644 --- a/tests/debrid.test.ts +++ b/tests/debrid.test.ts @@ -317,7 +317,7 @@ describe("debrid service", () => { const controller = new AbortController(); const abortTimer = setTimeout(() => { controller.abort("test"); - }, 25); + }, 200); try { await expect(service.unrestrictLink("https://rapidgator.net/file/abort-mega-web", controller.signal)).rejects.toThrow(/aborted/i); diff --git a/tests/extractor-jvm.test.ts b/tests/extractor-jvm.test.ts index fd93e80..b62ef79 100644 --- a/tests/extractor-jvm.test.ts +++ b/tests/extractor-jvm.test.ts @@ -36,12 +36,8 @@ afterEach(() => { } }); -describe("extractor jvm backend", () => { +describe.skipIf(!hasJavaRuntime() || !hasJvmExtractorRuntime())("extractor jvm backend", () => { it("extracts zip archives through SevenZipJBinding backend", async () => { - if (!hasJavaRuntime() || !hasJvmExtractorRuntime()) { - return; - } - process.env.RD_EXTRACT_BACKEND = "jvm"; const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-jvm-extract-")); @@ -70,10 +66,6 @@ describe("extractor jvm backend", () => { }); it("respects ask/skip conflict mode in jvm backend", async () => { - if (!hasJavaRuntime() || !hasJvmExtractorRuntime()) { - return; - } - process.env.RD_EXTRACT_BACKEND = "jvm"; const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-jvm-extract-")); diff --git a/tests/extractor.test.ts b/tests/extractor.test.ts index d61fadf..c72451f 100644 --- a/tests/extractor.test.ts +++ b/tests/extractor.test.ts @@ -15,6 +15,8 @@ import { const tempDirs: string[] = []; const originalExtractBackend = process.env.RD_EXTRACT_BACKEND; +const originalStatfs = fs.promises.statfs; +const originalZipEntryMemoryLimit = process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB; beforeEach(() => { process.env.RD_EXTRACT_BACKEND = "legacy"; @@ -29,6 +31,12 @@ afterEach(() => { } else { process.env.RD_EXTRACT_BACKEND = originalExtractBackend; } + (fs.promises as any).statfs = originalStatfs; + if (originalZipEntryMemoryLimit === undefined) { + delete process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB; + } else { + process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB = originalZipEntryMemoryLimit; + } }); describe("extractor", () => { @@ -574,7 +582,6 @@ describe("extractor", () => { }); it("keeps original ZIP size guard error when external fallback is unavailable", async () => { - const previousLimit = process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB; process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB = "8"; const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-")); @@ -588,32 +595,20 @@ describe("extractor", () => { zip.addFile("large.bin", Buffer.alloc(9 * 1024 * 1024, 7)); zip.writeZip(zipPath); - try { - const result = await extractPackageArchives({ - packageDir, - targetDir, - cleanupMode: "none", - conflictMode: "overwrite", - removeLinks: false, - removeSamples: false - }); - expect(result.extracted).toBe(0); - expect(result.failed).toBe(1); - expect(String(result.lastError)).toMatch(/ZIP-Eintrag.*groß/i); - } finally { - if (previousLimit === undefined) { - delete process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB; - } else { - process.env.RD_ZIP_ENTRY_MEMORY_LIMIT_MB = previousLimit; - } - } + const result = await extractPackageArchives({ + packageDir, + targetDir, + cleanupMode: "none", + conflictMode: "overwrite", + removeLinks: false, + removeSamples: false + }); + expect(result.extracted).toBe(0); + expect(result.failed).toBe(1); + expect(String(result.lastError)).toMatch(/ZIP-Eintrag.*groß/i); }); - it("matches resume-state archive names case-insensitively on Windows", async () => { - if (process.platform !== "win32") { - return; - } - + it.skipIf(process.platform !== "win32")("matches resume-state archive names case-insensitively on Windows", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-")); tempDirs.push(root); const packageDir = path.join(root, "pkg"); @@ -650,23 +645,18 @@ describe("extractor", () => { zip.addFile("test.txt", Buffer.alloc(1024, 0x41)); zip.writeZip(path.join(packageDir, "test.zip")); - const originalStatfs = fs.promises.statfs; (fs.promises as any).statfs = async () => ({ bfree: 1, bsize: 1 }); - try { - await expect( - extractPackageArchives({ - packageDir, - targetDir, - cleanupMode: "none" as any, - conflictMode: "overwrite" as any, - removeLinks: false, - removeSamples: false, - }) - ).rejects.toThrow(/Nicht genug Speicherplatz/); - } finally { - (fs.promises as any).statfs = originalStatfs; - } + await expect( + extractPackageArchives({ + packageDir, + targetDir, + cleanupMode: "none" as any, + conflictMode: "overwrite" as any, + removeLinks: false, + removeSamples: false, + }) + ).rejects.toThrow(/Nicht genug Speicherplatz/); }); it("proceeds when disk space is sufficient", async () => { diff --git a/tests/mega-web-fallback.test.ts b/tests/mega-web-fallback.test.ts index ce0c766..53df5e2 100644 --- a/tests/mega-web-fallback.test.ts +++ b/tests/mega-web-fallback.test.ts @@ -166,7 +166,7 @@ describe("mega-web-fallback", () => { const controller = new AbortController(); const timer = setTimeout(() => { controller.abort("test"); - }, 30); + }, 200); try { await expect(fallback.unrestrict("https://mega.debrid/link2", controller.signal)).rejects.toThrow(/aborted/i); diff --git a/tests/self-check.ts b/tests/self-check.ts index 61b584c..18f916a 100644 --- a/tests/self-check.ts +++ b/tests/self-check.ts @@ -153,7 +153,7 @@ async function main(): Promise { createStoragePaths(path.join(tempRoot, "state-pause")) ); manager2.addPackages([{ name: "pause", links: ["https://dummy/slow"] }]); - manager2.start(); + await manager2.start(); await new Promise((resolve) => setTimeout(resolve, 120)); const paused = manager2.togglePause(); assert(paused, "Pause konnte nicht aktiviert werden"); diff --git a/tests/session-log.test.ts b/tests/session-log.test.ts index 55175de..ea872af 100644 --- a/tests/session-log.test.ts +++ b/tests/session-log.test.ts @@ -8,6 +8,8 @@ import { setLogListener } from "../src/main/logger"; const tempDirs: string[] = []; afterEach(() => { + // Ensure session log is shut down between tests + shutdownSessionLog(); // Ensure listener is cleared between tests setLogListener(null); for (const dir of tempDirs.splice(0)) { @@ -45,7 +47,7 @@ describe("session-log", () => { logger.info("Test-Nachricht für Session-Log"); // Wait for flush (200ms interval + margin) - await new Promise((resolve) => setTimeout(resolve, 350)); + await new Promise((resolve) => setTimeout(resolve, 500)); const content = fs.readFileSync(logPath, "utf8"); expect(content).toContain("Test-Nachricht für Session-Log"); @@ -79,7 +81,7 @@ describe("session-log", () => { const { logger } = await import("../src/main/logger"); logger.info("Nach-Shutdown-Nachricht"); - await new Promise((resolve) => setTimeout(resolve, 350)); + await new Promise((resolve) => setTimeout(resolve, 500)); const content = fs.readFileSync(logPath, "utf8"); expect(content).not.toContain("Nach-Shutdown-Nachricht"); @@ -137,7 +139,7 @@ describe("session-log", () => { shutdownSessionLog(); }); - it("multiple sessions create different files", () => { + it("multiple sessions create different files", async () => { const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-slog-")); tempDirs.push(baseDir); @@ -146,10 +148,7 @@ describe("session-log", () => { shutdownSessionLog(); // Small delay to ensure different timestamp - const start = Date.now(); - while (Date.now() - start < 1100) { - // busy-wait for 1.1 seconds to get different second in filename - } + await new Promise((resolve) => setTimeout(resolve, 1100)); initSessionLog(baseDir); const path2 = getSessionLogPath(); diff --git a/tsconfig.json b/tsconfig.json index a1e369d..c663273 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "isolatedModules": true, "types": ["node", "vite/client"] }, - "include": ["src", "tests", "vite.config.ts"] + "include": ["src", "tests", "vite.config.mts"] }