Fix: Startup-Dedup ersetzt keine gute kanonische Datei mehr durch ein kleineres Duplikat

Beim Start gleicht das Tool Duplikat-benannte Dateien ("Name (1).ext") gegen die
kanonische Datei ("Name.ext") ab. Im Zweig "Duplikat rangiert hoeher und das
kanonische Item ist nicht 'completed'" wurde bisher bedingungslos
fs.rmSync(kanonisch) + fs.renameSync(Duplikat → kanonisch) ausgefuehrt — ohne die
echten Dateigroessen zu vergleichen. Der Rang mischt den persistierten STATUS mit
der Disk-Existenz; nach einem Absturz kann der Status von der Realitaet abweichen.
War die kanonische Datei auf der Platte tatsaechlich vollstaendig/gut, ihr Item
aber z.B. "failed"/"queued", waehrend das Duplikat-Item ein veraltetes
"completed" ueber einer kleineren/partiellen Datei trug, wurde die gute
kanonische Datei geloescht und durch das schlechtere Duplikat ersetzt —
unwiederbringlicher Verlust der besseren Datei. Zusatzrisiko: rmSync erfolgreich,
renameSync wirft EXDEV → kanonisch weg, nichts installiert.

Fix:
- Size-Guard: Ist die kanonische Datei >= dem Duplikat, wird sie behalten und das
  (kleinere/gleich grosse) Duplikat verworfen. Eine kleinere Datei kann eine
  groessere nie ueberschreiben.
- Sichere Reihenfolge fuer den echten Austausch (kanonisch kleiner): kanonisch →
  ".dedupbak" umbenennen, dann Duplikat → kanonisch; bei Fehler (EXDEV o.ae.)
  wird die kanonische Datei aus dem Backup wiederhergestellt. Kein Loss-Fenster
  mehr. Erst nach Erfolg wird ".dedupbak" entfernt.

Test: kanonisch 1024 B (gut) + Duplikat 256 B (partiell), primaeres Item "failed"
(Duplikat rangiert hoeher) → die kanonische Datei bleibt mit 1024 B erhalten.
Ohne den Size-Guard wird sie durch die 256-B-Datei ersetzt (rot bewiesen). Volle
download-manager-Suite (168) gruen, tsc unveraendert 6.
This commit is contained in:
Sucukdeluxe
2026-06-17 05:13:40 +02:00
parent 45789918b0
commit c8ea2f6765
3 changed files with 168 additions and 18 deletions
+32 -13
View File
@@ -6392,19 +6392,38 @@ export class DownloadManager extends EventEmitter {
} catch {
}
} else if (duplicateExists && canonicalExists && !primaryWins && primaryItem.status !== "completed") {
try {
fs.rmSync(canonicalPath, { force: true });
fs.renameSync(duplicateTargetPath, canonicalPath);
canonicalExists = true;
this.logVerifiedRenameSync("startup-dedup (Austausch)", duplicateTargetPath, canonicalPath);
logger.info(`startupDuplicateMerge: ersetze verwaisten Originalpfad ${canonicalBaseName} durch ${path.basename(duplicateTargetPath)}`);
} catch (err) {
logDesktopRename("ERROR", "startup-dedup (Austausch): Rename fehlgeschlagen", {
source: path.basename(duplicateTargetPath),
target: canonicalBaseName,
error: compactErrorText(err)
});
logger.warn(`startupDuplicateMerge: Austausch fehlgeschlagen ${canonicalPath}: ${compactErrorText(err)}`);
let canonicalSize = -1;
let duplicateSize = -1;
try { canonicalSize = fs.statSync(canonicalPath).size; } catch { }
try { duplicateSize = fs.statSync(duplicateTargetPath).size; } catch { }
if (canonicalSize >= 0 && duplicateSize >= 0 && canonicalSize >= duplicateSize) {
try {
fs.rmSync(duplicateTargetPath, { force: true });
} catch {
}
logger.info(`startupDuplicateMerge: kanonische Datei behalten (${canonicalSize}B >= Duplikat ${duplicateSize}B), Duplikat verworfen: ${canonicalBaseName}`);
} else {
const dedupBackupPath = `${canonicalPath}.dedupbak`;
try {
fs.renameSync(canonicalPath, dedupBackupPath);
try {
fs.renameSync(duplicateTargetPath, canonicalPath);
try { fs.rmSync(dedupBackupPath, { force: true }); } catch { }
canonicalExists = true;
this.logVerifiedRenameSync("startup-dedup (Austausch)", duplicateTargetPath, canonicalPath);
logger.info(`startupDuplicateMerge: ersetze verwaisten Originalpfad ${canonicalBaseName} durch ${path.basename(duplicateTargetPath)}`);
} catch (swapErr) {
try { fs.renameSync(dedupBackupPath, canonicalPath); } catch { }
throw swapErr;
}
} catch (err) {
logDesktopRename("ERROR", "startup-dedup (Austausch): Rename fehlgeschlagen", {
source: path.basename(duplicateTargetPath),
target: canonicalBaseName,
error: compactErrorText(err)
});
logger.warn(`startupDuplicateMerge: Austausch fehlgeschlagen ${canonicalPath}: ${compactErrorText(err)}`);
}
}
}