Pithos: Share is one click, and links last 10 years
Share opened a dialog with a duration and a Create button before anything happened. Now Share copies the link already made for that file, folder or drive, or makes one and copies it, then shows Open and Manage; the full list (and Switch off) is behind Manage. A Sia share link is signed with an end date, so new links get the longest Pithos allows, 10 years. When a file cannot reach Sia yet, the message says why instead of passing s3d's admin error through.
This commit is contained in:
parent
e4e4b3b587
commit
540828ba06
3 changed files with 54 additions and 7 deletions
|
|
@ -566,7 +566,13 @@ export async function createPithos(opts = {}) {
|
|||
try { return await createShareUrl(dir, bucket, key, until); }
|
||||
catch (e) {
|
||||
if (e.code !== 'pending') throw new HttpError(e.status || 502, e.message);
|
||||
await flushNow(); // send what is waiting, then try once more
|
||||
// send what is waiting, then try once more
|
||||
try { await flushNow(); } catch (fe) {
|
||||
const why = /not enough hosts/i.test(fe.message) ? 'not enough Sia hosts are reachable right now'
|
||||
: /failed to open upload|cannot find the file/i.test(fe.message) ? 'some waiting files lost their copy on this computer (see the Drives page)'
|
||||
: 'the upload to Sia failed';
|
||||
throw new HttpError(502, `This file is not on Sia yet, and sending it now did not work: ${why}. Try again in a minute.`);
|
||||
}
|
||||
try { return await createShareUrl(dir, bucket, key, until); }
|
||||
catch (e2) { throw new HttpError(e2.status || 502, e2.code === 'pending' ? 'This file is still uploading to Sia. Try again in a minute.' : e2.message); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -481,3 +481,7 @@ a.brand:hover > span:first-of-type { color: var(--accent); }
|
|||
.stuck-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
||||
.stuck-list li { display: flex; gap: 8px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
|
||||
.stuck-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 220px; }
|
||||
|
||||
/* Toasts with actions (Share: Open / Manage) */
|
||||
.toast.action { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.toast.action > span { flex: 1 1 auto; }
|
||||
|
|
|
|||
49
ui/app.js
49
ui/app.js
|
|
@ -80,6 +80,15 @@ function fmtDuration(ms) {
|
|||
return `${Math.floor(s / 86400)}d ${Math.floor((s % 86400) / 3600)}h`;
|
||||
}
|
||||
|
||||
// A toast with buttons ([label, fn] pairs); stays a little longer.
|
||||
function actionToast(text, actions = [], ms = 9000) {
|
||||
const el = h('div', { class: 'toast action' }, h('span', {}, text),
|
||||
actions.map(([label, fn]) => h('button', { type: 'button', class: 'btn small', onclick: () => { el.remove(); fn(); } }, label)));
|
||||
$('#toasts').append(el);
|
||||
setTimeout(() => el.remove(), ms);
|
||||
return el;
|
||||
}
|
||||
|
||||
async function copy(text, what = 'Copied') {
|
||||
try { await navigator.clipboard.writeText(text); toast(what); }
|
||||
catch { prompt('Copy:', text); }
|
||||
|
|
@ -1704,7 +1713,7 @@ function objectBrowser(head, body, user, bucket, prefix, userSel) {
|
|||
} catch { put(accessChip, 'Access'); }
|
||||
}
|
||||
paintAccess();
|
||||
const shareHere = () => (isHosted() ? folderShareDialog(user, bucket, prefix).then(paintAccess) : siaFolderLink(prefix));
|
||||
const shareHere = () => (isHosted() ? folderShareDialog(user, bucket, prefix).then(paintAccess) : quickShare({ prefix }));
|
||||
const shareBtn = h('button', { type: 'button', class: 'chip', title: prefix ? 'Share this folder' : 'Share this drive', onclick: shareHere }, '🔗 Share');
|
||||
put(head,
|
||||
h('div', { class: 'stack' }, h('div', { class: 'row crumb-row' }, crumbs, accessChip, shareBtn), accountLine()),
|
||||
|
|
@ -1749,14 +1758,14 @@ function objectBrowser(head, body, user, bucket, prefix, userSel) {
|
|||
}
|
||||
const folderName = (f) => f.slice(prefix.length).replace(/\/$/, '');
|
||||
const fileName = (o) => o.key.slice(prefix.length);
|
||||
const folderMenu = (f) => itemMenu([['Open', () => go(f)], isHosted() ? ['Share…', () => folderShareDialog(user, bucket, f)] : ['Public link…', () => siaFolderLink(f)], ['Rename…', () => renameItem(f)], ['Delete', () => deleteFolder(f), 'danger']]);
|
||||
const fileMenu = (o) => itemMenu([[fileKind(o.key) === 'audio' ? 'Play' : isHtml(o.key) ? 'Show as text' : 'Open', () => openViewer(o)], isHtml(o.key) && ['Open as page ↗', () => openPage(o.key)], ['Download', () => downloadObject(o.key)], ['Share link…', () => share(o.key)], ['Rename…', () => renameItem(o.key)], ['Delete', () => deleteObject(o.key), 'danger']]);
|
||||
const folderMenu = (f) => itemMenu([['Open', () => go(f)], isHosted() ? ['Share…', () => folderShareDialog(user, bucket, f)] : ['Copy public link', () => quickShare({ prefix: f })], !isHosted() && ['Public links…', () => siaFolderLink(f)], ['Rename…', () => renameItem(f)], ['Delete', () => deleteFolder(f), 'danger']]);
|
||||
const fileMenu = (o) => itemMenu([[fileKind(o.key) === 'audio' ? 'Play' : isHtml(o.key) ? 'Show as text' : 'Open', () => openViewer(o)], isHtml(o.key) && ['Open as page ↗', () => openPage(o.key)], ['Download', () => downloadObject(o.key)], !isHosted() && ['Copy public link', () => quickShare({ key: o.key })], ['Share options…', () => share(o.key)], ['Rename…', () => renameItem(o.key)], ['Delete', () => deleteObject(o.key), 'danger']]);
|
||||
const more = last?.truncated && h('div', { class: 'row', style: 'justify-content:center;margin-top:12px' }, h('button', { class: 'btn', onclick: () => load(last.nextToken) }, 'Load more'));
|
||||
const stop = (e) => e.stopPropagation();
|
||||
// Narrow screens: Download and Share as icons beside the ⋯ menu.
|
||||
const quickIcons = (o) => h('span', { class: 'narrow-only icon-row' },
|
||||
iconButton('download', 'Download', () => downloadObject(o.key)),
|
||||
iconButton('share', 'Share link', () => share(o.key)));
|
||||
iconButton('share', 'Copy public link', () => quickShare({ key: o.key })));
|
||||
|
||||
if (mode === 'details') {
|
||||
put(card, h('table', { class: 'files' },
|
||||
|
|
@ -1773,7 +1782,7 @@ function objectBrowser(head, body, user, bucket, prefix, userSel) {
|
|||
h('td', { class: 'hide-sm muted' }, fmtDate(o.modified)),
|
||||
h('td', { class: 'actions', onclick: stop },
|
||||
h('span', { class: 'wide-only' }, h('button', { class: 'btn small', onclick: () => downloadObject(o.key) }, 'Download'), ' ',
|
||||
h('button', { class: 'btn small', onclick: () => share(o.key) }, 'Share'), ' '),
|
||||
h('button', { class: 'btn small', title: 'Copy a public link', onclick: () => quickShare({ key: o.key }) }, 'Share'), ' '),
|
||||
quickIcons(o), fileMenu(o)))))),
|
||||
more);
|
||||
} else if (mode === 'list') {
|
||||
|
|
@ -2007,12 +2016,40 @@ 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.
|
||||
// Share in one click: the link already made for this file, folder or
|
||||
// drive, or a new one valid for 10 years (a Sia share link is signed with
|
||||
// an end date; Switch off ends it sooner), copied at once. Manage opens
|
||||
// the full list. Drives on Silent Mode keep their own Share dialog.
|
||||
async function quickShare({ key, prefix }) {
|
||||
if (isHosted()) return key != null ? hostedShare(key) : folderShareDialog(user, bucket, prefix);
|
||||
const folder = key == null;
|
||||
const mine = (l) => l.bucket === bucket && (folder ? l.key == null && (l.prefix ?? null) === prefix : l.key === key);
|
||||
const manage = () => (folder ? siaFolderLink(prefix) : share(key));
|
||||
const busy = h('div', { class: 'toast' }, h('span', { class: 'spinner' }), ' Making the public link…');
|
||||
$('#toasts').append(busy);
|
||||
try {
|
||||
let links = [];
|
||||
try { links = await api('GET', '/api/sia-links'); } catch {}
|
||||
let link = links.filter(mine).filter((l) => l.until * 1000 > Date.now()).sort((a, b) => b.until - a.until)[0];
|
||||
if (!link) link = await api('POST', `${base}/sia-link`, { key, prefix, days: 3650 });
|
||||
busy.remove();
|
||||
let copied = true;
|
||||
try { await navigator.clipboard.writeText(link.url); } catch { copied = false; }
|
||||
actionToast(copied ? 'Public link copied' : 'Public link ready', [['Open', () => window.open(link.url, '_blank', 'noopener')], ['Manage', manage]]);
|
||||
if (!copied) prompt('Copy:', link.url);
|
||||
} catch (e) {
|
||||
busy.remove();
|
||||
const msg = 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;
|
||||
actionToast(msg, [['Manage', manage]], 12000).classList.add('error');
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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 days = h('select', { 'aria-label': 'Works for' }, [['365', '1 year'], ['1825', '5 years'], ['3650', '10 years']].map(([v, l]) => h('option', { value: v, selected: v === '3650' }, 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 ''; } };
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue