upload: ETA

Massimo Melina committed Jan 24, 2023 at 18:55 UTC b806f31b98f5e1a6998ee864d0f4daacf26910e4
1 file changed +50 -7
frontend/src/upload.ts
+50 -7
@@ -1,6 +1,6 @@
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 -import { createElement as h, useState } from 'react'
3 +import { createElement as h, useMemo, useState } from 'react'
4 import { Flex, FlexV } from './components'
5 import { DialogCloser, formatBytes, hIcon, newDialog, prefix } from './misc'
6 import _ from 'lodash'
@@ -16,8 +16,14 @@ export const uploadState = proxy<{
16 qs: { to: string, files: File[] }[]
17 paused: boolean
18 uploading?: File
19 - progress: number
19 + progress: number // percentage
20 + partial: number // relative to uploading file. This is how much we have done of the current queue.
21 + speed: number
22 + eta: number
23 }>({
24 + eta: 0,
25 + speed: 0,
26 + partial: 0,
27 progress: 0,
28 paused: false,
29 qs: [],
@@ -26,6 +32,25 @@ export const uploadState = proxy<{
32 done: 0,
33 })
34
35 +// keep track of speed
36 +let bytesSentTimestamp = Date.now()
37 +let bytesSent = 0
38 +setInterval(() => {
39 + const now = Date.now()
40 + const passed = (now - bytesSentTimestamp) / 1000
41 + if (passed < 3 && uploadState.speed) return
42 + uploadState.speed = bytesSent / passed
43 + bytesSent = 0 // reset counter
44 + bytesSentTimestamp = now
45 +}, 1_000)
46 +
47 +// keep track of ETA
48 +setInterval(() => {
49 + const qBytes = _.sumBy(uploadState.qs, q => _.sumBy(q.files, f => f.size))
50 + const left = (qBytes - uploadState.partial)
51 + uploadState.eta = uploadState.speed && Math.round(left / uploadState.speed)
52 +}, 1000)
53 +
54 let reloadOnClose = false
55
56 export function showUpload() {
@@ -49,7 +74,9 @@ export function showUpload() {
74
75 function Content(){
76 const [files, setFiles] = useState([] as File[])
52 - const { qs, done, doneByte, paused, errors } = useSnapshot(uploadState)
77 + const { qs, done, doneByte, paused, errors, eta } = useSnapshot(uploadState)
78 + const etaStr = useMemo(() => !eta ? '' : formatTime(eta*1000, 0, 2), [eta])
79 +
80 return h(FlexV, {},
81 h(Flex, { gap: '.5em', flexWrap: 'wrap', justifyContent: 'center', position: 'sticky', top: -4, background: 'var(--bg)', boxShadow: '0 3px 3px #000' },
82 h('button',{ onClick: () => selectFiles() }, "Add file(s)"),
@@ -81,8 +108,7 @@ export function showUpload() {
108 h('div', {}, [done && `${done} finished (${formatBytes(doneByte)})`, errors && `${errors} failed`].filter(Boolean).join(' – ')),
109 qs.length > 0 && h('div', {},
110 h(Flex, { alignItems: 'center', justifyContent: 'center', borderTop: '1px dashed', padding: '.5em' },
84 - "Queue",
85 - `(${_.sumBy(qs, q => q.files.length)})`,
111 + `${_.sumBy(qs, q => q.files.length)} in queue${prefix(', ', etaStr)}`,
112 iconBtn('trash', ()=> {
113 uploadState.qs = []
114 abortCurrentUpload()
@@ -155,6 +181,17 @@ function formatPerc(p: number) {
181 return (p*100).toFixed(1) + '%'
182 }
183
184 +function formatTime(t: number, decimals=0, length=Infinity) {
185 + t /= 1000
186 + const ret = [(t % 1).toFixed(decimals).slice(1)]
187 + for (const [c,mod,pad] of [['s', 60, 2], ['m', 60, 2], ['h', 24], ['d', 36], ['y', 1 ]] as [string,number,number|undefined][]) {
188 + ret.push( _.padStart(String(t % mod | 0), pad || 0,'0') + c )
189 + t /= mod
190 + if (t < 1) break
191 + }
192 + return ret.slice(-length).reverse().join('')
193 +}
194 +
195 /// Manage upload queue
196
197 subscribe(uploadState, () => {
@@ -191,8 +228,13 @@ async function startUpload(f: File, to: string, resume=0) {
228 if (!resuming)
229 next()
230 }
194 - req.upload.onprogress = (e:any) =>
195 - uploadState.progress = (e.loaded + resume) / (e.total + resume)
231 + let lastProgress = 0
232 + req.upload.onprogress = (e:any) => {
233 + uploadState.partial = e.loaded + resume
234 + uploadState.progress = uploadState.partial / (e.total + resume)
235 + bytesSent += e.loaded - lastProgress
236 + lastProgress = e.loaded
237 + }
238 req.open('POST', to + '?' + new URLSearchParams({ notificationChannel, resume: String(resume) }), true)
239 const form = new FormData()
240 form.append('file', f.slice(resume), path(f))
@@ -241,6 +283,7 @@ async function startUpload(f: File, to: string, resume=0) {
283 function next() {
284 closeResumeDialog?.()
285 uploadState.uploading = undefined
286 + uploadState.partial = 0
287 const { qs } = uploadState
288 if (!qs.length) return
289 qs[0].files.shift()