Public Access
`w` in the Storage App starts a small HTTP server and shows its address, as a QR code and in letters, with a six-digit code. A browser on the same network that has typed the code can list, download, upload, make folders and delete, under the Storage App's rules. The server runs only while that screen is open. Nothing is encrypted, and the screen says so. Uploads are streamed to the card under a temporary name and renamed when whole. Every access to the card is handed to the storage task, 8 KB at a time, from the server's own task. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
70 lines
5.5 KiB
C++
70 lines
5.5 KiB
C++
#pragma once
|
|
|
|
// The page a phone's browser gets (issue #88): one file, no other request but the API's. Kept
|
|
// small: it is sent from flash as it is.
|
|
namespace roro {
|
|
|
|
inline constexpr char kWebSharePage[] = R"HTML(<!doctype html>
|
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>roro9stack files</title>
|
|
<style>
|
|
:root{color-scheme:dark light;--bg:#000;--fg:#fff;--mu:#9ab;--ac:#2cf;--ln:#345;--wa:#fa3}
|
|
@media (prefers-color-scheme:light){:root{--bg:#fff;--fg:#012;--mu:#567;--ac:#06a;--ln:#bcd;--wa:#b50}}
|
|
*{box-sizing:border-box}body{margin:0;font:16px/1.4 system-ui,sans-serif;background:var(--bg);color:var(--fg)}
|
|
main{max-width:720px;margin:0 auto;padding:12px}h1{font-size:18px;margin:4px 0 12px}
|
|
button,input,.btn{font:inherit;min-height:44px;padding:0 14px;border:1px solid var(--ln);background:none;color:inherit;border-radius:6px}
|
|
button,.btn{cursor:pointer}.main{background:var(--ac);color:#000;border-color:var(--ac)}
|
|
#crumbs{margin:8px 0;word-break:break-all}#crumbs a{color:var(--ac);text-decoration:none;padding:6px 2px;display:inline-block}
|
|
ul{list-style:none;margin:0;padding:0}li{display:flex;align-items:center;gap:8px;border-top:1px solid var(--ln);min-height:48px}
|
|
li a{flex:1;color:inherit;text-decoration:none;padding:10px 0;word-break:break-all}li.d a{color:var(--ac)}
|
|
li small{color:var(--mu);white-space:nowrap}li button{min-height:36px;padding:0 10px;color:var(--mu)}
|
|
#bar{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}#msg{color:var(--wa);min-height:24px;margin:8px 0}
|
|
progress{width:100%}.hide{display:none}#login input{width:9em;font-size:22px;letter-spacing:.15em;text-align:center}
|
|
</style></head><body><main>
|
|
<h1>roro9stack: the SD card</h1>
|
|
<form id="login" class="hide"><p>The code on the device's screen:</p>
|
|
<input id="code" inputmode="numeric" autocomplete="off" maxlength="7" autofocus> <button class="main">Open</button></form>
|
|
<div id="app" class="hide">
|
|
<div id="crumbs"></div>
|
|
<div id="bar"><label class="btn main" style="display:inline-flex;align-items:center">Upload files<input id="pick" type="file" multiple class="hide"></label>
|
|
<button id="mk">New folder</button></div>
|
|
<progress id="prog" class="hide" max="100" value="0"></progress>
|
|
<ul id="list"></ul></div>
|
|
<p id="msg"></p>
|
|
</main><script>
|
|
const $=i=>document.getElementById(i),E=encodeURIComponent;let cwd='/';
|
|
const say=t=>{$('msg').textContent=t||''};
|
|
const join=(d,n)=>d=='/'?'/'+n:d+'/'+n;
|
|
const size=s=>s<1024?s+' B':s<1048576?(s/1024).toFixed(1)+' KB':(s/1048576).toFixed(1)+' MB';
|
|
function show(app){$('login').classList.toggle('hide',app);$('app').classList.toggle('hide',!app)}
|
|
async function call(u,o){const r=await fetch(u,o);if(r.status==401){show(false);throw new Error('The code, please')}
|
|
if(!r.ok)throw new Error(await r.text()||('Error '+r.status));return r}
|
|
async function login(code){const r=await fetch('/api/login',{method:'POST',body:'code='+E(code)});
|
|
if(r.ok){show(true);list('/')}else say(await r.text())}
|
|
async function list(p){try{const j=await(await call('/api/list?path='+E(p))).json();cwd=j.path;say(j.more?'Only the first '+j.items.length+' are shown':'');
|
|
const c=$('crumbs');c.textContent='';let at='';const parts=['/'].concat(cwd.split('/').filter(x=>x));
|
|
parts.forEach((n,i)=>{at=i?join(at||'/',n):'/';const a=document.createElement('a');a.href='#';a.textContent=i?n:'SD card';const to=at;a.onclick=e=>{e.preventDefault();list(to)};
|
|
if(i)c.append(' / ');c.append(a)});
|
|
const ul=$('list');ul.textContent='';j.items.sort((a,b)=>b.d-a.d||a.n.localeCompare(b.n));
|
|
if(!j.items.length){const li=document.createElement('li');li.innerHTML='<small>Nothing here</small>';ul.append(li)}
|
|
for(const it of j.items){const li=document.createElement('li'),a=document.createElement('a'),s=document.createElement('small'),b=document.createElement('button'),full=join(cwd,it.n);
|
|
a.textContent=it.n+(it.d?'/':'');if(it.d){li.className='d';a.href='#';a.onclick=e=>{e.preventDefault();list(full)}}else{a.href='/dl?path='+E(full);a.download=it.n;s.textContent=size(it.s)}
|
|
b.textContent='Delete';b.onclick=async()=>{if(!confirm('Delete '+it.n+'?'))return;try{await call('/api/delete?path='+E(full),{method:'POST'});list(cwd)}catch(e){say(e.message)}};
|
|
li.append(a,s,b);ul.append(li)}}catch(e){say(e.message)}}
|
|
function put(f,replace){return new Promise((ok,no)=>{const x=new XMLHttpRequest();x.open('PUT','/up?path='+E(join(cwd,f.name))+(replace?'&replace=1':''));
|
|
x.upload.onprogress=e=>{if(e.lengthComputable)$('prog').value=100*e.loaded/e.total};
|
|
x.onload=()=>x.status==200?ok():x.status==409&&!replace&&confirm(f.name+' is there already. Replace it?')?put(f,1).then(ok,no):x.status==409?ok():no(new Error(x.responseText||'Error '+x.status));
|
|
x.onerror=()=>no(new Error('The connection was lost'));x.send(f)})}
|
|
$('pick').onchange=async e=>{const fs=[...e.target.files];$('prog').classList.remove('hide');
|
|
try{let n=0;for(const f of fs){say('Uploading '+f.name+' ('+(++n)+' of '+fs.length+')');$('prog').value=0;await put(f)}say('')}catch(err){say(err.message)}
|
|
$('prog').classList.add('hide');e.target.value='';list(cwd)};
|
|
$('mk').onclick=async()=>{const n=prompt('The new folder\'s name');if(!n)return;try{await call('/api/mkdir?path='+E(join(cwd,n)),{method:'POST'});list(cwd)}catch(e){say(e.message)}};
|
|
$('login').onsubmit=e=>{e.preventDefault();login($('code').value)};
|
|
async function start(){const h=location.hash.slice(1);if(h){history.replaceState(null,'','/');await login(h)}
|
|
else{const r=await fetch('/api/list?path=%2F');if(r.ok){show(true);list('/')}else show(false)}}
|
|
onhashchange=start;start();
|
|
</script></body></html>
|
|
)HTML";
|
|
|
|
} // namespace roro
|