Fix: Stille Datei-Korruption auf Windows bei vorab-allozierten Downloads (Null-Bytes am Ende)
Auf Windows wird die Zieldatei vor dem Download auf die erwartete Groesse vorab-alloziert (zero-extend via fd.truncate(item.totalBytes)) und dann ueber einen r+/start:0-Stream beschrieben — dieser ueberschreibt ab Offset 0, kuerzt die Datei aber nie. item.totalBytes ist dabei die Provider-Metadaten-Groesse (knownTotal), nicht zwingend die echte content-length. Lieferte der Server real weniger Bytes als vorab-alloziert (content-length kleiner als die Provider-Angabe, oder Verbindungsabbruch mit sauberem EOF), blieb der Schwanz der Datei mit Null-Bytes gefuellt. Die anschliessende stat-Korrektur hat `written` dann FAELSCHLICH auf die On-Disk-Groesse (= Alloc-Groesse inkl. Null-Padding) HOCHgesetzt. Dadurch wurde sowohl die Underflow-Pruefung als auch das nachgelagerte Pre-alloc-Truncate ausgehebelt: Das Item galt als zu 100% fertig, obwohl ein grosser Teil der Datei Nullen waren — echte Datentraeger- Korruption, exakt auf der Plattform des Nutzers (win32). Fix: Neue reine Funktion reconcileFinalizedSize() entscheidet die Korrektur. Fuer vorab-allozierte Dateien wird `written` nur noch nach UNTEN korrigiert (echter Kurz-Write), niemals nach oben auf das Null-Padding. Damit greifen die bestehende Underflow-Validierung und das Pre-alloc-Truncate wieder wie vorgesehen: - Voll heruntergeladen (content-length erreicht): Null-Padding wird abgeschnitten, Datei ist korrekt. - Echt zu kurz: wird korrekt als Underflow erkannt und neu versucht. Verhaltensaenderung (bewusst): Wenn die Provider-Metadaten-Groesse die echte Dateigroesse UEBERSCHAETZT (Provider sagt 1 GB, echte Datei ist 300 MB und vollstaendig), galt der Download bisher still als fertig (mit Null-Padding- Korruption). Jetzt wird er als Underflow gewertet und neu versucht. Sichtbarer Fehlschlag ist einem still korrupten "fertigen" File vorzuziehen. Die ALLOCATION_UNIT_SIZE-Toleranz deckt kleine Abweichungen weiterhin ab. (Falls je ein "wiederholt denselben Link" gemeldet wird, fuehrt die Spur hierher.) Test: reconcileFinalizedSize-Matrix (Pre-alloc+Padding bleibt echte Byte-Zahl, Pre-alloc+Kurz-Write schrumpft, Nicht-Pre-alloc beidseitig, Invalid-stat, No-op-Gleichstand) — der Padding-Fall ist rot ohne den Fix.
This commit is contained in:
parent
21fb09b208
commit
2646cba1c7
@ -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;
|
||||
|
||||
@ -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 {
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user