diff --git a/src/main/download-completion.ts b/src/main/download-completion.ts index 13fdc30..d0a52f8 100644 --- a/src/main/download-completion.ts +++ b/src/main/download-completion.ts @@ -72,6 +72,22 @@ export function planDownloadCompletion(args: { }; } +export function reconcileFinalizedSize( + streamedBytes: number, + statSize: number, + preAllocated: boolean +): number { + const streamed = Math.max(0, Math.floor(Number(streamedBytes) || 0)); + if (!Number.isFinite(statSize) || statSize < 0) { + return streamed; + } + const onDisk = Math.floor(statSize); + if (preAllocated && onDisk > streamed) { + return streamed; + } + return onDisk; +} + export function validateDownloadedFileCompletion(args: { actualBytes: number; plan: DownloadCompletionPlan; diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 01dcee7..7f0f249 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -51,7 +51,7 @@ function releaseTlsSkip(): void { } } import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup"; -import { planDownloadCompletion, validateDownloadedFileCompletion } from "./download-completion"; +import { planDownloadCompletion, reconcileFinalizedSize, validateDownloadedFileCompletion } from "./download-completion"; import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getMegaDebridAccountCooldownState, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid"; import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo } from "./extractor"; import { validateFileAgainstManifest } from "./integrity"; @@ -10357,13 +10357,15 @@ export class DownloadManager extends EventEmitter { try { const finalizedStat = await fs.promises.stat(effectiveTargetPath); - if (Number.isFinite(finalizedStat.size) && finalizedStat.size >= 0 && finalizedStat.size !== written) { + const reconciledSize = reconcileFinalizedSize(written, finalizedStat.size, preAllocated); + if (reconciledSize !== written) { logAttemptEvent("WARN", "Dateigroesse nach Stream-Abschluss korrigiert", { attempt, previousWritten: written, - statSize: finalizedStat.size + statSize: finalizedStat.size, + reconciledSize }); - written = finalizedStat.size; + written = reconciledSize; } } catch { } diff --git a/tests/download-completion.test.ts b/tests/download-completion.test.ts index b5b7f6a..72b9ccf 100644 --- a/tests/download-completion.test.ts +++ b/tests/download-completion.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { planDownloadCompletion, validateDownloadedFileCompletion } from "../src/main/download-completion"; +import { planDownloadCompletion, reconcileFinalizedSize, validateDownloadedFileCompletion } from "../src/main/download-completion"; describe("download-completion", () => { describe("planDownloadCompletion", () => { @@ -58,4 +58,33 @@ describe("download-completion", () => { expect(result.ok).toBe(false); }); }); + + describe("reconcileFinalizedSize", () => { + it("keeps the streamed count for a pre-allocated file whose on-disk size is the zero-padding (corruption guard)", () => { + expect(reconcileFinalizedSize(300_000_000, 1_000_000_000, true)).toBe(300_000_000); + }); + + it("shrinks to the on-disk size when a pre-allocated file is genuinely short (real partial write)", () => { + expect(reconcileFinalizedSize(500, 300, true)).toBe(300); + }); + + it("reconciles in both directions for a non-pre-allocated file (stat is authoritative)", () => { + expect(reconcileFinalizedSize(300, 1000, false)).toBe(1000); + expect(reconcileFinalizedSize(1000, 300, false)).toBe(300); + }); + + it("returns the streamed count unchanged when the stat is invalid", () => { + expect(reconcileFinalizedSize(1234, Number.NaN, true)).toBe(1234); + expect(reconcileFinalizedSize(1234, -1, false)).toBe(1234); + }); + + it("is a no-op when on-disk size already equals the streamed count", () => { + expect(reconcileFinalizedSize(777, 777, true)).toBe(777); + expect(reconcileFinalizedSize(777, 777, false)).toBe(777); + }); + + it("does not block legitimate overshoot on a pre-allocated file (server sent more than pre-alloc)", () => { + expect(reconcileFinalizedSize(900, 900, true)).toBe(900); + }); + }); });