// 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-/ 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'; const keep = (name) => name === 's3d.yml' || name.startsWith(ARCHIVE_PREFIX); 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 || [], 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'); 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 }; }