spb/drive Public
SPB Drive — self-hosted personal cloud drive (files, previews, sharing) on the MacLustr cluster.
JavaScript 82.7%
CSS 10.6%
Nunjucks 3.6%
Shell 1.8%
SQL 1.3%
1/**2 * ─────────────────────────────────────────────3 * SPB Drive — Personal Cloud Drive4 * ─────────────────────────────────────────────5 * Author : Simon-Pierre Boucher6 * Contact : contact@spboucher.ai7 * File : src/web/assets/js/request-page.js8 * Purpose : Public file-request page — chunked uploads from strangers9 * License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { h, fmtSize, toast } from './ui.js';1415const page = document.querySelector('.share-page');16const token = page.dataset.token;17const maxFiles = page.dataset.maxFiles ? Number(page.dataset.maxFiles) : null;18let uploadedCount = Number(page.dataset.received || 0);1920const drop = document.getElementById('requestDrop');21const list = document.getElementById('requestList');22const pick = document.getElementById('requestPick');2324const base = `/r/${encodeURIComponent(token)}`;2526async function jsonOrThrow(res) {27 const data = res.headers.get('content-type')?.includes('json') ? await res.json() : null;28 if (!res.ok) throw new Error(data?.error?.message ?? `HTTP ${res.status}`);29 return data;30}3132async function uploadFile(file) {33 if (maxFiles !== null && uploadedCount >= maxFiles) {34 toast('This link has reached its file limit', { error: true });35 return;36 }37 const row = h('div.upload-item', {},38 h('div.fname', {},39 h('span.n', {}, file.name),40 h('span.s', {}, 'Starting…'),41 h('div.prog', {}, h('i'))));42 list.append(row);43 const setStatus = (text) => { row.querySelector('.s').textContent = text; };44 const setPct = (pct) => { row.querySelector('.prog i').style.width = `${pct}%`; };4546 try {47 const init = await jsonOrThrow(await fetch(`${base}/upload/init`, {48 method: 'POST',49 headers: { 'content-type': 'application/json' },50 body: JSON.stringify({ name: file.name, size: file.size }),51 }));52 const { uploadId, chunkSize, nChunks } = init;53 for (let n = 0; n < nChunks; n += 1) {54 const slice = file.slice(n * chunkSize, Math.min((n + 1) * chunkSize, file.size));55 let attempt = 0;56 for (;;) {57 try {58 const res = await fetch(`${base}/upload/${uploadId}/chunk/${n}`, {59 method: 'PUT',60 headers: { 'content-type': 'application/octet-stream' },61 body: slice,62 });63 if (!res.ok) throw new Error(`HTTP ${res.status}`);64 break;65 } catch (err) {66 attempt += 1;67 if (attempt > 3) throw err;68 await new Promise((r) => setTimeout(r, 1000 * attempt));69 }70 }71 const sent = Math.min((n + 1) * chunkSize, file.size);72 setPct(file.size ? (sent / file.size) * 100 : 100);73 setStatus(`${fmtSize(sent)} / ${fmtSize(file.size)}`);74 }75 const done = await jsonOrThrow(await fetch(`${base}/upload/${uploadId}/complete`, {76 method: 'POST',77 headers: { 'content-type': 'application/json' },78 body: JSON.stringify({ mime: file.type || undefined }),79 }));80 uploadedCount += 1;81 row.classList.add('done');82 setPct(100);83 setStatus(`Sent · ${done.name}`);84 } catch (err) {85 row.classList.add('err');86 setStatus(`Failed — ${err.message}`);87 }88}8990async function handleFiles(files) {91 const queue = [...files];92 if (!queue.length) return;93 if (maxFiles !== null && uploadedCount + queue.length > maxFiles) {94 toast(`Only ${Math.max(0, maxFiles - uploadedCount)} more file(s) allowed on this link`, { error: true });95 queue.length = Math.max(0, maxFiles - uploadedCount);96 }97 for (const file of queue) await uploadFile(file);98}99100drop.addEventListener('click', () => pick.click());101drop.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick.click(); } });102pick.addEventListener('change', (e) => { handleFiles(e.target.files); e.target.value = ''; });103104for (const ev of ['dragenter', 'dragover']) {105 drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.add('active'); });106}107for (const ev of ['dragleave', 'drop']) {108 drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.remove('active'); });109}110drop.addEventListener('drop', (e) => handleFiles(e.dataTransfer?.files ?? []));111