@samitouri / QOSami-HFS / commits / 4df66052

folder-size: show partial

Massimo Melina committed Jan 16, 2025 at 12:45 UTC 4df660528670c4284b9076b7290bd5b1f4bd93a6
2 files changed +33 -13
frontend/src/fileMenu.ts
+18 -9
@@ -1,6 +1,6 @@
1 import {
2 dontBotherWithKeys, formatBytes, getHFS, hfsEvent, hIcon, newDialog, prefix, with_, working,
3 - pathEncode, closeDialog, anyDialogOpen, Falsy, operationSuccessful
3 + pathEncode, closeDialog, anyDialogOpen, Falsy, operationSuccessful, randomId
4 } from './misc'
5 import { createElement as h, Fragment, isValidElement, MouseEvent, ReactNode, useState } from 'react'
6 import { Btn, Bytes, Spinner } from './components'
@@ -15,6 +15,7 @@ import { apiCall, useApi } from '@hfs/shared/api'
15 import { inputComment } from './upload'
16 import { cut } from './clip'
17 import { loginDialog } from './login'
18 +import { useInterval } from 'usehooks-ts'
19 import i18n from './i18n'
20 const { t, useI18N } = i18n
21
@@ -145,14 +146,22 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (Falsy
146 })
147
148 function FolderSize() {
148 - const [size, setSize] = useState<any>(null)
149 - useApi(size !== null && 'get_folder_size', { uri: entry.uri }, {
150 - timeout: false,
151 - onResponse: (res, data) => setSize(data.bytes)
152 - })
153 - return size === null ? h(Btn, { asText: true, label: t`Calculate`, onClick() { setSize(true) } })
154 - : size === true ? h(Btn, { asText: true, label: t`Cancel`, icon: h(Spinner), onClick() { setSize(null) } })
155 - : h(Bytes, { bytes: size })
149 + const [go, setGo] = useState(false)
150 + const [id] = useState(() => randomId())
151 + const { data } = useApi(go && 'get_folder_size', { uri: entry.uri, id }, { timeout: false })
152 + const partial = useApi(go && !data && 'get_folder_size_partial', { id })
153 + useInterval(partial.reload, 1000)
154 + return data ? showRes(data)
155 + : !go ? h(Btn, { asText: true, label: t`Calculate`, onClick() { setGo(true) } })
156 + : h('span', {},
157 + showRes(partial.data),
158 + ' ',
159 + h(Btn, { asText: true, label: t`Cancel`, icon: h(Spinner), onClick() { setGo(false) } })
160 + )
161 +
162 + function showRes(data: any) {
163 + return data && h('span', {}, h(Bytes, _.pick(data,'bytes')), ' / ', t('n_files', { n: data.files.toLocaleString() }, '{n,plural,one{# file} other{# files}}') )
164 + }
165 }
166 }
167
src/frontEndApis.ts
+15 -4
@@ -17,12 +17,14 @@ import fs from 'fs'
17 import { mkdir, rename, copyFile, unlink } from 'fs/promises'
18 import { basename, dirname, join } from 'path'
19 import { getUploadMeta } from './upload'
20 -import { apiAssertTypes, deleteNode } from './misc'
20 +import { apiAssertTypes, deleteNode, popKey } from './misc'
21 import { getCommentFor, setCommentFor } from './comments'
22 import { SendListReadable } from './SendList'
23 import { ctxAdminAccess } from './adminApis'
24 import _ from 'lodash'
25
26 +const partialFolderSize: any = {}
27 +
28 export const frontEndApis: ApiHandlers = {
29 get_file_list,
30 ...api_auth,
@@ -171,7 +173,12 @@ export const frontEndApis: ApiHandlers = {
173 return {}
174 },
175
174 - async get_folder_size({ uri }, ctx) {
176 + async get_folder_size_partial({ id }, ctx) {
177 + apiAssertTypes({ string: { id } })
178 + return partialFolderSize[id] || new ApiError(HTTP_NOT_FOUND)
179 + },
180 +
181 + async get_folder_size({ uri, id }, ctx) {
182 apiAssertTypes({ string: { uri } })
183 const folder = await urlToNode(uri, ctx)
184 if (!folder)
@@ -181,9 +188,13 @@ export const frontEndApis: ApiHandlers = {
188 if (statusCodeForMissingPerm(folder, 'can_list', ctx))
189 return new ApiError(ctx.status)
190 let bytes = 0
184 - for await (const n of walkNode(folder, { ctx, onlyFiles: true, depth: Infinity }))
191 + let files = 0
192 + for await (const n of walkNode(folder, { ctx, onlyFiles: true, depth: Infinity })) {
193 bytes += await nodeStats(n).then(x => x?.size || 0, () => 0)
186 - return { bytes }
194 + files++
195 + partialFolderSize[id] = { bytes, files }
196 + }
197 + return popKey(partialFolderSize, id)
198 },
199 }
200