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()