Pithos: a drive shows whether it is private or public, with Share beside it

Whether a drive was readable by anyone was only visible in the Drives
list or behind the Access button. The drive's path now carries a chip
(Private / Public / Public + list) that changes it in one click, and a
Share button for the folder you are in, or the whole drive at its top
level: a navigate.st/sia page on this computer's s3d, the Share… rules
on Drives on Silent Mode. The public-link route accepts the drive's top
level for that.
This commit is contained in:
Local Dev 2026-10-05 23:39:11 +02:00
parent cd4f24777d
commit 6a14f46657
3 changed files with 35 additions and 8 deletions

View file

@ -512,8 +512,9 @@ export async function createPithos(opts = {}) {
const { key, prefix, days = 365 } = await jsonBody(req);
const until = Math.floor(Date.now() / 1000) + Math.min(3650, Math.max(1, Number(days) || 365)) * 86400;
let body;
if (prefix) {
if (!prefix.endsWith('/')) throw new HttpError(400, 'a folder ends with /');
// prefix "" (with no key) is the drive's top level
if (prefix != null && !key) {
if (prefix && !prefix.endsWith('/')) throw new HttpError(400, 'a folder ends with /');
const s3 = await s3For(p.user);
const files = [];
let token;
@ -525,7 +526,7 @@ export async function createPithos(opts = {}) {
if (!files.length) throw new HttpError(400, 'This folder has no files to share (files in sub-folders are not included).');
const out = [];
for (const k of files) out.push({ name: k.slice(prefix.length), url: (await shareUrlFor(p.bucket, k, until)).url });
body = { name: prefix.slice(0, -1).split('/').pop(), folder: true, files: out };
body = { name: prefix ? prefix.slice(0, -1).split('/').pop() : p.bucket, folder: true, files: out };
} else {
if (!key || key.endsWith('/')) throw new HttpError(400, 'a file is required');
body = { name: key.split('/').pop(), url: (await shareUrlFor(p.bucket, key, until)).url };

View file

@ -457,3 +457,11 @@ nav a.nav-account.active .na-label { color: var(--accent); }
/* The Pithos name is a link to Overview */
a.brand { color: inherit; text-decoration: none; }
a.brand:hover > span:first-of-type { color: var(--accent); }
/* Drive header: who can read it, and Share, beside the path */
.crumb-row { gap: 8px; align-items: center; }
.chip { border: 1px solid var(--border); background: var(--surface-2); color: inherit; font: inherit; font-size: 12.5px; padding: 3px 10px; border-radius: 999px; cursor: pointer; white-space: nowrap; }
.chip:hover { border-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; } }

View file

@ -1616,11 +1616,28 @@ function objectBrowser(head, body, user, bucket, prefix, userSel) {
prefix.split('/').filter(Boolean).map((part, i, all) => [h('span', { class: 'sep' }, '/'), h('button', { onclick: () => go(all.slice(0, i + 1).join('/')) }, part)]));
const fileInput = h('input', { type: 'file', multiple: true, hidden: true, onchange: (e) => { uploadFiles([...e.target.files]); e.target.value = ''; } });
// Private or public at a glance, one click to change; Share for this folder
// (or, at the top, the whole drive).
const accessChip = h('button', { type: 'button', class: 'chip access-chip', title: 'Who can read this drive. Click to change.', onclick: async () => { await accessDialog(user, bucket); paintAccess(); } }, '…');
async function paintAccess() {
try {
let mode;
if (isHosted()) {
const rules = await api('GET', '/api/hosted/rules');
const pub = rules.find((r) => r.bucket === bucket && r.prefix === '' && r.type === 'public');
mode = pub ? pub.rights : 'private';
} else mode = (await api('GET', `${base}/policy`)).public;
accessChip.className = `chip access-chip ${mode === 'private' ? 'private' : 'public'}`;
put(accessChip, { private: isHosted() ? '🔒 Private · encrypted' : '🔒 Private', read: '🌐 Public', 'read-list': '🌐 Public + list', custom: '⚙ Custom' }[mode] || mode);
} catch { put(accessChip, 'Access'); }
}
paintAccess();
const shareHere = () => (isHosted() ? folderShareDialog(user, bucket, prefix).then(paintAccess) : siaFolderLink(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' }, crumbs, accountLine()),
h('div', { class: 'stack' }, h('div', { class: 'row crumb-row' }, crumbs, accessChip, shareBtn), accountLine()),
h('div', { class: 'row' }, userPicker(userSel),
h('button', { class: 'btn', onclick: newFolder }, 'New folder'),
h('button', { class: 'btn', onclick: () => accessDialog(user, bucket) }, 'Access'),
h('button', { class: 'btn primary', onclick: () => fileInput.click() }, 'Upload'), fileInput));
const uploads = h('div', { class: 'uploads' });
@ -1919,6 +1936,7 @@ 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 }) {
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 btn = h('button', { type: 'button', class: 'btn primary', onclick: async () => {
@ -1932,15 +1950,15 @@ function objectBrowser(head, body, user, bucket, prefix, userSel) {
} }, 'Create public link');
return h('div', { class: 'stack' },
h('h3', { style: 'margin:0' }, 'Public link'),
h('p', { class: 'small muted', style: 'margin:0' }, prefix
? 'A page with the files in this folder that anyone can open, on any device, even with this computer off. Files in sub-folders are not included.'
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);
}
async function siaFolderLink(prefix) {
await modal(`Public link for ${prefix.slice(0, -1).split('/').pop()}`, siaLinkBlock({ prefix }), [{ label: 'Done', kind: 'primary', submit: true, result: true }]);
await modal(`Public link for ${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