Files
2026-05-21 21:37:00 +01:00

144 lines
8.4 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Dashboard - fileshares.cfd</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:monospace;padding:40px;background:white;color:black}
h1{margin-bottom:20px}
.box{border:1px solid black;padding:20px;max-width:620px;margin-bottom:20px}
button{border:1px solid black;background:white;padding:8px 16px;cursor:pointer;font-family:monospace}
button:hover{background:black;color:white}
input[type="file"]{margin-bottom:10px;display:block}
#status{margin-top:10px}
#progress-wrap{display:none;margin-top:10px}
#progress-bar{width:100%;height:20px;border:1px solid black}
#progress-fill{height:100%;background:black;width:0%}
#progress-text{margin-top:4px}
#link{margin-top:10px;word-break:break-all}
#link a,.files a{color:black;font-weight:bold}
.files{max-width:620px}
.file{border-top:1px solid black;padding:10px 0}
.file:first-child{border-top:0}
.meta{font-size:12px;margin-top:4px}
.empty{margin-top:10px}
.nav{margin-top:40px;font-size:12px}
a{color:black}
</style>
</head>
<body>
<h1>Dashboard</h1>
<div class="box">
<input type="file" id="fileInput">
<button onclick="upload()">Upload</button>
<div id="status"></div>
<div id="progress-wrap">
<div id="progress-bar"><div id="progress-fill"></div></div>
<div id="progress-text"></div>
</div>
<div id="link"></div>
</div>
<div class="box files">
<h2>Files</h2>
<div id="files" class="empty">Loading...</div>
</div>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a href="/dashboard">Dashboard</a> | <a href="/terms">Terms</a> | <a href="/privacy">Privacy</a></div>
<script>
function getToken(){return localStorage.getItem('upload_token')||''}
function authHeaders(){return{'Authorization':'Bearer '+getToken()}}
function formatBytes(b){if(b<1024)return b+' B';if(b<1048576)return(b/1024).toFixed(1)+' KB';if(b<1073741824)return(b/1048576).toFixed(1)+' MB';return(b/1073741824).toFixed(2)+' GB'}
function formatTime(s){if(s<60)return Math.ceil(s)+'s';if(s<3600)return Math.floor(s/60)+'m '+Math.ceil(s%60)+'s';return Math.floor(s/3600)+'h '+Math.floor((s%3600)/60)+'m'}
function formatDate(s){const d=new Date(s);return isNaN(d)?s:d.toLocaleString()}
const CHUNK_SIZE=25*1024*1024;
const MAX_CONCURRENT=8;
if(!getToken()){
document.getElementById('status').innerHTML='No token set. <a href="/login">Login first</a>.';
document.getElementById('files').innerHTML='Login to see your files.';
}else{
loadFiles();
}
async function loadFiles(){
const files=document.getElementById('files');
files.textContent='Loading...';
const resp=await fetch('/api/files',{headers:authHeaders()});
const data=await resp.json();
if(!resp.ok){files.textContent='Could not load files.';return}
if(!data.files.length){files.textContent='No files uploaded yet.';return}
files.className='';
files.innerHTML=data.files.map(file=>{
const url=location.origin+'/'+file.slug;
return '<div class="file"><a href="/'+file.slug+'">'+escapeHtml(file.filename)+'</a><div class="meta">'+formatBytes(file.size)+' | '+formatDate(file.uploaded_at)+' | <a href="'+url+'">'+url+'</a></div></div>';
}).join('');
}
function escapeHtml(value){
return String(value).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
async function upload(){
const input=document.getElementById('fileInput');
const status=document.getElementById('status');
const wrap=document.getElementById('progress-wrap');
const fill=document.getElementById('progress-fill');
const text=document.getElementById('progress-text');
const link=document.getElementById('link');
link.innerHTML='';
if(!input.files.length){status.textContent='No file selected';return}
if(!getToken()){status.innerHTML='No token set. <a href="/login">Login first</a>.';return}
const file=input.files[0];
if(file.size<10*1024*1024) await uploadSimple(file,status,wrap,fill,text,link);
else await uploadMultipart(file,status,wrap,fill,text,link);
}
async function uploadSimple(file,status,wrap,fill,text,link){
status.textContent='Requesting upload URL...';
wrap.style.display='none';
const resp=await fetch('/api/upload',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({filename:file.name,size:file.size})});
const data=await resp.json();
if(!resp.ok){status.textContent='Error: '+data.error;return}
status.textContent='Uploading...';
wrap.style.display='block';fill.style.width='0%';
const startTime=Date.now();
await new Promise((resolve,reject)=>{
const xhr=new XMLHttpRequest();
xhr.open('PUT',data.upload_url);
xhr.upload.onprogress=function(e){if(!e.lengthComputable)return;const pct=(e.loaded/e.total)*100;fill.style.width=pct+'%';const elapsed=(Date.now()-startTime)/1000;const speed=e.loaded/elapsed;text.textContent=pct.toFixed(1)+'% | '+formatBytes(e.loaded)+' / '+formatBytes(e.total)+' | '+formatBytes(speed)+'/s | ETA '+formatTime((e.total-e.loaded)/speed)};
xhr.onload=function(){if(xhr.status>=200&&xhr.status<300){fill.style.width='100%';const elapsed=(Date.now()-startTime)/1000;text.textContent='100% | '+formatBytes(file.size)+' in '+formatTime(elapsed)+' | avg '+formatBytes(file.size/elapsed)+'/s';status.textContent='Done!';link.innerHTML='<a href="/'+data.slug+'">'+location.origin+'/'+data.slug+'</a>';loadFiles();resolve()}else{status.textContent='Upload failed: '+xhr.status;reject()}};
xhr.onerror=function(){status.textContent='Upload failed: network error';reject()};
xhr.send(file);
});
}
async function uploadMultipart(file,status,wrap,fill,text,link){
const numParts=Math.ceil(file.size/CHUNK_SIZE);
status.textContent='Initiating multipart ('+numParts+' parts)...';
wrap.style.display='none';
const resp=await fetch('/api/multipart/initiate',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({filename:file.name,size:file.size,content_type:file.type||'application/octet-stream',num_parts:numParts})});
const data=await resp.json();
if(!resp.ok){status.textContent='Error: '+data.error;return}
status.textContent='Uploading '+numParts+' parts...';
wrap.style.display='block';fill.style.width='0%';
const startTime=Date.now();
const partProgress=new Array(numParts).fill(0);
const completedParts=[];
let failed=false;
function updateProgress(){const loaded=partProgress.reduce((a,b)=>a+b,0);const pct=(loaded/file.size)*100;fill.style.width=pct+'%';const elapsed=(Date.now()-startTime)/1000;const speed=loaded/elapsed;text.textContent=pct.toFixed(1)+'% | '+formatBytes(loaded)+' / '+formatBytes(file.size)+' | '+formatBytes(speed)+'/s | ETA '+formatTime((file.size-loaded)/speed)}
function uploadPart(i){return new Promise((resolve,reject)=>{const start=i*CHUNK_SIZE;const end=Math.min(start+CHUNK_SIZE,file.size);const chunk=file.slice(start,end);const xhr=new XMLHttpRequest();xhr.open('PUT',data.part_urls[i]);xhr.upload.onprogress=function(e){if(e.lengthComputable){partProgress[i]=e.loaded;updateProgress()}};xhr.onload=function(){if(xhr.status>=200&&xhr.status<300){partProgress[i]=end-start;completedParts.push({part_number:i+1,etag:xhr.getResponseHeader('ETag')});updateProgress();resolve()}else reject(new Error('Part '+(i+1)+' failed'))};xhr.onerror=function(){reject(new Error('Part '+(i+1)+' network error'))};xhr.send(chunk)})}
const queue=Array.from({length:numParts},(_,i)=>i);
const workers=[];
for(let i=0;i<MAX_CONCURRENT;i++){workers.push((async()=>{while(queue.length&&!failed){const idx=queue.shift();try{await uploadPart(idx)}catch(e){failed=true;status.textContent=e.message}}})())}
await Promise.all(workers);
if(failed)return;
status.textContent='Finalizing...';
completedParts.sort((a,b)=>a.part_number-b.part_number);
const complete=await fetch('/api/multipart/complete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({object_key:data.object_key,upload_id:data.upload_id,parts:completedParts})});
if(complete.ok){const elapsed=(Date.now()-startTime)/1000;fill.style.width='100%';text.textContent='100% | '+formatBytes(file.size)+' in '+formatTime(elapsed)+' | avg '+formatBytes(file.size/elapsed)+'/s';status.textContent='Done!';link.innerHTML='<a href="/'+data.slug+'">'+location.origin+'/'+data.slug+'</a>';loadFiles()}
else status.textContent='Failed to finalize upload';
}
</script>
</body>
</html>