Show available upload disk space refreshed every minute
This commit is contained in:
parent
d7b502bab3
commit
9fffdc62dc
2 changed files with 27 additions and 3 deletions
|
|
@ -1,17 +1,23 @@
|
|||
<!doctype html>
|
||||
<html lang="ru"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><title>Drop</title>
|
||||
<style>
|
||||
:root{--hue:0;--saturation:0%;--app-background:#000;--surface:hsla(var(--hue),var(--saturation),10%,.8);--text:#eee;--muted:#969696;--border:#2c2c2c;--accent:hsl(var(--hue),var(--saturation),78%);--on-accent:#161616;--danger:#ffb4ab}*{box-sizing:border-box}body{margin:0;background:var(--app-background);color:var(--text);font:15px system-ui,sans-serif;min-height:100svh;display:grid;place-items:center;padding:24px}body.drag::after{content:'';position:fixed;inset:12px;border:1px dashed var(--accent);border-radius:16px;pointer-events:none}main{width:100%;max-width:400px}h1{font-size:22px;font-weight:550;margin:0 0 24px;letter-spacing:-.6px}button,input{font:inherit}button{cursor:pointer;border:0;min-height:48px;border-radius:12px;background:var(--accent);color:var(--on-accent);font-weight:600;padding:0 20px}button:hover{filter:brightness(1.08)}button:active{transform:translateY(1px)}button:disabled{opacity:.4;cursor:default}input{min-height:48px;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:12px;color:var(--text);width:100%}input[type=number]{appearance:textfield;-moz-appearance:textfield}input::-webkit-inner-spin-button,input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}:focus-visible{outline:2px solid var(--accent);outline-offset:4px}label{color:var(--muted);font-size:13px}#login,#upload{display:grid;gap:16px}#drop{background:var(--surface);border:1px solid var(--border);color:var(--text);width:100%;font-weight:500}#filename{overflow-wrap:anywhere;margin:0;color:var(--muted);font-size:13px}small{font-size:12px;color:var(--muted);font-weight:400}.bottom{display:flex;align-items:end;gap:12px}.days{width:110px;display:grid;gap:8px}.bottom button{flex:1}.error{color:var(--danger);font-size:13px;margin:12px 0 0;min-height:18px}#result{position:relative}#result h2{font-size:20px;margin:0 48px 24px 0;font-weight:500}.close{position:absolute;right:-12px;top:-12px;background:transparent;color:var(--muted);padding:0;width:48px;font-size:26px}.copy{width:100%;margin-top:16px}#link{font-size:13px}#copied{font-size:13px;color:var(--muted);min-height:18px;margin-bottom:0}#busy{display:grid;place-content:center;text-align:center;gap:20px;min-height:180px}.spinner{width:32px;height:32px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;margin:auto}#progress{font-variant-numeric:tabular-nums;color:var(--muted)}[hidden]{display:none!important}@keyframes spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinner{animation:none}button:active{transform:none}}
|
||||
:root{--hue:0;--saturation:0%;--app-background:#000;--surface:hsla(var(--hue),var(--saturation),10%,.8);--text:#eee;--muted:#969696;--border:#2c2c2c;--accent:hsl(var(--hue),var(--saturation),78%);--on-accent:#161616;--danger:#ffb4ab}*{box-sizing:border-box}body{margin:0;background:var(--app-background);color:var(--text);font:15px system-ui,sans-serif;min-height:100svh;display:grid;place-items:center;padding:24px}body.drag::after{content:'';position:fixed;inset:12px;border:1px dashed var(--accent);border-radius:16px;pointer-events:none}main{width:100%;max-width:400px}h1{font-size:22px;font-weight:550;margin:0 0 24px;letter-spacing:-.6px}button,input{font:inherit}button{cursor:pointer;border:0;min-height:48px;border-radius:12px;background:var(--accent);color:var(--on-accent);font-weight:600;padding:0 20px}button:hover{filter:brightness(1.08)}button:active{transform:translateY(1px)}button:disabled{opacity:.4;cursor:default}input{min-height:48px;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:12px;color:var(--text);width:100%}input[type=number]{appearance:textfield;-moz-appearance:textfield}input::-webkit-inner-spin-button,input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}:focus-visible{outline:2px solid var(--accent);outline-offset:4px}label{color:var(--muted);font-size:13px}#login,#upload{display:grid;gap:16px}#drop{background:var(--surface);border:1px solid var(--border);color:var(--text);width:100%;font-weight:500}#storage{margin:0;font-size:13px;color:var(--muted)}#filename{overflow-wrap:anywhere;margin:0;color:var(--muted);font-size:13px}small{font-size:12px;color:var(--muted);font-weight:400}.bottom{display:flex;align-items:end;gap:12px}.days{width:110px;display:grid;gap:8px}.bottom button{flex:1}.error{color:var(--danger);font-size:13px;margin:12px 0 0;min-height:18px}#result{position:relative}#result h2{font-size:20px;margin:0 48px 24px 0;font-weight:500}.close{position:absolute;right:-12px;top:-12px;background:transparent;color:var(--muted);padding:0;width:48px;font-size:26px}.copy{width:100%;margin-top:16px}#link{font-size:13px}#copied{font-size:13px;color:var(--muted);min-height:18px;margin-bottom:0}#busy{display:grid;place-content:center;text-align:center;gap:20px;min-height:180px}.spinner{width:32px;height:32px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;margin:auto}#progress{font-variant-numeric:tabular-nums;color:var(--muted)}[hidden]{display:none!important}@keyframes spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinner{animation:none}button:active{transform:none}}
|
||||
</style>
|
||||
<main>
|
||||
<section id="entry" hidden><h1>Drop</h1><form id="login" hidden><label for="password">Пароль</label><input id="password" type="password" autocomplete="current-password" required><button>Войти</button></form><form id="upload" hidden><input type="file" id="file" hidden><button type="button" id="drop">Выбрать файл</button><p id="filename" aria-live="polite" hidden></p><div class="bottom"><div class="days"><label for="days">Срок, дней</label><input id="days" type="number" min="1" max="3650" step="1" value="1" required></div><button id="send" disabled>Upload</button></div></form><p class="error" id="error" role="alert"></p></section>
|
||||
<section id="entry" hidden><h1>Drop</h1><form id="login" hidden><label for="password">Пароль</label><input id="password" type="password" autocomplete="current-password" required><button>Войти</button></form><form id="upload" hidden><input type="file" id="file" hidden><button type="button" id="drop">Выбрать файл</button><p id="filename" aria-live="polite" hidden></p><div class="bottom"><div class="days"><label for="days">Срок, дней</label><input id="days" type="number" min="1" max="3650" step="1" value="1" required></div><button id="send" disabled>Upload</button></div><p id="storage" hidden></p></form><p class="error" id="error" role="alert"></p></section>
|
||||
<section id="busy" hidden role="status" aria-live="polite" tabindex="-1"><div class="spinner"></div><span>Загружаем</span><span id="progress">0%</span></section>
|
||||
<section id="result" hidden aria-labelledby="resultTitle"><button type="button" class="close" id="close" aria-label="Закрыть">×</button><h2 id="resultTitle" tabindex="-1">Ссылка готова</h2><input id="link" readonly aria-label="Ссылка на файл"><button type="button" id="copy" class="copy">Copy link</button><p id="copied" aria-live="polite"></p></section>
|
||||
</main>
|
||||
<script>
|
||||
const $=id=>document.getElementById(id);let selected=null,uploading=false,authenticated=false,dragDepth=0;
|
||||
function view(id){for(const name of ['entry','busy','result'])$(name).hidden=name!==id;document.body.classList.remove('drag');dragDepth=0}
|
||||
function session(ok){authenticated=ok;$('login').hidden=ok;$('upload').hidden=!ok;view('entry');if(!ok)$('password').focus()}
|
||||
function session(ok){authenticated=ok;if(ok)refreshStorage();else $('storage').hidden=true;$('login').hidden=ok;$('upload').hidden=!ok;view('entry');if(!ok)$('password').focus()}
|
||||
async function refreshStorage(){
|
||||
if(!authenticated)return;
|
||||
try{const response=await fetch('/api/storage',{cache:'no-store'});if(!response.ok)throw Error();const space=await response.json();if(!authenticated)return;$('storage').textContent=`Свободное место - ${(space.free/1e9).toFixed(1)} / ${(space.total/1e9).toFixed(1)} GB`;$('storage').hidden=false}
|
||||
catch{$('storage').hidden=true}
|
||||
}
|
||||
setInterval(refreshStorage,60000);
|
||||
fetch('/api/session').then(r=>r.json()).then(r=>session(r.authenticated)).catch(()=>session(false));
|
||||
$('login').onsubmit=async e=>{e.preventDefault();$('error').textContent='';try{const r=await fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:$('password').value})});const j=await r.json();if(!r.ok)throw Error(j.error);$('password').value='';session(true);$('drop').focus()}catch(e){$('error').textContent=e.message}};
|
||||
function choose(f){if(uploading)return;$('error').textContent='';if(f&&f.size>4*1024**3){$('error').textContent='Максимальный размер 4 ГБ';return}selected=f||null;$('filename').hidden=!f;$('filename').textContent=f?f.name:'';$('send').disabled=!f}
|
||||
|
|
|
|||
18
server.js
18
server.js
|
|
@ -31,6 +31,24 @@ app.post('/api/login', (req, res) => {
|
|||
res.json({ ok: true });
|
||||
});
|
||||
app.get('/api/session', (req, res) => { res.set('Cache-Control', 'no-store').json({ authenticated: authenticated(req) }); });
|
||||
let diskSpace = null;
|
||||
async function refreshDiskSpace() {
|
||||
try {
|
||||
const stats = await fs.promises.statfs(DATA);
|
||||
diskSpace = { free: stats.bavail * stats.bsize, total: stats.blocks * stats.bsize };
|
||||
} catch (error) {
|
||||
diskSpace = null;
|
||||
console.error('Cannot check storage space:', error.message);
|
||||
}
|
||||
}
|
||||
refreshDiskSpace();
|
||||
setInterval(refreshDiskSpace, 60000).unref();
|
||||
app.get('/api/storage', (req, res) => {
|
||||
res.set('Cache-Control', 'no-store');
|
||||
if (!authenticated(req)) return res.sendStatus(401);
|
||||
if (!diskSpace) return res.status(503).json({ error: 'Не удалось проверить свободное место' });
|
||||
res.json(diskSpace);
|
||||
});
|
||||
app.post('/api/upload', async (req, res) => {
|
||||
if (!authenticated(req)) return res.status(401).json({ error: 'Введите пароль заново' });
|
||||
if (req.headers['sec-fetch-site'] === 'cross-site') return res.sendStatus(403);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue