Pithos 0.3.21: clearer Share dialog with pithos.sia/l links, restart prompt after in-place updates
This commit is contained in:
parent
62c2f269af
commit
c2bc27b0a1
5 changed files with 90 additions and 23 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "pithos",
|
||||
"name": "Pithos",
|
||||
"version": "0.3.20",
|
||||
"version": "0.3.21",
|
||||
"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",
|
||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,15 @@ class HttpError extends Error {
|
|||
constructor(status, message) { super(message); this.status = status; }
|
||||
}
|
||||
|
||||
// Pithos's own version: addon.json in the Theseus add-on, package.json elsewhere.
|
||||
function readOwnVersion() {
|
||||
for (const f of ['../addon.json', '../package.json']) {
|
||||
try { const v = JSON.parse(fs.readFileSync(new URL(f, import.meta.url), 'utf8')).version; if (v) return v; } catch {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const LOADED_VERSION = readOwnVersion();
|
||||
|
||||
export async function createPithos(opts = {}) {
|
||||
const {
|
||||
host = '127.0.0.1',
|
||||
|
|
@ -90,13 +99,11 @@ export async function createPithos(opts = {}) {
|
|||
const tickets = new Map();
|
||||
const subscribers = new Set();
|
||||
let versionCache = null;
|
||||
// Pithos's own version: addon.json in the Theseus add-on, package.json elsewhere.
|
||||
const pithosVersion = (() => {
|
||||
for (const f of ['../addon.json', '../package.json']) {
|
||||
try { const v = JSON.parse(fs.readFileSync(new URL(f, import.meta.url), 'utf8')).version; if (v) return v; } catch {}
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
// The version of the code running (read when this module loaded) and the one
|
||||
// on disk now. Theseus can swap an add-on's files in place, but Node keeps
|
||||
// the modules it already loaded, so they differ until Theseus restarts.
|
||||
const pithosVersion = LOADED_VERSION;
|
||||
const installedVersion = readOwnVersion();
|
||||
|
||||
let hosted = null; // "On Silent Mode", installed below
|
||||
const broadcast = (event, data) => {
|
||||
|
|
@ -182,6 +189,8 @@ export async function createPithos(opts = {}) {
|
|||
const local = {
|
||||
host: hostName,
|
||||
pithos: pithosVersion,
|
||||
// differs from pithos after an in-place update until the host restarts
|
||||
installed: readOwnVersion() || installedVersion,
|
||||
// electron-builder's portable exe sets this; updates then fetch the portable build.
|
||||
portable: hostName === 'desktop' && !!process.env.PORTABLE_EXECUTABLE_FILE,
|
||||
platform: process.platform,
|
||||
|
|
@ -535,7 +544,7 @@ export async function createPithos(opts = {}) {
|
|||
const out = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new HttpError(res.status === 429 ? 429 : 502, out.error || `navigate.st answered ${res.status}`);
|
||||
const list = readSiaLinks();
|
||||
list.unshift({ id: out.id, url: out.url, deleteToken: out.deleteToken, user: p.user, bucket: p.bucket, key: key || null, prefix: prefix || null, until, created: Date.now(), files: body.files ? body.files.length : 1 });
|
||||
list.unshift({ id: out.id, url: out.url, deleteToken: out.deleteToken, user: p.user, bucket: p.bucket, key: key || null, prefix: key ? null : (prefix ?? null), until, created: Date.now(), files: body.files ? body.files.length : 1 });
|
||||
writeSiaLinks(list);
|
||||
return { id: out.id, url: out.url, until };
|
||||
});
|
||||
|
|
|
|||
|
|
@ -180,6 +180,10 @@ module.exports = {
|
|||
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);
|
||||
// pithos.sia/l/<id>/<name>: a public link, served by navigate.st/sia
|
||||
if (/^\/l\/[A-Za-z0-9_-]{4,64}(\/|$)/.test(u.pathname)) {
|
||||
return new Response(null, { status: 302, headers: { location: `https://navigate.st/sia/${u.pathname.slice(3)}${u.search}`, "cache-control": "no-store" } });
|
||||
}
|
||||
const fetchSite = request.headers.get("sec-fetch-site");
|
||||
const origin = request.headers.get("origin");
|
||||
const own = fetchSite ? fetchSite === "same-origin" || fetchSite === "none"
|
||||
|
|
|
|||
|
|
@ -465,3 +465,13 @@ a.brand:hover > span:first-of-type { color: var(--accent); }
|
|||
.access-chip.private { color: var(--muted); }
|
||||
.access-chip.public { color: var(--accent); border-color: var(--accent); }
|
||||
@media (max-width: 760px) { .crumb-row { row-gap: 6px; } .chip { font-size: 12px; padding: 2px 8px; } }
|
||||
|
||||
/* Public links in the Share dialogs */
|
||||
.link-card { border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; background: var(--surface-2); }
|
||||
.link-card.fresh { border-color: var(--accent); }
|
||||
.link-card .link-url { color: var(--accent); font-family: ui-monospace, Consolas, monospace; font-size: 13px; word-break: break-all; text-decoration: none; }
|
||||
.link-card .link-url:hover { text-decoration: underline; }
|
||||
.link-card code { font-size: 12px; word-break: break-all; }
|
||||
.link-card .row { gap: 6px; flex-wrap: wrap; }
|
||||
.link-make { gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
.link-card a.btn { text-decoration: none; }
|
||||
|
|
|
|||
|
|
@ -1921,7 +1921,7 @@ function objectBrowser(head, body, user, bucket, prefix, userSel) {
|
|||
endpoint.addEventListener('change', gen);
|
||||
gen();
|
||||
await modal('Share', h('div', { class: 'stack' },
|
||||
siaLinkBlock({ key }),
|
||||
siaLinkBlock({ key, heading: true }),
|
||||
h('details', { class: 'more-opts', open: !!savedEndpoint },
|
||||
h('summary', {}, 'Link for this computer, or your own server'),
|
||||
h('div', { class: 'stack', style: 'margin-top:8px' },
|
||||
|
|
@ -1935,30 +1935,64 @@ function objectBrowser(head, body, user, bucket, prefix, userSel) {
|
|||
|
||||
// A public link anyone can open, at navigate.st/sia: the file is read from
|
||||
// your Sia account through Silent Mode, so it works with this computer off.
|
||||
function siaLinkBlock({ key, prefix }) {
|
||||
// The links already made for this file, folder or drive come first, each
|
||||
// with Copy, Open and Switch off; then one line to make a new one.
|
||||
function siaLinkBlock({ key, prefix, heading = false }) {
|
||||
const folder = prefix != null && key == null;
|
||||
const days = h('select', {}, [['30', '1 month'], ['365', '1 year'], ['1825', '5 years']].map(([v, l]) => h('option', { value: v, selected: v === '365' }, l)));
|
||||
const out = h('div');
|
||||
const mine = (l) => l.bucket === bucket && (folder ? l.key == null && (l.prefix ?? null) === prefix : l.key === key);
|
||||
const days = h('select', { 'aria-label': 'Works for' }, [['30', '1 month'], ['365', '1 year'], ['1825', '5 years']].map(([v, l]) => h('option', { value: v, selected: v === '365' }, l)));
|
||||
const list = h('div', { class: 'stack' });
|
||||
const err = h('p', { class: 'error small', hidden: true });
|
||||
const theseusForm = (u) => { try { return `pithos.sia/l/${new URL(u).pathname.replace(/^\/sia\//, '')}`; } catch { return ''; } };
|
||||
const card = (l, fresh) => h('div', { class: `link-card${fresh ? ' fresh' : ''}` },
|
||||
h('a', { class: 'link-url', href: l.url, target: '_blank', rel: 'noopener', title: l.url }, l.url.replace(/^https:\/\//, '')),
|
||||
h('div', { class: 'small muted' }, `Works until ${new Date(l.until * 1000).toLocaleDateString()}${l.files > 1 ? ` · ${l.files} files` : ''}`,
|
||||
theseusForm(l.url) && [' · in Theseus also ', h('code', {}, theseusForm(l.url))]),
|
||||
h('div', { class: 'row' },
|
||||
h('button', { type: 'button', class: 'btn small primary', onclick: () => copy(l.url, 'Link copied') }, 'Copy link'),
|
||||
h('a', { class: 'btn small', href: l.url, target: '_blank', rel: 'noopener' }, 'Open'),
|
||||
h('button', { type: 'button', class: 'btn small danger', onclick: async () => {
|
||||
if (!(await confirmModal('Switch this link off?', 'Anyone who has it will no longer be able to open it.', 'Switch off'))) return;
|
||||
try { await api('DELETE', `/api/sia-links/${encodeURIComponent(l.id)}`); toast('Link switched off'); refresh(); } catch (e) { fail(e); }
|
||||
} }, 'Switch off')));
|
||||
let freshId = null;
|
||||
async function refresh() {
|
||||
let links = [];
|
||||
try { links = (await api('GET', '/api/sia-links')).filter(mine); } catch {}
|
||||
put(list, links.map((l) => card(l, l.id === freshId)));
|
||||
put(btn, links.length ? 'Make another link' : 'Create public link');
|
||||
}
|
||||
const btn = h('button', { type: 'button', class: 'btn primary', onclick: async () => {
|
||||
btn.disabled = true; put(btn, h('span', { class: 'spinner' }), ' Making the link…');
|
||||
btn.disabled = true; err.hidden = true;
|
||||
put(btn, h('span', { class: 'spinner' }), ' Making the link…');
|
||||
try {
|
||||
const r = await api('POST', `${base}/sia-link`, { key, prefix, days: Number(days.value) });
|
||||
put(out, secretRow('Public link', r.url), h('p', { class: 'small muted', style: 'margin:0' }, `Works until ${new Date(r.until * 1000).toLocaleDateString()}. Switch it off any time in Settings › Public links.`));
|
||||
put(btn, 'Make another link');
|
||||
} catch (e) { put(out, h('p', { class: 'error small', style: 'margin:0' }, e.message)); put(btn, 'Try again'); }
|
||||
freshId = r.id;
|
||||
await refresh();
|
||||
copy(r.url, 'Link created and copied');
|
||||
} catch (e) {
|
||||
// an engine from before this version reads a whole-drive link as a file
|
||||
err.textContent = folder && /a file is required/.test(e.message)
|
||||
? 'Pithos is still running its previous engine. Restart Theseus to finish the update, then try again.'
|
||||
: e.message;
|
||||
err.hidden = false;
|
||||
await refresh();
|
||||
}
|
||||
btn.disabled = false;
|
||||
} }, 'Create public link');
|
||||
refresh();
|
||||
return h('div', { class: 'stack' },
|
||||
h('h3', { style: 'margin:0' }, 'Public link'),
|
||||
heading && h('h3', { style: 'margin:0' }, 'Public link'),
|
||||
h('p', { class: 'small muted', style: 'margin:0' }, folder
|
||||
? `A page with the files ${prefix ? 'in this folder' : 'at the top of this drive'} that anyone can open, on any device, even with this computer off. Files in sub-folders are not included.`
|
||||
: 'Anyone with the link can open this file on any device, even with this computer off. It is read from your Sia account through navigate.st; only this file is shared.'),
|
||||
h('div', { class: 'row' }, h('label', { class: 'row small' }, h('span', { class: 'muted' }, 'Works for'), days), btn),
|
||||
out);
|
||||
? `Anyone with the link sees ${prefix ? 'the files in this folder' : 'the files at the top of this drive'} and can open them on any device, even with this computer off. Sub-folders are not included.`
|
||||
: 'Anyone with the link can open this file on any device, even with this computer off. Only this file is shared.'),
|
||||
list,
|
||||
h('div', { class: 'row link-make' }, h('label', { class: 'row small' }, h('span', { class: 'muted' }, 'Works for'), days), btn),
|
||||
err);
|
||||
}
|
||||
|
||||
async function siaFolderLink(prefix) {
|
||||
await modal(`Public link for ${prefix ? prefix.slice(0, -1).split('/').pop() : bucket}`, siaLinkBlock({ prefix }), [{ label: 'Done', kind: 'primary', submit: true, result: true }]);
|
||||
await modal(`Share ${prefix ? prefix.slice(0, -1).split('/').pop() : bucket}`, siaLinkBlock({ prefix }), [{ label: 'Done', kind: 'primary', submit: true, result: true }]);
|
||||
}
|
||||
|
||||
// On Silent Mode a link is a rule on the server, and the file is stored
|
||||
|
|
@ -2732,6 +2766,16 @@ function setupFooter() {
|
|||
}
|
||||
check.hidden = false;
|
||||
check.addEventListener('click', () => scan(true));
|
||||
// An update applied in place swaps the files, but the engine keeps running
|
||||
// the version it loaded until Theseus restarts.
|
||||
const installed = state.status?.installed;
|
||||
if (installed && installed !== current) {
|
||||
busy = true;
|
||||
showChip(`↻ Restart to finish v${installed}`, '', `Pithos v${installed} is installed, but its engine still runs v${current} until Theseus restarts. Theseus reopens your tabs.`, async () => {
|
||||
try { await bridge.invoke('update', { step: 'apply' }); } catch (e) { flash('⚠ Restart failed', 'err', e.message); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
setTimeout(() => scan(false), 1500);
|
||||
setInterval(() => scan(false), 6 * 3600 * 1000);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue