fix: download and upload files (#223)

* fix: download for unknown mime types
* fix: async process two files from the queue
* refactor: upload functions
This commit is contained in:
Bálint Ujvári
2026-03-12 15:19:53 +01:00
parent 76bf2211ca
commit e8e01c054a
5 changed files with 223 additions and 129 deletions
@@ -7,7 +7,6 @@ import HistoryIcon from 'remixicon-react/HistoryLineIcon'
import { Context as FMContext } from '../../../../providers/FileManager' import { Context as FMContext } from '../../../../providers/FileManager'
import { TOOLTIPS } from '../../constants/tooltips' import { TOOLTIPS } from '../../constants/tooltips'
import { ActionTag, DownloadProgress, TrackDownloadProps } from '../../constants/transfers' import { ActionTag, DownloadProgress, TrackDownloadProps } from '../../constants/transfers'
import { useTransfers } from '../../hooks/useTransfers'
import { ConflictAction, useUploadConflictDialog } from '../../hooks/useUploadConflictDialog' import { ConflictAction, useUploadConflictDialog } from '../../hooks/useUploadConflictDialog'
import { verifyDriveSpace } from '../../utils/bee' import { verifyDriveSpace } from '../../utils/bee'
import { indexStrToBigint, truncateNameMiddle } from '../../utils/common' import { indexStrToBigint, truncateNameMiddle } from '../../utils/common'
@@ -31,15 +30,12 @@ type RenameConfirmState = {
interface VersionHistoryModalProps { interface VersionHistoryModalProps {
fileInfo: FileInfo fileInfo: FileInfo
onCancelClick: () => void onCancelClick: () => void
onDownload?: (props: TrackDownloadProps) => (dp: DownloadProgress) => void onDownload: (props: TrackDownloadProps) => (dp: DownloadProgress) => void
} }
export function VersionHistoryModal({ fileInfo, onCancelClick, onDownload }: VersionHistoryModalProps): ReactElement { export function VersionHistoryModal({ fileInfo, onCancelClick, onDownload }: VersionHistoryModalProps): ReactElement {
const { fm, files, currentDrive, currentStamp, refreshStamp } = useContext(FMContext) const { fm, files, currentDrive, currentStamp, refreshStamp } = useContext(FMContext)
const localTransfers = useTransfers({})
const trackDownload = onDownload ?? localTransfers.trackDownload
const [openConflict, conflictPortal] = useUploadConflictDialog() const [openConflict, conflictPortal] = useUploadConflictDialog()
const modalRoot = document.querySelector('.fm-main') || document.body const modalRoot = document.querySelector('.fm-main') || document.body
@@ -345,7 +341,7 @@ export function VersionHistoryModal({ fileInfo, onCancelClick, onDownload }: Ver
versions={!error && !loading ? pageVersions : []} versions={!error && !loading ? pageVersions : []}
headFi={fileInfo} headFi={fileInfo}
restoreVersion={restoreVersion} restoreVersion={restoreVersion}
onDownload={trackDownload} onDownload={onDownload}
/> />
</div> </div>
</div> </div>
@@ -39,7 +39,6 @@ export enum FileAction {
export enum DownloadState { export enum DownloadState {
InProgress = 'in-progress', InProgress = 'in-progress',
Completed = 'completed',
Cancelled = 'cancelled', Cancelled = 'cancelled',
Error = 'error', Error = 'error',
} }
+111 -77
View File
@@ -22,6 +22,7 @@ import { ConflictAction, useUploadConflictDialog } from './useUploadConflictDial
const SAMPLE_WINDOW_MS = 500 const SAMPLE_WINDOW_MS = 500
const ETA_SMOOTHING = 0.3 const ETA_SMOOTHING = 0.3
const MAX_UPLOAD_FILES = 10 const MAX_UPLOAD_FILES = 10
const MAX_PARALLEL_UPLOAD_FILES = 2
const ABORT_EVENT = 'abort' const ABORT_EVENT = 'abort'
type ResolveResult = { type ResolveResult = {
@@ -156,6 +157,19 @@ const updateTransferItems = <T extends TransferItem>(items: T[], uuid: string, u
}) })
} }
const getUploadCancelPromise = (signal: AbortSignal | undefined): { promise: Promise<never>; cleanup: () => void } => {
let reject: (reason?: Error) => void
const abortHandler = () => {
reject(new Error('Upload cancelled'))
}
const promise = new Promise<never>((_, rej) => {
reject = rej
signal?.addEventListener(ABORT_EVENT, abortHandler)
})
return { promise, cleanup: () => signal?.removeEventListener(ABORT_EVENT, abortHandler) }
}
const createTransferItem = ( const createTransferItem = (
uuid: string, uuid: string,
name: string, name: string,
@@ -185,10 +199,10 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
const { beeApi } = useContext(SettingsContext) const { beeApi } = useContext(SettingsContext)
const [openConflict, conflictPortal] = useUploadConflictDialog() const [openConflict, conflictPortal] = useUploadConflictDialog()
const isMountedRef = useRef(true) const isMountedRef = useRef<boolean>(true)
const uploadAbortsRef = useRef<AbortManager>(new AbortManager()) const uploadAbortsRef = useRef<AbortManager>(new AbortManager())
const uploadTaskQueueRef = useRef<UploadTask[]>([]) const uploadTaskQueueRef = useRef<UploadTask[]>([])
const runningRef = useRef(false) const runningRef = useRef<boolean>(false)
const cancelledQueuedRef = useRef<Set<string>>(new Set()) const cancelledQueuedRef = useRef<Set<string>>(new Set())
const cancelledUploadingRef = useRef<Set<string>>(new Set()) const cancelledUploadingRef = useRef<Set<string>>(new Set())
const cancelledDownloadingRef = useRef<Set<string>>(new Set()) const cancelledDownloadingRef = useRef<Set<string>>(new Set())
@@ -221,11 +235,10 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
const idx = prev.findIndex(p => p.uuid === uuid && p.status !== TransferStatus.Done) const idx = prev.findIndex(p => p.uuid === uuid && p.status !== TransferStatus.Done)
const base = createTransferItem(uuid, name, size, kind, driveName, TransferStatus.Queued) const base = createTransferItem(uuid, name, size, kind, driveName, TransferStatus.Queued)
if (idx !== -1) {
clearAllFlagsFor(uuid)
}
if (idx === -1) return [...prev, base] if (idx === -1) return [...prev, base]
clearAllFlagsFor(uuid)
const copy = [...prev] const copy = [...prev]
copy[idx] = base copy[idx] = base
@@ -378,14 +391,6 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
topic: task.isReplace ? task.replaceTopic : undefined, topic: task.isReplace ? task.replaceTopic : undefined,
} }
const progressCb = trackUpload(
task.uuid,
task.finalName,
task.prettySize,
task.isReplace ? FileTransferType.Update : FileTransferType.Upload,
taskDrive.name,
)
safeSetState( safeSetState(
isMountedRef, isMountedRef,
setUploadItems, setUploadItems,
@@ -400,33 +405,29 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
uploadAbortsRef.current.create(task.uuid) uploadAbortsRef.current.create(task.uuid)
const signal = uploadAbortsRef.current.getSignal(task.uuid) const signal = uploadAbortsRef.current.getSignal(task.uuid)
let reject: (reason?: Error) => void const { promise: checkCancellation, cleanup: cleanupCancelPromise } = getUploadCancelPromise(signal)
const abortHandler = () => {
reject(new Error('Upload cancelled'))
}
const checkCancellation = new Promise<never>((_, rej) => {
reject = rej
signal?.addEventListener(ABORT_EVENT, abortHandler)
})
try { try {
if (signal?.aborted) { if (signal?.aborted) {
throw new Error('Upload cancelled') throw new Error('Upload cancelled')
} }
const onUploadProgress = trackUpload(
task.uuid,
task.finalName,
task.prettySize,
task.isReplace ? FileTransferType.Update : FileTransferType.Upload,
taskDrive.name,
)
const uploadPromise = fm.upload( const uploadPromise = fm.upload(
taskDrive, taskDrive,
{ ...info, onUploadProgress: progressCb }, { ...info, onUploadProgress },
{ actHistoryAddress: task.isReplace ? task.replaceHistory : undefined }, { actHistoryAddress: task.isReplace ? task.replaceHistory : undefined },
{ signal }, { signal },
) )
await Promise.race([uploadPromise, checkCancellation]) await Promise.race([uploadPromise, checkCancellation])
if (currentStamp) {
await refreshStamp(currentStamp.batchID.toString())
}
} catch (error) { } catch (error) {
const wasCancelled = cancelledUploadingRef.current.has(task.uuid) || signal?.aborted const wasCancelled = cancelledUploadingRef.current.has(task.uuid) || signal?.aborted
@@ -445,7 +446,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
}), }),
) )
} finally { } finally {
signal?.removeEventListener(ABORT_EVENT, abortHandler) cleanupCancelPromise()
const wasCancelled = cancelledUploadingRef.current.has(task.uuid) || signal?.aborted const wasCancelled = cancelledUploadingRef.current.has(task.uuid) || signal?.aborted
@@ -456,7 +457,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
} }
} }
}, },
[fm, files, currentStamp, trackUpload, refreshStamp, setShowError, setErrorMessage], [fm, files, trackUpload, setShowError, setErrorMessage],
) )
const trackDownload = useCallback((props: TrackDownloadProps) => { const trackDownload = useCallback((props: TrackDownloadProps) => {
@@ -686,37 +687,65 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
if (runningRef.current) return if (runningRef.current) return
runningRef.current = true runningRef.current = true
while (uploadTaskQueueRef.current.length > 0) { const processNextTask = async (): Promise<void> => {
const task = uploadTaskQueueRef.current[0] while (uploadTaskQueueRef.current.length > 0) {
const task = uploadTaskQueueRef.current.shift()
if (!task) break if (!task) break
if (cancelledQueuedRef.current.has(task.uuid)) { if (cancelledQueuedRef.current.has(task.uuid)) {
safeSetState( safeSetState(
isMountedRef, isMountedRef,
setUploadItems, setUploadItems,
)(prev => updateTransferItems(prev, task.uuid, { status: TransferStatus.Cancelled })) )(prev => updateTransferItems(prev, task.uuid, { status: TransferStatus.Cancelled }))
cancelledQueuedRef.current.delete(task.uuid) cancelledQueuedRef.current.delete(task.uuid)
} else { } else {
await executeUploadTask(task) await executeUploadTask(task)
}
} }
uploadTaskQueueRef.current.shift()
} }
const workers: Promise<void>[] = []
for (let i = 0; i < MAX_PARALLEL_UPLOAD_FILES; i++) {
workers.push(processNextTask())
}
await Promise.all(workers)
runningRef.current = false runningRef.current = false
// Race guard: uploadFiles may have appended tasks and called runUploadQueue() again // Race guard: uploadFiles may have appended tasks and called runUploadQueue() again
if (uploadTaskQueueRef.current.length > 0) { if (uploadTaskQueueRef.current.length > 0) {
runUploadQueue() void runUploadQueue()
} }
}, [executeUploadTask])
const uploadFiles = useCallback( if (currentStamp) {
(picked: FileList | File[]): void => { await refreshStamp(currentStamp.batchID.toString())
const filesArr = Array.from(picked) }
}, [currentStamp, executeUploadTask, refreshStamp])
if (filesArr.length === 0 || !fm || !currentDrive || !currentStamp) return const verifyUploadConditions = useCallback(
async (filesArr: File[]): Promise<boolean> => {
if (filesArr.length === 0) {
setErrorMessage?.('Nothing to upload.')
setShowError(true)
return false
}
if (!fm || !currentDrive) {
setErrorMessage?.('File manager is not ready or no drive is selected.')
setShowError(true)
return false
}
if (!currentStamp || !currentStamp.usable) {
setErrorMessage?.('Stamp is not usable.')
setShowError(true)
return false
}
const currentlyQueued = uploadTaskQueueRef.current.length const currentlyQueued = uploadTaskQueueRef.current.length
const newFilesCount = filesArr.length const newFilesCount = filesArr.length
@@ -728,36 +757,41 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
) )
setShowError(true) setShowError(true)
return false
}
if (beeApi) {
const batchID = currentStamp.batchID
const stampValid = await validateStampStillExists(beeApi, batchID)
if (!stampValid) {
setErrorMessage?.(
`The selected stamp ${batchID.toString().slice(0, 4)} is no longer valid or has been deleted. Please select a different stamp.`,
)
setShowError(true)
return false
}
}
return true
},
[fm, currentStamp, currentDrive, beeApi, setShowError, setErrorMessage],
)
const uploadFiles = useCallback(
async (picked: FileList | File[]): Promise<void> => {
const filesArr = Array.from(picked)
if (!(await verifyUploadConditions(filesArr))) {
return return
} }
void (async () => { const tasks = await preflight(filesArr)
if (!currentStamp || !currentStamp.usable) { uploadTaskQueueRef.current = uploadTaskQueueRef.current.concat(tasks)
setErrorMessage?.('Stamp is not usable.') runUploadQueue()
setShowError(true)
return
}
if (beeApi) {
const stampValid = await validateStampStillExists(beeApi, currentStamp.batchID)
if (!stampValid) {
setErrorMessage?.(
'The selected stamp is no longer valid or has been deleted. Please select a different stamp.',
)
setShowError(true)
return
}
}
const tasks = await preflight(filesArr)
uploadTaskQueueRef.current = uploadTaskQueueRef.current.concat(tasks)
runUploadQueue()
})()
}, },
[fm, currentStamp, currentDrive, beeApi, setShowError, setErrorMessage, preflight, runUploadQueue], [verifyUploadConditions, preflight, runUploadQueue],
) )
const cancelOrDismissUpload = useCallback( const cancelOrDismissUpload = useCallback(
@@ -865,13 +899,13 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
}, []) }, [])
return { return {
uploadFiles,
isUploading, isUploading,
uploadItems, uploadItems,
trackDownload,
isDownloading, isDownloading,
downloadItems, downloadItems,
conflictPortal, conflictPortal,
uploadFiles,
trackDownload,
cancelOrDismissUpload, cancelOrDismissUpload,
cancelOrDismissDownload, cancelOrDismissDownload,
dismissAllUploads, dismissAllUploads,
+107 -39
View File
@@ -6,6 +6,8 @@ import { AbortManager } from './abortManager'
import { isDirectoryPickerSupported, isPickerSupported } from './fileOperations' import { isDirectoryPickerSupported, isPickerSupported } from './fileOperations'
import { guessMime, VIEWERS } from './view' import { guessMime, VIEWERS } from './view'
const DefaultDownloadFolder = 'downloads'
const downloadAborts = new AbortManager() const downloadAborts = new AbortManager()
enum Errors { enum Errors {
@@ -148,10 +150,7 @@ const isUserCancellation = (error: unknown): boolean => {
return errName === Errors.AbortError || errName === Errors.NotAllowedError || errName === Errors.SecurityError return errName === Errors.AbortError || errName === Errors.NotAllowedError || errName === Errors.SecurityError
} }
const getSingleFileHandle = async ( const getSingleFileHandle = async (infoWithId: FileInfoWithUUID): Promise<FileInfoWithHandle | undefined> => {
infoWithId: FileInfoWithUUID,
defaultDownloadFolder: string,
): Promise<FileInfoWithHandle[] | undefined> => {
const { mime, ext } = guessMime(infoWithId.info.name, infoWithId.info.customMetadata) const { mime, ext } = guessMime(infoWithId.info.name, infoWithId.info.customMetadata)
const pickerOptions: { const pickerOptions: {
@@ -160,7 +159,7 @@ const getSingleFileHandle = async (
types?: Array<{ accept: Record<string, string[]> }> types?: Array<{ accept: Record<string, string[]> }>
} = { } = {
suggestedName: infoWithId.info.name, suggestedName: infoWithId.info.name,
startIn: defaultDownloadFolder, startIn: DefaultDownloadFolder,
} }
if (ext) { if (ext) {
@@ -171,24 +170,24 @@ const getSingleFileHandle = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const handle = (await (window as any).showSaveFilePicker(pickerOptions)) as FileSystemFileHandle const handle = (await (window as any).showSaveFilePicker(pickerOptions)) as FileSystemFileHandle
return [{ infoWithId, handle }] return { infoWithId, handle }
} catch (error: unknown) { } catch (error: unknown) {
return isUserCancellation(error) ? [{ infoWithId, cancelled: true }] : undefined return isUserCancellation(error) ? { infoWithId, cancelled: true } : undefined
} }
} }
const getMultipleFileHandles = async ( const getMultipleFileHandles = async (
infoWithIdList: FileInfoWithUUID[], infoWithIdList: FileInfoWithUUID[],
defaultDownloadFolder: string,
): Promise<FileInfoWithHandle[] | undefined> => { ): Promise<FileInfoWithHandle[] | undefined> => {
if (!isDirectoryPickerSupported()) { if (!isDirectoryPickerSupported()) {
const handles: FileInfoWithHandle[] = [] const handles: FileInfoWithHandle[] = []
for (const info of infoWithIdList) { for (const info of infoWithIdList) {
const result = await getSingleFileHandle(info, defaultDownloadFolder) const result = await getSingleFileHandle(info)
if (!result) return undefined if (!result) return undefined
handles.push(result[0])
handles.push(result)
} }
return handles return handles
@@ -198,7 +197,7 @@ const getMultipleFileHandles = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const dirHandle = (await (window as any).showDirectoryPicker({ const dirHandle = (await (window as any).showDirectoryPicker({
mode: 'readwrite', mode: 'readwrite',
startIn: defaultDownloadFolder, startIn: DefaultDownloadFolder,
})) as FileSystemDirectoryHandle })) as FileSystemDirectoryHandle
const handles: FileInfoWithHandle[] = [] const handles: FileInfoWithHandle[] = []
@@ -224,16 +223,16 @@ const getMultipleFileHandles = async (
} }
} }
const getFileHandles = (infoWithIdList: FileInfoWithUUID[]): Promise<FileInfoWithHandle[] | undefined> => { const getFileHandles = async (infoWithIdList: FileInfoWithUUID[]): Promise<FileInfoWithHandle[] | undefined> => {
const defaultDownloadFolder = 'downloads'
if (!isPickerSupported()) return Promise.resolve(infoWithIdList.map(infoWithId => ({ infoWithId }))) if (!isPickerSupported()) return Promise.resolve(infoWithIdList.map(infoWithId => ({ infoWithId })))
if (infoWithIdList.length === 1) { if (infoWithIdList.length === 1) {
return getSingleFileHandle(infoWithIdList[0], defaultDownloadFolder) const fh = await getSingleFileHandle(infoWithIdList[0])
return fh ? [fh] : undefined
} }
return getMultipleFileHandles(infoWithIdList, defaultDownloadFolder) return getMultipleFileHandles(infoWithIdList)
} }
const downloadToDisk = async ( const downloadToDisk = async (
@@ -258,20 +257,25 @@ const downloadToDisk = async (
} }
} }
interface BlobDownloadResult {
success: boolean
cancelled: boolean
}
const downloadToBlob = async ( const downloadToBlob = async (
streams: ReadableStream<Uint8Array>[], streams: ReadableStream<Uint8Array>[],
info: FileInfo, info: FileInfo,
onDownloadProgress?: (progress: DownloadProgress) => void, onDownloadProgress?: (progress: DownloadProgress) => void,
isOpenWindow?: boolean, isOpenWindow?: boolean,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<boolean> => { ): Promise<BlobDownloadResult> => {
try { try {
for (const stream of streams) { for (const stream of streams) {
const { mime } = guessMime(info.name, info.customMetadata) const { mime } = guessMime(info.name, info.customMetadata)
const blob = await streamToBlob(stream, mime, onDownloadProgress, signal) const blob = await streamToBlob(stream, mime, onDownloadProgress, signal)
if (!blob) { if (!blob) {
return false return { success: false, cancelled: false }
} }
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
@@ -282,50 +286,104 @@ const downloadToBlob = async (
} }
if (!opened) { if (!opened) {
if (isOpenWindow && isPickerSupported()) {
const result = await saveBlobWithPicker(blob, info, onDownloadProgress, signal)
URL.revokeObjectURL(url)
return result
}
downloadFromUrl(url, info.name) downloadFromUrl(url, info.name)
} }
} }
return true return { success: true, cancelled: false }
} catch (error: unknown) { } catch (error: unknown) {
if ((error as { name?: string }).name !== Errors.AbortError) { if ((error as { name?: string }).name !== Errors.AbortError) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('Error during download and open: ', error) console.error('Error during download and open: ', error)
} }
return false return { success: false, cancelled: false }
} }
} }
const openNewWindow = (name: string, mime: string, url: string): boolean => { const openNewWindow = (name: string, mime: string, url: string): boolean => {
const viewer = VIEWERS.find(v => v.test(mime)) const viewer = VIEWERS.find(v => v.test(mime))
if (!viewer) return false
const win = window.open('', '_blank') const win = window.open('', '_blank')
if (viewer && win) { if (!win) return false
viewer.render(win, url, mime, name)
return true try {
viewer.render(win, url, mime, name)
} catch (err: unknown) {
win.close()
// eslint-disable-next-line no-console
console.error('Failed to render file in a new window: ', err)
return false
} }
win?.close() return true
return false
} }
const saveBlobWithPicker = async (
blob: Blob,
info: FileInfo,
onDownloadProgress?: (progress: DownloadProgress) => void,
signal?: AbortSignal,
): Promise<BlobDownloadResult> => {
const infoWithId: FileInfoWithUUID = { uuid: '', info }
try {
if (signal?.aborted) {
throw new DOMException('Aborted', Errors.AbortError)
}
const fh = await getSingleFileHandle(infoWithId)
if (!fh || !fh.handle) {
return { success: false, cancelled: false }
}
if (fh.cancelled) {
return { success: false, cancelled: true }
}
await processStream(blob.stream(), fh.handle, onDownloadProgress, signal)
return { success: true, cancelled: false }
} catch (err: unknown) {
if (isUserCancellation(err)) {
return { success: false, cancelled: true }
}
// eslint-disable-next-line no-console
console.error('Failed to save blob using file picker: ', err)
return { success: false, cancelled: false }
}
}
const RevokeUrlTimeout = 1000
const downloadFromUrl = (url: string, fileName: string): void => { const downloadFromUrl = (url: string, fileName: string): void => {
const a = document.createElement('a') const a = document.createElement('a')
a.href = url a.href = url
a.download = fileName a.download = fileName
document.body.appendChild(a) document.body.appendChild(a)
a.click() a.click()
window.URL.revokeObjectURL(url) window.setTimeout(() => window.URL.revokeObjectURL(url), RevokeUrlTimeout)
document.body.removeChild(a) document.body.removeChild(a)
} }
export const startDownloadingQueue = async ( export const startDownloadingQueue = async (
fm: FileManager, fm: FileManager,
infoListWithIds: FileInfoWithUUID[], infoListWithIds: FileInfoWithUUID[],
trackers?: Array<(progress: DownloadProgress) => void>, trackers: Array<(progress: DownloadProgress) => void>,
isOpenWindow?: boolean, isOpenWindow?: boolean,
): Promise<void> => { ): Promise<void> => {
if (!infoListWithIds.length || (trackers && trackers.length !== infoListWithIds.length)) return if (!infoListWithIds.length || (trackers && trackers.length !== infoListWithIds.length)) return
@@ -339,7 +397,7 @@ export const startDownloadingQueue = async (
await Promise.all( await Promise.all(
fileHandles.map(async (fh, i) => { fileHandles.map(async (fh, i) => {
const tracker = trackers ? trackers[i] : undefined const tracker = trackers[i]
const uuid = fh.infoWithId.uuid const uuid = fh.infoWithId.uuid
createDownloadAbort(uuid) createDownloadAbort(uuid)
@@ -347,7 +405,7 @@ export const startDownloadingQueue = async (
try { try {
if (fh.cancelled) { if (fh.cancelled) {
tracker?.({ progress: 0, isDownloading: false, state: DownloadState.Cancelled }) tracker({ progress: 0, isDownloading: false, state: DownloadState.Cancelled })
return return
} }
@@ -359,23 +417,29 @@ export const startDownloadingQueue = async (
if (!dataStreams || dataStreams.length === 0) { if (!dataStreams || dataStreams.length === 0) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`No data streams returned for ${fh.infoWithId.info.name}`) console.error(`No data streams returned for ${fh.infoWithId.info.name}`)
tracker?.({ progress: 0, isDownloading: false, state: DownloadState.Error }) tracker({ progress: 0, isDownloading: false, state: DownloadState.Error })
return return
} }
let success = false let success = false
let userCancelled = false
if (isOpenWindow || !fh.handle) { if (isOpenWindow || !fh.handle) {
success = await downloadToBlob(dataStreams, fh.infoWithId.info, tracker, isOpenWindow, signal) const { success: saved, cancelled } = await downloadToBlob(
dataStreams,
fh.infoWithId.info,
tracker,
isOpenWindow,
signal,
)
success = saved
userCancelled = cancelled
} else { } else {
success = await downloadToDisk(dataStreams, fh.handle, tracker, signal) success = await downloadToDisk(dataStreams, fh.handle, tracker, signal)
} }
if (!tracker) {
return
}
if (success) { if (success) {
const size = fh.infoWithId.info.customMetadata?.size const size = fh.infoWithId.info.customMetadata?.size
const finalProgress = size ? Number(size) : 0 const finalProgress = size ? Number(size) : 0
@@ -385,18 +449,22 @@ export const startDownloadingQueue = async (
} }
if (!signal?.aborted) { if (!signal?.aborted) {
tracker({ progress: 0, isDownloading: false, state: DownloadState.Error }) tracker({
progress: 0,
isDownloading: false,
state: userCancelled ? DownloadState.Cancelled : DownloadState.Error,
})
} }
} catch (error: unknown) { } catch (error: unknown) {
const isAbortError = (error as { name?: string }).name === Errors.AbortError const isAbortError = (error as { name?: string }).name === Errors.AbortError
if (!isAbortError) { if (!isAbortError) {
tracker?.({ progress: 0, isDownloading: false, state: DownloadState.Error }) tracker({ progress: 0, isDownloading: false, state: DownloadState.Error })
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('download queue error: ', error) console.error('download queue error: ', error)
} else { } else {
tracker?.({ progress: 0, isDownloading: false, state: DownloadState.Cancelled }) tracker({ progress: 0, isDownloading: false, state: DownloadState.Cancelled })
} }
} finally { } finally {
downloadAborts.abort(uuid) downloadAborts.abort(uuid)
+3 -6
View File
@@ -186,12 +186,9 @@ export function FileManagerPage(): ReactElement {
}, []) }, [])
useEffect(() => { useEffect(() => {
if (status.all !== CheckState.OK) { const isApiError = status.apiConnection.checkState !== CheckState.OK || !status.apiConnection.isEnabled
setShowConnectionError(true) setShowConnectionError(isApiError)
} else { }, [status.apiConnection])
setShowConnectionError(false)
}
}, [status.all])
useEffect(() => { useEffect(() => {
if (!beeApi) { if (!beeApi) {