admin/monitor: progress with resume

Massimo Melina committed Jun 21, 2023 at 22:25 UTC c6be31aec83baf99c20b118639ef19e165bb26ad
7 files changed +51 -20
admin/src/MonitorPage.ts
+7 -3
@@ -118,10 +118,14 @@ function Connections() {
118 h(Box, { ml: 2, color: 'text.secondary' }, value)
119 )
120 const i = value?.lastIndexOf('/')
121 - const progress = row.uploadProgress ?? row.downloadProgress
121 return h(Fragment, {},
123 - progress !== undefined
124 - && h(IconProgress, { icon: row.uploadProgress ? Upload : Download, progress, sx: { mr: 1 } }),
122 + row.op && h(IconProgress, {
123 + icon: row.op === 'upload' ? Upload : Download,
124 + progress: row.opProgress,
125 + offset: row.opOffset,
126 + addTitle: row.opTotal && h('div', {}, "Total: " + formatBytes(row.opTotal)),
127 + sx: { mr: 1 }
128 + }),
129 value.slice(i + 1),
130 i > 0 && h(Box, { ml: 2, color: 'text.secondary' }, value.slice(0, i))
131 )
admin/src/misc.ts
+18 -4
@@ -171,18 +171,32 @@ export function isCtrlKey(ev: KeyboardEvent) {
171 return (ev.ctrlKey || isMac && ev.metaKey) && ev.key
172 }
173
174 -export function IconProgress({ icon, progress, sx }: { icon: SvgIconComponent, progress: number, sx?: SxProps }) {
174 +interface IconProgressProps {
175 + icon: SvgIconComponent,
176 + progress: number,
177 + offset?: number,
178 + sx?: SxProps,
179 + addTitle?: ReactNode
180 +}
181 +export function IconProgress({ icon, progress, offset, addTitle, sx }: IconProgressProps) {
182 return h(Fragment, {},
183 h(icon, { sx: { position:'absolute', ml: '4px' } }),
184 + h(CircularProgress, {
185 + value: progress * 100,
186 + variant: 'determinate',
187 + size: 32,
188 + sx: { position: 'absolute' },
189 + }),
190 h(Tooltip, {
178 - title: formatPerc(progress),
191 + title: h(Fragment, {}, formatPerc(progress), addTitle),
192 children: h(CircularProgress, {
180 - value: progress*100,
193 + color: 'success',
194 + value: (offset || 1e-7) * 100,
195 variant: 'determinate',
196 size: 32,
197 sx,
198 }),
185 - }),
199 + })
200 )
201 }
202
src/api.monitor.ts
+1 -1
@@ -75,7 +75,7 @@ const apis: ApiHandlers = {
75 v: (socket.remoteFamily?.endsWith('6') ? 6 : 4),
76 got: socket.bytesRead,
77 sent: socket.bytesWritten,
78 - downloadProgress: conn.downloadProgress,
78 + ..._.pick(conn, ['op', 'opTotal', 'opOffset', 'opProgress']),
79 started,
80 secure: (secure || undefined) as boolean|undefined, // undefined will save some space once json-ed
81 ...fromCtx(conn.ctx),
src/connections.ts
+4 -2
@@ -11,8 +11,10 @@ export class Connection {
11 got = 0
12 outSpeed?: number
13 inSpeed?: number
14 - downloadProgress?: number
15 - uploadProgress?: number
14 + op?: 'download' | 'upload'
15 + opTotal?: number
16 + opProgress?: number
17 + opOffset?: number
18 uploadPath?: string
19 ctx?: Koa.Context
20 private _cachedIp?: string
src/serveFile.ts
+14 -6
@@ -18,6 +18,7 @@ import { matches } from './misc'
18 import _ from 'lodash'
19 import path from 'path'
20 import { promisify } from 'util'
21 +import { updateConnection } from './connections'
22
23 const allowedReferer = defineConfig('allowed_referer', '')
24
@@ -32,17 +33,17 @@ export function serveFileNode(ctx: Koa.Context, node: VfsNode) {
33 if (ref && ref !== host() // automatic accept if referer is basically the hosting domain
34 && !matches(ref, allowed))
35 return ctx.status = HTTP_FORBIDDEN
35 -
36 - function host() {
37 - const s = ctx.get('host')
38 - return s[0] === '[' ? s.slice(1, s.indexOf(']')) : s?.split(':')[0]
39 - }
36 }
37
38 ctx.vfsNode = node // useful to tell service files from files shared by the user
39 if ('dl' in ctx.query) // please, download
40 ctx.attachment(name)
41 return serveFile(ctx, source||'', mimeString)
42 +
43 + function host() {
44 + const s = ctx.get('host')
45 + return s[0] === '[' ? s.slice(1, s.indexOf(']')) : s?.split(':')[0]
46 + }
47 }
48
49 const mimeCfg = defineConfig<Record<string,string>>('mime', { '*': 'auto' })
@@ -73,8 +74,15 @@ export async function serveFile(ctx: Koa.Context, source:string, mime?:string, c
74 return ctx.status = HTTP_NOT_MODIFIED
75 if (content !== undefined)
76 return ctx.body = content
76 - const range = getRange(ctx, stats.size)
77 + const { size } = stats
78 + const range = getRange(ctx, size)
79 ctx.body = createReadStream(source, range)
80 + if (ctx.vfsNode)
81 + updateConnection(ctx.state.connection, {
82 + op: 'download',
83 + opTotal: stats.size,
84 + opOffset: range && (range.start / size),
85 + })
86 }
87 catch (e: any) {
88 return ctx.status = HTTP_NOT_FOUND
src/throttler.ts
+1 -1
@@ -52,7 +52,7 @@ export const throttler: Koa.Middleware = async (ctx, next) => {
52 updateConnection(conn, {
53 outSpeed,
54 sent: conn.socket.bytesWritten,
55 - downloadProgress: ts.getBytesSent() / downloadTotal,
55 + opProgress: (conn.opOffset || 0) + ts.getBytesSent() / conn.opTotal!,
56 })
57 /* in case this stream stands still for a while (before the end), we'll have neither 'sent' or 'close' events,
58 * so who will take care to updateConnection? This artificial next-call will ensure just that */
src/upload.ts
+6 -3
@@ -49,7 +49,7 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
49 const resumable = fs.existsSync(tempName) && tempName
50 if (resumable)
51 tempName = join(dir, 'hfs$upload2-' + keepName)
52 - const resume = Number(ctx.query.resume)
52 + let resume = Number(ctx.query.resume)
53 const size = resumable && try_(() => fs.statSync(resumable).size)
54 if (size === undefined) // stat failed
55 return fail(HTTP_SERVER_ERROR)
@@ -65,6 +65,8 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
65 }) )
66 }
67 const resuming = resume && resumable
68 + if (!resuming)
69 + resume = 0
70 const ret = resuming ? fs.createWriteStream(resumable, { flags: 'r+', start: resume })
71 : fs.createWriteStream(tempName)
72 if (resuming) {
@@ -103,14 +105,15 @@ export function uploadWriter(base: VfsNode, path: string, ctx: Koa.Context) {
105 const conn = socket2connection(ctx.socket)
106 if (!conn) return ()=>{}
107 ctx.state.uploadPath = ctx.path + path
106 - updateConnection(conn, { ctx })
108 + const opTotal = reqSize + resume
109 + updateConnection(conn, { ctx, op: 'upload', opTotal, opOffset: resume / opTotal })
110 const h = setInterval(() => {
111 const now = Date.now()
112 const got = ret.bytesWritten
113 const inSpeed = roundSpeed((got - lastGot) / (now - lastGotTime))
114 lastGot = got
115 lastGotTime = now
113 - updateConnection(conn, { inSpeed, got, uploadProgress: _.round(got / reqSize, 3) })
116 + updateConnection(conn, { inSpeed, got, opProgress: (resume + got) / opTotal })
117 }, 1000)
118 ret.once('close', () => clearInterval(h) )
119 }