import React, { useEffect, useState } from 'react' import { api, WhoAmI } from './api' import * as tus from 'tus-js-client' type FileRow = { name: string; path: string; is_dir: boolean; size: number; mtime: number } type Sel = { file: File; desc: string; finalName: string } function sanitizeDesc(s:string){ s = s.trim().replace(/[^A-Za-z0-9 _-]+/g,'_').replace(/\s+/g,'_') if(!s) s = 'upload' return s.slice(0,64) } function extOf(n:string){ const i=n.lastIndexOf('.'); return i>-1 ? n.slice(i+1).toLowerCase() : 'bin' } function composeName(date:string, desc:string, orig:string){ const d = date || new Date().toISOString().slice(0,10) // YYYY-MM-DD const [Y,M,D] = d.split('-'); return `${Y}.${M}.${D}.${sanitizeDesc(desc)}.${extOf(orig)}` } export default function Uploader(){ const [me, setMe] = useState() const [cwd, setCwd] = useState('') const [rows, setRows] = useState([]) const [status, setStatus] = useState('') const [date, setDate] = useState(new Date().toISOString().slice(0,10)) const [sel, setSel] = useState([]) async function refresh(path=''){ const m = await api('/api/whoami'); setMe(m) const list = await api('/api/list?path='+encodeURIComponent(path)); setRows(list); setCwd(path) } useEffect(()=>{ refresh('') }, []) function handleChoose(files: FileList){ const arr = Array.from(files).map(f=>{ const base = (f as any).webkitRelativePath || f.name const name = base.split('/').pop() || f.name const guess = name.replace(/\.[^/.]+$/,'').replace(/[_-]+/g,' ') const desc = sanitizeDesc(guess) return { file:f, desc, finalName: composeName(date, desc, name) } }) setSel(arr) } useEffect(()=>{ setSel(old => old.map(x=> ({...x, finalName: composeName(date, x.desc, x.file.name)}))) }, [date]) async function doUpload(){ if(!me) return if(sel.some(s => !s.desc.trim())){ alert('Please provide a brief description for every file.'); return } setStatus('') for(const s of sel){ await new Promise((resolve,reject)=>{ const up = new tus.Upload(s.file, { endpoint: '/tus/', chunkSize: 5*1024*1024, retryDelays: [0, 1000, 3000, 5000, 10000], withCredentials: true, metadata: { filename: s.file.name, subdir: cwd || "", date, // YYYY-MM-DD desc: s.desc // server composes YYYY.MM.DD.desc.ext }, onError: (err)=>{ setStatus(`✖ ${s.file.name}: ${err}`); reject(err) }, onProgress: (sent, total)=>{ const pct = Math.floor(sent/total*100); setStatus(`⬆ ${s.finalName}: ${pct}%`) }, onSuccess: ()=>{ setStatus(`✔ ${s.finalName} uploaded`); resolve() }, }) up.start() }) } setSel([]); refresh(cwd) } async function rename(oldp:string){ const name = prompt('New name (YYYY.MM.DD.description.ext):', oldp.split('/').pop()||''); if(!name) return await api('/api/rename', { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({from:oldp, to: (oldp.split('/').slice(0,-1).concat(name)).join('/')}) }) refresh(cwd) } async function del(p:string, recursive:boolean){ if(!confirm(`Delete ${p}${recursive?' (recursive)':''}?`)) return await api('/api/file?path='+encodeURIComponent(p)+'&recursive='+(recursive?'true':'false'), { method:'DELETE' }) refresh(cwd) } return (<>
Signed in: {me?.username} · root: /{me?.root}
{cwd ? `/${cwd}` : 'Choose a folder below'}
setDate(e.target.value)} />
e.target.files && handleChoose(e.target.files)} />
{sel.length>0 && (

Ready to upload

{sel.map((s,i)=>(
{s.file.name}
{ const v=e.target.value; setSel(old => old.map((x,idx)=> idx===i ? ({...x, desc:v, finalName: composeName(date, v, x.file.name)}) : x )) }} />
→ {s.finalName}
))}
)}
{status}

Folder

{cwd && ( )} {rows.sort((a,b)=> (Number(b.is_dir)-Number(a.is_dir)) || a.name.localeCompare(b.name)).map(f=>
{f.is_dir?'📁':'🎞️'} {f.name}
{f.is_dir?'folder':fmt(f.size)} · {new Date(f.mtime*1000).toLocaleString()}
)}
) } function fmt(n:number){ if(n<1024) return n+' B'; const u=['KB','MB','GB','TB']; let i=-1; do{ n/=1024; i++ }while(n>=1024&&i