192 lines
6.6 KiB
HTML
192 lines
6.6 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||
<title>Datei-Upload</title>
|
||
<style>
|
||
:root { color-scheme: light dark; }
|
||
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
|
||
h1 { margin-bottom: .25rem; }
|
||
.muted { color: #888; font-size: .9rem; }
|
||
.drop {
|
||
margin-top: 1.5rem; border: 2px dashed #888; border-radius: 12px;
|
||
padding: 3rem 1rem; text-align: center; transition: background .15s, border-color .15s;
|
||
}
|
||
.drop.drag { background: rgba(0,120,255,.08); border-color: #0078ff; }
|
||
.buttons { margin-top: 1rem; display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; }
|
||
button, label.btn {
|
||
padding: .6rem 1rem; border-radius: 8px; border: 1px solid #888;
|
||
cursor: pointer; background: transparent; font: inherit;
|
||
}
|
||
label.btn input { display: none; }
|
||
#list { margin-top: 1.5rem; }
|
||
.file { display: flex; justify-content: space-between; gap: 1rem; padding: .4rem 0; border-bottom: 1px solid #333; font-size: .9rem; }
|
||
.file .status { font-variant-numeric: tabular-nums; }
|
||
.ok { color: #2ecc71; }
|
||
.err { color: #e74c3c; }
|
||
.gate { margin-top: 1rem; display: none; }
|
||
.gate input { padding: .5rem; border-radius: 6px; border: 1px solid #888; background: transparent; color: inherit; }
|
||
progress { width: 100%; height: 8px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1 id="title">Datei-Upload</h1>
|
||
<div class="muted" id="info"></div>
|
||
|
||
<div class="gate" id="gate">
|
||
<p>Dieser Link ist passwortgeschützt.</p>
|
||
<input type="password" id="pw" placeholder="Passwort" />
|
||
<button id="pwBtn">Entsperren</button>
|
||
<div id="pwErr" class="err" style="display:none;margin-top:.5rem">Passwort falsch.</div>
|
||
</div>
|
||
|
||
<div id="main" style="display:none">
|
||
<div class="drop" id="drop">
|
||
<div>Dateien oder Ordner hier hineinziehen</div>
|
||
<div class="muted">oder</div>
|
||
<div class="buttons">
|
||
<label class="btn">Dateien wählen<input type="file" id="fileInput" multiple /></label>
|
||
<label class="btn">Ordner wählen<input type="file" id="dirInput" webkitdirectory multiple /></label>
|
||
</div>
|
||
</div>
|
||
<div id="list"></div>
|
||
</div>
|
||
|
||
<script>
|
||
const token = location.pathname.split('/').filter(Boolean)[1];
|
||
let password = '';
|
||
|
||
const info = document.getElementById('info');
|
||
const gate = document.getElementById('gate');
|
||
const main = document.getElementById('main');
|
||
const drop = document.getElementById('drop');
|
||
const list = document.getElementById('list');
|
||
|
||
async function init() {
|
||
const r = await fetch(`/u/${token}/info`);
|
||
if (!r.ok) { document.body.innerHTML = '<h1>Link ungültig oder abgelaufen.</h1>'; return; }
|
||
const data = await r.json();
|
||
document.getElementById('title').textContent = `Upload für ${data.name}`;
|
||
if (data.expires_at) {
|
||
info.textContent = `Gültig bis: ${new Date(data.expires_at).toLocaleString()}`;
|
||
}
|
||
if (data.has_password) gate.style.display = 'block';
|
||
else main.style.display = 'block';
|
||
}
|
||
|
||
document.getElementById('pwBtn').onclick = async () => {
|
||
const pw = document.getElementById('pw').value;
|
||
const r = await fetch(`/u/${token}/auth`, {
|
||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({ password: pw }),
|
||
});
|
||
const j = await r.json();
|
||
if (j.ok) { password = pw; gate.style.display='none'; main.style.display='block'; }
|
||
else document.getElementById('pwErr').style.display='block';
|
||
};
|
||
|
||
function fmtSize(n) {
|
||
if (n < 1024) return n + ' B';
|
||
if (n < 1024*1024) return (n/1024).toFixed(1) + ' KB';
|
||
if (n < 1024*1024*1024) return (n/1024/1024).toFixed(1) + ' MB';
|
||
return (n/1024/1024/1024).toFixed(2) + ' GB';
|
||
}
|
||
|
||
function addRow(name, size) {
|
||
const row = document.createElement('div');
|
||
row.className = 'file';
|
||
row.innerHTML = `<div>${name}</div><div class="status">${fmtSize(size)} – <span>wartet</span><progress max="100" value="0"></progress></div>`;
|
||
list.appendChild(row);
|
||
return row;
|
||
}
|
||
|
||
async function uploadOne(file, relPath) {
|
||
const row = addRow(relPath, file.size);
|
||
const status = row.querySelector('.status span');
|
||
const bar = row.querySelector('progress');
|
||
const fd = new FormData();
|
||
// path first, so multer has it available when processing the file
|
||
fd.append('path', relPath);
|
||
fd.append('file', file, file.name);
|
||
|
||
await new Promise((resolve) => {
|
||
const xhr = new XMLHttpRequest();
|
||
xhr.open('POST', `/u/${token}/upload`);
|
||
if (password) xhr.setRequestHeader('X-Upload-Password', password);
|
||
xhr.upload.onprogress = (e) => {
|
||
if (e.lengthComputable) bar.value = (e.loaded / e.total) * 100;
|
||
};
|
||
xhr.onload = () => {
|
||
if (xhr.status >= 200 && xhr.status < 300) {
|
||
status.textContent = 'fertig';
|
||
status.className = 'ok';
|
||
bar.value = 100;
|
||
} else {
|
||
status.textContent = 'Fehler';
|
||
status.className = 'err';
|
||
}
|
||
resolve();
|
||
};
|
||
xhr.onerror = () => { status.textContent='Fehler'; status.className='err'; resolve(); };
|
||
xhr.send(fd);
|
||
});
|
||
}
|
||
|
||
async function uploadFiles(items) {
|
||
for (const { file, path } of items) {
|
||
await uploadOne(file, path);
|
||
}
|
||
}
|
||
|
||
document.getElementById('fileInput').onchange = (e) => {
|
||
const items = [...e.target.files].map(f => ({ file: f, path: f.name }));
|
||
uploadFiles(items);
|
||
};
|
||
document.getElementById('dirInput').onchange = (e) => {
|
||
const items = [...e.target.files].map(f => ({ file: f, path: f.webkitRelativePath || f.name }));
|
||
uploadFiles(items);
|
||
};
|
||
|
||
// Drag & drop with directory support
|
||
async function traverse(entry, prefix='') {
|
||
const out = [];
|
||
if (entry.isFile) {
|
||
const file = await new Promise(r => entry.file(r));
|
||
out.push({ file, path: prefix + entry.name });
|
||
} else if (entry.isDirectory) {
|
||
const reader = entry.createReader();
|
||
const entries = await new Promise(r => reader.readEntries(r));
|
||
for (const e of entries) {
|
||
const sub = await traverse(e, prefix + entry.name + '/');
|
||
out.push(...sub);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
drop.addEventListener('dragover', (e) => { e.preventDefault(); drop.classList.add('drag'); });
|
||
drop.addEventListener('dragleave', () => drop.classList.remove('drag'));
|
||
drop.addEventListener('drop', async (e) => {
|
||
e.preventDefault();
|
||
drop.classList.remove('drag');
|
||
const items = [...e.dataTransfer.items];
|
||
const all = [];
|
||
for (const it of items) {
|
||
const entry = it.webkitGetAsEntry && it.webkitGetAsEntry();
|
||
if (entry) {
|
||
const sub = await traverse(entry);
|
||
all.push(...sub);
|
||
} else if (it.kind === 'file') {
|
||
const f = it.getAsFile();
|
||
all.push({ file: f, path: f.name });
|
||
}
|
||
}
|
||
uploadFiles(all);
|
||
});
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|