@samitouri / QOSami-HFS / commits / 1f469d3b

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 || '.'))