folder menu: "folder size" #535
Massimo Melina committed
Dec 29, 2024 at 20:01 UTC
1f469d3bb5a2862cc278760a8db58ff1c2744a8a
5 files changed
+32
-8
frontend/src/dialog.ts
+2
-2
@@ -115,13 +115,13 @@ export async function formDialog({ ...rest }: DialogOptions): Promise<any> {
115
116
export type AlertType = 'error' | 'warning' | 'info'
117
118
-export function alertDialog(msg: ReactElement | string | Error, type:AlertType='info') {
118
+export function alertDialog(msg: ReactElement | string | Error, type:AlertType='info', title='') {
119
if (msg instanceof Error)
120
type = 'error'
121
const ret = pendingPromise()
122
return Object.assign(ret, newDialog({
123
className: 'dialog-alert dialog-alert-'+type,
124
- title: t(_.capitalize(type)),
124
+ title: title || t(_.capitalize(type)),
125
icon: '!',
126
onClose: ret.resolve,
127
dialogProps: { role: 'alertdialog' },
frontend/src/fileMenu.ts
+6
@@ -67,6 +67,7 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (Falsy
67
}),
68
state.props?.can_delete && { id: 'rename', label: t`Rename`, icon: 'edit', onClick: () => rename(entry) },
69
state.props?.can_delete && { id: 'cut', label: t`Cut`, icon: 'cut', onClick: () => close(cut([entry])) },
70
+ isFolder && { id: 'folderSize', label: t`Folder size`, icon: 'total', onClick: () => folderSize() },
71
isFolder && !entry.web && !entry.cantOpen && { id: 'list', label: t`Get list`, href: uri + '?get=list&folders=*', icon: 'list' },
72
].filter(Boolean)
73
const folder = entry.n.slice(0, -entry.name.length - (entry.isFolder ? 2 : 1))
@@ -141,6 +142,11 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (Falsy
142
)
143
}
144
})
145
+
146
+ async function folderSize() {
147
+ const { bytes } = await apiCall('get_folder_size', { uri: entry.uri }, { modal: working, timeout: false })
148
+ await alertDialog(formatBytes(bytes), 'info', t`Folder size`)
149
+ }
150
}
151
152
async function rename(entry: DirEntry) {
frontend/src/sysIcons.ts
+1
@@ -44,5 +44,6 @@ export const SYS_ICONS: Record<string, [string] | [string, string | false]> = {
44
video: ['🎥'],
45
image: ['📸'],
46
cancel: ['❌','cancel'],
47
+ total: ['➕', 'spin6'],
48
}
49
src/frontEndApis.ts
+20
-3
@@ -6,9 +6,13 @@ import * as api_auth from './api.auth'
6
import events from './events'
7
import Koa from 'koa'
8
import { dirTraversal, isValidFileName } from './util-files'
9
-import { HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FAILED_DEPENDENCY, HTTP_FORBIDDEN,
10
- HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_UNAUTHORIZED } from './const'
11
-import { hasPermission, statusCodeForMissingPerm, urlToNode, VfsNode } from './vfs'
9
+import {
10
+ HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FAILED_DEPENDENCY, HTTP_FORBIDDEN, HTTP_METHOD_NOT_ALLOWED,
11
+ HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_UNAUTHORIZED
12
+} from './const'
13
+import {
14
+ hasPermission, nodeIsDirectory, nodeStats, statusCodeForMissingPerm, urlToNode, VfsNode, walkNode
15
+} from './vfs'
16
import fs from 'fs'
17
import { mkdir, rename, copyFile, unlink } from 'fs/promises'
18
import { basename, dirname, join } from 'path'
@@ -166,6 +170,19 @@ export const frontEndApis: ApiHandlers = {
170
await setCommentFor(node.source, comment)
171
return {}
172
},
173
+
174
+ async get_folder_size({ uri }, ctx) {
175
+ apiAssertTypes({ string: { uri } })
176
+ const folder = await urlToNode(uri, ctx)
177
+ if (!folder)
178
+ throw new ApiError(HTTP_NOT_FOUND)
179
+ if (!await nodeIsDirectory(folder))
180
+ throw new ApiError(HTTP_METHOD_NOT_ALLOWED)
181
+ let bytes = 0
182
+ for await (const n of walkNode(folder, { ctx, onlyFiles: true, depth: Infinity }))
183
+ bytes += await nodeStats(n).then(x => x?.size || 0, () => 0)
184
+ return { bytes }
185
+ },
186
}
187
188
export function notifyClient(channel: string | Koa.Context, name: string, data: any) {
src/vfs.ts
+3
-3
@@ -127,7 +127,7 @@ export async function urlToNode(url: string, ctx?: Koa.Context, parent: VfsNode=
127
return ret
128
}
129
130
-async function nodeStats(ret: VfsNode) {
130
+export async function nodeStats(ret: VfsNode) {
131
if (ret.stats)
132
if (_.isPlainObject(ret.stats)) delete ret.stats // legacy pre-55-alpha1
133
else return ret.stats
@@ -298,7 +298,7 @@ export async function* walkNode(parent: VfsNode, {
298
try {
299
let lastDir = prefixPath.slice(0, -1) || '.'
300
parentsCache.set(lastDir, parent)
301
- for await (const entry of dirStream(source, { depth, onlyFolders, onlyFiles, hidden: showHiddenFiles.get() })) {
301
+ for await (const entry of dirStream(source, { depth, onlyFolders, hidden: showHiddenFiles.get() })) {
302
if (ctx?.req.aborted)
303
return
304
const {path} = entry
@@ -319,7 +319,7 @@ export async function* walkNode(parent: VfsNode, {
319
}
320
if (isFolder) // store it even if we can't see it (masks), as its children can be produced by dirStream
321
parentsCache.set(name, item)
322
- if (await canSee(item))
322
+ if (!(onlyFiles && isFolder) && await canSee(item))
323
yield item
324
entry.closingBranch?.then(p =>
325
parentsCache.delete(p || '.'))