fix: use upload and download abort signals (#212)
* fix: use upload and download abort signals * fix: progress trackers chore: update fm-lib and bee-js * chore: bump-up fm-lib version
This commit is contained in:
@@ -62,6 +62,8 @@ export function CreateDriveModal({
|
||||
const validationError = duplicate && nameExists ? 'Drive already exists. Please choose another name.' : ''
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
}
|
||||
|
||||
@@ -210,6 +210,7 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
const [confirmBulkRestore, setConfirmBulkRestore] = useState(false)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [pendingCancelUpload, setPendingCancelUpload] = useState<string | null>(null)
|
||||
const [pendingCancelDownload, setPendingCancelDownload] = useState<string | null>(null)
|
||||
|
||||
const q = query.trim().toLowerCase()
|
||||
const isSearchMode = q.length > 0
|
||||
@@ -371,6 +372,16 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
setShowError,
|
||||
])
|
||||
|
||||
const handleDownloadClose = (uuid: string) => {
|
||||
const row = downloadItems.find(i => i.uuid === uuid)
|
||||
|
||||
if (row?.status === TransferStatus.Downloading) {
|
||||
setPendingCancelDownload(uuid)
|
||||
} else {
|
||||
cancelOrDismissDownload(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadClose = (uuid: string) => {
|
||||
const row = uploadItems.find(i => i.uuid === uuid)
|
||||
|
||||
@@ -425,6 +436,18 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showContext, pos, contextRef])
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
|
||||
if (rafIdRef.current) {
|
||||
cancelAnimationFrame(rafIdRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let title = currentDrive?.name || ''
|
||||
|
||||
@@ -446,16 +469,6 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSearchMode])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
|
||||
if (rafIdRef.current) {
|
||||
cancelAnimationFrame(rafIdRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const doRefresh = async () => {
|
||||
handleCloseContext()
|
||||
|
||||
@@ -610,6 +623,14 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
}
|
||||
}}
|
||||
onCancelUploadCancel={() => setPendingCancelUpload(null)}
|
||||
pendingCancelDownload={pendingCancelDownload}
|
||||
onCancelDownloadConfirm={() => {
|
||||
if (pendingCancelDownload) {
|
||||
cancelOrDismissDownload(pendingCancelDownload)
|
||||
setPendingCancelDownload(null)
|
||||
}
|
||||
}}
|
||||
onCancelDownloadCancel={() => setPendingCancelDownload(null)}
|
||||
/>
|
||||
|
||||
{isRefreshing && (
|
||||
@@ -644,7 +665,7 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
type={FileTransferType.Download}
|
||||
open={isDownloading}
|
||||
items={downloadItems}
|
||||
onRowClose={(name: string) => cancelOrDismissDownload(name)}
|
||||
onRowClose={handleDownloadClose}
|
||||
onCloseAll={() => dismissAllDownloads()}
|
||||
/>
|
||||
<NotificationBar setErrorMessage={setErrorMessage} />
|
||||
|
||||
@@ -17,6 +17,7 @@ interface FileBrowserModalsProps {
|
||||
confirmBulkRestore: boolean
|
||||
showDestroyDriveModal: boolean
|
||||
pendingCancelUpload: string | null
|
||||
pendingCancelDownload: string | null
|
||||
onDeleteCancel: () => void
|
||||
onDeleteProceed: (action: FileAction) => void
|
||||
onForgetConfirm: () => Promise<void>
|
||||
@@ -27,6 +28,8 @@ interface FileBrowserModalsProps {
|
||||
onDestroyConfirm: () => Promise<void>
|
||||
onCancelUploadConfirm: () => void
|
||||
onCancelUploadCancel: () => void
|
||||
onCancelDownloadConfirm: () => void
|
||||
onCancelDownloadCancel: () => void
|
||||
}
|
||||
|
||||
export function FileBrowserModals({
|
||||
@@ -38,6 +41,7 @@ export function FileBrowserModals({
|
||||
confirmBulkRestore,
|
||||
showDestroyDriveModal,
|
||||
pendingCancelUpload,
|
||||
pendingCancelDownload,
|
||||
onDeleteCancel,
|
||||
onDeleteProceed,
|
||||
onForgetConfirm,
|
||||
@@ -48,6 +52,8 @@ export function FileBrowserModals({
|
||||
onDestroyConfirm,
|
||||
onCancelUploadConfirm,
|
||||
onCancelUploadCancel,
|
||||
onCancelDownloadCancel,
|
||||
onCancelDownloadConfirm,
|
||||
}: FileBrowserModalsProps): ReactElement {
|
||||
return (
|
||||
<>
|
||||
@@ -124,6 +130,17 @@ export function FileBrowserModals({
|
||||
onCancel={onCancelUploadCancel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{pendingCancelDownload && (
|
||||
<ConfirmModal
|
||||
title="Cancel download?"
|
||||
message="Are you sure you want to cancel this download?"
|
||||
confirmLabel="Cancel download"
|
||||
cancelLabel="Keep downloading"
|
||||
onConfirm={onCancelDownloadConfirm}
|
||||
onCancel={onCancelDownloadCancel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { DownloadProgress, FileAction, TrackDownloadProps, ViewType } from '../.
|
||||
import { useContextMenu } from '../../../hooks/useContextMenu'
|
||||
import { getUsableStamps, handleDestroyAndForgetDrive, verifyDriveSpace } from '../../../utils/bee'
|
||||
import { Dir, formatBytes, isTrashed, safeSetState, truncateNameMiddle } from '../../../utils/common'
|
||||
import { createDownloadAbort, startDownloadingQueue } from '../../../utils/download'
|
||||
import { startDownloadingQueue } from '../../../utils/download'
|
||||
import { FileOperation, performFileOperation } from '../../../utils/fileOperations'
|
||||
import { GetIconElement } from '../../../utils/GetIconElement'
|
||||
import type { FilePropertyGroup } from '../../../utils/infoGroups'
|
||||
@@ -168,15 +168,14 @@ export function FileItem({
|
||||
|
||||
const rawSize = latestFileInfo.customMetadata?.size
|
||||
const expectedSize = rawSize ? Number(rawSize) : undefined
|
||||
|
||||
createDownloadAbort(latestFileInfo.name)
|
||||
const uuid = uuidV4()
|
||||
|
||||
await startDownloadingQueue(
|
||||
fm,
|
||||
[latestFileInfo],
|
||||
[{ uuid, info: latestFileInfo }],
|
||||
[
|
||||
onDownload({
|
||||
uuid: uuidV4(),
|
||||
uuid,
|
||||
name: latestFileInfo.name,
|
||||
size: formatBytes(rawSize),
|
||||
expectedSize,
|
||||
|
||||
@@ -89,6 +89,8 @@ export function InitialModal({
|
||||
const isMountedRef = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
}
|
||||
|
||||
@@ -159,6 +159,14 @@ export function UpgradeDriveModal({
|
||||
[beeApi, walletBalance, isMountedRef, setErrorMessage, setShowError],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSizes = () => {
|
||||
const sizes = Array.from(Utils.getStampEffectiveBytesBreakpoints(false, defaultErasureCodeLevel).values())
|
||||
@@ -207,12 +215,6 @@ export function UpgradeDriveModal({
|
||||
setValidityEndDate(getExpiryDateByLifetime(lifetimeIndex, stamp.duration.toEndDate()))
|
||||
}, [lifetimeIndex, stamp.duration])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const batchIdStr = stamp.batchID.toString()
|
||||
const shortBatchId = batchIdStr.length > 12 ? `${batchIdStr.slice(0, 4)}...${batchIdStr.slice(-4)}` : batchIdStr
|
||||
|
||||
|
||||
@@ -426,13 +426,16 @@ export function VersionsList({ versions, headFi, restoreVersion, onDownload }: V
|
||||
handleCloseContext()
|
||||
|
||||
if (!fm || !beeApi) return
|
||||
|
||||
const rawSize = fileInfo.customMetadata?.size
|
||||
const expectedSize = rawSize ? Number(rawSize) : undefined
|
||||
const driveName = drives.find(d => d.id.toString() === fileInfo.driveId.toString())?.name ?? currentDrive?.name
|
||||
const uuid = uuidV4()
|
||||
|
||||
await startDownloadingQueue(
|
||||
fm,
|
||||
[fileInfo],
|
||||
[onDownload({ uuid: uuidV4(), name: fileInfo.name, size: formatBytes(rawSize), expectedSize, driveName })],
|
||||
[{ uuid, info: fileInfo }],
|
||||
[onDownload({ uuid, name: fileInfo.name, size: formatBytes(rawSize), expectedSize, driveName })],
|
||||
)
|
||||
},
|
||||
[handleCloseContext, fm, beeApi, onDownload, drives, currentDrive],
|
||||
|
||||
@@ -8,7 +8,7 @@ import { uuidV4 } from '../../../utils'
|
||||
import { DownloadProgress, TrackDownloadProps } from '../constants/transfers'
|
||||
import { getUsableStamps } from '../utils/bee'
|
||||
import { formatBytes, getFileId, safeSetState } from '../utils/common'
|
||||
import { startDownloadingQueue } from '../utils/download'
|
||||
import { FileInfoWithUUID, startDownloadingQueue } from '../utils/download'
|
||||
import { FileOperation, performBulkFileOperation } from '../utils/fileOperations'
|
||||
|
||||
interface BulkOptions {
|
||||
@@ -77,23 +77,28 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
|
||||
async (list: FileInfo[]) => {
|
||||
if (!fm || !list?.length) return
|
||||
|
||||
const trackers: Array<(progress: DownloadProgress) => void> = []
|
||||
for (const fi of list) {
|
||||
const trackers = new Array<(progress: DownloadProgress) => void>(list.length)
|
||||
const infoListWitIDs: FileInfoWithUUID[] = new Array<FileInfoWithUUID>(list.length)
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const fi = list[i]
|
||||
const rawSize = fi.customMetadata?.size as string | number | undefined
|
||||
const prettySize = formatBytes(rawSize)
|
||||
const expected = rawSize ? Number(rawSize) : undefined
|
||||
const driveName = drives.find(d => d.id.toString() === fi.driveId.toString())?.name
|
||||
const tracker = trackDownload({
|
||||
uuid: uuidV4(),
|
||||
const uuid = uuidV4()
|
||||
|
||||
infoListWitIDs[i] = { uuid, info: fi }
|
||||
trackers[i] = trackDownload({
|
||||
uuid,
|
||||
name: fi.name,
|
||||
size: prettySize,
|
||||
expectedSize: expected,
|
||||
driveName,
|
||||
})
|
||||
trackers.push(tracker)
|
||||
}
|
||||
|
||||
await startDownloadingQueue(fm, list, trackers)
|
||||
await startDownloadingQueue(fm, infoListWitIDs, trackers)
|
||||
},
|
||||
[fm, trackDownload, drives],
|
||||
)
|
||||
|
||||
@@ -172,6 +172,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
||||
const { fm, adminDrive, currentDrive, currentStamp, files, setShowError, refreshStamp } = useContext(FMContext)
|
||||
const { beeApi } = useContext(SettingsContext)
|
||||
const [openConflict, conflictPortal] = useUploadConflictDialog()
|
||||
|
||||
const isMountedRef = useRef(true)
|
||||
const uploadAbortsRef = useRef<AbortManager>(new AbortManager())
|
||||
const queueRef = useRef<UploadTask[]>([])
|
||||
@@ -406,6 +407,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
||||
taskDrive,
|
||||
{ ...info, onUploadProgress: progressCb },
|
||||
{ actHistoryAddress: task.isReplace ? task.replaceHistory : undefined },
|
||||
{ signal },
|
||||
)
|
||||
|
||||
await Promise.race([uploadPromise, checkCancellation])
|
||||
@@ -827,6 +829,14 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
||||
cancelledDownloadingRef.current.clear()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleFileUploaded = (e: Event) => {
|
||||
const { fileInfo } = (e as CustomEvent).detail || {}
|
||||
@@ -853,7 +863,6 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(FILE_MANAGER_EVENTS.FILE_UPLOADED, handleFileUploaded as EventListener)
|
||||
isMountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@ enum Errors {
|
||||
SecurityError = 'SecurityError',
|
||||
}
|
||||
|
||||
export function createDownloadAbort(name: string): void {
|
||||
downloadAborts.create(name)
|
||||
export function createDownloadAbort(id: string): void {
|
||||
downloadAborts.create(id)
|
||||
}
|
||||
|
||||
export function abortDownload(name: string): void {
|
||||
downloadAborts.abort(name)
|
||||
export function abortDownload(id: string): void {
|
||||
downloadAborts.abort(id)
|
||||
}
|
||||
|
||||
const processStream = async (
|
||||
@@ -130,8 +130,13 @@ const streamToBlob = async (
|
||||
return new Blob([combined], { type: mimeType })
|
||||
}
|
||||
|
||||
interface FileInfoWithHandle {
|
||||
export interface FileInfoWithUUID {
|
||||
uuid: string
|
||||
info: FileInfo
|
||||
}
|
||||
|
||||
interface FileInfoWithHandle {
|
||||
infoWithId: FileInfoWithUUID
|
||||
handle?: FileSystemFileHandle
|
||||
cancelled?: boolean
|
||||
}
|
||||
@@ -149,17 +154,17 @@ const isUserCancellation = (error: unknown): boolean => {
|
||||
}
|
||||
|
||||
const getSingleFileHandle = async (
|
||||
info: FileInfo,
|
||||
infoWithId: FileInfoWithUUID,
|
||||
defaultDownloadFolder: string,
|
||||
): Promise<FileInfoWithHandle[] | undefined> => {
|
||||
const { mime, ext } = guessMime(info.name, info.customMetadata)
|
||||
const { mime, ext } = guessMime(infoWithId.info.name, infoWithId.info.customMetadata)
|
||||
|
||||
const pickerOptions: {
|
||||
suggestedName: string
|
||||
startIn: string
|
||||
types?: Array<{ accept: Record<string, string[]> }>
|
||||
} = {
|
||||
suggestedName: info.name,
|
||||
suggestedName: infoWithId.info.name,
|
||||
startIn: defaultDownloadFolder,
|
||||
}
|
||||
|
||||
@@ -171,20 +176,20 @@ const getSingleFileHandle = async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handle = (await (window as any).showSaveFilePicker(pickerOptions)) as FileSystemFileHandle
|
||||
|
||||
return [{ info, handle }]
|
||||
return [{ infoWithId, handle }]
|
||||
} catch (error: unknown) {
|
||||
return isUserCancellation(error) ? [{ info, cancelled: true }] : undefined
|
||||
return isUserCancellation(error) ? [{ infoWithId, cancelled: true }] : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const getMultipleFileHandles = async (
|
||||
infoList: FileInfo[],
|
||||
infoWithIdList: FileInfoWithUUID[],
|
||||
defaultDownloadFolder: string,
|
||||
): Promise<FileInfoWithHandle[] | undefined> => {
|
||||
if (!isDirectoryPickerSupported()) {
|
||||
const handles: FileInfoWithHandle[] = []
|
||||
|
||||
for (const info of infoList) {
|
||||
for (const info of infoWithIdList) {
|
||||
const result = await getSingleFileHandle(info, defaultDownloadFolder)
|
||||
|
||||
if (!result) return undefined
|
||||
@@ -203,37 +208,37 @@ const getMultipleFileHandles = async (
|
||||
|
||||
const handles: FileInfoWithHandle[] = []
|
||||
|
||||
for (const info of infoList) {
|
||||
for (const infoWithId of infoWithIdList) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const fileHandle = (await (dirHandle as any).getFileHandle(info.name, {
|
||||
const fileHandle = (await (dirHandle as any).getFileHandle(infoWithId.info.name, {
|
||||
create: true,
|
||||
})) as FileSystemFileHandle
|
||||
|
||||
handles.push({ info, handle: fileHandle })
|
||||
handles.push({ infoWithId, handle: fileHandle })
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Failed to create file handle for ${info.name}:`, error)
|
||||
handles.push({ info, cancelled: true })
|
||||
console.error(`Failed to create file handle for ${infoWithId.info.name}:`, error)
|
||||
handles.push({ infoWithId, cancelled: true })
|
||||
}
|
||||
}
|
||||
|
||||
return handles
|
||||
} catch (error: unknown) {
|
||||
return isUserCancellation(error) ? infoList.map(info => ({ info, cancelled: true })) : undefined
|
||||
return isUserCancellation(error) ? infoWithIdList.map(infoWithId => ({ infoWithId, cancelled: true })) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const getFileHandles = (infoList: FileInfo[]): Promise<FileInfoWithHandle[] | undefined> => {
|
||||
const getFileHandles = (infoWithIdList: FileInfoWithUUID[]): Promise<FileInfoWithHandle[] | undefined> => {
|
||||
const defaultDownloadFolder = 'downloads'
|
||||
|
||||
if (!isPickerSupported()) return Promise.resolve(infoList.map(info => ({ info })))
|
||||
if (!isPickerSupported()) return Promise.resolve(infoWithIdList.map(infoWithId => ({ infoWithId })))
|
||||
|
||||
if (infoList.length === 1) {
|
||||
return getSingleFileHandle(infoList[0], defaultDownloadFolder)
|
||||
if (infoWithIdList.length === 1) {
|
||||
return getSingleFileHandle(infoWithIdList[0], defaultDownloadFolder)
|
||||
}
|
||||
|
||||
return getMultipleFileHandles(infoList, defaultDownloadFolder)
|
||||
return getMultipleFileHandles(infoWithIdList, defaultDownloadFolder)
|
||||
}
|
||||
|
||||
const downloadToDisk = async (
|
||||
@@ -324,26 +329,26 @@ const downloadFromUrl = (url: string, fileName: string): void => {
|
||||
|
||||
export const startDownloadingQueue = async (
|
||||
fm: FileManager,
|
||||
infoList: FileInfo[],
|
||||
infoListWithIds: FileInfoWithUUID[],
|
||||
trackers?: Array<(progress: DownloadProgress) => void>,
|
||||
isOpenWindow?: boolean,
|
||||
): Promise<void> => {
|
||||
if (!infoList.length || (trackers && trackers.length !== infoList.length)) return
|
||||
if (!infoListWithIds.length || (trackers && trackers.length !== infoListWithIds.length)) return
|
||||
|
||||
try {
|
||||
const fileHandles: FileInfoWithHandle[] | undefined = isOpenWindow
|
||||
? infoList.map(info => ({ info }))
|
||||
: await getFileHandles(infoList)
|
||||
? infoListWithIds.map(infoWithId => ({ infoWithId }))
|
||||
: await getFileHandles(infoListWithIds)
|
||||
|
||||
if (!fileHandles) return
|
||||
|
||||
await Promise.all(
|
||||
fileHandles.map(async (fh, i) => {
|
||||
const name = fh.info.name
|
||||
const tracker = trackers ? trackers[i] : undefined
|
||||
|
||||
createDownloadAbort(name)
|
||||
const signal = downloadAborts.getSignal(name)
|
||||
const uuid = fh.infoWithId.uuid
|
||||
createDownloadAbort(uuid)
|
||||
const signal = downloadAborts.getSignal(uuid)
|
||||
|
||||
try {
|
||||
if (fh.cancelled) {
|
||||
@@ -352,11 +357,13 @@ export const startDownloadingQueue = async (
|
||||
return
|
||||
}
|
||||
|
||||
const dataStreams = (await fm.download(fh.info)) as ReadableStream<Uint8Array>[]
|
||||
const dataStreams = (await fm.download(fh.infoWithId.info, undefined, undefined, {
|
||||
signal,
|
||||
})) as ReadableStream<Uint8Array>[]
|
||||
|
||||
if (!dataStreams || dataStreams.length === 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`No data streams returned for ${name}`)
|
||||
console.error(`No data streams returned for ${fh.infoWithId.info.name}`)
|
||||
tracker?.({ progress: 0, isDownloading: false, state: DownloadState.Error })
|
||||
|
||||
return
|
||||
@@ -365,7 +372,7 @@ export const startDownloadingQueue = async (
|
||||
let success = false
|
||||
|
||||
if (isOpenWindow || !fh.handle) {
|
||||
success = await downloadToBlob(dataStreams, fh.info, tracker, isOpenWindow, signal)
|
||||
success = await downloadToBlob(dataStreams, fh.infoWithId.info, tracker, isOpenWindow, signal)
|
||||
} else {
|
||||
success = await downloadToDisk(dataStreams, fh.handle, tracker, signal)
|
||||
}
|
||||
@@ -375,7 +382,7 @@ export const startDownloadingQueue = async (
|
||||
}
|
||||
|
||||
if (success) {
|
||||
const size = fh.info.customMetadata?.size
|
||||
const size = fh.infoWithId.info.customMetadata?.size
|
||||
const finalProgress = size ? Number(size) : 0
|
||||
tracker({ progress: finalProgress, isDownloading: false })
|
||||
|
||||
@@ -397,7 +404,7 @@ export const startDownloadingQueue = async (
|
||||
tracker?.({ progress: 0, isDownloading: false, state: DownloadState.Cancelled })
|
||||
}
|
||||
} finally {
|
||||
downloadAborts.abort(name)
|
||||
downloadAborts.abort(uuid)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user