Pithos 0.3.20: drive mode chip and Share, pithos.sia/app, Linux built on Hephaestus
This commit is contained in:
parent
1e45428dec
commit
62c2f269af
4 changed files with 39 additions and 9 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "pithos",
|
||||
"name": "Pithos",
|
||||
"version": "0.3.19",
|
||||
"version": "0.3.20",
|
||||
"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=",
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -960,6 +961,7 @@ export function handoffRoute(p) {
|
|||
if (!segs.length || segs.some((x) => !/^[A-Za-z0-9._~%=-]{1,120}$/.test(x))) return '#/overview';
|
||||
const pages = ['overview', 'users', 'buckets', 'account', 'settings', 'logs'];
|
||||
if (segs[0] === 'drives') return '#/buckets';
|
||||
if (segs[0] === 'app') return segs.length > 1 ? handoffRoute('/' + segs.slice(1).join('/')) : '#/overview';
|
||||
if (pages.includes(segs[0])) return `#/${segs.join('/')}`;
|
||||
return `#/u/${segs.join('/')}`; // /<user>/<drive>/<folder>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; } }
|
||||
|
|
|
|||
|
|
@ -221,6 +221,8 @@ function currentRoute() {
|
|||
const segs = location.pathname.split('/').filter(Boolean);
|
||||
if (!segs.length) return [];
|
||||
const first = decodeURIComponent(segs[0]);
|
||||
// pithos.sia/app: the app's front door (the bare pithos.sia is the website)
|
||||
if (first === 'app') return segs.slice(1);
|
||||
if (first === 'drives') return ['buckets'];
|
||||
if (views[first]) return segs;
|
||||
if (state.selectedUser !== first) { state.selectedUser = first; try { localStorage.setItem('pithos.user', first); } catch {} }
|
||||
|
|
@ -1614,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' });
|
||||
|
|
@ -1917,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 () => {
|
||||
|
|
@ -1930,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