From 8f186ad894880f562ae3310fb028c6e87a1c4533 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sun, 1 Mar 2026 17:04:12 +0100 Subject: [PATCH] Fix updater SHA512 mismatch by patching latest.yml filenames and adding integrity retry - Patch latest.yml during release to use actual filenames (spaces) instead of electron-builder's dashed names - Add download size validation before SHA512 check to catch incomplete downloads - Retry download on integrity mismatch (up to 3 passes) with API refresh - Re-resolve digest from latest.yml on each retry pass Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- scripts/release_codeberg.mjs | 13 +++++++++++ src/main/update.ts | 44 ++++++++++++++++++++++++------------ 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index e0ef155..d86a539 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.4.60", + "version": "1.4.61", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/scripts/release_codeberg.mjs b/scripts/release_codeberg.mjs index 7d556f2..986a6ea 100644 --- a/scripts/release_codeberg.mjs +++ b/scripts/release_codeberg.mjs @@ -148,6 +148,18 @@ function updatePackageVersion(rootDir, version) { fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8"); } +function patchLatestYml(releaseDir, version) { + const ymlPath = path.join(releaseDir, "latest.yml"); + let content = fs.readFileSync(ymlPath, "utf8"); + const setupName = `Real-Debrid-Downloader Setup ${version}.exe`; + const dashedName = `Real-Debrid-Downloader-Setup-${version}.exe`; + if (content.includes(dashedName)) { + content = content.split(dashedName).join(setupName); + fs.writeFileSync(ymlPath, content, "utf8"); + process.stdout.write(`Patched latest.yml: replaced "${dashedName}" with "${setupName}"\n`); + } +} + function ensureAssetsExist(rootDir, version) { const releaseDir = path.join(rootDir, "release"); const files = [ @@ -162,6 +174,7 @@ function ensureAssetsExist(rootDir, version) { throw new Error(`Missing release artifact: ${fullPath}`); } } + patchLatestYml(releaseDir, version); return { releaseDir, files }; } diff --git a/src/main/update.ts b/src/main/update.ts index 95ec28b..260e169 100644 --- a/src/main/update.ts +++ b/src/main/update.ts @@ -689,7 +689,7 @@ export async function checkGitHubUpdate(repo: string): Promise { +async function downloadFile(url: string, targetPath: string, onProgress?: UpdateProgressCallback): Promise<{ expectedBytes: number | null }> { const shutdownSignal = activeUpdateAbortController?.signal; if (shutdownSignal?.aborted) { throw new Error("aborted:update_shutdown"); @@ -808,6 +808,15 @@ async function downloadFile(url: string, targetPath: string, onProgress?: Update } emitDownloadProgress(true); logger.info(`Update-Download abgeschlossen: ${targetPath}`); + + if (totalBytes) { + const actualSize = (await fs.promises.stat(targetPath)).size; + if (actualSize !== totalBytes) { + throw new Error(`Update Download unvollständig (${actualSize} / ${totalBytes} Bytes)`); + } + } + + return { expectedBytes: totalBytes }; } async function sleep(ms: number, signal?: AbortSignal): Promise { @@ -992,8 +1001,8 @@ export async function installLatestUpdate( let verified = false; let lastVerifyError: unknown = null; let integrityError: unknown = null; - for (let pass = 0; pass < 2 && !verified; pass += 1) { - logger.info(`Update-Download Kandidaten (${pass + 1}/2): ${candidates.join(" | ")}`); + for (let pass = 0; pass < 3 && !verified; pass += 1) { + logger.info(`Update-Download Kandidaten (${pass + 1}/3): ${candidates.join(" | ")}`); lastVerifyError = null; for (let index = 0; index < candidates.length; index += 1) { const candidate = candidates[index]; @@ -1034,8 +1043,10 @@ export async function installLatestUpdate( } const status = readHttpStatusFromError(lastVerifyError); + const wasIntegrityError = integrityError !== null; let shouldRetryAfterRefresh = false; - if (pass === 0 && status === 404) { + + if (pass < 2 && (status === 404 || wasIntegrityError)) { const refreshed = await resolveSetupAssetFromApi(safeRepo, effectiveCheck.latestTag); if (refreshed) { effectiveCheck = { @@ -1045,15 +1056,14 @@ export async function installLatestUpdate( setupAssetDigest: refreshed.setupAssetDigest || effectiveCheck.setupAssetDigest }; } - if (!effectiveCheck.setupAssetDigest && effectiveCheck.setupAssetUrl) { - const digestFromYml = await resolveSetupDigestFromLatestYml(safeRepo, effectiveCheck.latestTag, effectiveCheck.setupAssetName || ""); - if (digestFromYml) { - effectiveCheck = { - ...effectiveCheck, - setupAssetDigest: digestFromYml - }; - logger.info("Update-Integritätsdigest aus latest.yml übernommen"); - } + + const digestFromYml = await resolveSetupDigestFromLatestYml(safeRepo, effectiveCheck.latestTag, effectiveCheck.setupAssetName || ""); + if (digestFromYml) { + effectiveCheck = { + ...effectiveCheck, + setupAssetDigest: digestFromYml + }; + logger.info("Update-Integritätsdigest aus latest.yml übernommen"); } const refreshedCandidates = buildDownloadCandidates(safeRepo, effectiveCheck); @@ -1061,9 +1071,13 @@ export async function installLatestUpdate( && (refreshedCandidates.length !== candidates.length || refreshedCandidates.some((value, idx) => value !== candidates[idx])); if (changed) { - logger.warn("Update-404 erkannt, Kandidatenliste aus API neu geladen"); + logger.warn(`Update-Fehler erkannt (${wasIntegrityError ? "integrity" : "404"}), Kandidatenliste aus API neu geladen`); candidates = refreshedCandidates; - shouldRetryAfterRefresh = true; + } + shouldRetryAfterRefresh = true; + if (wasIntegrityError) { + integrityError = null; + logger.warn("SHA512-Mismatch erkannt, erneuter Download-Versuch"); } } if (!shouldRetryAfterRefresh) {