@samitouri / QOSami-HFS / commits / 54b87058

admin/fs: show disk-space inside file-picker

Massimo Melina committed Mar 1, 2024 at 11:45 UTC 54b87058b4baeb36d8737278a9c65c6e9dd22adc
6 files changed +32 -34
admin/src/FilePicker.ts
+10 -4
@@ -4,9 +4,9 @@ import { createElement as h, Fragment, useEffect, useMemo, useRef, useState } fr
4 import { apiCall, useApiList } from './api'
5 import _ from 'lodash'
6 import { Alert, Box, Button, Checkbox, ListItemIcon, ListItemText, MenuItem, TextField, Typography } from '@mui/material'
7 -import { enforceFinal, formatBytes, isWindowsDrive, err2msg, basename } from './misc'
8 -import { spinner, Center, IconBtn, Flex } from './mui'
9 -import { ArrowUpward, CreateNewFolder, VerticalAlignTop } from '@mui/icons-material'
7 +import { enforceFinal, formatBytes, isWindowsDrive, err2msg, basename, formatPerc } from './misc'
8 +import { spinner, Center, IconBtn, Flex, IconProgress } from './mui'
9 +import { ArrowUpward, CreateNewFolder, Storage, VerticalAlignTop } from '@mui/icons-material'
10 import { StringField } from '@hfs/mui-grid-form'
11 import { FileIcon, FolderIcon } from './VfsTree'
12 import { FixedSizeList } from 'react-window'
@@ -34,7 +34,7 @@ export default function FilePicker({ onSelect, multiple=true, files=true, folder
34 }
35 }).finally(() => setReady(true))
36 }, [from])
37 - const { list, error, connecting, reload } = useApiList<DirEntry>(ready && 'get_ls', { path: cwd, files, fileMask })
37 + const { list, props, error, connecting, reload } = useApiList<DirEntry>(ready && 'get_ls', { path: cwd, files, fileMask })
38 useEffect(() => {
39 setSel([])
40 setFilter('')
@@ -168,6 +168,12 @@ export default function FilePicker({ onSelect, multiple=true, files=true, folder
168 reload()
169 }
170 }),
171 + props?.total > 0 && h(IconProgress, {
172 + icon: Storage,
173 + progress: 1,
174 + offset: (props.total - props.free) / props.total,
175 + title: `${formatBytes(props.free)} available (${formatPerc(props.free / props.total)}) of ${formatBytes(props.total)}`,
176 + }),
177 ),
178 )
179 )
admin/src/MonitorPage.ts
+5 -12
@@ -6,17 +6,10 @@ import { apiCall, useApiEvents, useApiEx, useApiList } from "./api"
6 import { LinkOff, Lock, Block, FolderZip, Upload, Download, ChevronRight } from '@mui/icons-material'
7 import { Box, Chip, ChipProps } from '@mui/material'
8 import { DataTable } from './DataTable'
9 -import {
10 - formatBytes,
11 - ipForUrl,
12 - manipulateConfig,
13 - CFG,
14 - formatSpeed,
15 - with_,
16 - createDurationFormatter,
17 - formatTimestamp
18 -} from "./misc"
19 -import { IconBtn, IconProgress, iconTooltip, usePauseButton, useBreakpoint, Country, hTooltip, Btn } from './mui'
9 +import { formatBytes, ipForUrl, manipulateConfig, CFG, formatSpeed, with_, createDurationFormatter, formatTimestamp,
10 + formatPerc } from "./misc"
11 +import { IconBtn, IconProgress, iconTooltip, usePauseButton, useBreakpoint, Country, hTooltip } from './mui'
12 +import md from './md'
13 import { Field, SelectField } from '@hfs/mui-grid-form'
14 import { StandardCSSProperties } from '@mui/system/styleFunctionSx/StandardCssProperties'
15 import { agentIcons } from './LogsPage'
@@ -160,7 +153,7 @@ function Connections() {
153 icon: row.archive ? FolderZip : row.op === 'upload' ? Upload : Download,
154 progress: row.opProgress ?? row.opOffset,
155 offset: row.opOffset,
163 - addTitle: row.opTotal && ("Total: " + formatBytes(row.opTotal)),
156 + title: md(formatPerc(row.opProgress) + (row.opTotal ? "\nTotal: " + formatBytes(row.opTotal) : '')),
157 sx: { mr: 1 }
158 }),
159 row.archive ? h(Box, {}, value, h(Box, { fontSize: 'x-small', color: 'text.secondary' }, row.archive))
admin/src/mui.ts
+4 -9
@@ -48,10 +48,10 @@ interface IconProgressProps {
48 progress: number,
49 offset?: number,
50 sx?: SxProps,
51 - addTitle?: ReactNode
51 + title?: ReactNode
52 }
53 -export function IconProgress({ icon, progress, offset, addTitle, sx }: IconProgressProps) {
54 - return h(Fragment, {},
53 +export function IconProgress({ icon, progress, offset, title, sx }: IconProgressProps) {
54 + return h(Flex, { vert: true, center: true },
55 h(icon, { sx: { position:'absolute', ml: '4px' } }),
56 h(CircularProgress, {
57 value: progress * 100 || 0,
@@ -59,12 +59,7 @@ export function IconProgress({ icon, progress, offset, addTitle, sx }: IconProgr
59 size: 32,
60 sx: { position: 'absolute' },
61 }),
62 - hTooltip(
63 - h(Fragment, {},
64 - _.isNumber(progress) ? formatPerc(progress) : "Size unknown",
65 - addTitle && h('div', {}, addTitle)
66 - ),
67 - '',
62 + hTooltip(title ?? (_.isNumber(progress) ? formatPerc(progress) : "Size unknown"), '',
63 h(CircularProgress, {
64 color: 'success',
65 value: (offset || 1e-7) * 100,
src/api.vfs.ts
+3 -1
@@ -9,7 +9,7 @@ import { dirname, extname, join, resolve } from 'path'
9 import { dirStream, enforceFinal, isDirectory, isValidFileName, isWindowsDrive, makeMatcher, PERM_KEYS,
10 VfsNodeAdminSend } from './misc'
11 import { IS_WINDOWS, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE } from './const'
12 -import { getDrives } from './util-os'
12 +import { getDiskSpaceSync, getDrives } from './util-os'
13 import { getBaseUrlOrDefault, getServerStatus } from './listen'
14 import { promisify } from 'util'
15 import { execFile } from 'child_process'
@@ -191,6 +191,8 @@ const apis: ApiHandlers = {
191 }
192 return
193 }
194 + try { list.props(getDiskSpaceSync(path)) }
195 + catch {} // continue anyway
196 try {
197 const matching = makeMatcher(fileMask)
198 path = isWindowsDrive(path) ? path + '\\' : resolve(path || '/')
src/upload.ts
+2 -2
@@ -11,7 +11,7 @@ import fs from 'fs'
11 import { Callback, dirTraversal, escapeHTML, loadFileAttr, storeFileAttr, try_ } from './misc'
12 import { notifyClient } from './frontEndApis'
13 import { defineConfig } from './config'
14 -import { getFreeDiskSync } from './util-os'
14 +import { getDiskSpaceSync } from './util-os'
15 import { updateConnection, updateConnectionForCtx } from './connections'
16 import { roundSpeed } from './throttler'
17 import { getCurrentUsername } from './auth'
@@ -48,7 +48,7 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
48 const reqSize = Number(ctx.headers["content-length"])
49 if (reqSize)
50 try {
51 - const free = getFreeDiskSync(dir)
51 + const { free } = getDiskSpaceSync(dir)
52 if (typeof free !== 'number' || isNaN(free))
53 throw ''
54 if (reqSize > free - (min || 0))
src/util-os.ts
+8 -6
@@ -6,14 +6,16 @@ import { pid } from 'node:process'
6 import { promisify } from 'util'
7 import { IS_WINDOWS } from './const'
8
9 -export function getFreeDiskSync(path: string) {
9 +export function getDiskSpaceSync(path: string) {
10 if (IS_WINDOWS) {
11 const drive = resolve(path).slice(0, 2).toUpperCase()
12 - const out = execSync('wmic logicaldisk get FreeSpace,name /format:list').toString().replace(/\r/g, '')
12 + const out = execSync('wmic logicaldisk get size,FreeSpace,name /format:list').toString().replace(/\r/g, '')
13 const one = out.split(/\n\n+/).find(x => x.includes('Name=' + drive))
14 if (!one)
15 throw Error('miss')
16 - return Number(/FreeSpace=(\d+)/.exec(one)?.[1])
16 + const free = Number(/FreeSpace=(\d+)/.exec(one)?.[1])
17 + const total = Number(/Size=(\d+)/.exec(one)?.[1])
18 + return { free, total }
19 }
20 const out = try_(() => execSync(`df -k "${path}"`).toString(),
21 err => {
@@ -23,9 +25,9 @@ export function getFreeDiskSync(path: string) {
25 })
26 if (!out?.startsWith('Filesystem'))
27 throw Error('unsupported')
26 - const one = out.split('\n')[1]
27 - const free = Number(one.split(/\s+/)[3])
28 - return free * 1024
28 + const one = out.split('\n')[1] as string
29 + const [used, free] = one.split(/\s+/).slice(2, 4).map(x => Number(x) * 1024) as [number, number]
30 + return { free, total: used + free }
31 }
32
33 export async function getDrives() {