first commit

This commit is contained in:
cappuch
2026-05-21 21:37:00 +01:00
commit ffa778a0ee
25 changed files with 47476 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
<!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>
+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<title>fileshares.cfd</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:monospace;padding:40px;background:white;color:black;max-width:680px}
h1{margin-bottom:12px;font-size:32px}
p{margin-bottom:12px;line-height:1.5}
.actions{margin-top:24px}
a.button{display:inline-block;border:1px solid black;background:white;color:black;padding:8px 16px;text-decoration:none;margin-right:8px}
a.button:hover{background:black;color:white}
.nav{margin-top:40px;font-size:12px}
a{color:black}
</style>
</head>
<body>
<h1>fileshares.cfd</h1>
<p>Simple, fast file hosting without the bloat.</p>
<p>Upload a file, get a clean link, and come back to your dashboard whenever you need older uploads. Access is request-only, so you will need an upload token before you can use the dashboard.</p>
<div class="actions">
<a class="button" id="mainAction" href="/login">Login</a>
<a class="button" href="/speedtest">Speed Test</a>
</div>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a id="navAuth" href="/login">Login</a> | <a href="/terms">Terms</a> | <a href="/privacy">Privacy</a></div>
<script>
function hasToken(){return !!localStorage.getItem('upload_token')}
const main=document.getElementById('mainAction');
const nav=document.getElementById('navAuth');
if(hasToken()){
main.href='/dashboard';
main.textContent='Dashboard';
nav.href='/dashboard';
nav.textContent='Dashboard';
}
</script>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<title>Login</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:500px}
button{border:1px solid black;background:white;padding:8px 16px;cursor:pointer;font-family:monospace}
button:hover{background:black;color:white}
input[type="text"]{width:100%;font-family:monospace;border:1px solid black;padding:8px;margin-bottom:10px}
#status{margin-top:10px}
.nav{margin-top:40px;font-size:12px}
a{color:black}
</style>
</head>
<body>
<h1>Login</h1>
<div class="box">
<p style="margin-bottom:10px">Enter your upload token:</p>
<input type="text" id="tokenInput" placeholder="Paste token here">
<button onclick="save()">Save</button>
<button onclick="clear_()">Clear</button>
<div id="status"></div>
</div>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a id="navAuth" href="/login">Login</a> | <a href="/terms">Terms</a> | <a href="/privacy">Privacy</a></div>
<script>
const input=document.getElementById('tokenInput');
const status=document.getElementById('status');
const nav=document.getElementById('navAuth');
const saved=localStorage.getItem('upload_token');
function updateNav(){if(localStorage.getItem('upload_token')){nav.href='/dashboard';nav.textContent='Dashboard'}else{nav.href='/login';nav.textContent='Login'}}
if(saved){input.value=saved;status.textContent='Token loaded from storage.'}
updateNav();
function save(){const t=input.value.trim();if(t){localStorage.setItem('upload_token',t);status.innerHTML='Saved. <a href="/dashboard">Open dashboard</a>.';updateNav()}else status.textContent='Enter a token first.'}
function clear_(){localStorage.removeItem('upload_token');input.value='';status.textContent='Cleared.';updateNav()}
</script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>Privacy Policy</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:monospace;padding:40px;background:white;color:black;max-width:600px}
h1{margin-bottom:20px}
p{margin-bottom:10px}
.nav{margin-top:40px;font-size:12px}
a{color:black}
</style>
</head>
<body>
<h1>Privacy Policy</h1>
<p>We keep this simple.</p>
<p>1. Uploaded files are stored on our servers.</p>
<p>2. File metadata, including name, size, and upload time, is stored so the service can show and serve your files.</p>
<p>3. No cookies are used.</p>
<p>4. Authentication tokens are saved in your browser.</p>
<p>5. No analytics or tracking.</p>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a id="navAuth" href="/login">Login</a> | <a href="/terms">Terms</a> | <a href="/privacy">Privacy</a></div>
<script>
const nav=document.getElementById('navAuth');
if(localStorage.getItem('upload_token')){nav.href='/dashboard';nav.textContent='Dashboard'}
</script>
</body>
</html>
+100
View File
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<title>Speed Test</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:500px}
button{border:1px solid black;background:white;padding:8px 16px;cursor:pointer;font-family:monospace;margin:2px}
button:hover{background:black;color:white}
#status{margin-top:10px}
#progress-wrap{display:none;margin-top:10px}
#bar{width:100%;height:20px;border:1px solid black}
#fill{height:100%;background:black;width:0%}
#text{margin-top:4px}
.nav{margin-top:40px;font-size:12px}
a{color:black}
.section{margin-bottom:10px}
</style>
</head>
<body>
<h1>Speed Test</h1>
<div class="box">
<div class="section">
<b>Download:</b><br>
<button onclick="testDown(10)">10 MB</button>
<button onclick="testDown(50)">50 MB</button>
<button onclick="testDown(100)">100 MB</button>
<button onclick="testDown(250)">250 MB</button>
</div>
<div class="section" id="uploadSection" style="display:none">
<b>Upload:</b><br>
<button onclick="testUp(10)">10 MB</button>
<button onclick="testUp(50)">50 MB</button>
<button onclick="testUp(100)">100 MB</button>
<button onclick="testUp(250)">250 MB</button>
</div>
<div id="status"></div>
<div id="progress-wrap">
<div id="bar"><div id="fill"></div></div>
<div id="text"></div>
</div>
</div>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a id="navAuth" href="/login">Login</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'}
const nav=document.getElementById('navAuth');
if(getToken()){
nav.href='/dashboard';
nav.textContent='Dashboard';
document.getElementById('uploadSection').style.display='block';
}
async function testDown(mb){
const status=document.getElementById('status');
const wrap=document.getElementById('progress-wrap');
const fill=document.getElementById('fill');
const text=document.getElementById('text');
const total=mb*1024*1024;
status.textContent='Getting download URL...';wrap.style.display='block';fill.style.width='0%';text.textContent='';
const resp=await fetch('/api/speedtest/download-url/'+mb);
const data=await resp.json();
status.textContent='Downloading '+mb+' MB...';
const xhr=new XMLHttpRequest();
xhr.open('GET',data.download_url);xhr.responseType='blob';
const start=Date.now();
xhr.onprogress=function(e){if(!e.lengthComputable)return;const pct=(e.loaded/e.total)*100;fill.style.width=pct+'%';const elapsed=(Date.now()-start)/1000;const speed=e.loaded/elapsed;text.textContent=pct.toFixed(1)+'% | '+formatBytes(e.loaded)+' / '+formatBytes(e.total)+' | '+formatBytes(speed)+'/s'};
xhr.onload=function(){if(xhr.status>=200&&xhr.status<300){const elapsed=(Date.now()-start)/1000;const speed=total/elapsed;fill.style.width='100%';status.textContent='Download: '+formatBytes(speed)+'/s ('+mb+' MB in '+formatTime(elapsed)+')';text.textContent='100%'}else{status.textContent='Download failed: '+xhr.status+' (upload test data first)'}};
xhr.onerror=function(){status.textContent='Download failed (upload test data for this size first)'};
xhr.send();
}
async function testUp(mb){
const status=document.getElementById('status');
const wrap=document.getElementById('progress-wrap');
const fill=document.getElementById('fill');
const text=document.getElementById('text');
const total=mb*1024*1024;
if(!getToken()){status.textContent='No token. Go to Login page first.';return}
status.textContent='Getting upload URL...';wrap.style.display='block';fill.style.width='0%';text.textContent='';
const resp=await fetch('/api/speedtest/upload-url/'+mb,{headers:authHeaders()});
const data=await resp.json();
if(!resp.ok){status.textContent='Error: '+(data.error||'unauthorized');return}
status.textContent='Uploading '+mb+' MB...';
const blob=new Blob([new ArrayBuffer(total)]);
const xhr=new XMLHttpRequest();
xhr.open('PUT',data.upload_url);
const start=Date.now();
xhr.upload.onprogress=function(e){if(!e.lengthComputable)return;const pct=(e.loaded/e.total)*100;fill.style.width=pct+'%';const elapsed=(Date.now()-start)/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){const elapsed=(Date.now()-start)/1000;const speed=total/elapsed;fill.style.width='100%';status.textContent='Upload: '+formatBytes(speed)+'/s ('+mb+' MB in '+formatTime(elapsed)+')';text.textContent='100%'}else{status.textContent='Upload failed: '+xhr.status}};
xhr.onerror=function(){status.textContent='Upload failed: network error'};
xhr.send(blob);
}
</script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>Terms of Service</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:monospace;padding:40px;background:white;color:black;max-width:600px}
h1{margin-bottom:20px}
p{margin-bottom:10px}
.nav{margin-top:40px;font-size:12px}
a{color:black}
</style>
</head>
<body>
<h1>Terms of Service</h1>
<p>By using this service, you agree to the following:</p>
<p>1. Do not upload illegal content.</p>
<p>2. Do not abuse the service.</p>
<p>3. Files may be removed at any time without notice.</p>
<p>4. No warranty is provided. Use at your own risk.</p>
<p>5. We reserve the right to terminate access at any time.</p>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a id="navAuth" href="/login">Login</a> | <a href="/terms">Terms</a> | <a href="/privacy">Privacy</a></div>
<script>
const nav=document.getElementById('navAuth');
if(localStorage.getItem('upload_token')){nav.href='/dashboard';nav.textContent='Dashboard'}
</script>
</body>
</html>