Pithos 0.3.4: show the Sia account, label it, switch and restore accounts
"Acting as" read like a choice of Sia account; it picks an S3 user on
this s3d, and every user shares the one Sia account the s3d is connected
to. So:
- The picker is "S3 user", with a tooltip saying all users share one Sia
account; Buckets explains what a bucket is.
- Buckets and the Overview show "Sia account: <label> (indexer)". Pithos
cannot see the account's email, so the Sia connection page lets the
user label it (kept in pithos.json beside the s3d data).
- "Switch Sia account" stops s3d and moves its data (connection, users,
keys, buckets, upload buffer) into previous-account-<stamp>/ next to
s3d.yml, never deleting it; the guide then connects another account.
It warns about uploads still waiting for Sia and offers a flush, and
refuses while another s3d serves the config.
- "Earlier Sia accounts" lists those folders with Restore, which sets the
current account aside the same way and swaps the old one back.
Also: a fresh connect now records which indexer it used, and a "not
connected" check is re-run after 30 s so a login made outside Pithos
shows up.
2026-10-03 22:57:46 +02:00
|
|
|
// Switching the Sia account an s3d is connected to. s3d has no "log out":
|
|
|
|
|
// its connection (the app key), users, keys, buckets and upload buffer all
|
|
|
|
|
// live in its data folder. A switch moves that folder's contents into
|
|
|
|
|
// previous-account-<stamp>/ beside it (never deleted), so s3d starts fresh
|
|
|
|
|
// and can be connected to another account; a restore swaps one back.
|
|
|
|
|
//
|
|
|
|
|
// s3d.yml stays where it is: it holds addresses and the admin password, not
|
|
|
|
|
// the account. pithos.json (the user's label for the account) moves with the
|
|
|
|
|
// data, because the label belongs to that account.
|
|
|
|
|
|
|
|
|
|
import fs from 'node:fs';
|
|
|
|
|
import path from 'node:path';
|
|
|
|
|
|
|
|
|
|
const ARCHIVE_PREFIX = 'previous-account-';
|
|
|
|
|
const META = 'pithos-archive.json';
|
|
|
|
|
const LABEL_FILE = 'pithos.json';
|
|
|
|
|
|
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
|
|
|
// Files that belong to this computer, not to a Sia account. The hosted key
|
|
|
|
|
// file above all: moving or deleting it would make encrypted files unreadable.
|
|
|
|
|
const LOCAL = new Set(['s3d.yml', 'pithos-prefs.json', 'pithos-hosted-secret.json']);
|
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
|
|
|
const keep = (name) => LOCAL.has(name) || name.startsWith('pithos-hosted-secret') || name.startsWith(ARCHIVE_PREFIX);
|
Pithos 0.3.4: show the Sia account, label it, switch and restore accounts
"Acting as" read like a choice of Sia account; it picks an S3 user on
this s3d, and every user shares the one Sia account the s3d is connected
to. So:
- The picker is "S3 user", with a tooltip saying all users share one Sia
account; Buckets explains what a bucket is.
- Buckets and the Overview show "Sia account: <label> (indexer)". Pithos
cannot see the account's email, so the Sia connection page lets the
user label it (kept in pithos.json beside the s3d data).
- "Switch Sia account" stops s3d and moves its data (connection, users,
keys, buckets, upload buffer) into previous-account-<stamp>/ next to
s3d.yml, never deleting it; the guide then connects another account.
It warns about uploads still waiting for Sia and offers a flush, and
refuses while another s3d serves the config.
- "Earlier Sia accounts" lists those folders with Restore, which sets the
current account aside the same way and swaps the old one back.
Also: a fresh connect now records which indexer it used, and a "not
connected" check is re-run after 30 s so a login made outside Pithos
shows up.
2026-10-03 22:57:46 +02:00
|
|
|
|
|
|
|
|
function stamp(d = new Date()) {
|
|
|
|
|
const p = (n) => String(n).padStart(2, '0');
|
|
|
|
|
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function readLabel(dataDir) {
|
|
|
|
|
try { return String(JSON.parse(fs.readFileSync(path.join(dataDir, LABEL_FILE), 'utf8')).accountLabel || ''); } catch { return ''; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function writeLabel(dataDir, label) {
|
|
|
|
|
const clean = String(label || '').trim().slice(0, 120);
|
|
|
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
|
|
|
let cur = {};
|
|
|
|
|
try { cur = JSON.parse(fs.readFileSync(path.join(dataDir, LABEL_FILE), 'utf8')); } catch {}
|
|
|
|
|
fs.writeFileSync(path.join(dataDir, LABEL_FILE), JSON.stringify({ ...cur, accountLabel: clean }, null, 2));
|
|
|
|
|
return clean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function listArchives(dataDir) {
|
|
|
|
|
let names = [];
|
|
|
|
|
try { names = fs.readdirSync(dataDir).filter((n) => n.startsWith(ARCHIVE_PREFIX)); } catch { return []; }
|
|
|
|
|
return names.sort().reverse().map((name) => {
|
|
|
|
|
const dir = path.join(dataDir, name);
|
|
|
|
|
let meta = {};
|
|
|
|
|
try { meta = JSON.parse(fs.readFileSync(path.join(dir, META), 'utf8')); } catch {}
|
|
|
|
|
return {
|
|
|
|
|
name,
|
|
|
|
|
archivedAt: meta.archivedAt || null,
|
|
|
|
|
indexerUrl: meta.indexerUrl || null,
|
|
|
|
|
label: meta.label || readLabel(dir),
|
|
|
|
|
users: meta.users || [],
|
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
|
|
|
movedTo: meta.movedTo || null,
|
Pithos 0.3.4: show the Sia account, label it, switch and restore accounts
"Acting as" read like a choice of Sia account; it picks an S3 user on
this s3d, and every user shares the one Sia account the s3d is connected
to. So:
- The picker is "S3 user", with a tooltip saying all users share one Sia
account; Buckets explains what a bucket is.
- Buckets and the Overview show "Sia account: <label> (indexer)". Pithos
cannot see the account's email, so the Sia connection page lets the
user label it (kept in pithos.json beside the s3d data).
- "Switch Sia account" stops s3d and moves its data (connection, users,
keys, buckets, upload buffer) into previous-account-<stamp>/ next to
s3d.yml, never deleting it; the guide then connects another account.
It warns about uploads still waiting for Sia and offers a flush, and
refuses while another s3d serves the config.
- "Earlier Sia accounts" lists those folders with Restore, which sets the
current account aside the same way and swaps the old one back.
Also: a fresh connect now records which indexer it used, and a "not
connected" check is re-run after 30 s so a login made outside Pithos
shows up.
2026-10-03 22:57:46 +02:00
|
|
|
hasData: fs.existsSync(path.join(dir, 's3d.db')),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Moves the current account's data aside. `meta` is stored with it for the
|
|
|
|
|
// restore list: { indexerUrl, users }.
|
|
|
|
|
export function archiveCurrent(dataDir, meta = {}) {
|
|
|
|
|
if (!fs.existsSync(path.join(dataDir, 's3d.db'))) throw new Error('there is no s3d data to move aside');
|
|
|
|
|
let name = ARCHIVE_PREFIX + stamp();
|
|
|
|
|
while (fs.existsSync(path.join(dataDir, name))) name += '-1';
|
|
|
|
|
const dest = path.join(dataDir, name);
|
|
|
|
|
fs.mkdirSync(dest);
|
|
|
|
|
const label = readLabel(dataDir);
|
|
|
|
|
for (const entry of fs.readdirSync(dataDir)) {
|
|
|
|
|
if (keep(entry) || entry === name) continue;
|
|
|
|
|
fs.renameSync(path.join(dataDir, entry), path.join(dest, entry));
|
|
|
|
|
}
|
|
|
|
|
fs.writeFileSync(path.join(dest, META), JSON.stringify({ archivedAt: new Date().toISOString(), label, ...meta }, null, 2));
|
|
|
|
|
return { name, path: dest };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Swaps an archived account back in. Whatever is current is archived first
|
|
|
|
|
// (when there is anything), so a restore never loses data either.
|
|
|
|
|
export function restoreArchive(dataDir, name, metaForCurrent = {}) {
|
|
|
|
|
if (!name.startsWith(ARCHIVE_PREFIX) || name.includes('/') || name.includes('\\') || name.includes('..')) throw new Error('not an account archive');
|
|
|
|
|
const src = path.join(dataDir, name);
|
|
|
|
|
if (!fs.existsSync(path.join(src, 's3d.db'))) throw new Error('that archive has no s3d data');
|
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
|
|
|
let meta = {};
|
|
|
|
|
try { meta = JSON.parse(fs.readFileSync(path.join(src, META), 'utf8')); } catch {}
|
|
|
|
|
// The copy left behind when drives moved to Silent Mode is out of date the
|
|
|
|
|
// moment the server s3d writes anything; running it would fork the account.
|
|
|
|
|
if (meta.movedTo) throw new Error(`these drives now live on ${meta.movedTo}; move them back from Settings instead`);
|
Pithos 0.3.4: show the Sia account, label it, switch and restore accounts
"Acting as" read like a choice of Sia account; it picks an S3 user on
this s3d, and every user shares the one Sia account the s3d is connected
to. So:
- The picker is "S3 user", with a tooltip saying all users share one Sia
account; Buckets explains what a bucket is.
- Buckets and the Overview show "Sia account: <label> (indexer)". Pithos
cannot see the account's email, so the Sia connection page lets the
user label it (kept in pithos.json beside the s3d data).
- "Switch Sia account" stops s3d and moves its data (connection, users,
keys, buckets, upload buffer) into previous-account-<stamp>/ next to
s3d.yml, never deleting it; the guide then connects another account.
It warns about uploads still waiting for Sia and offers a flush, and
refuses while another s3d serves the config.
- "Earlier Sia accounts" lists those folders with Restore, which sets the
current account aside the same way and swaps the old one back.
Also: a fresh connect now records which indexer it used, and a "not
connected" check is re-run after 30 s so a login made outside Pithos
shows up.
2026-10-03 22:57:46 +02:00
|
|
|
const archivedCurrent = fs.existsSync(path.join(dataDir, 's3d.db')) ? archiveCurrent(dataDir, metaForCurrent).name : null;
|
|
|
|
|
// Anything left (tmp folders from a fresh, unconnected s3d) is clutter.
|
|
|
|
|
for (const entry of fs.readdirSync(dataDir)) {
|
|
|
|
|
if (keep(entry)) continue;
|
|
|
|
|
fs.rmSync(path.join(dataDir, entry), { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
for (const entry of fs.readdirSync(src)) {
|
|
|
|
|
if (entry === META) continue;
|
|
|
|
|
fs.renameSync(path.join(src, entry), path.join(dataDir, entry));
|
|
|
|
|
}
|
|
|
|
|
fs.rmSync(src, { recursive: true, force: true });
|
|
|
|
|
return { restored: name, archivedCurrent };
|
|
|
|
|
}
|