Compare commits

...

2 Commits

Author SHA1 Message Date
Sucukdeluxe
fef9ff6318 Release v1.7.19 2026-03-07 19:27:04 +01:00
Sucukdeluxe
0e0c211d35 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 <noreply@anthropic.com>
2026-03-07 19:26:29 +01:00
2 changed files with 24 additions and 5 deletions

View File

@ -1,6 +1,6 @@
{
"name": "real-debrid-downloader",
"version": "1.7.18",
"version": "1.7.19",
"description": "Desktop downloader",
"main": "build/main/main/main.js",
"author": "Sucukdeluxe",

View File

@ -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) {