Fix: file-manager and swarm-desktop bugs (#714)

- drive capacity display with stamp polling
- download/upload progress handling
- overlay and tooltip issues
- FileMaganger readme
- ultra-light mode handling
- account feed view page
- download media files
- remove not found syncing link
- fix ultra light node wallet page
- tooltip issues
---------
Co-authored-by: Andrei Mitrea <andrei.mitrea.hq@gmail.com>
Co-authored-by: nidishk <nidishkrishnan45@gmail.com>
Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com>
Co-authored-by: Nándor Komlódi <nandor.komlodi@gmail.com>
Co-authored-by: rolandlor <33499567+rolandlor@users.noreply.github.com>
This commit is contained in:
Bálint Ujvári
2026-01-26 12:57:14 +01:00
committed by GitHub
parent ecadafd21d
commit 0d5138f5bc
78 changed files with 3961 additions and 1194 deletions
+297 -94
View File
@@ -1,6 +1,13 @@
import { BatchId, Bee, BZZ, Duration, PostageBatch, RedundancyLevel, Size } from '@ethersphere/bee-js'
import { FileManagerBase, DriveInfo } from '@solarpunkltd/file-manager-lib'
import {
FileManagerBase,
DriveInfo,
estimateDriveListMetadataSize,
estimateFileInfoMetadataSize,
FileInfo,
} from '@solarpunkltd/file-manager-lib'
import { getHumanReadableFileSize } from '../../../utils/file'
import { ActionTag } from '../constants/transfers'
export const getUsableStamps = async (bee: Bee | null): Promise<PostageBatch[]> => {
if (!bee) {
@@ -16,6 +23,19 @@ export const getUsableStamps = async (bee: Bee | null): Promise<PostageBatch[]>
}
}
export const validateStampStillExists = async (bee: Bee, batchId: BatchId): Promise<boolean> => {
try {
const stamp = await bee.getPostageBatch(batchId.toString())
return stamp.usable
} catch (error) {
// eslint-disable-next-line no-console
console.warn(`Failed to validate stamp ${batchId.toString().slice(0, 8)}...:`, error)
return false
}
}
export const fmGetStorageCost = async (
capacity: number,
validityEndDate: Date,
@@ -50,6 +70,7 @@ export const fmFetchCost = async (
beeApi: Bee | null,
setCost: (cost: BZZ) => void,
currentFetch: React.MutableRefObject<Promise<void> | null>,
onError?: (error: unknown) => void,
) => {
if (currentFetch.current) {
await currentFetch.current
@@ -58,10 +79,22 @@ export const fmFetchCost = async (
let isCurrentFetch = true
const fetchPromise = (async () => {
const cost = await fmGetStorageCost(capacity, validityEndDate, encryption, erasureCodeLevel, beeApi)
try {
const cost = await fmGetStorageCost(capacity, validityEndDate, encryption, erasureCodeLevel, beeApi)
if (isCurrentFetch) {
setCost(cost ?? BZZ.fromDecimalString('0'))
if (isCurrentFetch) {
if (cost) {
setCost(cost)
} else {
setCost(BZZ.fromDecimalString('0'))
onError?.(new Error('Storage cost unavailable - node may be syncing'))
}
}
} catch (error) {
if (isCurrentFetch) {
setCost(BZZ.fromDecimalString('0'))
onError?.(error)
}
}
})()
@@ -72,32 +105,95 @@ export const fmFetchCost = async (
currentFetch.current = null
}
export const handleCreateDrive = async (
beeApi: Bee | null,
fm: FileManagerBase | null,
size: Size,
duration: Duration,
label: string,
encryption: boolean,
erasureCodeLevel: RedundancyLevel,
isAdmin: boolean,
resetState: boolean,
existingBatch: PostageBatch | null,
onSuccess?: () => void,
onError?: (error: unknown) => void,
): Promise<void> => {
if (!beeApi || !fm) return
export interface CreateDriveOptions {
beeApi: Bee | null
fm: FileManagerBase | null
size: Size
duration: Duration
label: string
encryption: boolean
redundancyLevel: RedundancyLevel
adminRedundancy: RedundancyLevel
isAdmin: boolean
resetState: boolean
existingBatch: PostageBatch | null
onSuccess?: () => void
onError?: (error: unknown) => void
}
export const handleCreateDrive = async (options: CreateDriveOptions): Promise<void> => {
const {
beeApi,
fm,
size,
duration,
label,
encryption,
redundancyLevel,
adminRedundancy,
isAdmin,
resetState,
existingBatch,
onSuccess,
onError,
} = { ...options }
if (!beeApi || !fm) {
// eslint-disable-next-line no-console
console.error('Error creating drive: Bee API or FM is invalid!')
onError?.('Error creating drive: Bee API or FM is invalid!')
return
}
try {
let batchId: BatchId
if (!existingBatch) {
batchId = await beeApi.buyStorage(size, duration, { label }, undefined, encryption, erasureCodeLevel)
if (!isAdmin) {
if (!fm.adminStamp) {
// eslint-disable-next-line no-console
console.error('Error creating drive: admin stamp is not available')
throw new Error('Error creating drive: admin stamp is not available')
}
verifyDriveSpace({
fm,
redundancyLevel,
stamp: fm.adminStamp,
adminRedundancy,
cb: err => {
throw new Error(err)
},
})
}
batchId = await beeApi.buyStorage(size, duration, { label }, undefined, encryption, redundancyLevel)
} else {
const isValid = await validateStampStillExists(beeApi, existingBatch.batchID)
if (!isValid) {
throw new Error(
'The stamp is no longer valid or has been deleted. Please select a different stamp from the list.',
)
}
verifyDriveSpace({
fm,
redundancyLevel,
stamp: existingBatch,
adminRedundancy,
cb: err => {
throw new Error(err)
},
})
batchId = existingBatch.batchID
}
await fm.createDrive(batchId, label, isAdmin, erasureCodeLevel, resetState)
await fm.createDrive(batchId, label, isAdmin, redundancyLevel, resetState)
onSuccess?.()
} catch (e) {
@@ -107,68 +203,22 @@ export const handleCreateDrive = async (
}
}
interface StampCapacityMetrics {
capacityPct: number
usedSize: string
totalSize: string
usedBytes: number
totalBytes: number
remainingBytes: number
export interface DestroyDriveOptions {
beeApi?: Bee | null
fm: FileManagerBase | null
drive: DriveInfo
adminDrive: DriveInfo | null
isDestroy: boolean
onSuccess?: () => void
onError?: (error: unknown) => void
}
export const calculateStampCapacityMetrics = (
stamp: PostageBatch | null,
drive?: DriveInfo | null,
): StampCapacityMetrics => {
if (!stamp) {
return {
capacityPct: 0,
usedSize: '—',
totalSize: '—',
usedBytes: 0,
totalBytes: 0,
remainingBytes: 0,
}
}
export const handleDestroyAndForgetDrive = async (options: DestroyDriveOptions): Promise<void> => {
const { beeApi, fm, adminDrive, drive, isDestroy, onSuccess, onError } = { ...options }
let usedBytes = 0
let totalBytes = 0
let capacityPct = 0
let remainingBytes = 0
if (!beeApi || !fm || !fm.adminStamp || !adminDrive) {
onError?.('Error destroying drive: Admin Drive, Bee API or FM is invalid!')
if (drive) {
totalBytes = stamp.calculateSize(false, drive.redundancyLevel).toBytes()
remainingBytes = stamp.calculateRemainingSize(false, drive.redundancyLevel).toBytes()
usedBytes = totalBytes - remainingBytes
capacityPct = ((totalBytes - remainingBytes) / totalBytes) * 100
} else {
capacityPct = stamp.usage * 100
usedBytes = stamp.size.toBytes() - stamp.remainingSize.toBytes()
totalBytes = stamp.size.toBytes()
remainingBytes = totalBytes - usedBytes
}
const usedSize = getHumanReadableFileSize(usedBytes)
const totalSize = getHumanReadableFileSize(totalBytes)
return {
capacityPct,
usedSize,
totalSize,
usedBytes,
totalBytes,
remainingBytes,
}
}
export const handleDestroyDrive = async (
beeApi: Bee | null,
fm: FileManagerBase | null,
drive: DriveInfo,
onSuccess?: () => void,
onError?: (error: unknown) => void,
): Promise<void> => {
if (!beeApi || !fm) {
return
}
@@ -179,11 +229,26 @@ export const handleDestroyDrive = async (
throw new Error(`Postage stamp (${drive.batchId}) for the current drive (${drive.name}) not found`)
}
verifyDriveSpace({
fm,
driveId: drive.id.toString(),
redundancyLevel: drive.redundancyLevel,
stamp: fm.adminStamp,
isRemove: true,
adminRedundancy: adminDrive.redundancyLevel,
cb: err => {
throw new Error(err)
},
})
const ttlDays = stamp.duration.toDays()
if (ttlDays <= 2) {
// eslint-disable-next-line no-console
console.warn(`Stamp TTL ${ttlDays} <= 2 days, skipping drive destruction: forgetting the drive.`)
if (ttlDays <= 2 || !isDestroy) {
if (isDestroy) {
// eslint-disable-next-line no-console
console.warn(`Stamp TTL ${ttlDays} <= 2 days, skipping drive destruction: forgetting the drive.`)
}
await fm.forgetDrive(drive)
return
@@ -197,18 +262,156 @@ export const handleDestroyDrive = async (
}
}
export const handleForgetDrive = async (
fm: FileManagerBase | null,
drive: DriveInfo,
onSuccess?: () => void,
onError?: (error: unknown) => void,
): Promise<void> => {
if (!fm) return
export interface StampCapacityMetrics {
capacityPct: number
usedSize: string
stampSize: string
usedBytes: number
stampSizeBytes: number
remainingBytes: number
}
try {
await fm.forgetDrive(drive)
onSuccess?.()
} catch (e) {
onError?.(e)
export const calculateStampCapacityMetrics = (
stamp: PostageBatch,
files: FileInfo[],
redundancyLevel?: RedundancyLevel,
useReportedOnly?: boolean,
): StampCapacityMetrics => {
let stampSizeBytes = 0
let remainingReportedBytes = 0
if (redundancyLevel !== undefined) {
stampSizeBytes = stamp.calculateSize(false, redundancyLevel).toBytes()
remainingReportedBytes = stamp.calculateRemainingSize(false, redundancyLevel).toBytes()
} else {
stampSizeBytes = stamp.size.toBytes()
remainingReportedBytes = stamp.remainingSize.toBytes()
}
const usedBytesReported = stampSizeBytes - remainingReportedBytes
const pctReportedStampUsage = stamp.usage * 100
let usedSizeMaxBytes = usedBytesReported
let pctFromDriveUsage = pctReportedStampUsage
let remainingBytes = remainingReportedBytes
if (!useReportedOnly) {
const usedBytesFromFiles = files
.map(f => {
let rawSize = 0
const lifecycle = (f.customMetadata?.lifecycle || '').toString().toLowerCase()
const isLifecycleOperation =
lifecycle === ActionTag.Trashed || lifecycle === ActionTag.Recovered || lifecycle === ActionTag.Restored
if (
(f.customMetadata?.size && !isLifecycleOperation) ||
(f.customMetadata?.size && !f.customMetadata?.accumulatedSize)
) {
rawSize = Number(f.customMetadata.size)
}
const accumulatedSize = Number(f.customMetadata?.accumulatedSize || rawSize || 0)
return accumulatedSize
})
.reduce((acc, current) => acc + current, 0)
const remainingBytesFromFiles = stampSizeBytes - usedBytesFromFiles > 0 ? stampSizeBytes - usedBytesFromFiles : 0
remainingBytes = Math.min(remainingReportedBytes, remainingBytesFromFiles)
usedSizeMaxBytes = Math.max(usedBytesFromFiles, usedBytesReported)
pctFromDriveUsage = stampSizeBytes > 0 ? (usedSizeMaxBytes / stampSizeBytes) * 100 : 0
}
const usedSizeMax = getHumanReadableFileSize(usedSizeMaxBytes)
const capacityPct = Math.max(pctFromDriveUsage, pctReportedStampUsage)
const stampSize = getHumanReadableFileSize(stampSizeBytes)
return {
capacityPct,
usedSize: usedSizeMax,
stampSize,
usedBytes: usedSizeMaxBytes,
stampSizeBytes,
remainingBytes,
}
}
export interface DriveSpaceOptions {
fm: FileManagerBase
driveId?: string
redundancyLevel: RedundancyLevel
stamp: PostageBatch
adminRedundancy?: RedundancyLevel
useInfoSize?: boolean
isRemove?: boolean
fileSize?: number
fileCount?: number
cb?: (msg: string) => void
}
export const verifyDriveSpace = (
options: DriveSpaceOptions,
): { remainingBytes: number; totalSizeBytes: number; ok: boolean } => {
const { fm, driveId, redundancyLevel, stamp, adminRedundancy, useInfoSize, isRemove, fileSize, fileCount, cb } = {
...options,
}
const drives = [...fm.driveList]
let filesPerDrives: FileInfo[] = []
// new drivelist state size calc.
if (isRemove) {
const driveIx = drives.findIndex(d => d.id.toString() === driveId?.toString())
if (driveIx === -1) {
cb?.(`Admin drive not found during stamp verification`)
return { remainingBytes: 0, totalSizeBytes: 0, ok: false }
}
drives.splice(driveIx, 1)
filesPerDrives = fm.fileInfoList.filter(fi => fi.driveId !== driveId)
} else {
filesPerDrives = driveId ? fm.fileInfoList.filter(fi => fi.driveId === driveId) : []
}
// admin stamp capacity calcl., needed for forget, destroy, create
if (adminRedundancy !== undefined && fm.adminStamp) {
// upper limit estimate on the drivelist metadata state size based on the number of drives and files
const estimatedDlSizeBytes = estimateDriveListMetadataSize(drives) * drives.length
const { remainingBytes: remainingAdminBytes } = calculateStampCapacityMetrics(fm.adminStamp, [], adminRedundancy)
const ok = remainingAdminBytes >= estimatedDlSizeBytes
if (!ok) {
cb?.(
`Insufficient admin drive capacity. Required: ~${getHumanReadableFileSize(
estimatedDlSizeBytes,
)} bytes, Available: ${getHumanReadableFileSize(
remainingAdminBytes,
)} bytes. Please top up the admin drive/stamp.`,
)
return { remainingBytes: remainingAdminBytes, totalSizeBytes: estimatedDlSizeBytes, ok }
}
}
// other fileinfo metadata size calc.
const estimatedFiSize = estimateFileInfoMetadataSize()
const count = fileCount ?? 1
const estimateReqSizeBytes = Number(Boolean(useInfoSize)) * estimatedFiSize * count + (fileSize ? fileSize : 0)
const { remainingBytes } = calculateStampCapacityMetrics(stamp, filesPerDrives, redundancyLevel)
const ok = remainingBytes >= estimateReqSizeBytes
if (!ok) {
cb?.(
`Insufficient capacity. Required: ~${getHumanReadableFileSize(
estimateReqSizeBytes,
)} bytes, Available: ${getHumanReadableFileSize(remainingBytes)} bytes. Please top up the drive/stamp.`,
)
}
return { remainingBytes, totalSizeBytes: estimateReqSizeBytes, ok }
}