Frontend created and rewritten a few times, with some backend fixes #1
|
@ -1,155 +1,224 @@
|
||||||
<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 => {
|
}
|
||||||
const ws = connect(uploadUrl, {
|
let upqueue = [] as File[]
|
||||||
open(ev: Event) { resolve(ws) },
|
|
||||||
error(ev: Event) {
|
// TODO: Rewrite as WebSocket class
|
||||||
console.error('Upload socket error', ev)
|
const WSCreate = async () => await new Promise<WebSocket>(resolve => {
|
||||||
documentStore.error = 'Upload socket error'
|
const ws = connect(uploadUrl, {
|
||||||
},
|
open(ev: Event) { resolve(ws) },
|
||||||
message(ev: MessageEvent) {
|
error(ev: Event) {
|
||||||
const res = JSON.parse(ev!.data)
|
console.error('Upload socket error', ev)
|
||||||
if ('error' in res) {
|
documentStore.error = 'Upload socket error'
|
||||||
console.error('Upload socket error', res.error)
|
},
|
||||||
documentStore.error = res.error.message
|
message(ev: MessageEvent) {
|
||||||
return
|
const res = JSON.parse(ev!.data)
|
||||||
}
|
if ('error' in res) {
|
||||||
if (res.status === 'ack') {
|
console.error('Upload socket error', res.error)
|
||||||
// Upload progress upgrade on SERVER ACK
|
documentStore.error = res.error.message
|
||||||
const fstart = uprogress.filestart[uprogress.fileidx]
|
return
|
||||||
uprogress.uploaded = fstart + res.req.end
|
}
|
||||||
uprogress.filepos = res.req.end
|
if (res.status === 'ack') {
|
||||||
uprogress.tlast = Date.now()
|
statUpdate(res.req)
|
||||||
} else console.log('Unknown upload response', res)
|
} else console.log('Unknown upload response', res)
|
||||||
},
|
},
|
||||||
})
|
|
||||||
ws.binaryType = 'arraybuffer'
|
|
||||||
})
|
})
|
||||||
async function wsFlush(limit: number) {
|
// @ts-ignore
|
||||||
|
ws.sendMsg = (msg: any) => ws.send(JSON.stringify(msg))
|
||||||
|
// @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)
|
|
||||||
// Upload files
|
|
||||||
for (const file of files) {
|
|
||||||
let pos = 0
|
|
||||||
uprogress.filename = file.name
|
|
||||||
uprogress.filesize = file.size
|
|
||||||
uprogress.filepos = 0
|
|
||||||
const name = loc + '/' + (file.webkitRelativePath || file.name)
|
|
||||||
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)
|
// Note: files may get modified while we're uploading, f is ours
|
||||||
console.log('Uploaded', name, pos)
|
const start = f.cloudPos
|
||||||
++uprogress.fileidx
|
const end = Math.min(f.size, start + (1<<20))
|
||||||
|
const control = { name: f.cloudName, size: f.size, start, end }
|
||||||
|
const data = f.slice(start, end)
|
||||||
|
ws.sendMsg(control)
|
||||||
|
await ws.sendData(data)
|
||||||
|
f.cloudPos = end
|
||||||
}
|
}
|
||||||
resetProgress()
|
if (upqueue.length) startWorker()
|
||||||
|
uprogress.status = "idle"
|
||||||
}
|
}
|
||||||
|
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 class="filename">{{ uprogress.filename }}
|
|
||||||
<span v-if="uprogress.filesize > 1e7 && uprogress.filestart.length > 1" class="percent">
|
|
||||||
{{ (uprogress.filepos / uprogress.filesize * 100).toFixed(0) }} %
|
|
||||||
</span>
|
</span>
|
||||||
|
<span class="filename">{{ uprogress.filename.split('/').pop() }}
|
||||||
|
<span v-if="uprogress.filesize > 1e7" class="percent">
|
||||||
|
{{ (uprogress.filepos / uprogress.filesize * 100).toFixed(0) + '\u202F%' }}
|
||||||
|
</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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user