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
}