Rewritten uploads with queuing multiple tasks etc.

This commit is contained in:
Leo Vasanko 2023-11-08 12:06:06 -08:00
parent 348f8e183e
commit a3a6b3771c

View File

@ -1,41 +1,121 @@
<script setup lang="ts"> <script setup lang="ts">
import { connect, uploadUrl } from '@/repositories/WS'; import { connect, uploadUrl } from '@/repositories/WS';
import { useDocumentStore } from '@/stores/documents' import { useDocumentStore } from '@/stores/documents'
import { computed, reactive, ref } from 'vue' import { collator } from '@/utils';
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
const fileUploadButton = ref() const fileInput = ref()
const folderUploadButton = ref() const folderInput = ref()
const documentStore = useDocumentStore() const documentStore = useDocumentStore()
const isNotificationOpen = ref(false)
const props = defineProps({ const props = defineProps({
path: Array<string> path: Array<string>
}) })
function uploadHandler(event: Event) {
event.preventDefault()
event.stopPropagation()
// @ts-ignore
let infiles = Array.from(event.dataTransfer?.files || event.target.files) as File[]
if (!infiles.length) return
const loc = props.path!.join('/')
for (const f of infiles) {
f.cloudName = loc + '/' + (f.webkitRelativePath || f.name)
f.cloudPos = 0
}
const dotfiles = infiles.filter(f => f.cloudName.includes('/.'))
if (dotfiles.length) {
documentStore.error = "Won't upload dotfiles"
console.log("Dotfiles omitted", dotfiles)
infiles = infiles.filter(f => !f.cloudName.includes('/.'))
}
if (!infiles.length) return
infiles.sort((a, b) => collator.compare(a.cloudName, b.cloudName))
// @ts-ignore
upqueue = upqueue.concat(infiles)
statsAdd(infiles)
startWorker()
}
const cancelUploads = () => {
upqueue = []
statReset()
}
const uprogress_init = { const uprogress_init = {
total: 0, total: 0,
uploaded: 0, uploaded: 0,
t0: 0, t0: 0,
tlast: 0, tlast: 0,
filestart: [] as number[], statbytes: 0,
statdur: 0,
files: [],
filestart: 0,
fileidx: 0, fileidx: 0,
filecount: 0,
filename: '', filename: '',
filepos: 0,
filesize: 0, filesize: 0,
filepos: 0,
} }
const uprogress = reactive({...uprogress_init}) const uprogress = reactive({...uprogress_init})
const speed = computed(() => uprogress.uploaded / (uprogress.tlast - uprogress.t0) / 1e3) const percent = computed(() => uprogress.uploaded / uprogress.total * 100)
const resetProgress = () => { const speed = computed(() => {
let s = uprogress.statbytes / uprogress.statdur / 1e3
const tsince = (Date.now() - uprogress.tlast) / 1e3
if (tsince > 5 / s) return 0 // Less than fifth of previous speed => stalled
if (tsince > 1 / s) return 1 / tsince // Next block is late or not coming, decay
return s // "Current speed"
})
const speeddisp = computed(() => speed.value ? speed.value.toFixed(speed.value < 100 ? 1 : 0) + '\u202FMB/s': 'stalled')
setInterval(() => {
if (Date.now() - uprogress.tlast > 3000) {
// Reset
uprogress.statbytes = 0
uprogress.statdur = 1
} else {
// Running average by decay
uprogress.statbytes *= .9
uprogress.statdur *= .9
}
}, 100)
const statUpdate = ({name, size, start, end}) => {
if (name !== uprogress.filename) return // If stats have been reset
const now = Date.now()
uprogress.uploaded = uprogress.filestart + end
uprogress.filepos = end
uprogress.statbytes += end - start
uprogress.statdur += now - uprogress.tlast
uprogress.tlast = now
// File finished?
if (end === size) {
uprogress.filestart += size
statNextFile()
if (++uprogress.fileidx >= uprogress.filecount) statReset()
}
}
const statNextFile = () => {
const f = uprogress.files.shift()
if (!f) return statReset()
uprogress.filepos = 0
uprogress.filesize = f.size
uprogress.filename = f.cloudName
}
const statReset = () => {
Object.assign(uprogress, uprogress_init) Object.assign(uprogress, uprogress_init)
uprogress.t0 = Date.now() uprogress.t0 = Date.now()
uprogress.tlast = uprogress.t0 + 1 uprogress.tlast = uprogress.t0 + 1
} }
async function uploadHandler(event: Event) { const statsAdd = (f: Array<File>) => {
const target = event.target as HTMLInputElement if (uprogress.files.length === 0) statReset()
if (!target?.files?.length) { uprogress.total += f.reduce((a, b) => a + b.size, 0)
documentStore.error = 'No files selected' uprogress.filecount += f.length
return uprogress.files = uprogress.files.concat(f)
} statNextFile()
const ws = await new Promise<WebSocket>(resolve => { }
let upqueue = [] as File[]
// TODO: Rewrite as WebSocket class
const WSCreate = async () => await new Promise<WebSocket>(resolve => {
const ws = connect(uploadUrl, { const ws = connect(uploadUrl, {
open(ev: Event) { resolve(ws) }, open(ev: Event) { resolve(ws) },
error(ev: Event) { error(ev: Event) {
@ -50,106 +130,95 @@ async function uploadHandler(event: Event) {
return return
} }
if (res.status === 'ack') { if (res.status === 'ack') {
// Upload progress upgrade on SERVER ACK statUpdate(res.req)
const fstart = uprogress.filestart[uprogress.fileidx]
uprogress.uploaded = fstart + res.req.end
uprogress.filepos = res.req.end
uprogress.tlast = Date.now()
} else console.log('Unknown upload response', res) } else console.log('Unknown upload response', res)
}, },
}) })
ws.binaryType = 'arraybuffer' // @ts-ignore
}) ws.sendMsg = (msg: any) => ws.send(JSON.stringify(msg))
async function wsFlush(limit: number) { // @ts-ignore
ws.sendData = async (data: any) => {
// Wait until the WS is ready to send another message
uprogress.status = "uploading"
await new Promise(resolve => { await new Promise(resolve => {
const t = setInterval(() => { const t = setInterval(() => {
if (ws.bufferedAmount > limit) return if (ws.bufferedAmount > 1<<20) return
resolve(undefined) resolve(undefined)
clearInterval(t) clearInterval(t)
}, 1) }, 1)
}) })
uprogress.status = "processing"
ws.send(data)
} }
const loc = props.path!.join('/') })
const files = Array.from(target.files) const worker = async () => {
// Progress stats const ws = await WSCreate()
resetProgress() while (upqueue.length) {
for (const file of files) { const f = upqueue[0]
uprogress.filestart.push(uprogress.total) if (f.cloudPos === f.size) {
uprogress.total += file.size upqueue.shift()
continue
} }
console.log(uprogress) // Note: files may get modified while we're uploading, f is ours
// Upload files const start = f.cloudPos
for (const file of files) { const end = Math.min(f.size, start + (1<<20))
let pos = 0 const control = { name: f.cloudName, size: f.size, start, end }
uprogress.filename = file.name const data = f.slice(start, end)
uprogress.filesize = file.size ws.sendMsg(control)
uprogress.filepos = 0 await ws.sendData(data)
const name = loc + '/' + (file.webkitRelativePath || file.name) f.cloudPos = end
console.log('Uploading', name, file.size)
while (pos < file.size) {
const end = Math.min(file.size, pos + (1<<20))
const value = file.slice(pos, end)
ws.send(
JSON.stringify({
name,
size: file.size,
start: pos,
end,
})
)
ws.send(value)
// Wait until the WebSocket is ready to send the next message
await wsFlush(1<<20)
pos = end
} }
await wsFlush(0) if (upqueue.length) startWorker()
console.log('Uploaded', name, pos) uprogress.status = "idle"
++uprogress.fileidx
}
resetProgress()
} }
let workerRunning: any = false
const startWorker = () => {
if (workerRunning === false) workerRunning = setTimeout(() => {
workerRunning = true
worker()
}, 0)
}
onMounted(() => {
// Need to prevent both to prevent browser from opening the file
addEventListener('dragover', uploadHandler)
addEventListener('drop', uploadHandler)
})
onUnmounted(() => {
removeEventListener('dragover', uploadHandler)
removeEventListener('drop', uploadHandler)
})
</script> </script>
<template> <template>
<template> <template>
<input <input ref="fileInput" @change="uploadHandler" type="file" multiple>
ref="fileUploadButton" <input ref="folderInput" @change="uploadHandler" type="file" webkitdirectory>
@change="uploadHandler"
class="upload-input"
type="file"
multiple
/>
<input
ref="folderUploadButton"
@change="uploadHandler"
class="upload-input"
type="file"
webkitdirectory
/>
</template> </template>
<SvgButton name="add-file" data-tooltip="Upload files" @click="fileUploadButton.click()" /> <SvgButton name="add-file" data-tooltip="Upload files" @click="fileInput.click()" />
<SvgButton name="add-folder" data-tooltip="Upload folder" @click="folderUploadButton.click()" /> <SvgButton name="add-folder" data-tooltip="Upload folder" @click="folderInput.click()" />
<div class="uploadprogress" v-if="uprogress.total"> <div class="uploadprogress" v-if="uprogress.total" :style="`background: linear-gradient(to right, var(--bar) 0, var(--bar) ${percent}%, var(--nobar) ${percent}%, var(--nobar) 100%);`">
<p> <div class="statustext">
<span v-if="uprogress.filestart.length > 1" class="index"> <span v-if="uprogress.filecount > 1" class="index">
[{{ 1 + uprogress.fileidx }}/{{ uprogress.filestart.length }}] [{{ uprogress.fileidx }}/{{ uprogress.filecount }}]
</span> </span>
<span class="filename">{{ uprogress.filename }} <span class="filename">{{ uprogress.filename.split('/').pop() }}
<span v-if="uprogress.filesize > 1e7 && uprogress.filestart.length > 1" class="percent"> <span v-if="uprogress.filesize > 1e7" class="percent">
{{ (uprogress.filepos / uprogress.filesize * 100).toFixed(0) }} % {{ (uprogress.filepos / uprogress.filesize * 100).toFixed(0) + '\u202F%' }}
</span> </span>
</span> </span>
<span class="position" v-if="uprogress.filesize > 1e7"> <span class="position" v-if="uprogress.filesize > 1e7">
{{ (uprogress.uploaded / 1e6).toFixed(0) }} / {{ (uprogress.total / 1e6).toFixed(0) }} MB {{ (uprogress.uploaded / 1e6).toFixed(0) + '\u202F/\u202F' + (uprogress.total / 1e6).toFixed(0) + '\u202FMB' }}
</span> </span>
<span class="speed">{{ speed.toFixed(speed < 100 ? 1 : 0) }} MB/s</span> <span class="speed">{{ speeddisp }}</span>
</p> <button class="close" @click="cancelUploads"></button>
<progress :value="uprogress.uploaded" :max="uprogress.total"></progress> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.uploadprogress { .uploadprogress {
background: #8888; --bar: var(--accent-color);
--nobar: var(--header-background);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
color: var(--primary-color); color: var(--primary-color);
@ -158,16 +227,15 @@ async function uploadHandler(event: Event) {
bottom: 0; bottom: 0;
width: 100vw; width: 100vw;
} }
.uploadprogress p { .statustext {
display: flex; display: flex;
width: 100vw; padding: 0.5rem 0;
margin: 0;
} }
span { span {
color: #ccc; color: #ccc;
margin: 0 0.5em;
white-space: nowrap; white-space: nowrap;
text-align: right; text-align: right;
padding: 0 0.5em;
} }
.filename { .filename {
color: #fff; color: #fff;
@ -177,17 +245,7 @@ span {
text-overflow: ellipsis; text-overflow: ellipsis;
text-align: left; text-align: left;
} }
.index { .index { min-width: 3.5em }
min-width: 3.5em; .position { min-width: 4em }
} .speed { min-width: 4em }
.position {
min-width: 4em;
}
.speed {
min-width: 4em;
}
progress {
appearance: progress-bar;
width: 100%;
}
</style> </style>