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