Skip to content

Commit

Permalink
admin/monitor: show upload progress
Browse files Browse the repository at this point in the history
  • Loading branch information
rejetto committed Mar 7, 2023
1 parent 5c05331 commit 10a3593
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 21 deletions.
19 changes: 12 additions & 7 deletions admin/src/MonitorPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import _ from "lodash"
import { createElement as h, useMemo, Fragment, useState } from "react"
import { apiCall, useApiEvents, useApiEx, useApiList } from "./api"
import { PauseCircle, PlayCircle, Delete, Lock, Block, FolderZip } from '@mui/icons-material'
import { PauseCircle, PlayCircle, Delete, Lock, Block, FolderZip, Upload } from '@mui/icons-material'
import { Alert, Box, Chip, ChipProps } from '@mui/material'
import { DataGrid } from "@mui/x-data-grid"
import { formatBytes, IconBtn, iconTooltip, manipulateConfig, useBreakpoint } from "./misc"
import { formatBytes, IconBtn, IconProgress, iconTooltip, manipulateConfig, useBreakpoint } from "./misc"
import { Field, SelectField } from '@hfs/mui-grid-form'
import { GridColumns } from '@mui/x-data-grid/models/colDef/gridColDef'
import { StandardCSSProperties } from '@mui/system/styleFunctionSx/StandardCssProperties'
Expand All @@ -33,6 +33,7 @@ function MoreInfo() {
md && pair('https', { label: "HTTPS", render: port }),
sm && pair('connections'),
pair('sent', { render: formatBytes, minWidth: '4em' }),
sm && pair('got', { render: formatBytes, minWidth: '4em' }),
pair('outSpeed', { label: "Output speed", render: formatSpeed }),
)

Expand Down Expand Up @@ -117,8 +118,12 @@ function Connections() {
h(Box, { ml: 2, color: 'text.secondary' }, value)
)
const i = value?.lastIndexOf('/')
return h(Fragment, {}, value.slice(i + 1),
i > 0 && h(Box, { ml: 2, color: 'text.secondary' }, value.slice(0, i)))
return h(Fragment, {},
row.uploadProgress !== undefined
&& h(IconProgress, { icon: Upload, progress: row.uploadProgress, sx: { mr: 1 } }),
value.slice(i + 1),
i > 0 && h(Box, { ml: 2, color: 'text.secondary' }, value.slice(0, i))
)
}
},
{
Expand All @@ -135,13 +140,13 @@ function Connections() {
field: 'outSpeed',
headerName: "Speed",
type: 'number',
valueFormatter: ({ value }) => formatSpeed(value)
renderCell: ({ value, row }) => formatSpeed(Math.max(value||0, row.inSpeed||0))
},
{
field: 'sent',
headerName: "Total",
type: 'number',
valueFormatter: ({ value }) => formatBytes(value as number)
renderCell: ({ value, row}) => formatBytes(Math.max(value||0, row.got||0))
},
{
field: 'agent',
Expand Down Expand Up @@ -176,7 +181,7 @@ function Connections() {
fullWidth: false,
value: filtered,
onChange: setFiltered as any,
options: { "Show only downloads": true, "Show all connections": false }
options: { "Show only files": true, "Show all connections": false }
}),

h(Box, { flex: 1 }),
Expand Down
18 changes: 16 additions & 2 deletions admin/src/misc.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt

import { createElement as h, FC, ReactNode } from 'react'
import { createElement as h, FC, Fragment, ReactNode } from 'react'
import { Box, Breakpoint, CircularProgress, IconButton, Link, Tooltip, useMediaQuery } from '@mui/material'
import { Link as RouterLink } from 'react-router-dom'
import { SxProps } from '@mui/system'
import { Refresh, SvgIconComponent } from '@mui/icons-material'
import { alertDialog, confirmDialog } from './dialog'
import { apiCall } from './api'
import { findFirst, onlyTruthy, useStateMounted } from '@hfs/shared'
import { findFirst, formatPerc, onlyTruthy, useStateMounted } from '@hfs/shared'
export * from '@hfs/shared'

export function spinner() {
Expand Down Expand Up @@ -140,3 +140,17 @@ export function isCtrlKey(ev: React.KeyboardEvent) {
return (ev.ctrlKey || isMac && ev.metaKey) && ev.key
}

export function IconProgress({ icon, progress, sx }: { icon: SvgIconComponent, progress: number, sx?: SxProps }) {
return h(Fragment, {},
h(icon, { sx: { position:'absolute', ml: '4px' } }),
h(Tooltip, {
title: formatPerc(progress),
children: h(CircularProgress, {
value: progress*100,
variant: 'determinate',
size: 32,
sx,
}),
}),
)
}
6 changes: 1 addition & 5 deletions frontend/src/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { createElement as h, Fragment, useMemo, useState } from 'react'
import { Flex, FlexV } from './components'
import { closeDialog, DialogCloser, formatBytes, hIcon, newDialog, prefix, selectFiles } from './misc'
import { closeDialog, DialogCloser, formatBytes, formatPerc, hIcon, newDialog, prefix, selectFiles } from './misc'
import _ from 'lodash'
import { proxy, ref, subscribe, useSnapshot } from 'valtio'
import { alertDialog, confirmDialog, promptDialog } from './dialog'
Expand Down Expand Up @@ -183,10 +183,6 @@ function iconBtn(icon: string, onClick: () => any, { small=true, style={}, ...pr
)
}

function formatPerc(p: number) {
return (p*100).toFixed(1) + '%'
}

function formatTime(time: number, decimals=0, length=Infinity) {
time /= 1000
const ret = [(time % 1).toFixed(decimals).slice(1)]
Expand Down
6 changes: 5 additions & 1 deletion shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,8 @@ export function readFile(f: File | Blob) {
})
reader.readAsText(f)
})
}
}

