245 lines
7.2 KiB
Vue
245 lines
7.2 KiB
Vue
<script setup lang="ts">
|
|
import { connect, uploadUrl } from '@/repositories/WS';
|
|
import { useMainStore } from '@/stores/main'
|
|
import { collator } from '@/utils';
|
|
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
|
|
const fileInput = ref()
|
|
const folderInput = ref()
|
|
const store = useMainStore()
|
|
const props = defineProps({
|
|
path: Array<string>
|
|
})
|
|
|
|
type CloudFile = {
|
|
file: File
|
|
cloudName: string
|
|
cloudPos: number
|
|
}
|
|
function pasteHandler(event: ClipboardEvent) {
|
|
const items = Array.from(event.clipboardData?.items ?? [])
|
|
const infiles = [] as File[]
|
|
const dirs = [] as FileSystemDirectoryEntry[]
|
|
for (const item of items) {
|
|
if (item.kind !== 'file') continue
|
|
const entry = item.webkitGetAsEntry()
|
|
if (entry?.isFile) {
|
|
const file = item.getAsFile()
|
|
if (file) infiles.push(file)
|
|
} else if (entry?.isDirectory) {
|
|
dirs.push(entry as FileSystemDirectoryEntry)
|
|
}
|
|
}
|
|
if (infiles.length || dirs.length) {
|
|
event.preventDefault()
|
|
uploadFiles(infiles)
|
|
for (const entry of dirs) pasteDirectory(entry, `${props.path!.join('/')}/${entry.name}`)
|
|
}
|
|
}
|
|
const pasteDirectory = async (entry: FileSystemDirectoryEntry, loc: string) => {
|
|
const reader = entry.createReader()
|
|
const entries = await new Promise<any[]>(resolve => reader.readEntries(resolve))
|
|
const cloudfiles = [] as CloudFile[]
|
|
for (const entry of entries) {
|
|
const cloudName = `${loc}/${entry.name}`
|
|
if (entry.isFile) {
|
|
const file = await new Promise(resolve => entry.file(resolve)) as File
|
|
cloudfiles.push({file, cloudName, cloudPos: 0})
|
|
} else if (entry.isDirectory) {
|
|
await pasteDirectory(entry, cloudName)
|
|
}
|
|
}
|
|
if (cloudfiles.length) uploadCloudFiles(cloudfiles)
|
|
}
|
|
function uploadHandler(event: Event) {
|
|
event.preventDefault()
|
|
// @ts-ignore
|
|
const input = event.target as HTMLInputElement | null
|
|
const infiles = Array.from((input ?? (event as DragEvent).dataTransfer)?.files ?? []) as File[]
|
|
if (input) input.value = ''
|
|
if (infiles.length) uploadFiles(infiles)
|
|
}
|
|
|
|
const uploadFiles = (infiles: File[]) => {
|
|
const loc = props.path!.join('/')
|
|
let files = []
|
|
for (const file of infiles) {
|
|
files.push({
|
|
file,
|
|
cloudName: loc + '/' + (file.webkitRelativePath || file.name),
|
|
cloudPos: 0,
|
|
})
|
|
}
|
|
uploadCloudFiles(files)
|
|
}
|
|
const uploadCloudFiles = (files: CloudFile[]) => {
|
|
const dotfiles = files.filter(f => f.cloudName.includes('/.'))
|
|
if (dotfiles.length) {
|
|
store.error = "Won't upload dotfiles"
|
|
console.log("Dotfiles omitted", dotfiles)
|
|
files = files.filter(f => !f.cloudName.includes('/.'))
|
|
}
|
|
if (!files.length) return
|
|
files.sort((a, b) => collator.compare(a.cloudName, b.cloudName))
|
|
// @ts-ignore
|
|
upqueue = [...upqueue, ...files]
|
|
statsAdd(files)
|
|
startWorker()
|
|
}
|
|
|
|
const cancelUploads = () => {
|
|
upqueue = []
|
|
statReset()
|
|
}
|
|
|
|
const uprogress_init = {
|
|
total: 0,
|
|
xfer: 0,
|
|
t0: 0,
|
|
tlast: 0,
|
|
statbytes: 0,
|
|
statdur: 0,
|
|
files: [] as CloudFile[],
|
|
filestart: 0,
|
|
fileidx: 0,
|
|
filecount: 0,
|
|
filename: '',
|
|
filesize: 0,
|
|
filepos: 0,
|
|
status: 'idle',
|
|
}
|
|
const uprogress = reactive({...uprogress_init})
|
|
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}: {name: string, size: number, start: number, end: number}) => {
|
|
if (name !== uprogress.filename) return // If stats have been reset
|
|
const now = Date.now()
|
|
uprogress.xfer = 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.file.size
|
|
uprogress.filename = f.cloudName
|
|
}
|
|
const statReset = () => {
|
|
Object.assign(uprogress, uprogress_init)
|
|
uprogress.t0 = Date.now()
|
|
uprogress.tlast = uprogress.t0 + 1
|
|
}
|
|
const statsAdd = (f: CloudFile[]) => {
|
|
if (uprogress.files.length === 0) statReset()
|
|
uprogress.total += f.reduce((a, b) => a + b.file.size, 0)
|
|
uprogress.filecount += f.length
|
|
uprogress.files = [...uprogress.files, ...f]
|
|
statNextFile()
|
|
}
|
|
let upqueue = [] as CloudFile[]
|
|
|
|
// TODO: Rewrite as WebSocket class
|
|
const WSCreate = async () => await new Promise<WebSocket>(resolve => {
|
|
const ws = connect(uploadUrl, {
|
|
open(ev: Event) { resolve(ws) },
|
|
error(ev: Event) {
|
|
console.error('Upload socket error', ev)
|
|
store.error = 'Upload socket error'
|
|
},
|
|
message(ev: MessageEvent) {
|
|
const res = JSON.parse(ev!.data)
|
|
if ('error' in res) {
|
|
console.error('Upload socket error', res.error)
|
|
store.error = res.error.message
|
|
return
|
|
}
|
|
if (res.status === 'ack') {
|
|
statUpdate(res.req)
|
|
} else console.log('Unknown upload response', res)
|
|
},
|
|
})
|
|
// @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 => {
|
|
const t = setInterval(() => {
|
|
if (ws.bufferedAmount > 1<<20) return
|
|
resolve(undefined)
|
|
clearInterval(t)
|
|
}, 1)
|
|
})
|
|
uprogress.status = "processing"
|
|
ws.send(data)
|
|
}
|
|
})
|
|
const worker = async () => {
|
|
const ws = await WSCreate()
|
|
while (upqueue.length) {
|
|
const f = upqueue[0]
|
|
const start = f.cloudPos
|
|
const end = Math.min(f.file.size, start + (1<<20))
|
|
const control = { name: f.cloudName, size: f.file.size, start, end }
|
|
const data = f.file.slice(start, end)
|
|
f.cloudPos = end
|
|
// Note: files may get modified during I/O
|
|
// @ts-ignore FIXME proper WebSocket class, avoid attaching functions to WebSocket object
|
|
ws.sendMsg(control)
|
|
// @ts-ignore
|
|
await ws.sendData(data)
|
|
if (f.cloudPos === f.file.size) upqueue.shift()
|
|
}
|
|
if (upqueue.length) startWorker()
|
|
uprogress.status = "idle"
|
|
workerRunning = false
|
|
}
|
|
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)
|
|
addEventListener('paste', pasteHandler)
|
|
})
|
|
onUnmounted(() => {
|
|
removeEventListener('paste', pasteHandler)
|
|
removeEventListener('dragover', uploadHandler)
|
|
removeEventListener('drop', uploadHandler)
|
|
})
|
|
</script>
|
|
<template>
|
|
<template>
|
|
<input ref="fileInput" @change="uploadHandler" type="file" multiple>
|
|
<input ref="folderInput" @change="uploadHandler" type="file" webkitdirectory>
|
|
</template>
|
|
<SvgButton name="add-file" data-tooltip="Upload files" @click="fileInput.click()" />
|
|
<SvgButton name="add-folder" data-tooltip="Upload folder" @click="folderInput.click()" />
|
|
<TransferBar :status=uprogress @cancel=cancelUploads />
|
|
</template>
|