Theseus: bundle Pithos 0.3.6
Rebuilt from Pithos/ with scripts/build-theseus-addon.mjs.
This commit is contained in:
parent
1244dae25e
commit
3de25581b3
5 changed files with 41 additions and 13 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "pithos",
|
"id": "pithos",
|
||||||
"name": "Pithos",
|
"name": "Pithos",
|
||||||
"version": "0.3.5",
|
"version": "0.3.6",
|
||||||
"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.",
|
"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",
|
"author": "Silent Mode",
|
||||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,17 @@ export async function createPithos(opts = {}) {
|
||||||
const total = Number((up.headers['content-range'] || '').split('/')[1]) || body.length;
|
const total = Number((up.headers['content-range'] || '').split('/')[1]) || body.length;
|
||||||
return { text: body, truncated: total > 524288, size: total };
|
return { text: body, truncated: total > 524288, size: total };
|
||||||
});
|
});
|
||||||
|
// Drive usage: files and bytes in a bucket, counted up to 20 pages.
|
||||||
|
route('GET', '/api/s3/:user/buckets/:bucket/usage', async (req, url, p) => {
|
||||||
|
const client = await s3For(p.user);
|
||||||
|
let files = 0, bytes = 0, token, pages = 0;
|
||||||
|
do {
|
||||||
|
const page = await client.listObjects(p.bucket, { delimiter: '', token });
|
||||||
|
for (const o of page.objects) { if (!o.key.endsWith('/')) { files++; bytes += o.size || 0; } }
|
||||||
|
token = page.truncated ? page.nextToken : null;
|
||||||
|
} while (token && ++pages < 20);
|
||||||
|
return { files, bytes, partial: !!token };
|
||||||
|
});
|
||||||
route('POST', '/api/host/show-panel', () => {
|
route('POST', '/api/host/show-panel', () => {
|
||||||
if (!showPanel) throw new HttpError(400, 'not supported by this host');
|
if (!showPanel) throw new HttpError(400, 'not supported by this host');
|
||||||
showPanel();
|
showPanel();
|
||||||
|
|
|
||||||
|
|
@ -243,3 +243,10 @@ html, body { overflow-x: hidden; }
|
||||||
.viewer-media { display: block; max-width: 100%; max-height: 70vh; margin: 0 auto; border-radius: 8px; background: var(--surface-2); }
|
.viewer-media { display: block; max-width: 100%; max-height: 70vh; margin: 0 auto; border-radius: 8px; background: var(--surface-2); }
|
||||||
.viewer-audio { width: 100%; }
|
.viewer-audio { width: 100%; }
|
||||||
.viewer-text { max-height: 60vh; white-space: pre-wrap; overflow-wrap: anywhere; }
|
.viewer-text { max-height: 60vh; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
|
|
||||||
|
/* Drive icon (a bucket): a small disk with a status light */
|
||||||
|
.drive-icon { width: 46px; height: 30px; border-radius: 7px; position: relative;
|
||||||
|
background: linear-gradient(180deg, var(--surface) 0%, var(--surface-2) 100%);
|
||||||
|
border: 2px solid var(--muted); box-shadow: inset 0 -7px 0 color-mix(in srgb, var(--muted) 25%, transparent); }
|
||||||
|
.drive-icon::after { content: ""; position: absolute; right: 7px; bottom: 3px; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
|
||||||
|
.tile:hover .drive-icon { border-color: var(--accent); }
|
||||||
|
|
|
||||||
|
|
@ -829,7 +829,7 @@ function users(main) {
|
||||||
h('td', {}, h('strong', {}, u.name)),
|
h('td', {}, h('strong', {}, u.name)),
|
||||||
h('td', {}, `${u.keys.length}`),
|
h('td', {}, `${u.keys.length}`),
|
||||||
h('td', { class: 'actions' },
|
h('td', { class: 'actions' },
|
||||||
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); state.selectedUser = u.name; localStorage.setItem('pithos.user', u.name); location.hash = '#/buckets'; } }, 'Buckets'),
|
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); state.selectedUser = u.name; localStorage.setItem('pithos.user', u.name); location.hash = '#/buckets'; } }, 'Drives'),
|
||||||
' ',
|
' ',
|
||||||
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); newKey(u.name); } }, 'New key'),
|
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); newKey(u.name); } }, 'New key'),
|
||||||
' ',
|
' ',
|
||||||
|
|
@ -857,7 +857,7 @@ function users(main) {
|
||||||
name.addEventListener('input', () => { if (!bucketEdited) bucket.value = suggestBucket(name.value); });
|
name.addEventListener('input', () => { if (!bucketEdited) bucket.value = suggestBucket(name.value); });
|
||||||
const v = await modal('New user', h('div', { class: 'stack' },
|
const v = await modal('New user', h('div', { class: 'stack' },
|
||||||
h('label', { class: 'field' }, h('span', {}, 'Name'), name, h('small', {}, 'Letters, digits, . _ @ -')),
|
h('label', { class: 'field' }, h('span', {}, 'Name'), name, h('small', {}, 'Letters, digits, . _ @ -')),
|
||||||
h('label', { class: 'field' }, h('span', {}, 'Their bucket'), bucket, h('small', {}, 'Created with the user, like a home folder. Lowercase letters, digits and hyphens. They can make more later.')),
|
h('label', { class: 'field' }, h('span', {}, 'Their drive'), bucket, h('small', {}, 'Created with the user, like a home drive (an S3 bucket). Lowercase letters, digits and hyphens. They can add more later.')),
|
||||||
h('label', { class: 'check' }, withKey, 'Create an access key now')),
|
h('label', { class: 'check' }, withKey, 'Create an access key now')),
|
||||||
[{ label: 'Cancel', result: null }, { label: 'Create', kind: 'primary', submit: true, value: () => ({ name: name.value.trim(), withKey: withKey.checked, bucket: bucket.value.trim() || undefined }) }]);
|
[{ label: 'Cancel', result: null }, { label: 'Create', kind: 'primary', submit: true, value: () => ({ name: name.value.trim(), withKey: withKey.checked, bucket: bucket.value.trim() || undefined }) }]);
|
||||||
if (!v) return;
|
if (!v) return;
|
||||||
|
|
@ -1072,7 +1072,7 @@ function buckets(main, [bucket, ...prefixParts]) {
|
||||||
catch (e) { put(body, h('div', { class: 'card' }, h('p', { class: 'error' }, e.message))); return; }
|
catch (e) { put(body, h('div', { class: 'card' }, h('p', { class: 'error' }, e.message))); return; }
|
||||||
const usable = users.filter((u) => u.keys.length);
|
const usable = users.filter((u) => u.keys.length);
|
||||||
if (!usable.length) {
|
if (!usable.length) {
|
||||||
put(head, h('div', {}, h('h1', {}, 'Buckets')));
|
put(head, h('div', {}, h('h1', {}, 'Drives')));
|
||||||
put(body, h('div', { class: 'card empty' }, h('p', {}, users.length ? 'None of your users has an access key yet.' : 'Create a user first; buckets belong to users.'), h('a', { class: 'btn primary', href: '#/users' }, 'Users & keys')));
|
put(body, h('div', { class: 'card empty' }, h('p', {}, users.length ? 'None of your users has an access key yet.' : 'Create a user first; buckets belong to users.'), h('a', { class: 'btn primary', href: '#/users' }, 'Users & keys')));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1080,7 +1080,7 @@ function buckets(main, [bucket, ...prefixParts]) {
|
||||||
put(userSel, ...usable.map((u) => h('option', { value: u.name, selected: u.name === state.selectedUser }, u.name)));
|
put(userSel, ...usable.map((u) => h('option', { value: u.name, selected: u.name === state.selectedUser }, u.name)));
|
||||||
const st = daemonState();
|
const st = daemonState();
|
||||||
if (st !== 'running' && st !== 'external') {
|
if (st !== 'running' && st !== 'external') {
|
||||||
put(head, h('div', {}, h('h1', {}, 'Buckets')));
|
put(head, h('div', {}, h('h1', {}, 'Drives')));
|
||||||
put(body, h('div', { class: 'card empty' }, h('p', {}, 's3d is not running.'), h('button', { class: 'btn primary', onclick: async () => { await daemonAction('start'); setTimeout(route, 1500); } }, 'Start s3d')));
|
put(body, h('div', { class: 'card empty' }, h('p', {}, 's3d is not running.'), h('button', { class: 'btn primary', onclick: async () => { await daemonAction('start'); setTimeout(route, 1500); } }, 'Start s3d')));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1119,8 +1119,8 @@ function accountLine() {
|
||||||
|
|
||||||
async function bucketList(head, body, user, userSel) {
|
async function bucketList(head, body, user, userSel) {
|
||||||
put(head,
|
put(head,
|
||||||
h('div', {}, h('h1', {}, 'Buckets'), h('p', { class: 'muted' }, 'Top-level storage containers, like drives. Each belongs to one S3 user.'), accountLine()),
|
h('div', {}, h('h1', {}, 'Drives'), h('p', { class: 'muted' }, 'Each drive is an S3 bucket: top-level storage owned by one S3 user. S3 apps call it a bucket.'), accountLine()),
|
||||||
h('div', { class: 'row' }, userPicker(userSel), h('button', { class: 'btn primary', onclick: create }, 'New bucket')));
|
h('div', { class: 'row' }, userPicker(userSel), h('button', { class: 'btn primary', onclick: create }, 'New drive')));
|
||||||
const card = h('div', { class: 'card' }, h('p', { class: 'muted' }, 'Loading…'));
|
const card = h('div', { class: 'card' }, h('p', { class: 'muted' }, 'Loading…'));
|
||||||
put(body, card);
|
put(body, card);
|
||||||
const base = `/api/s3/${encodeURIComponent(user)}/buckets`;
|
const base = `/api/s3/${encodeURIComponent(user)}/buckets`;
|
||||||
|
|
@ -1128,8 +1128,8 @@ async function bucketList(head, body, user, userSel) {
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const list = await api('GET', base);
|
const list = await api('GET', base);
|
||||||
if (!list.length) { put(card, h('div', { class: 'empty' }, h('p', {}, 'No buckets yet.'), h('button', { class: 'btn primary', onclick: create }, 'Create a bucket'))); return; }
|
if (!list.length) { put(card, h('div', { class: 'empty' }, h('p', {}, 'No drives yet.'), h('button', { class: 'btn primary', onclick: create }, 'Create a drive'))); return; }
|
||||||
// Buckets read as top-level folders.
|
// Buckets read as drives: the top level you put folders into.
|
||||||
put(card, h('div', { class: 'tiles' }, list.map((b) => {
|
put(card, h('div', { class: 'tiles' }, list.map((b) => {
|
||||||
const access = h('span', { class: 'badge' }, '…');
|
const access = h('span', { class: 'badge' }, '…');
|
||||||
api('GET', `${base}/${encodeURIComponent(b.name)}/policy`).then((p) => {
|
api('GET', `${base}/${encodeURIComponent(b.name)}/policy`).then((p) => {
|
||||||
|
|
@ -1138,8 +1138,9 @@ async function bucketList(head, body, user, userSel) {
|
||||||
return h('div', { class: 'tile', tabindex: '0', role: 'button',
|
return h('div', { class: 'tile', tabindex: '0', role: 'button',
|
||||||
onclick: () => { location.hash = `#/buckets/${encodeURIComponent(b.name)}`; },
|
onclick: () => { location.hash = `#/buckets/${encodeURIComponent(b.name)}`; },
|
||||||
onkeydown: (e) => { if (e.key === 'Enter') location.hash = `#/buckets/${encodeURIComponent(b.name)}`; } },
|
onkeydown: (e) => { if (e.key === 'Enter') location.hash = `#/buckets/${encodeURIComponent(b.name)}`; } },
|
||||||
h('div', { class: 'tile-icon' }, '📁'),
|
h('div', { class: 'drive-icon', 'aria-hidden': 'true' }),
|
||||||
h('div', { class: 'tile-name' }, b.name),
|
h('div', { class: 'tile-name' }, b.name),
|
||||||
|
usageLine(b.name),
|
||||||
h('div', { class: 'tile-meta' }, access),
|
h('div', { class: 'tile-meta' }, access),
|
||||||
h('div', { class: 'tile-actions' },
|
h('div', { class: 'tile-actions' },
|
||||||
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); accessDialog(user, b.name).then(load); } }, 'Access'),
|
h('button', { class: 'btn small', onclick: (e) => { e.stopPropagation(); accessDialog(user, b.name).then(load); } }, 'Access'),
|
||||||
|
|
@ -1148,8 +1149,17 @@ async function bucketList(head, body, user, userSel) {
|
||||||
} catch (e) { put(card, h('p', { class: 'error' }, e.message)); }
|
} catch (e) { put(card, h('p', { class: 'error' }, e.message)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Files and size on a drive, counted in the background.
|
||||||
|
function usageLine(bucketName) {
|
||||||
|
const el = h('div', { class: 'small muted' }, 'counting…');
|
||||||
|
api('GET', `${base}/${encodeURIComponent(bucketName)}/usage`)
|
||||||
|
.then((u) => put(el, `${u.files.toLocaleString()}${u.partial ? '+' : ''} file${u.files === 1 ? '' : 's'} · ${fmtBytes(u.bytes)}${u.partial ? '+' : ''}`))
|
||||||
|
.catch(() => put(el, ''));
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
const name = h('input', { type: 'text', required: true, pattern: '[a-z0-9][a-z0-9.\\-]{1,61}[a-z0-9]', placeholder: 'my-bucket' });
|
const name = h('input', { type: 'text', required: true, pattern: '[a-z0-9][a-z0-9.\\-]{1,61}[a-z0-9]', placeholder: 'my-drive' });
|
||||||
const v = await modal('New bucket', h('label', { class: 'field' }, h('span', {}, 'Name'), name, h('small', {}, '3-63 characters: lowercase letters, digits, dots and hyphens. Names are unique across this s3d.')),
|
const v = await modal('New bucket', h('label', { class: 'field' }, h('span', {}, 'Name'), name, h('small', {}, '3-63 characters: lowercase letters, digits, dots and hyphens. Names are unique across this s3d.')),
|
||||||
[{ label: 'Cancel', result: null }, { label: 'Create', kind: 'primary', submit: true, value: () => name.value.trim() }]);
|
[{ label: 'Cancel', result: null }, { label: 'Create', kind: 'primary', submit: true, value: () => name.value.trim() }]);
|
||||||
if (!v) return;
|
if (!v) return;
|
||||||
|
|
@ -1193,7 +1203,7 @@ function objectBrowser(head, body, user, bucket, prefix, userSel) {
|
||||||
const go = (p) => { location.hash = `#/buckets/${encodeURIComponent(bucket)}${p ? '/' + p.split('/').filter(Boolean).map(encodeURIComponent).join('/') : ''}`; };
|
const go = (p) => { location.hash = `#/buckets/${encodeURIComponent(bucket)}${p ? '/' + p.split('/').filter(Boolean).map(encodeURIComponent).join('/') : ''}`; };
|
||||||
|
|
||||||
const crumbs = h('div', { class: 'crumbs' },
|
const crumbs = h('div', { class: 'crumbs' },
|
||||||
h('button', { onclick: () => { location.hash = '#/buckets'; } }, 'Buckets'), h('span', { class: 'sep' }, '/'),
|
h('button', { onclick: () => { location.hash = '#/buckets'; } }, 'Drives'), h('span', { class: 'sep' }, '/'),
|
||||||
h('button', { onclick: () => go('') }, bucket),
|
h('button', { onclick: () => go('') }, bucket),
|
||||||
prefix.split('/').filter(Boolean).map((part, i, all) => [h('span', { class: 'sep' }, '/'), h('button', { onclick: () => go(all.slice(0, i + 1).join('/')) }, part)]));
|
prefix.split('/').filter(Boolean).map((part, i, all) => [h('span', { class: 'sep' }, '/'), h('button', { onclick: () => go(all.slice(0, i + 1).join('/')) }, part)]));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
<nav id="nav">
|
<nav id="nav">
|
||||||
<a href="#/start" data-view="start">Get started</a>
|
<a href="#/start" data-view="start">Get started</a>
|
||||||
<a href="#/overview" data-view="overview">Overview</a>
|
<a href="#/overview" data-view="overview">Overview</a>
|
||||||
<a href="#/buckets" data-view="buckets">Buckets</a>
|
<a href="#/buckets" data-view="buckets">Drives</a>
|
||||||
<a href="#/users" data-view="users">Users & keys</a>
|
<a href="#/users" data-view="users">Users & keys</a>
|
||||||
<a href="#/setup" data-view="setup">Sia connection</a>
|
<a href="#/setup" data-view="setup">Sia connection</a>
|
||||||
<a href="#/settings" data-view="settings">Settings</a>
|
<a href="#/settings" data-view="settings">Settings</a>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue