diff --git a/core/download/aria2_backend.py b/core/download/aria2_backend.py index d02e000..ad44774 100644 --- a/core/download/aria2_backend.py +++ b/core/download/aria2_backend.py @@ -323,9 +323,31 @@ def delete_badsha_download( target_bad_path = f"{candidate}.badsha" if not target_bad_path and path: - target_bad_path = str(path).strip() + cand = str(path).strip() + if cand.lower().endswith(".badsha"): + target_bad_path = cand + elif cand and facade.os.path.isfile(f"{cand}.badsha"): + target_bad_path = f"{cand}.badsha" + elif cand: + target_bad_path = cand if not target_bad_path: + if download_id: + with facade.download_lock: + facade.download_progress.pop(download_id, None) + facade.cancelled_downloads.discard(download_id) + remove_persisted = getattr(facade, "remove_persisted_download", None) + if callable(remove_persisted): + try: + remove_persisted(download_id) + except Exception: + pass + return { + "success": True, + "already_removed": True, + "message": "Corrupted download already removed", + "removed_files": [], + } return {"success": False, "error": "No .badsha file path found"} normalized = facade.os.path.normpath(target_bad_path) diff --git a/tests/test_downloader_boundaries.py b/tests/test_downloader_boundaries.py index 9fdcdae..bfdacc3 100644 --- a/tests/test_downloader_boundaries.py +++ b/tests/test_downloader_boundaries.py @@ -782,6 +782,22 @@ class DownloaderBoundaryTests(unittest.TestCase): self.assertIn("only files ending with .badsha", result["error"].lower()) self.assertTrue(os.path.exists(safe_model)) + def test_delete_badsha_download_idempotent_when_already_cleared(self): + result = delete_badsha_download(download_id="already-removed-id", dependencies=downloader) + self.assertTrue(result["success"]) + self.assertTrue(result.get("already_removed")) + + def test_delete_badsha_download_finds_badsha_from_base_path(self): + with tempfile.TemporaryDirectory() as temp_dir: + base_model = os.path.join(temp_dir, "base_model.safetensors") + badsha_file = f"{base_model}.badsha" + with open(badsha_file, "wb") as f: + f.write(b"corrupt payload") + + result = delete_badsha_download(path=base_model, dependencies=downloader) + self.assertTrue(result["success"]) + self.assertFalse(os.path.exists(badsha_file)) + def test_parse_retry_after(self): self.assertEqual(parse_retry_after("120"), 120) self.assertEqual(parse_retry_after("0"), 1) diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index 9b0eb05..a8a93af 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -504,6 +504,21 @@ export const resolveDownloadMethods = { return next; }, + clearDownloadSnapshot(downloadId, missing = null) { + const store = this.getDownloadProgressStore?.(); + if (store instanceof Map) { + const missingKey = missing ? this.getDownloadStateKey(missing) : null; + for (const [key, snap] of Array.from(store.entries())) { + if ( + (downloadId && String(snap?.downloadId || '') === String(downloadId)) + || (missingKey && key === missingKey) + ) { + store.delete(key); + } + } + } + }, + cloneLocalMatches(matches = []) { if (!Array.isArray(matches)) return []; try { @@ -1122,12 +1137,32 @@ export const resolveDownloadMethods = { return { progressDiv, downloadBtn }; }, - async deleteBadDownload(downloadId) { + async deleteBadDownload(downloadId) { if (!downloadId) return; - const info = this.activeDownloads?.[downloadId]; - const badPath = info?.lastProgress?.bad_path || info?.statusSnapshot?.progress?.bad_path || ''; + const activeInfo = this.activeDownloads?.[downloadId]; + let snapshotInfo = null; + const store = this.getDownloadProgressStore?.(); + if (store instanceof Map) { + for (const [, snap] of store.entries()) { + if (String(snap?.downloadId || '') === String(downloadId)) { + snapshotInfo = snap; + break; + } + } + } + const info = activeInfo || snapshotInfo || null; + const missing = activeInfo?.missing || snapshotInfo?.missing || null; + const badPath = activeInfo?.lastProgress?.bad_path + || activeInfo?.statusSnapshot?.progress?.bad_path + || snapshotInfo?.progress?.bad_path + || snapshotInfo?.bad_path + || activeInfo?.downloadPath + || snapshotInfo?.downloadPath + || ''; + + let res = null; try { - const res = await this.fetchJson( + res = await this.fetchJson( `/model_resolver/delete_bad_download/${encodeURIComponent(downloadId)}`, { method: 'POST', @@ -1136,30 +1171,55 @@ export const resolveDownloadMethods = { }, 'Delete corrupted download' ); - if (res && res.success) { - this.showNotification('Corrupted (.badsha) file deleted', 'info'); - delete this.activeDownloads?.[downloadId]; - this.removeActiveDownloadRecovery?.(downloadId); - this.updateDownloadAllButtonState?.(); - this.updateQueuePanel?.(); - if (info?.missing) { - const elements = this.resolveDownloadUiElements(info); - if (elements.progressDiv) { - elements.progressDiv.classList.add('mr-is-hidden'); - elements.progressDiv.classList.remove('mr-is-visible'); - elements.progressDiv.innerHTML = ''; - } - if (elements.downloadBtn) { - this.restoreDownloadButtonReadyState(elements.downloadBtn); - } - this.refreshLocalMatchesUiForMissing?.(info.missing); - } - } else { - this.showNotification(res?.error || 'Failed to delete corrupted file', 'error'); - } } catch (err) { - console.error('Model Resolver: Failed to delete corrupted download:', err); - this.showNotification('Failed to delete corrupted file: ' + err.message, 'error'); + console.warn('Model Resolver: Delete bad download request error:', err); + } + + const wasSuccess = Boolean(res && res.success); + delete this.activeDownloads?.[downloadId]; + this.removeActiveDownloadRecovery?.(downloadId); + this.clearDownloadSnapshot(downloadId, missing); + + const targetMissing = missing || info?.missing || null; + if (targetMissing) { + const elements = this.resolveDownloadUiElements(info || { missing: targetMissing }); + if (elements.progressDiv) { + elements.progressDiv.classList.add('mr-is-hidden'); + elements.progressDiv.classList.remove('mr-is-visible'); + elements.progressDiv.innerHTML = ''; + } + if (elements.downloadBtn) { + this.restoreDownloadButtonReadyState(elements.downloadBtn); + } + this.refreshLocalMatchesUiForMissing?.(targetMissing); + } + + const root = this.contentElement || (typeof document !== 'undefined' ? document : null); + if (root?.querySelectorAll) { + const safeId = String(downloadId).replace(/["\\]/g, '\\$&'); + const queueItems = root.querySelectorAll(`.mr-download-queue-item[data-download-id="${safeId}"]`); + for (const item of queueItems) { + const progressSlot = item.closest?.('.mr-download-progress-slot'); + if (progressSlot) { + progressSlot.classList.add('mr-is-hidden'); + progressSlot.classList.remove('mr-is-visible'); + progressSlot.innerHTML = ''; + } else { + item.remove(); + } + } + } + + this.updateDownloadAllButtonState?.(); + this.updateQueuePanel?.(); + + if (wasSuccess) { + this.showNotification( + res?.already_removed ? 'Corrupted download removed' : 'Corrupted (.badsha) file deleted', + 'info' + ); + } else if (res?.error) { + this.showNotification(res.error, 'warning'); } }, @@ -2394,46 +2454,76 @@ export const resolveDownloadMethods = { * Cancel an active download */ cancelDownload(downloadId) { - const info = this.activeDownloads[downloadId]; + const info = this.activeDownloads?.[downloadId]; + let snapshotInfo = null; + if (!info) { + const store = this.getDownloadProgressStore?.(); + if (store instanceof Map) { + for (const [, snap] of store.entries()) { + if (String(snap?.downloadId || '') === String(downloadId)) { + snapshotInfo = snap; + break; + } + } + } + } + const effectiveInfo = info || snapshotInfo || null; const currentStatus = String( - info?.pendingDownloadStatus - || info?.lastStatus - || info?.lastProgress?.status + effectiveInfo?.pendingDownloadStatus + || effectiveInfo?.lastStatus + || effectiveInfo?.lastProgress?.status + || effectiveInfo?.status || '' ).toLowerCase(); if (currentStatus === 'error' || currentStatus === 'refresh_error') { - delete this.activeDownloads[downloadId]; + delete this.activeDownloads?.[downloadId]; this.removeActiveDownloadRecovery?.(downloadId); - if (info) { - const elements = this.resolveDownloadUiElements?.(info) || {}; - const cancelledSnapshot = this.rememberDownloadUiState( - downloadId, - info, - { ...(info.lastProgress || {}), status: 'cancelled' }, - { - status: 'cancelled', - message: 'Download dismissed', - type: 'info', - isActive: false - } - ); - this.renderDownloadSnapshot?.(downloadId, cancelledSnapshot, elements); - this.refreshLocalMatchesUiForMissing?.(info.missing); + this.clearDownloadSnapshot(downloadId, effectiveInfo?.missing); + + const targetMissing = effectiveInfo?.missing || null; + if (targetMissing) { + const elements = this.resolveDownloadUiElements?.(effectiveInfo || { missing: targetMissing }) || {}; + if (elements.progressDiv) { + elements.progressDiv.classList.add('mr-is-hidden'); + elements.progressDiv.classList.remove('mr-is-visible'); + elements.progressDiv.innerHTML = ''; + } + if (elements.downloadBtn) { + this.restoreDownloadButtonReadyState?.(elements.downloadBtn); + } + this.refreshLocalMatchesUiForMissing?.(targetMissing); } + + const root = this.contentElement || (typeof document !== 'undefined' ? document : null); + if (root?.querySelectorAll) { + const safeId = String(downloadId).replace(/["\\]/g, '\\$&'); + const queueItems = root.querySelectorAll(`.mr-download-queue-item[data-download-id="${safeId}"]`); + for (const item of queueItems) { + const progressSlot = item.closest?.('.mr-download-progress-slot'); + if (progressSlot) { + progressSlot.classList.add('mr-is-hidden'); + progressSlot.classList.remove('mr-is-visible'); + progressSlot.innerHTML = ''; + } else { + item.remove(); + } + } + } + this.updateDownloadAllButtonState?.(); this.updateQueuePanel?.(); - this.fetchJson(`/model_resolver/cancel/${downloadId}`, { + this.fetchJson(`/model_resolver/cancel/${encodeURIComponent(downloadId)}`, { method: 'POST' }, 'Cancel download').catch(() => {}); return; } - if (info) { - this.setPendingDownloadStatus(info, 'cancelling', 30000); + if (effectiveInfo) { + this.setPendingDownloadStatus(effectiveInfo, 'cancelling', 30000); const snapshot = this.rememberDownloadUiState( downloadId, - info, + effectiveInfo, { ...(info.lastProgress || {}), status: 'cancelling' }, { status: 'cancelling', @@ -2442,7 +2532,7 @@ export const resolveDownloadMethods = { isActive: true } ); - const elements = this.resolveDownloadUiElements(info); + const elements = this.resolveDownloadUiElements(effectiveInfo); this.renderDownloadSnapshot(downloadId, snapshot, elements); this.updateQueuePanel?.(); } @@ -2453,7 +2543,7 @@ export const resolveDownloadMethods = { // The request only acknowledges cancellation. Progress polling // releases the file and enables retry when the worker has stopped. }).catch((error) => { - this.clearPendingDownloadStatus(info); + if (effectiveInfo) this.clearPendingDownloadStatus(effectiveInfo); console.error('Model Resolver: Cancel error:', error); this.showNotification('Failed to cancel download', 'error'); });