fix: upload q and drive size error (#220)

* refactor: usetransfers hook functions
* refactor: bulk actions code style and readability
* fix: use upload q inflight size for subsequent uploads
* refactor: runUploadQueue execution
This commit is contained in:
Bálint Ujvári
2026-03-10 12:42:34 +01:00
parent bc2c0addbb
commit 8992c189fd
8 changed files with 348 additions and 328 deletions
@@ -16,7 +16,7 @@ import { useView } from '../../../../pages/filemanager/ViewContext'
import { Context as FMContext } from '../../../../providers/FileManager'
import { Context as SettingsContext } from '../../../../providers/Settings'
import { FileAction, FileTransferType, TransferStatus, ViewType } from '../../constants/transfers'
import { useBulkActions } from '../../hooks/useBulkActions'
import { BulkActionsResult, useBulkActions } from '../../hooks/useBulkActions'
import { useContextMenu } from '../../hooks/useContextMenu'
import { useDragAndDrop } from '../../hooks/useDragAndDrop'
import { useFileFiltering } from '../../hooks/useFileFiltering'
@@ -118,7 +118,7 @@ type FileBrowserContextMenuBlockProps = {
dropDir: Dir
drives: DriveInfo[]
view: ViewType
bulk: ReturnType<typeof useBulkActions>
bulk: BulkActionsResult
adminStamp: PostageBatch | undefined
doRefresh: () => void
onContextUploadFile: () => void
@@ -162,11 +162,11 @@ function FileBrowserContextMenuBlock({
onRefresh={doRefresh}
enableRefresh={Boolean(adminStamp)}
onUploadFile={onContextUploadFile}
onBulkDownload={() => bulk.bulkDownload(bulk.selectedFiles)}
onBulkDownload={() => bulk.download(bulk.selectedFiles)}
onBulkRestore={() => setConfirmBulkRestore(true)}
onBulkDelete={() => setShowBulkDeleteModal(true)}
onBulkDestroy={() => setShowDestroyDriveModal(true)}
onBulkForget={() => bulk.bulkForget(bulk.selectedFiles)}
onBulkForget={() => bulk.forget(bulk.selectedFiles)}
/>
</div>
)
@@ -196,7 +196,6 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
const [safePos, setSafePos] = useState<Point>(pos)
const [dropDir, setDropDir] = useState<Dir>(Dir.Down)
const legacyUploadRef = useRef<HTMLInputElement | null>(null)
const contentRef = useRef<HTMLDivElement | null>(null)
const bodyRef = useRef<HTMLDivElement | null>(null)
const isMountedRef = useRef(true)
@@ -281,20 +280,7 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
}
const onContextUploadFile = () => {
const el = legacyUploadRef.current
if (!el) return
try {
if (typeof (el as HTMLInputElement).showPicker === 'function') {
;(el as HTMLInputElement).showPicker()
} else {
el.click()
}
} catch {
el.click()
}
bulk.uploadFromPicker()
requestAnimationFrame(() => handleCloseContext())
}
@@ -323,7 +309,7 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
setShowBulkDeleteModal(false)
if (action === FileAction.Trash) {
return await bulk.bulkTrash(bulk.selectedFiles)
return await bulk.trash(bulk.selectedFiles)
}
if (action === FileAction.Forget) {
@@ -489,9 +475,9 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
const onBulk = useMemo(
() => ({
download: () => bulk.bulkDownload(bulk.selectedFiles),
download: () => bulk.download(bulk.selectedFiles),
restore: () => setConfirmBulkRestore(true),
forget: () => bulk.bulkForget(bulk.selectedFiles),
forget: () => bulk.forget(bulk.selectedFiles),
destroy: () => setShowDestroyDriveModal(true),
delete: () => setShowBulkDeleteModal(true),
}),
@@ -502,7 +488,6 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
<>
{conflictPortal}
<input type="file" ref={legacyUploadRef} style={{ display: 'none' }} onChange={onFileSelected} multiple />
<input type="file" ref={bulk.fileInputRef} style={{ display: 'none' }} onChange={onFileSelected} multiple />
<div className="fm-file-browser-container" data-search-mode={isSearchMode ? 'true' : 'false'}>
@@ -605,12 +590,12 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
onDeleteCancel={() => setShowBulkDeleteModal(false)}
onDeleteProceed={handleDeleteModalProceed}
onForgetConfirm={async () => {
await bulk.bulkForget(bulk.selectedFiles)
await bulk.forget(bulk.selectedFiles)
setConfirmBulkForget(false)
}}
onForgetCancel={() => setConfirmBulkForget(false)}
onRestoreConfirm={async () => {
await bulk.bulkRestore(bulk.selectedFiles)
await bulk.restore(bulk.selectedFiles)
setConfirmBulkRestore(false)
}}
onRestoreCancel={() => setConfirmBulkRestore(false)}
@@ -1,14 +1,14 @@
import { ReactElement } from 'react'
import DownIcon from 'remixicon-react/ArrowDownSLineIcon'
import { useBulkActions } from '../../../hooks/useBulkActions'
import { BulkActionsResult } from '../../../hooks/useBulkActions'
import { SortDir, SortKey } from '../../../hooks/useSorting'
import { capitalizeFirstLetter } from '@/modules/filemanager/utils/common'
interface FileBrowserHeaderProps {
isSearchMode: boolean
bulk: ReturnType<typeof useBulkActions>
bulk: BulkActionsResult
sortKey: SortKey
sortDir: SortDir
onSortName: () => void
@@ -435,7 +435,15 @@ export function VersionsList({ versions, headFi, restoreVersion, onDownload }: V
await startDownloadingQueue(
fm,
[{ uuid, info: fileInfo }],
[onDownload({ uuid, name: fileInfo.name, size: formatBytes(rawSize), expectedSize, driveName })],
[
onDownload({
uuid,
name: fileInfo.name,
size: formatBytes(rawSize),
expectedSize,
driveName: driveName ?? 'unknown',
}),
],
)
},
[handleCloseContext, fm, beeApi, onDownload, drives, currentDrive],
@@ -49,7 +49,7 @@ export type TrackDownloadProps = {
name: string
size?: string
expectedSize?: number
driveName?: string
driveName: string
}
export interface DownloadProgress {
+55 -30
View File
@@ -1,6 +1,6 @@
import { PostageBatch } from '@ethersphere/bee-js'
import type { FileInfo } from '@solarpunkltd/file-manager-lib'
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { type RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { Context as FMContext } from '../../../providers/FileManager'
import { Context as SettingsContext } from '../../../providers/Settings'
@@ -9,16 +9,33 @@ import { DownloadProgress, TrackDownloadProps } from '../constants/transfers'
import { getUsableStamps } from '../utils/bee'
import { formatBytes, getFileId, safeSetState } from '../utils/common'
import { FileInfoWithUUID, startDownloadingQueue } from '../utils/download'
import { FileOperation, performBulkFileOperation } from '../utils/fileOperations'
import { FileOperation, isElementPickerSupported, performBulkFileOperation } from '../utils/fileOperations'
interface BulkOptions {
export interface BulkOptions {
listToRender: FileInfo[]
trackDownload: (props: TrackDownloadProps) => (dp: DownloadProgress) => void
setErrorMessage?: (error: string) => void
}
export function useBulkActions({ listToRender, setErrorMessage, trackDownload }: BulkOptions) {
const { fm, adminDrive, drives, refreshStamp, setShowError } = useContext(FMContext)
export interface BulkActionsResult {
selectedIds: Set<string>
selectedFiles: FileInfo[]
selectedCount: number
allChecked: boolean
someChecked: boolean
fileInputRef: RefObject<HTMLInputElement | null>
toggleOne: (fi: FileInfo, checked: boolean) => void
selectAll: () => void
clearAll: () => void
uploadFromPicker: () => void
download: (list: FileInfo[]) => Promise<void>
trash: (list: FileInfo[]) => Promise<void>
restore: (list: FileInfo[]) => Promise<void>
forget: (list: FileInfo[]) => Promise<void>
}
export function useBulkActions({ listToRender, trackDownload, setErrorMessage }: BulkOptions): BulkActionsResult {
const { fm, adminDrive, currentDrive, drives, refreshStamp, setShowError } = useContext(FMContext)
const { beeApi } = useContext(SettingsContext)
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
@@ -69,11 +86,23 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
const selectAll = useCallback(() => setSelectedIds(new Set(allIds)), [allIds])
const clearAll = useCallback(() => setSelectedIds(new Set()), [])
const bulkUploadFromPicker = useCallback(() => {
fileInputRef.current?.click()
const uploadFromPicker = useCallback(() => {
const el = fileInputRef.current
if (!el) return
try {
if (isElementPickerSupported(el)) {
el.showPicker()
} else {
el.click()
}
} catch {
el.click()
}
}, [])
const bulkDownload = useCallback(
const download = useCallback(
async (list: FileInfo[]) => {
if (!fm || !list?.length) return
@@ -85,7 +114,7 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
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 driveName = drives.find(d => d.id.toString() === fi.driveId.toString())?.name ?? currentDrive?.name
const uuid = uuidV4()
infoListWitIDs[i] = { uuid, info: fi }
@@ -94,16 +123,16 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
name: fi.name,
size: prettySize,
expectedSize: expected,
driveName,
driveName: driveName ?? 'unknown',
})
}
await startDownloadingQueue(fm, infoListWitIDs, trackers)
},
[fm, trackDownload, drives],
[fm, currentDrive, trackDownload, drives],
)
const bulkTrash = useCallback(
const trash = useCallback(
async (list: FileInfo[]) => {
if (!fm || !list?.length) return
@@ -126,7 +155,7 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
[fm, driveStamps, clearAll, refreshStamp, setErrorMessage, setShowError],
)
const bulkRestore = useCallback(
const restore = useCallback(
async (list: FileInfo[]) => {
if (!fm || !list?.length) return
@@ -149,7 +178,7 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
[fm, driveStamps, refreshStamp, clearAll, setErrorMessage, setShowError],
)
const bulkForget = useCallback(
const forget = useCallback(
async (list: FileInfo[]) => {
if (!fm || !fm.adminStamp || !adminDrive || !list?.length) return
@@ -176,26 +205,22 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
[fm, adminDrive, driveStamps, clearAll, refreshStamp, setErrorMessage, setShowError],
)
return useMemo(
return useMemo<BulkActionsResult>(
() => ({
// selection
selectedIds,
setSelectedIds,
selectedFiles,
selectedCount,
allChecked,
someChecked,
fileInputRef,
toggleOne,
selectAll,
clearAll,
// file input (for bulk upload)
fileInputRef,
bulkUploadFromPicker,
// actions
bulkDownload,
bulkTrash,
bulkRestore,
bulkForget,
uploadFromPicker,
download,
trash,
restore,
forget,
}),
[
selectedIds,
@@ -206,11 +231,11 @@ export function useBulkActions({ listToRender, setErrorMessage, trackDownload }:
toggleOne,
selectAll,
clearAll,
bulkUploadFromPicker,
bulkDownload,
bulkTrash,
bulkRestore,
bulkForget,
uploadFromPicker,
download,
trash,
restore,
forget,
],
)
}
+261 -263
View File
@@ -1,5 +1,5 @@
import type { FileInfo, FileInfoOptions, UploadProgress } from '@solarpunkltd/file-manager-lib'
import { useCallback, useContext, useEffect, useRef, useState } from 'react'
import type { DriveInfo, FileInfo, FileInfoOptions, UploadProgress } from '@solarpunkltd/file-manager-lib'
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { Context as FMContext } from '../../../providers/FileManager'
import { Context as SettingsContext } from '../../../providers/Settings'
@@ -27,7 +27,7 @@ const ABORT_EVENT = 'abort'
type ResolveResult = {
cancelled: boolean
finalName?: string
isReplace?: boolean
isReplace: boolean
replaceTopic?: string
replaceHistory?: string
}
@@ -65,6 +65,18 @@ type UploadTask = {
driveName: string
}
const isNameInvalid = (
finalName: string,
isReplace: boolean,
replaceHistory?: string,
replaceTopic?: string,
): boolean => {
const invalidCombo = isReplace && (!replaceHistory || !replaceTopic)
const invalidName = !finalName || finalName.trim().length === 0
return invalidCombo || invalidName
}
const normalizeCustomMetadata = (meta: UploadMeta): Record<string, string> => {
const out: Record<string, string> = {}
for (const [k, v] of Object.entries(meta)) out[k] = typeof v === 'string' ? v : String(v)
@@ -175,7 +187,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
const isMountedRef = useRef(true)
const uploadAbortsRef = useRef<AbortManager>(new AbortManager())
const queueRef = useRef<UploadTask[]>([])
const uploadTaskQueueRef = useRef<UploadTask[]>([])
const runningRef = useRef(false)
const cancelledQueuedRef = useRef<Set<string>>(new Set())
const cancelledUploadingRef = useRef<Set<string>>(new Set())
@@ -195,7 +207,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
cancelledQueuedRef.current.delete(uuid)
cancelledUploadingRef.current.delete(uuid)
uploadAbortsRef.current.abort(uuid)
queueRef.current = queueRef.current.filter(t => {
uploadTaskQueueRef.current = uploadTaskQueueRef.current.filter(t => {
return t.uuid !== uuid
})
}, [])
@@ -250,7 +262,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
})
if (choice.action === ConflictAction.Cancel) {
return { cancelled: true }
return { cancelled: true, isReplace: false }
}
if (choice.action === ConflictAction.KeepBoth) {
@@ -347,7 +359,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
[],
)
const processUploadTask = useCallback(
const executeUploadTask = useCallback(
async (task: UploadTask) => {
if (!fm) return
@@ -447,115 +459,266 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
[fm, files, currentStamp, trackUpload, refreshStamp, setShowError, setErrorMessage],
)
const trackDownload = useCallback(
(props: TrackDownloadProps) => {
if (!isMountedRef.current) {
return () => {
// No-op function for unmounted component
}
const trackDownload = useCallback((props: TrackDownloadProps) => {
if (!isMountedRef.current) {
return () => {
// No-op function for unmounted component
}
}
let startedAt: number | undefined
let etaState: ETAState = {
lastTs: undefined,
lastProcessed: 0,
lastEta: undefined,
}
setDownloadItems(prev => {
const row = createTransferItem(
props.uuid,
props.name,
props.size,
FileTransferType.Download,
props.driveName,
TransferStatus.Downloading,
)
row.startedAt = undefined
const idx = prev.findIndex(p => p.uuid === props.uuid)
if (idx === -1) return [...prev, row]
const out = [...prev]
out[idx] = { ...row, startedAt: prev[idx].startedAt ?? row.startedAt }
return out
})
const onProgress = (dp: DownloadProgress) => {
if (!isMountedRef.current) return
const now = Date.now()
if (!startedAt) {
startedAt = now
etaState.lastTs = now
}
const driveName = props.driveName ?? currentDrive?.name
let percent = 0
let etaSec: number | undefined
let startedAt: number | undefined
let etaState: ETAState = {
lastTs: undefined,
lastProcessed: 0,
lastEta: undefined,
if (props.expectedSize && props.expectedSize > 0 && dp.progress >= 0) {
percent = Math.floor((dp.progress / props.expectedSize) * 100)
const result = calculateETA(etaState, { processed: dp.progress, total: props.expectedSize }, startedAt, now)
etaSec = result.etaSec
etaState = result.updatedState
}
setDownloadItems(prev => {
const row = createTransferItem(
props.uuid,
props.name,
props.size,
FileTransferType.Download,
driveName,
TransferStatus.Downloading,
)
row.startedAt = undefined
const idx = prev.findIndex(p => p.uuid === props.uuid)
setDownloadItems(prev =>
updateTransferItems(prev, props.uuid, {
percent: Math.max(prev.find(it => it.uuid === props.uuid)?.percent || 0, percent),
etaSec,
startedAt: prev.find(it => it.uuid === props.uuid)?.startedAt ?? startedAt,
}),
)
if (idx === -1) return [...prev, row]
const out = [...prev]
out[idx] = { ...row, startedAt: prev[idx].startedAt ?? row.startedAt }
if (!dp.isDownloading) {
const finishedAt = Date.now()
return out
})
setDownloadItems(prev => {
const currentItem = prev.find(it => it.uuid === props.uuid)
const elapsedSec = currentItem?.startedAt ? Math.round((finishedAt - currentItem.startedAt) / 1000) : 0
const onProgress = (dp: DownloadProgress) => {
if (!isMountedRef.current) return
if (dp.state === DownloadState.Cancelled || dp.state === DownloadState.Error) {
const wasCancelled = dp.state === DownloadState.Cancelled || cancelledDownloadingRef.current.has(props.uuid)
const now = Date.now()
if (!startedAt) {
startedAt = now
etaState.lastTs = now
}
let percent = 0
let etaSec: number | undefined
if (props.expectedSize && props.expectedSize > 0 && dp.progress >= 0) {
percent = Math.floor((dp.progress / props.expectedSize) * 100)
const result = calculateETA(etaState, { processed: dp.progress, total: props.expectedSize }, startedAt, now)
etaSec = result.etaSec
etaState = result.updatedState
}
setDownloadItems(prev =>
updateTransferItems(prev, props.uuid, {
percent: Math.max(prev.find(it => it.uuid === props.uuid)?.percent || 0, percent),
etaSec,
startedAt: prev.find(it => it.uuid === props.uuid)?.startedAt ?? startedAt,
}),
)
if (!dp.isDownloading) {
const finishedAt = Date.now()
setDownloadItems(prev => {
const currentItem = prev.find(it => it.uuid === props.uuid)
const elapsedSec = currentItem?.startedAt ? Math.round((finishedAt - currentItem.startedAt) / 1000) : 0
if (dp.state === DownloadState.Cancelled || dp.state === DownloadState.Error) {
const wasCancelled =
dp.state === DownloadState.Cancelled || cancelledDownloadingRef.current.has(props.uuid)
cancelledDownloadingRef.current.delete(props.uuid)
return updateTransferItems(prev, props.uuid, {
status: wasCancelled ? TransferStatus.Cancelled : TransferStatus.Error,
etaSec: undefined,
elapsedSec: 0,
percent: currentItem?.percent ?? 0,
})
}
cancelledDownloadingRef.current.delete(props.uuid)
return updateTransferItems(prev, props.uuid, {
percent: 100,
status: TransferStatus.Done,
etaSec: 0,
elapsedSec,
status: wasCancelled ? TransferStatus.Cancelled : TransferStatus.Error,
etaSec: undefined,
elapsedSec: 0,
percent: currentItem?.percent ?? 0,
})
}
return updateTransferItems(prev, props.uuid, {
percent: 100,
status: TransferStatus.Done,
etaSec: 0,
elapsedSec,
})
}
})
}
}
return onProgress
}, [])
const { allTaken, reserved, sameDrive } = useMemo(() => {
if (!currentDrive) {
return { allTaken: new Set(''), reserved: new Set(''), sameDrive: [] }
}
const progressNames = new Set<string>(uploadItems.filter(u => u.driveName === currentDrive.name).map(u => u.name))
const sameDrive = collectSameDrive(currentDrive.id.toString())
const onDiskNames = new Set<string>(sameDrive.map((fi: FileInfo) => fi.name))
const reserved = new Set<string>()
const allTaken = new Set<string>([
...Array.from(onDiskNames),
...Array.from(reserved),
...Array.from(progressNames),
])
return {
allTaken,
reserved,
sameDrive,
}
}, [currentDrive, collectSameDrive, uploadItems])
const createUploadTask = useCallback(
async (file: File, drive: DriveInfo): Promise<UploadTask | null> => {
const uuid = uuidV4()
const meta = buildUploadMeta([file])
const prettySize = formatBytes(meta.size)
let { finalName, isReplace, replaceTopic, replaceHistory } = await resolveConflict(file.name, sameDrive, allTaken)
finalName = finalName ?? ''
if (isNameInvalid(finalName, isReplace, replaceHistory, replaceTopic)) {
return null
}
return onProgress
if (reserved.has(finalName)) {
const retryTaken = new Set<string>([...Array.from(allTaken), finalName])
const retry = await resolveConflict(finalName, sameDrive, retryTaken)
finalName = retry.finalName ?? ''
isReplace = retry.isReplace
replaceTopic = retry.replaceTopic
replaceHistory = retry.replaceHistory
}
if (isNameInvalid(finalName, isReplace, replaceHistory, replaceTopic)) {
return null
}
reserved.add(finalName)
ensureQueuedRow(
uuid,
finalName,
isReplace ? FileTransferType.Update : FileTransferType.Upload,
prettySize,
drive.name,
)
return {
uuid,
file,
finalName,
prettySize,
isReplace: Boolean(isReplace),
replaceTopic,
replaceHistory,
driveId: drive.id.toString(),
driveName: drive.name,
}
},
// currentDrive casues rerenders and flickering during the progress tracking
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
[allTaken, ensureQueuedRow, reserved, resolveConflict, sameDrive],
)
const preflight = useCallback(
async (filesArr: File[]): Promise<UploadTask[]> => {
if (!currentDrive || !fm) return []
if (!currentStamp || !currentStamp.usable) {
setErrorMessage?.('Stamp is not usable.')
setShowError(true)
return []
}
const tasks: UploadTask[] = []
const inFlightSize = uploadTaskQueueRef.current.reduce((sum, t) => sum + t.file.size, 0)
const inFlightCount = uploadTaskQueueRef.current.length
let currentFileSizeSum = inFlightSize
for (let i = 0; i < filesArr.length; i++) {
const file = filesArr[i]
currentFileSizeSum += file.size
const fileCount = inFlightCount + i + 1
const { ok } = verifyDriveSpace({
fm,
redundancyLevel: currentDrive.redundancyLevel,
stamp: currentStamp,
useInfoSize: true,
driveId: currentDrive.id.toString(),
adminRedundancy: adminDrive?.redundancyLevel,
fileSize: currentFileSizeSum,
fileCount,
cb: err => {
setErrorMessage?.(err + ' (' + truncateNameMiddle(file.name) + ')')
setShowError(true)
},
})
if (!ok) {
break
}
const task = await createUploadTask(file, currentDrive)
if (!task) {
break
}
tasks.push(task)
}
return tasks
},
[fm, currentDrive, currentStamp, adminDrive, createUploadTask, setErrorMessage, setShowError],
)
const runUploadQueue = useCallback(async () => {
if (runningRef.current) return
runningRef.current = true
while (uploadTaskQueueRef.current.length > 0) {
const task = uploadTaskQueueRef.current[0]
if (!task) break
if (cancelledQueuedRef.current.has(task.uuid)) {
safeSetState(
isMountedRef,
setUploadItems,
)(prev => updateTransferItems(prev, task.uuid, { status: TransferStatus.Cancelled }))
cancelledQueuedRef.current.delete(task.uuid)
} else {
await executeUploadTask(task)
}
uploadTaskQueueRef.current.shift()
}
runningRef.current = false
// Race guard: uploadFiles may have appended tasks and called runUploadQueue() again
if (uploadTaskQueueRef.current.length > 0) {
runUploadQueue()
}
}, [executeUploadTask])
const uploadFiles = useCallback(
(picked: FileList | File[]): void => {
const filesArr = Array.from(picked)
if (filesArr.length === 0 || !fm || !currentDrive || !currentStamp) return
const currentlyQueued = queueRef.current.length
const currentlyQueued = uploadTaskQueueRef.current.length
const newFilesCount = filesArr.length
const totalAfterAdd = currentlyQueued + newFilesCount
@@ -567,158 +730,6 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
return
}
// TODO: move out this function from the cb and use as a util for better readaility
const preflight = async (): Promise<UploadTask[]> => {
const progressNames = new Set<string>(
uploadItems.filter(u => u.driveName === currentDrive.name).map(u => u.name),
)
const sameDrive = collectSameDrive(currentDrive.id.toString())
const onDiskNames = new Set<string>(sameDrive.map((fi: FileInfo) => fi.name))
const reserved = new Set<string>()
const tasks: UploadTask[] = []
const allTaken = new Set<string>([
...Array.from(onDiskNames),
...Array.from(reserved),
...Array.from(progressNames),
])
// Track cumulative file sizes for capacity verification
let fileSizeSum = 0
let fileCount = 0
const processFile = async (file: File): Promise<UploadTask | null> => {
if (!currentStamp || !currentStamp.usable) {
setErrorMessage?.('Stamp is not usable.')
setShowError(true)
return null
}
const uuid = uuidV4()
const meta = buildUploadMeta([file])
const prettySize = formatBytes(meta.size)
fileSizeSum += file.size
fileCount += 1
const { ok } = verifyDriveSpace({
fm,
redundancyLevel: currentDrive.redundancyLevel,
stamp: currentStamp,
useInfoSize: true,
driveId: currentDrive.id.toString(),
adminRedundancy: adminDrive?.redundancyLevel,
fileSize: fileSizeSum,
fileCount,
cb: err => {
setErrorMessage?.(err + ' (' + truncateNameMiddle(file.name) + ')')
setShowError(true)
},
})
if (!ok) {
return null
}
let { finalName, isReplace, replaceTopic, replaceHistory } = await resolveConflict(
file.name,
sameDrive,
allTaken,
)
finalName = finalName ?? ''
const invalidCombo = Boolean(isReplace) && (!replaceHistory || !replaceTopic)
const invalidName = !finalName || finalName.trim().length === 0
if (!invalidCombo && !invalidName) {
if (reserved.has(finalName)) {
const retryTaken = new Set<string>([...Array.from(allTaken), finalName])
const retry = await resolveConflict(finalName, sameDrive, retryTaken)
finalName = retry.finalName ?? ''
isReplace = retry.isReplace
replaceTopic = retry.replaceTopic
replaceHistory = retry.replaceHistory
}
const retryInvalidCombo = Boolean(isReplace) && (!replaceHistory || !replaceTopic)
const retryInvalidName = !finalName || finalName.trim().length === 0
if (!retryInvalidCombo && !retryInvalidName) {
reserved.add(finalName)
ensureQueuedRow(
uuid,
finalName,
isReplace ? FileTransferType.Update : FileTransferType.Upload,
prettySize,
currentDrive.name,
)
return {
uuid,
file,
finalName,
prettySize,
isReplace: Boolean(isReplace),
replaceTopic,
replaceHistory,
driveId: currentDrive.id.toString(),
driveName: currentDrive.name,
}
}
}
return null
}
for (const file of filesArr) {
const task = await processFile(file)
if (task) {
tasks.push(task)
} else {
// Stop processing remaining files if capacity check failed
break
}
}
return tasks
}
const runQueue = async () => {
if (runningRef.current) return
runningRef.current = true
try {
while (queueRef.current.length > 0) {
const task = queueRef.current[0]
if (!task) break
const isCancelled = cancelledQueuedRef.current.has(task.uuid)
if (isCancelled) {
safeSetState(
isMountedRef,
setUploadItems,
)(prev => updateTransferItems(prev, task.uuid, { status: TransferStatus.Cancelled }))
cancelledQueuedRef.current.delete(task.uuid)
queueRef.current.shift()
} else {
await processUploadTask(task)
queueRef.current.shift()
}
}
} finally {
runningRef.current = false
if (queueRef.current.length > 0) {
runQueue()
}
}
}
void (async () => {
if (!currentStamp || !currentStamp.usable) {
@@ -741,25 +752,12 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
}
}
const tasks = await preflight()
queueRef.current = queueRef.current.concat(tasks)
runQueue()
const tasks = await preflight(filesArr)
uploadTaskQueueRef.current = uploadTaskQueueRef.current.concat(tasks)
runUploadQueue()
})()
},
[
fm,
currentDrive,
currentStamp,
collectSameDrive,
resolveConflict,
ensureQueuedRow,
processUploadTask,
uploadItems,
adminDrive,
setShowError,
setErrorMessage,
beeApi,
],
[fm, currentStamp, currentDrive, beeApi, setShowError, setErrorMessage, preflight, runUploadQueue],
)
const cancelOrDismissUpload = useCallback(
@@ -774,7 +772,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
if (row.status === TransferStatus.Queued) {
cancelledQueuedRef.current.add(row.uuid)
queueRef.current = queueRef.current.filter(t => t.uuid !== row.uuid)
uploadTaskQueueRef.current = uploadTaskQueueRef.current.filter(t => t.uuid !== row.uuid)
return prev.map(r => (r.uuid === row.uuid ? { ...r, status: TransferStatus.Cancelled } : r))
}
@@ -818,7 +816,7 @@ export function useTransfers({ setErrorMessage }: TransferProps) {
const dismissAllUploads = useCallback(() => {
uploadAbortsRef.current.clear()
queueRef.current = []
uploadTaskQueueRef.current = []
cancelledQueuedRef.current.clear()
cancelledUploadingRef.current.clear()
setUploadItems([])
+1 -6
View File
@@ -3,6 +3,7 @@ import { FileInfo, FileManager } from '@solarpunkltd/file-manager-lib'
import { DownloadProgress, DownloadState } from '../constants/transfers'
import { AbortManager } from './abortManager'
import { isDirectoryPickerSupported, isPickerSupported } from './fileOperations'
import { guessMime, VIEWERS } from './view'
const downloadAborts = new AbortManager()
@@ -141,12 +142,6 @@ interface FileInfoWithHandle {
cancelled?: boolean
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isPickerSupported = (): boolean => typeof (window as any).showSaveFilePicker === 'function'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isDirectoryPickerSupported = (): boolean => typeof (window as any).showDirectoryPicker === 'function'
const isUserCancellation = (error: unknown): boolean => {
const errName = (error as { name?: string })?.name
@@ -151,3 +151,12 @@ export async function performBulkFileOperation({
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isPickerSupported = (): boolean => typeof (window as any).showSaveFilePicker === 'function'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isDirectoryPickerSupported = (): boolean => typeof (window as any).showDirectoryPicker === 'function'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isElementPickerSupported = (el: any): boolean => typeof (el as HTMLInputElement).showPicker === 'function'