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:
@@ -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)}
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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([])
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user