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:
parent
cd4f24777d
commit
6a14f46657
3 changed files with 35 additions and 8 deletions
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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; } }
|
||||
|
|
|
|||
28
ui/app.js
28
ui/app.js
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue