Theseus: bundle Pithos 0.3.10
This commit is contained in:
parent
bdc8f951c6
commit
399e763745
4 changed files with 139 additions and 17 deletions
|
|
@ -1,14 +1,18 @@
|
||||||
{
|
{
|
||||||
"id": "pithos",
|
"id": "pithos",
|
||||||
"name": "Pithos",
|
"name": "Pithos",
|
||||||
"version": "0.3.9",
|
"version": "0.3.10",
|
||||||
"description": "Run s3d, the Sia S3 gateway, from the Theseus sidebar: connect it to a Sia indexer, create S3 users and access keys, browse and share buckets, and watch uploads reach Sia.",
|
"description": "Run s3d, the Sia S3 gateway, from the Theseus sidebar: connect it to a Sia indexer, create S3 users and access keys, browse and share buckets, and watch uploads reach Sia.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"sidebar-panel",
|
"sidebar-panel",
|
||||||
"vault-derive"
|
"vault-derive",
|
||||||
|
"site-route"
|
||||||
],
|
],
|
||||||
"updateURL": "https://navigate.st/bns/theseus.x/extensions/pithos/updates.json"
|
"updateURL": "https://navigate.st/bns/theseus.x/extensions/pithos/updates.json",
|
||||||
|
"siteRoutes": [
|
||||||
|
"pithos.sia"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,31 @@ export async function createPithos(opts = {}) {
|
||||||
const STREAMED = Symbol('streamed');
|
const STREAMED = Symbol('streamed');
|
||||||
let boundPort = port;
|
let boundPort = port;
|
||||||
|
|
||||||
|
// Site mode: Theseus relays pithos.sia/<path> here (header x-pithos-site).
|
||||||
|
// Pithos answers its own files, its pages and /<S3 user>/…; anything else
|
||||||
|
// is "not mine", so the pithos.sia website answers it instead.
|
||||||
|
const SITE_PAGES = new Set(['overview', 'drives', 'users', 'account', 'start', 'setup', 'settings', 'logs']);
|
||||||
|
const SITE_FILES = new Set(['app.js', 'app.css', 'icon.svg']);
|
||||||
|
let siteUsersCache = { at: 0, set: new Set() };
|
||||||
|
async function siteUsers() {
|
||||||
|
if (Date.now() - siteUsersCache.at > 10_000) {
|
||||||
|
try { siteUsersCache = { at: Date.now(), set: new Set(await cli.listUsers()) }; }
|
||||||
|
catch { siteUsersCache.at = Date.now(); } // keep the last good list
|
||||||
|
}
|
||||||
|
return siteUsersCache.set;
|
||||||
|
}
|
||||||
|
async function serveSite(req, res, url) {
|
||||||
|
const segs = url.pathname.split('/').filter(Boolean);
|
||||||
|
let first = '';
|
||||||
|
try { first = decodeURIComponent(segs[0] || ''); } catch {}
|
||||||
|
if (segs.length === 1 && SITE_FILES.has(first)) return serveStatic(req, res, url);
|
||||||
|
if (!(SITE_PAGES.has(first) || (await siteUsers()).has(first))) {
|
||||||
|
res.writeHead(404, { 'x-pithos-not-mine': '1' });
|
||||||
|
return res.end('not found');
|
||||||
|
}
|
||||||
|
return serveStatic(req, res, new URL('/', url), { base: '/' });
|
||||||
|
}
|
||||||
|
|
||||||
function hostAllowed(h) {
|
function hostAllowed(h) {
|
||||||
if (!h) return false;
|
if (!h) return false;
|
||||||
const ok = [`127.0.0.1:${boundPort}`, `localhost:${boundPort}`, `[::1]:${boundPort}`, ...allowedHosts];
|
const ok = [`127.0.0.1:${boundPort}`, `localhost:${boundPort}`, `[::1]:${boundPort}`, ...allowedHosts];
|
||||||
|
|
@ -501,6 +526,7 @@ export async function createPithos(opts = {}) {
|
||||||
throw new HttpError(404, 'no such endpoint');
|
throw new HttpError(404, 'no such endpoint');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.headers['x-pithos-site'] === '1') return await serveSite(req, res, url);
|
||||||
return serveStatic(req, res, url);
|
return serveStatic(req, res, url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const status = err instanceof HttpError ? err.status
|
const status = err instanceof HttpError ? err.status
|
||||||
|
|
@ -649,7 +675,7 @@ function describePolicy(policy) {
|
||||||
return 'custom';
|
return 'custom';
|
||||||
}
|
}
|
||||||
|
|
||||||
function serveStatic(req, res, url) {
|
function serveStatic(req, res, url, { base } = {}) {
|
||||||
if (req.method !== 'GET' && req.method !== 'HEAD') { res.writeHead(405); return res.end(); }
|
if (req.method !== 'GET' && req.method !== 'HEAD') { res.writeHead(405); return res.end(); }
|
||||||
let rel = decodeURIComponent(url.pathname);
|
let rel = decodeURIComponent(url.pathname);
|
||||||
if (rel === '/' || !path.extname(rel)) rel = '/index.html';
|
if (rel === '/' || !path.extname(rel)) rel = '/index.html';
|
||||||
|
|
@ -662,6 +688,8 @@ function serveStatic(req, res, url) {
|
||||||
'cache-control': 'no-cache',
|
'cache-control': 'no-cache',
|
||||||
'content-security-policy': "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'",
|
'content-security-policy': "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'",
|
||||||
});
|
});
|
||||||
|
// Pages under pithos.sia/<user>/<drive>/… load the app's files from the root.
|
||||||
|
if (base && file.endsWith('index.html')) data = Buffer.from(data.toString('utf8').replace('<head>', `<head><base href="${base}">`));
|
||||||
res.end(req.method === 'HEAD' ? undefined : data);
|
res.end(req.method === 'HEAD' ? undefined : data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,10 +133,52 @@ module.exports = {
|
||||||
});
|
});
|
||||||
api.onMessage("open-settings", ({ section = "" } = {}) => { api.openSettings?.(String(section)); return { ok: true }; });
|
api.onMessage("open-settings", ({ section = "" } = {}) => { api.openSettings?.(String(section)); return { ok: true }; });
|
||||||
|
|
||||||
// A full browser-tab view, for anyone who prefers it to the sidebar.
|
// ---- pithos.sia/<user>/<drive>/<folder>: the app at its own address ----
|
||||||
api.onMessage("open-in-tab", async () => {
|
// Theseus hands this add-on the requests for paths under pithos.sia (the
|
||||||
|
// root page stays the website). They go to the control server with the
|
||||||
|
// internal session, but only for pithos.sia's own requests and the
|
||||||
|
// address bar, never another site's fetch or form, and only while the
|
||||||
|
// vault is unlocked, the same as the sidebar panel.
|
||||||
|
const siteRoutes = typeof api.registerSiteRoute === "function";
|
||||||
|
if (siteRoutes) {
|
||||||
|
const json = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" } });
|
||||||
|
const FORWARD = ["content-type", "x-pithos", "range", "accept", "if-range"];
|
||||||
|
const DROP = new Set(["set-cookie", "connection", "keep-alive", "transfer-encoding"]);
|
||||||
|
api.registerSiteRoute({ host: "pithos.sia", handle: async (request) => {
|
||||||
|
const u = new URL(request.url);
|
||||||
|
const fetchSite = request.headers.get("sec-fetch-site");
|
||||||
|
const origin = request.headers.get("origin");
|
||||||
|
const own = fetchSite ? fetchSite === "same-origin" || fetchSite === "none"
|
||||||
|
: !origin || /^(bns|https):\/\/pithos\.sia$/i.test(origin);
|
||||||
|
const st = await vaultStatus();
|
||||||
|
const locked = st.setup && st.prompt && !st.unlocked;
|
||||||
|
if (u.pathname === "/api/host/unlock") {
|
||||||
|
if (request.method !== "POST" || request.headers.get("x-pithos") !== "1" || !own) return json(403, { error: "forbidden" });
|
||||||
|
if (!locked) return json(200, { ok: true });
|
||||||
|
const r = await vault.requestUnlock({ reason: "Open Pithos, your S3 storage on Sia." });
|
||||||
|
return json(200, { ok: !!r.ok });
|
||||||
|
}
|
||||||
|
if (locked && u.pathname === "/api/session") return json(200, { authenticated: false, locked: true, passwordLogin: false, host: "theseus" });
|
||||||
const p = await ensureServer();
|
const p = await ensureServer();
|
||||||
api.openTab(p.authUrl);
|
const headers = { "x-pithos-site": "1" };
|
||||||
|
for (const k of FORWARD) { const v = request.headers.get(k); if (v) headers[k] = v; }
|
||||||
|
if (own && !locked) headers.cookie = cookie;
|
||||||
|
const init = { method: request.method, headers, redirect: "manual" };
|
||||||
|
if (request.method !== "GET" && request.method !== "HEAD") init.body = Buffer.from(await request.arrayBuffer());
|
||||||
|
const res = await fetch(new URL(u.pathname + u.search, p.url), init);
|
||||||
|
if (res.status === 404 && res.headers.get("x-pithos-not-mine")) return null;
|
||||||
|
const out = new Headers();
|
||||||
|
for (const [k, v] of res.headers) if (!DROP.has(k.toLowerCase())) out.set(k, v);
|
||||||
|
return new Response(request.method === "HEAD" ? null : res.body, { status: res.status, headers: out });
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full page: Pithos at pithos.sia/<user>/<drive>/… in an ordinary tab.
|
||||||
|
// An older Theseus without site routes gets the loopback address.
|
||||||
|
api.onMessage("open-in-tab", async ({ path: appPath } = {}) => {
|
||||||
|
const p = await ensureServer();
|
||||||
|
const clean = /^\/[^\s]*$/.test(String(appPath || "")) && appPath !== "/" ? appPath : "/drives";
|
||||||
|
api.openTab(siteRoutes ? `https://pithos.sia${clean}` : p.authUrl);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ function put(el, ...children) {
|
||||||
// server origin to fetch from: every call goes over the add-on IPC bridge
|
// server origin to fetch from: every call goes over the add-on IPC bridge
|
||||||
// and the add-on forwards it to the control server.
|
// and the add-on forwards it to the control server.
|
||||||
const bridge = window.silentmode && typeof window.silentmode.invoke === 'function' ? window.silentmode : null;
|
const bridge = window.silentmode && typeof window.silentmode.invoke === 'function' ? window.silentmode : null;
|
||||||
|
// On pithos.sia (Theseus) pages have real addresses: pithos.sia/<user>/<drive>/<folder>,
|
||||||
|
// pithos.sia/settings… Elsewhere (sidebar, web console, desktop) they stay #/ routes.
|
||||||
|
const PATH_MODE = !bridge && (location.protocol === 'bns:' || location.hostname === 'pithos.sia');
|
||||||
|
|
||||||
async function api(method, path, body, { raw } = {}) {
|
async function api(method, path, body, { raw } = {}) {
|
||||||
if (bridge) {
|
if (bridge) {
|
||||||
|
|
@ -191,13 +194,43 @@ const views = { start: getStarted, account, overview, buckets, users, setup, set
|
||||||
// Pages that live under Account in the menu.
|
// Pages that live under Account in the menu.
|
||||||
const NAV_OF = { start: 'account', setup: 'account' };
|
const NAV_OF = { start: 'account', setup: 'account' };
|
||||||
|
|
||||||
|
// [page, ...parts] for the current address. On pithos.sia the first part is a
|
||||||
|
// page name or an S3 user (whose drives the rest addresses).
|
||||||
|
function currentRoute() {
|
||||||
|
if (!PATH_MODE) return location.hash.replace(/^#\/?/, '').split('/').filter((x, i) => i > 0 || x);
|
||||||
|
const segs = location.pathname.split('/').filter(Boolean);
|
||||||
|
if (!segs.length) return [];
|
||||||
|
const first = decodeURIComponent(segs[0]);
|
||||||
|
if (first === 'drives') return ['buckets'];
|
||||||
|
if (views[first]) return segs;
|
||||||
|
if (state.selectedUser !== first) { state.selectedUser = first; try { localStorage.setItem('pithos.user', first); } catch {} }
|
||||||
|
return ['buckets', ...segs.slice(1)];
|
||||||
|
}
|
||||||
|
// '#/page/parts' → the address it has on pithos.sia.
|
||||||
|
function hashToPath(hash) {
|
||||||
|
const [name = 'overview', ...rest] = String(hash).replace(/^#\/?/, '').split('/');
|
||||||
|
if (name === 'buckets') {
|
||||||
|
const user = state.selectedUser;
|
||||||
|
if (!user) return '/drives';
|
||||||
|
return `/${encodeURIComponent(user)}${rest.length ? '/' + rest.join('/') : ''}`;
|
||||||
|
}
|
||||||
|
return `/${[name, ...rest].join('/')}`;
|
||||||
|
}
|
||||||
|
// Go to a page given as '#/page/parts', whichever kind of address this host uses.
|
||||||
|
function navTo(hash) {
|
||||||
|
if (!PATH_MODE) { location.hash = hash; return; }
|
||||||
|
const p = hashToPath(hash);
|
||||||
|
if (p !== location.pathname) history.pushState(null, '', p);
|
||||||
|
route();
|
||||||
|
}
|
||||||
|
|
||||||
function route() {
|
function route() {
|
||||||
cleanup.forEach((fn) => fn());
|
cleanup.forEach((fn) => fn());
|
||||||
cleanup = [];
|
cleanup = [];
|
||||||
logSink = null;
|
logSink = null;
|
||||||
// First run lands on the guided setup until it has been finished once.
|
// First run lands on the guided setup until it has been finished once.
|
||||||
const fallback = !setupDone() && !state.status?.registration?.registered ? 'account' : 'overview';
|
const fallback = !setupDone() && !state.status?.registration?.registered ? 'account' : 'overview';
|
||||||
const [, name = fallback, ...rest] = location.hash.replace(/^#/, '').split('/');
|
const [name = fallback, ...rest] = currentRoute();
|
||||||
const view = views[name] ? name : fallback;
|
const view = views[name] ? name : fallback;
|
||||||
const navView = NAV_OF[view] || view;
|
const navView = NAV_OF[view] || view;
|
||||||
for (const a of document.querySelectorAll('#nav a')) a.classList.toggle('active', a.dataset.view === navView);
|
for (const a of document.querySelectorAll('#nav a')) a.classList.toggle('active', a.dataset.view === navView);
|
||||||
|
|
@ -700,7 +733,7 @@ function setup(main) {
|
||||||
h('button', { class: 'btn', onclick: async () => { try { await api('PUT', '/api/account/label', { label: label.value }); await loadAccount(); toast('Label saved'); } catch (e) { fail(e); } } }, 'Save')),
|
h('button', { class: 'btn', onclick: async () => { try { await api('PUT', '/api/account/label', { label: label.value }); await loadAccount(); toast('Label saved'); } catch (e) { fail(e); } } }, 'Save')),
|
||||||
h('small', {}, 'Pithos cannot see which login the account belongs to. On sia.storage it is the account whose "My apps" lists "S3d".')),
|
h('small', {}, 'Pithos cannot see which login the account belongs to. On sia.storage it is the account whose "My apps" lists "S3d".')),
|
||||||
h('div', { class: 'row', style: 'margin-top:16px' },
|
h('div', { class: 'row', style: 'margin-top:16px' },
|
||||||
daemonState() !== 'running' && h('button', { class: 'btn primary', onclick: async () => { await daemonAction(daemonState() === 'crashed' ? 'restart' : 'start'); location.hash = '#/overview'; } }, 'Start s3d'),
|
daemonState() !== 'running' && h('button', { class: 'btn primary', onclick: async () => { await daemonAction(daemonState() === 'crashed' ? 'restart' : 'start'); navTo('#/overview'); } }, 'Start s3d'),
|
||||||
h('a', { class: 'btn', href: '#/users' }, 'Users & keys'),
|
h('a', { class: 'btn', href: '#/users' }, 'Users & keys'),
|
||||||
h('button', { class: 'btn', onclick: switchAccount }, 'Add another account…')),
|
h('button', { class: 'btn', onclick: switchAccount }, 'Add another account…')),
|
||||||
archivesBlock());
|
archivesBlock());
|
||||||
|
|
@ -793,7 +826,7 @@ function setup(main) {
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
await loadAccount();
|
await loadAccount();
|
||||||
Object.assign(wiz, { step: 1, accountReady: false, openedUrl: null, key: null });
|
Object.assign(wiz, { step: 1, accountReady: false, openedUrl: null, key: null });
|
||||||
location.hash = '#/start';
|
navTo('#/start');
|
||||||
} catch (e) { fail(e); }
|
} catch (e) { fail(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -857,7 +890,7 @@ function users(main) {
|
||||||
h('td', {}, h('strong', {}, u.name)),
|
h('td', {}, h('strong', {}, u.name)),
|
||||||
h('td', {}, `${u.keys.length}`),
|
h('td', {}, `${u.keys.length}`),
|
||||||
h('td', { class: 'actions' },
|
h('td', { class: 'actions' },
|
||||||
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); state.selectedUser = u.name; localStorage.setItem('pithos.user', u.name); location.hash = '#/buckets'; } }, 'Drives'),
|
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); state.selectedUser = u.name; localStorage.setItem('pithos.user', u.name); navTo('#/buckets'); } }, 'Drives'),
|
||||||
' ',
|
' ',
|
||||||
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); newKey(u.name); } }, 'New key'),
|
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); newKey(u.name); } }, 'New key'),
|
||||||
' ',
|
' ',
|
||||||
|
|
@ -1102,7 +1135,7 @@ function itemMenu(items) {
|
||||||
});
|
});
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
const go = (hash) => () => { location.hash = hash; };
|
const go = (hash) => () => navTo(hash);
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
download: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.5v8M4.5 7.5 8 11l3.5-3.5M3 13.5h10"/></svg>',
|
download: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.5v8M4.5 7.5 8 11l3.5-3.5M3 13.5h10"/></svg>',
|
||||||
share: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6.6 9.4a2.6 2.6 0 0 0 3.7 0l2.2-2.2a2.6 2.6 0 0 0-3.7-3.7l-.9.9"/><path d="M9.4 6.6a2.6 2.6 0 0 0-3.7 0L3.5 8.8a2.6 2.6 0 0 0 3.7 3.7l.9-.9"/></svg>',
|
share: '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6.6 9.4a2.6 2.6 0 0 0 3.7 0l2.2-2.2a2.6 2.6 0 0 0-3.7-3.7l-.9.9"/><path d="M9.4 6.6a2.6 2.6 0 0 0-3.7 0L3.5 8.8a2.6 2.6 0 0 0 3.7 3.7l.9-.9"/></svg>',
|
||||||
|
|
@ -1142,7 +1175,7 @@ function buckets(main, [bucket, ...prefixParts]) {
|
||||||
main.append(head, body);
|
main.append(head, body);
|
||||||
|
|
||||||
let users = [];
|
let users = [];
|
||||||
const userSel = h('select', { onchange: (e) => { state.selectedUser = e.target.value; localStorage.setItem('pithos.user', e.target.value); location.hash = '#/buckets'; route(); } });
|
const userSel = h('select', { onchange: (e) => { state.selectedUser = e.target.value; localStorage.setItem('pithos.user', e.target.value); if (PATH_MODE) navTo('#/buckets'); else { location.hash = '#/buckets'; route(); } } });
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try { users = await api('GET', '/api/users'); }
|
try { users = await api('GET', '/api/users'); }
|
||||||
|
|
@ -1302,10 +1335,10 @@ async function accessDialog(user, bucket) {
|
||||||
function objectBrowser(head, body, user, bucket, prefix, userSel) {
|
function objectBrowser(head, body, user, bucket, prefix, userSel) {
|
||||||
if (prefix && !prefix.endsWith('/')) prefix += '/';
|
if (prefix && !prefix.endsWith('/')) prefix += '/';
|
||||||
const base = `/api/s3/${encodeURIComponent(user)}/buckets/${encodeURIComponent(bucket)}`;
|
const base = `/api/s3/${encodeURIComponent(user)}/buckets/${encodeURIComponent(bucket)}`;
|
||||||
const go = (p) => { location.hash = `#/buckets/${encodeURIComponent(bucket)}${p ? '/' + p.split('/').filter(Boolean).map(encodeURIComponent).join('/') : ''}`; };
|
const go = (p) => navTo(`#/buckets/${encodeURIComponent(bucket)}${p ? '/' + p.split('/').filter(Boolean).map(encodeURIComponent).join('/') : ''}`);
|
||||||
|
|
||||||
const crumbs = h('div', { class: 'crumbs' },
|
const crumbs = h('div', { class: 'crumbs' },
|
||||||
h('button', { onclick: () => { location.hash = '#/buckets'; } }, 'Drives'), h('span', { class: 'sep' }, '/'),
|
h('button', { onclick: () => navTo('#/buckets') }, 'Drives'), h('span', { class: 'sep' }, '/'),
|
||||||
h('button', { onclick: () => go('') }, bucket),
|
h('button', { onclick: () => go('') }, bucket),
|
||||||
prefix.split('/').filter(Boolean).map((part, i, all) => [h('span', { class: 'sep' }, '/'), h('button', { onclick: () => go(all.slice(0, i + 1).join('/')) }, part)]));
|
prefix.split('/').filter(Boolean).map((part, i, all) => [h('span', { class: 'sep' }, '/'), h('button', { onclick: () => go(all.slice(0, i + 1).join('/')) }, part)]));
|
||||||
|
|
||||||
|
|
@ -1789,7 +1822,7 @@ function addSidebarControls() {
|
||||||
bridge.sidebar?.isMax?.().then((max) => { if (max) bridge.sidebar.restore?.() ?? bridge.sidebar.toggleMax?.(); }).catch(() => {});
|
bridge.sidebar?.isMax?.().then((max) => { if (max) bridge.sidebar.restore?.() ?? bridge.sidebar.toggleMax?.(); }).catch(() => {});
|
||||||
const row = h('div', { class: 'host-controls' },
|
const row = h('div', { class: 'host-controls' },
|
||||||
h('button', { class: 'btn small', title: 'Open Pithos in a tab and close this panel', onclick: async () => {
|
h('button', { class: 'btn small', title: 'Open Pithos in a tab and close this panel', onclick: async () => {
|
||||||
try { await bridge.invoke('open-in-tab'); await bridge.sidebar?.close?.(); } catch (e) { fail(e); }
|
try { await bridge.invoke('open-in-tab', { path: hashToPath(location.hash || '#/overview') }); await bridge.sidebar?.close?.(); } catch (e) { fail(e); }
|
||||||
} }, '⤢ Full page'));
|
} }, '⤢ Full page'));
|
||||||
$('.sidebar').insertBefore(row, $('#daemon-pill'));
|
$('.sidebar').insertBefore(row, $('#daemon-pill'));
|
||||||
addMenuAction('⤢ Open as full page', () => row.querySelector('button').click());
|
addMenuAction('⤢ Open as full page', () => row.querySelector('button').click());
|
||||||
|
|
@ -1828,6 +1861,10 @@ function watchVaultLock() {
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#unlock-btn').addEventListener('click', async () => {
|
$('#unlock-btn').addEventListener('click', async () => {
|
||||||
|
if (PATH_MODE) {
|
||||||
|
try { const r = await api('POST', '/api/host/unlock'); if (r.ok) location.reload(); } catch (e) { fail(e); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (await vaultGate()) {
|
if (await vaultGate()) {
|
||||||
const first = $('#app').dataset.booted !== '1';
|
const first = $('#app').dataset.booted !== '1';
|
||||||
$('#app').hidden = false;
|
$('#app').hidden = false;
|
||||||
|
|
@ -1939,6 +1976,8 @@ function showSignin() {
|
||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
const s = await api('GET', '/api/session');
|
const s = await api('GET', '/api/session');
|
||||||
|
// pithos.sia in Theseus: locked with the vault, like the sidebar panel.
|
||||||
|
if (s.locked) { $('#locked').hidden = false; return; }
|
||||||
if (!s.authenticated) {
|
if (!s.authenticated) {
|
||||||
showSignin();
|
showSignin();
|
||||||
if (!s.passwordLogin) {
|
if (!s.passwordLogin) {
|
||||||
|
|
@ -1962,7 +2001,16 @@ async function boot() {
|
||||||
setupFooter();
|
setupFooter();
|
||||||
if (!bridge && state.status?.host === 'theseus') addBackToSidebar();
|
if (!bridge && state.status?.host === 'theseus') addBackToSidebar();
|
||||||
connectEvents();
|
connectEvents();
|
||||||
window.addEventListener('hashchange', route);
|
if (PATH_MODE) {
|
||||||
|
window.addEventListener('popstate', route);
|
||||||
|
// In-app links are written as #/ routes; follow them as real addresses.
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const a = e.target.closest?.('a[href^="#/"]');
|
||||||
|
if (!a || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
navTo(a.getAttribute('href'));
|
||||||
|
});
|
||||||
|
} else window.addEventListener('hashchange', route);
|
||||||
route();
|
route();
|
||||||
setInterval(refreshStatus, 15000);
|
setInterval(refreshStatus, 15000);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue