theseus/bundled-addons/pithos/core/accounts.js

101 lines
4.7 KiB
JavaScript
Raw Normal View History

// 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';
// 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']);
const keep = (name) => LOCAL.has(name) || name.startsWith('pithos-hosted-secret') || 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 || [],
movedTo: meta.movedTo || null,
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');
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`);
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 };
}