admin/monitoring: track progress for zip-s

Massimo Melina committed Dec 30, 2023 at 23:37 UTC 7ff0d2033bafa305147324b48632aa929379b07c
4 files changed +27 -23
admin/src/MonitorPage.ts
+7 -13
@@ -4,9 +4,9 @@ import _ from "lodash"
4 import { createElement as h, useMemo, Fragment, useState } from "react"
5 import { apiCall, useApiEvents, useApiEx, useApiList } from "./api"
6 import { LinkOff, Lock, Block, FolderZip, Upload, Download } from '@mui/icons-material'
7 -import { Box, Chip, ChipProps, Tooltip } from '@mui/material'
7 +import { Box, Chip, ChipProps } from '@mui/material'
8 import { DataTable } from './DataTable'
9 -import { formatBytes, ipForUrl, manipulateConfig, CFG, formatSpeed } from "./misc"
9 +import { formatBytes, ipForUrl, manipulateConfig, CFG, formatSpeed, with_ } from "./misc"
10 import { IconBtn, IconProgress, iconTooltip, usePauseButton, useBreakpoint, Country } from './mui'
11 import { Field, SelectField } from '@hfs/mui-grid-form'
12 import { StandardCSSProperties } from '@mui/system/styleFunctionSx/StandardCssProperties'
@@ -141,26 +141,20 @@ function Connections() {
141 flex: 1.5,
142 renderCell({ value, row }) {
143 if (!value) return
144 - if (row.archive)
145 - return h(Fragment, {},
146 - h(FolderZip, { sx: { mr: 1 } }),
147 - row.archive,
148 - h(Box, { ml: 2, color: 'text.secondary' }, value)
149 - )
144 if (!row.op)
145 return h(Box, {}, value, h(Box, { fontSize: 'x-small' }, "browsing"))
152 - const i = value?.lastIndexOf('/')
146 return h(Fragment, {},
147 h(IconProgress, {
155 - icon: row.op === 'upload' ? Upload : Download,
148 + icon: row.archive ? FolderZip : row.op === 'upload' ? Upload : Download,
149 progress: row.opProgress ?? row.opOffset,
150 offset: row.opOffset,
151 addTitle: row.opTotal && h('div', {}, "Total: " + formatBytes(row.opTotal)),
152 sx: { mr: 1 }
153 }),
161 - h(Box, {}, value.slice(i + 1),
162 - i > 0 && h(Box, { ml: 2, fontSize: 'x-small', color: 'text.secondary' }, value.slice(0, i))
163 - ),
154 + row.archive ? h(Box, {}, value, h(Box, { fontSize: 'x-small', color: 'text.secondary' }, row.archive))
155 + : with_(value?.lastIndexOf('/'), i => h(Box, {}, value.slice(i + 1),
156 + i > 0 && h(Box, { fontSize: 'x-small', color: 'text.secondary' }, value.slice(0, i))
157 + )),
158 )
159 }
160 },
admin/src/mui.ts
+1 -1
@@ -42,7 +42,7 @@ export function IconProgress({ icon, progress, offset, addTitle, sx }: IconProgr
42 sx: { position: 'absolute' },
43 }),
44 h(Tooltip, {
45 - title: h(Fragment, {}, formatPerc(progress), addTitle),
45 + title: h(Fragment, {}, _.isNumber(progress) ? formatPerc(progress) : "Size unknown", addTitle),
46 children: h(CircularProgress, {
47 color: 'success',
48 value: (offset || 1e-7) * 100,
src/serveFile.ts
+17 -8
@@ -14,6 +14,7 @@ import { promisify } from 'util'
14 import { updateConnection } from './connections'
15 import { getCurrentUsername } from './auth'
16 import { sendErrorPage } from './errorPages'
17 +import { Readable } from 'stream'
18
19 const allowedReferer = defineConfig('allowed_referer', '')
20 const limitDownloads = downloadLimiter(defineConfig(CFG.max_downloads, 0), () => true)
@@ -80,21 +81,29 @@ export async function serveFile(ctx: Koa.Context, source:string, mime?:string, c
81 return ctx.body = content
82 const { size } = stats
83 const range = getRange(ctx, size)
83 - ctx.body = createReadStream(source, range).on('end', () =>
84 - updateConnection(ctx.state.connection, { opProgress: 1 }) )
84 + ctx.body = createReadStream(source, range)
85 if (ctx.vfsNode)
86 - updateConnection(ctx.state.connection, {
87 - ctx, // this will cause 'path' to be sent as well
88 - op: 'download',
89 - opTotal: stats.size,
90 - opOffset: range && (range.start / size),
91 - })
86 + monitorAsDownload(ctx, size, range?.start)
87 }
88 catch (e: any) {
89 return ctx.status = HTTP_NOT_FOUND
90 }
91 }
92
93 +export function monitorAsDownload(ctx: Koa.Context, size?: number, offset?: number) {
94 + if (!(ctx.body instanceof Readable))
95 + throw 'incompatible body'
96 + const { connection } = ctx.state
97 + ctx.body.on('end', () =>
98 + updateConnection(connection, { opProgress: 1 }) )
99 + updateConnection(connection, {
100 + ctx, // this will cause 'path' to be sent as well
101 + op: 'download',
102 + opTotal: size,
103 + opOffset: size && offset && (offset / size),
104 + })
105 +}
106 +
107 export function getRange(ctx: Koa.Context, totalSize: number) {
108 ctx.set('Accept-Ranges', 'bytes')
109 const { range } = ctx.request.header
src/zip.ts
+2 -1
@@ -8,7 +8,7 @@ import { createReadStream } from 'fs'
8 import fs from 'fs/promises'
9 import { defineConfig } from './config'
10 import { basename, dirname } from 'path'
11 -import { getRange } from './serveFile'
11 +import { getRange, monitorAsDownload } from './serveFile'
12 import { HTTP_OK } from './const'
13
14 // expects 'node' to have had permissions checked by caller
@@ -76,6 +76,7 @@ export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
76 ctx.body = zip
77 ctx.req.on('close', ()=> zip.destroy())
78 ctx.state.archive = 'zip'
79 + monitorAsDownload(ctx, size, range?.start)
80 }
81
82 const zipSeconds = defineConfig('zip_calculate_size_for_seconds', 1)