From 0e0c211d353464de9ff0a7e95697f16e1b4fe49f Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 7 Mar 2026 19:26:29 +0100 Subject: [PATCH] Restore target path reservations on startup to prevent (1) duplicates After restart, reservedTargetPaths (in-memory) was empty so claimTargetPath could not distinguish between "file belongs to this item" and "file belongs to another item". The naive fix (allow overwrite if unclaimed) would have risked overwriting completed files from other items. Proper fix: restore reservedTargetPaths from persisted session data on init. This way each item's targetPath is correctly claimed, and claimTargetPath can safely reuse the item's own file while protecting other items' files. Co-Authored-By: Claude Opus 4.6 --- src/main/download-manager.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index cd7f149..5bc22a6 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -1073,6 +1073,7 @@ export class DownloadManager extends EventEmitter { void this.recoverRetryableItems("startup").catch((err) => logger.warn(`recoverRetryableItems Fehler (startup): ${compactErrorText(err)}`)); this.recoverPostProcessingOnStartup(); this.resolveExistingQueuedOpaqueFilenames(); + this.restoreTargetPathReservations(); this.checkExistingRapidgatorLinks(); void this.cleanupExistingExtractedArchives().catch((err) => logger.warn(`cleanupExistingExtractedArchives Fehler (constructor): ${compactErrorText(err)}`)); } @@ -3942,10 +3943,7 @@ export class DownloadManager extends EventEmitter { const owner = this.reservedTargetPaths.get(key); const existsOnDisk = fs.existsSync(candidate); const allowExistingCandidate = allowExistingFile && index === 0; - // If file exists on disk but no other item has reserved this path, allow overwrite. - // This prevents "(1)" suffixes after app restart when partial downloads remain on disk. - const unclaimedOnDisk = existsOnDisk && !owner && index === 0; - if ((!owner || owner === itemId) && (owner === itemId || !existsOnDisk || allowExistingCandidate || unclaimedOnDisk)) { + if ((!owner || owner === itemId) && (owner === itemId || !existsOnDisk || allowExistingCandidate)) { this.reservedTargetPaths.set(key, itemId); this.claimedTargetPathByItem.set(itemId, candidate); return candidate; @@ -3971,6 +3969,27 @@ export class DownloadManager extends EventEmitter { this.claimedTargetPathByItem.delete(itemId); } + /** Restore reservedTargetPaths from persisted session on startup so claimTargetPath + * knows which files belong to which items. Without this, after restart all paths are + * unclaimed and a new download with the same filename would create a "(1)" copy + * instead of reusing its own partial file — or worse, overwrite another item's file. */ + private restoreTargetPathReservations(): void { + let restored = 0; + for (const item of Object.values(this.session.items)) { + const tp = String(item.targetPath || "").trim(); + if (!tp) continue; + const key = pathKey(tp); + if (!this.reservedTargetPaths.has(key)) { + this.reservedTargetPaths.set(key, item.id); + this.claimedTargetPathByItem.set(item.id, tp); + restored += 1; + } + } + if (restored > 0) { + logger.info(`restoreTargetPathReservations: ${restored} Pfade aus Session wiederhergestellt`); + } + } + private assignItemTargetPath(item: DownloadItem, targetPath: string): string { const rawTargetPath = String(targetPath || "").trim(); if (!rawTargetPath) {