export function formatPerc(p: number) {
return (p*100).toFixed(1) + '%'
}
4 changes: 3 additions & 1 deletion src/api.monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ const apis: ApiHandlers = {
user: getCurrentUsername(ctx),
agent: getBrowser(ctx.get('user-agent')),
archive: ctx.state.archive,
path: (ctx.fileSource || ctx.state.archive) && ctx.path // only for downloading files
upload: ctx.state.uploadProgress,
path: ctx.state.uploadPath
|| (ctx.fileSource || ctx.state.archive) && ctx.path // only uploads and downloads
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions src/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import _ from 'lodash'
export class Connection {
readonly started = new Date()
sent = 0
got = 0
outSpeed?: number
inSpeed?: number
uploadProgress?: number
uploadPath?: string
ctx?: Koa.Context
private _cachedIp?: string
[rest:symbol]: any // let other modules add extra data, but using symbols to avoid name collision
Expand Down
8 changes: 4 additions & 4 deletions src/throttler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const throttler: Koa.Middleware = async (ctx, next) => {
const DELAY = 1000
const update = _.debounce(() => {
const ts = conn[SymThrStr] as ThrottledStream
const outSpeed = roundKb(ts.getSpeed())
const outSpeed = roundSpeed(ts.getSpeed())
updateConnection(conn, { outSpeed, sent: ts.getBytesSent() })
/* in case this stream stands still for a while (before the end), we'll have neither 'sent' or 'close' events,
* so who will take care to updateConnection? This artificial next-call will ensure just that */
Expand Down Expand Up @@ -77,7 +77,7 @@ export const throttler: Koa.Middleware = async (ctx, next) => {
ctx.response.length = bak
}

function roundKb(n: number) {
export function roundSpeed(n: number) {
return _.round(n, 1) || _.round(n, 3) // further precision if necessary
}

Expand All @@ -97,8 +97,8 @@ setInterval(() => {
lastSent = totalSent
const deltaGotKb = (totalGot - lastGot) / 1000
lastGot = totalGot
totalOutSpeed = roundKb(deltaSentKb / past)
totalInSpeed = roundKb(deltaGotKb / past)
totalOutSpeed = roundSpeed(deltaSentKb / past)
totalInSpeed = roundSpeed(deltaGotKb / past)
}, 1000)

events.on('connection', (c: Connection) =>
Expand Down
24 changes: 23 additions & 1 deletion src/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { Callback, try_ } from './misc'
import { notifyClient } from './frontEndApis'
import { defineConfig } from './config'
import { getFreeDiskSync } from './util-os'
import { socket2connection, updateConnection } from './connections'
import { roundSpeed } from './throttler'
import _ from 'lodash'

export const deleteUnfinishedUploadsAfter = defineConfig('delete_unfinished_uploads_after')
export const minAvailableMb = defineConfig('min_available_mb', 100)
Expand Down Expand Up @@ -67,7 +70,8 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
tempName = resumable
}
cancelDeletion(tempName)
ret.on('close', () => {
trackProgress()
ret.once('close', () => {
if (!ctx.req.aborted) {
let dest = fullPath
if (dontOverwriteUploading.get() && fs.existsSync(dest)) {
Expand All @@ -91,6 +95,24 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
})
return ret

function trackProgress() {
let lastGot = 0
let lastGotTime = 0
const conn = socket2connection(ctx.socket)
if (!conn) return ()=>{}
ctx.state.uploadPath = ctx.path + path
updateConnection(conn, { ctx })
const h = setInterval(() => {
const now = Date.now()
const got = ret.bytesWritten
const inSpeed = roundSpeed((got - lastGot) / (now - lastGotTime))
lastGot = got
lastGotTime = now
updateConnection(conn, { inSpeed, got, uploadProgress: _.round(got / reqSize, 3) })
}, 1000)
ret.once('close', () => clearInterval(h) )
}

function delayedDelete(path: string, secs: number, cb?: Callback) {
clearTimeout(waitingToBeDeleted[path])
waitingToBeDeleted[path] = setTimeout(() => {
Expand Down

0 comments on commit 10a3593

Please sign in to comment.