From e80948df54c9d4e16ae7a94ccfc424243fa680eb Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 7 Mar 2026 20:10:04 +0100 Subject: [PATCH] Fix disk-fallback in hybrid extract allowing partial files Two bugs in findReadyArchiveSets disk-fallback: 1. Failed items were explicitly excluded from the blocking check (status !== "failed"), so partial downloads with "failed" status would not block extraction. Now ANY non-completed item blocks. 2. The disk size check was only > 10 KB, allowing 627 MB partial files of 1001 MB archives to pass. Now requires the file to be within one allocation unit of the item's expected totalBytes. Added findItemByDiskPath helper to look up the owning item for a file on disk and get its expected size. Co-Authored-By: Claude Opus 4.6 --- src/main/download-manager.ts | 40 +++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 8489a61..e56e565 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -6926,33 +6926,38 @@ export class DownloadManager extends EventEmitter { continue; } - // Disk-fallback: if all parts exist on disk but some items lack "completed" status, - // allow extraction if none of those parts are actively downloading/validating. - // This handles items that finished downloading but whose status was not updated. + // Disk-fallback: if all parts exist on disk at their full expected size but some + // items lack "completed" status, allow extraction. This handles items that finished + // downloading but whose status was not updated (crash between write and persist). const missingParts = partsOnDisk.filter((part) => !completedPaths.has(pathKey(part))); - let allMissingExistOnDisk = true; + let allMissingFullOnDisk = true; for (const part of missingParts) { try { const stat = await fs.promises.stat(part); - if (stat.size < 10240) { - allMissingExistOnDisk = false; + // Find the item that owns this file to get its expected totalBytes + const ownerItem = this.findItemByDiskPath(pkg, part); + const minBytes = ownerItem?.totalBytes && ownerItem.totalBytes > 0 + ? ownerItem.totalBytes - ALLOCATION_UNIT_SIZE + : 10240; + if (stat.size < minBytes) { + allMissingFullOnDisk = false; break; } } catch { - allMissingExistOnDisk = false; + allMissingFullOnDisk = false; break; } } - if (!allMissingExistOnDisk) { + if (!allMissingFullOnDisk) { continue; } - // Any non-completed item blocks extraction — cancelled/stopped items may + // Any non-completed item blocks extraction — failed/cancelled/stopped items may // have partial files on disk that would corrupt the extraction. - const anyActivelyProcessing = missingParts.some((part) => { + const anyNonCompletedItem = missingParts.some((part) => { const status = pendingItemStatus.get(pathKey(part)); - return status !== undefined && status !== "failed"; + return status !== undefined; }); - if (anyActivelyProcessing) { + if (anyNonCompletedItem) { continue; } // Safety: if any pending item in the package has neither targetPath nor fileName, @@ -6975,6 +6980,17 @@ export class DownloadManager extends EventEmitter { return ready; } + private findItemByDiskPath(pkg: PackageEntry, diskPath: string): DownloadItem | undefined { + const key = pathKey(diskPath); + for (const itemId of pkg.itemIds) { + const item = this.session.items[itemId]; + if (!item) continue; + if (item.targetPath && pathKey(item.targetPath) === key) return item; + if (item.fileName && pkg.outputDir && pathKey(path.join(pkg.outputDir, item.fileName)) === key) return item; + } + return undefined; + } + private looksLikeArchivePart(fileName: string, entryPointName: string): boolean { const multipartMatch = entryPointName.match(/^(.*)\.part0*1\.rar$/i); if (multipartMatch) {