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>