<!DOCTYPE html> <title>Storage</title> <style> body { font-family: sans-serif; max-width: 100ch; margin: 0 auto; padding: 1em; background-color: #333; color: #eee; } td { text-align: right; padding: .5em; } td:first-child { text-align: left; } a { color: inherit; text-decoration: none; } </style> <div> <h2>Quick file upload</h2> <p>Uses parallel WebSocket connections for increased bandwidth /api/upload</p> <input type=file id=fileInput> <progress id=progressBar value=0 max=1></progress> </div> <div> <h2>Files</h2> <ul id=file_list></ul> </div> <script> let files = {} let flatfiles = {} function createWatchSocket() { const wsurl = new URL("/api/watch", location.href.replace(/^http/, 'ws')) const ws = new WebSocket(wsurl) ws.onmessage = event => { msg = JSON.parse(event.data) if (msg.update) { tree_update(msg.update) file_list(files) } else { console.log("Unkonwn message from watch socket", msg) } } } createWatchSocket() function tree_update(msg) { console.log("Tree update", msg) let node = files for (const elem of msg) { if (elem.deleted) { const p = node.dir[elem.name].path delete node.dir[elem.name] delete flatfiles[p] break } if (elem.name !== undefined) node = node.dir[elem.name] ||= {} if (elem.size !== undefined) node.size = elem.size if (elem.mtime !== undefined) node.mtime = elem.mtime if (elem.dir !== undefined) node.dir = elem.dir } // Update paths and flatfiles files.path = "/" const nodes = [files] flatfiles = {} while (node = nodes.pop()) { flatfiles[node.path] = node if (node.dir === undefined) continue for (const name of Object.keys(node.dir)) { const child = node.dir[name] child.path = node.path + name + (child.dir === undefined ? "" : "/") nodes.push(child) } } } var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'}); const compare_path = (a, b) => collator.compare(a.path, b.path) const compare_time = (a, b) => a.mtime > b.mtime function file_list(files) { const table = document.getElementById("file_list") const sorted = Object.values(flatfiles).sort(compare_time) table.innerHTML = "" for (const f of sorted) { const {path, size, mtime} = f const tr = document.createElement("tr") const name_td = document.createElement("td") const size_td = document.createElement("td") const mtime_td = document.createElement("td") const a = document.createElement("a") table.appendChild(tr) tr.appendChild(name_td) tr.appendChild(size_td) tr.appendChild(mtime_td) name_td.appendChild(a) size_td.textContent = size mtime_td.textContent = formatUnixDate(mtime) a.textContent = path a.href = `/files${path}` /*a.onclick = event => { if (window.showSaveFilePicker) { event.preventDefault() download_ws(name, size) } } a.download = ""*/ } } function formatUnixDate(t) { const date = new Date(t * 1000) const now = new Date() const diff = date - now const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }) if (Math.abs(diff) <= 60000) { return formatter.format(Math.round(diff / 1000), 'second') } if (Math.abs(diff) <= 3600000) { return formatter.format(Math.round(diff / 60000), 'minute') } if (Math.abs(diff) <= 86400000) { return formatter.format(Math.round(diff / 3600000), 'hour') } if (Math.abs(diff) <= 604800000) { return formatter.format(Math.round(diff / 86400000), 'day') } return date.toLocaleDateString() } async function download_ws(name, size) { const fh = await window.showSaveFilePicker({ suggestedName: name, }) const writer = await fh.createWritable() writer.truncate(size) const wsurl = new URL("/api/download", location.href.replace(/^http/, 'ws')) const ws = new WebSocket(wsurl) let pos = 0 ws.onopen = () => { console.log("Downloading over WebSocket", name, size) ws.send(JSON.stringify({name, start: 0, end: size, size})) } ws.onmessage = event => { if (typeof event.data === 'string') { const msg = JSON.parse(event.data) console.log("Download finished", msg) ws.close() return } console.log("Received chunk", name, pos, pos + event.data.size) pos += event.data.size writer.write(event.data) } ws.onclose = () => { if (pos < size) { console.log("Download aborted", name, pos) writer.truncate(pos) } writer.close() } } const fileInput = document.getElementById("fileInput") const progress = document.getElementById("progressBar") const numConnections = 2 const chunkSize = 1<<20 const wsConnections = new Set() //for (let i = 0; i < numConnections; i++) createUploadWS() function createUploadWS() { const wsurl = new URL("/api/upload", location.href.replace(/^http/, 'ws')) const ws = new WebSocket(wsurl) ws.binaryType = 'arraybuffer' ws.onopen = () => { wsConnections.add(ws) console.log("Upload socket connected") } ws.onmessage = event => { msg = JSON.parse(event.data) if (msg.written) progress.value += +msg.written else console.log(`Error: ${msg.error}`) } ws.onclose = () => { wsConnections.delete(ws) console.log("Upload socket disconnected, reconnecting...") setTimeout(createUploadWS, 1000) } } async function load(file, start, end) { const reader = new FileReader() const load = new Promise(resolve => reader.onload = resolve) reader.readAsArrayBuffer(file.slice(start, end)) const event = await load return event.target.result } async function sendChunk(file, start, end, ws) { const chunk = await load(file, start, end) ws.send(JSON.stringify({ name: file.name, size: file.size, start: start, end: end })) ws.send(chunk) } fileInput.addEventListener("change", async function() { const file = this.files[0] const numChunks = Math.ceil(file.size / chunkSize) progress.value = 0 progress.max = file.size console.log(wsConnections) for (let i = 0; i < numChunks; i++) { const ws = Array.from(wsConnections)[i % wsConnections.size] const start = i * chunkSize const end = Math.min(file.size, start + chunkSize) const res = await sendChunk(file, start, end, ws) } }) </script>