{"record":{"id":"386868ebe7024af1","repo":"Eugeny/tabby","slug":"download-cancelled","errorCode":null,"errorMessage":"Download cancelled","messagePattern":"Download cancelled","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"info","filePath":"tabby-ssh/src/components/sftpPanel.component.ts","lineNumber":301,"sourceCode":"        let totalSize = 0\n        const items = await this.sftp.readdir(folder.fullPath)\n        for (const item of items) {\n            if (item.isDirectory) {\n                totalSize += await this.calculateFolderSizeAndUpdate(item, transfer)\n            } else {\n                totalSize += item.size\n            }\n            transfer.setTotalSize(totalSize)\n        }\n        return totalSize\n    }\n\n    private async downloadFolderRecursive (folder: SFTPFile, transfer: DirectoryDownload, relativePath: string): Promise<void> {\n        const items = await this.sftp.readdir(folder.fullPath)\n\n        for (const item of items) {\n            if (transfer.isCancelled()) {\n                throw new Error('Download cancelled')\n            }\n\n            const itemRelativePath = relativePath ? `${relativePath}/${item.name}` : item.name\n\n            transfer.setStatus(itemRelativePath)\n            if (item.isDirectory) {\n                await transfer.createDirectory(itemRelativePath)\n                await this.downloadFolderRecursive(item, transfer, itemRelativePath)\n            } else {\n                const fileDownload = await transfer.createFile(itemRelativePath, item.mode, item.size)\n                await this.sftp.download(item.fullPath, fileDownload)\n            }\n        }\n    }\n\n    getModeString (item: SFTPFile): string {\n        const s = 'SGdrwxrwxrwx'\n        const e = '   ---------'","sourceCodeStart":283,"sourceCodeEnd":319,"githubUrl":"https://github.com/Eugeny/tabby/blob/14e2d60b9b6dee84a53c37f05eefeb803787de04/tabby-ssh/src/components/sftpPanel.component.ts#L283-L319","documentation":"Thrown inside `downloadFolderRecursive` when the in-progress `DirectoryDownload` transfer reports it has been cancelled (`transfer.isCancelled()`) partway through iterating remote directory entries. It aborts the recursive walk immediately rather than continuing to download files into a transfer the user has stopped.","triggerScenarios":"User clicks Cancel on the folder-download progress UI during an SFTP recursive directory download; `isCancelled()` flips true and the next loop iteration in `downloadFolderRecursive` throws before reading/copying the next item.","commonSituations":"Large directory download the user aborted; user changed their mind after a few files; network hiccup made the user cancel and retry with a smaller selection.","solutions":["Catch 'Download cancelled' in the caller of `downloadFolderRecursive` and treat it as a clean abort (close partial files, stop the progress UI, do not show an error toast).","Ensure `transfer.cancel()` is the only path that sets the cancelled flag, so the error reliably maps to a user action.","Clean up any partial files/directories created before cancellation if your UX requires it.","Avoid auto-retrying a cancelled download; the user explicitly stopped it."],"exampleFix":"// before\nfor (const item of items) {\n    if (transfer.isCancelled()) throw new Error('Download cancelled')\n    ...\n}\n\n// caller pattern\ntry { await this.downloadFolderRecursive(folder, transfer, '') }\ncatch (e) {\n    if (e instanceof Error && e.message === 'Download cancelled') {\n        await transfer.cleanup(); return  // user-initiated abort\n    }\n    throw e\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isCancelledTransfer (t: DirectoryDownload): boolean { return t.isCancelled() }","tryCatchPattern":"try {\n    await this.downloadFolderRecursive(folder, transfer, '')\n} catch (e) {\n    if (e instanceof Error && e.message === 'Download cancelled') {\n        await transfer.cleanup()  // user-initiated; clean up partial files\n        return\n    }\n    throw e\n}","preventionTips":["Treat 'Download cancelled' as a clean abort; never auto-retry.","Ensure only transfer.cancel() flips the cancelled flag so the error reliably maps to a user action.","Clean up partial files/dirs created before cancellation when your UX requires it.","Check isCancelled() at the top of each loop iteration (as the code does) to stop promptly."],"tags":["sftp","ssh","download","user-cancel","transfer"],"backgroundTag":null,"analyzedSha":"14e2d60b9b6dee84a53c37f05eefeb803787de04","analyzedAt":"2026-08-12T11:46:48.773Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}