pithos/core/config.js
Local Dev 6b8eeaf82b Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.

One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00

79 lines
3 KiB
JavaScript

// s3d.yml read/write. Edits go through the yaml Document API so keys the GUI
// does not know about (and the operator's comments) survive a save.
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import YAML from 'yaml';
import { defaultDataDir } from './paths.js';
// Fields the settings form exposes, as dotted yaml paths. Anything else in the
// file is left untouched.
export const EDITABLE = [
'apiAddress', 'apiHTTPSAddress', 'adminAddress', 'adminPassword', 'directory',
'sia.diskUsageLimit', 'sia.uploadThreads', 'sia.dataShards', 'sia.parityShards',
's3.hostBases',
'log.stdout.enabled', 'log.stdout.level', 'log.file.enabled', 'log.file.level',
];
export const DEFAULTS = {
apiAddress: '127.0.0.1:8000',
adminAddress: '127.0.0.1:8001',
};
export function readConfig(file) {
const exists = fs.existsSync(file);
const doc = exists ? YAML.parseDocument(fs.readFileSync(file, 'utf8')) : new YAML.Document({});
if (doc.errors.length) throw new Error(`${file}: ${doc.errors[0].message}`);
const data = doc.toJS() || {};
return {
file,
exists,
data,
// Effective values the daemon will use, for things the GUI needs to reach it.
apiAddress: data.apiAddress || DEFAULTS.apiAddress,
adminAddress: data.adminAddress || DEFAULTS.adminAddress,
adminPassword: data.adminPassword || '',
directory: process.env.S3D_DATA_DIR || data.directory || defaultDataDir(),
};
}
// patch: { 'sia.uploadThreads': 8, 'apiHTTPSAddress': null, ... }; null/'' deletes the key.
export function writeConfig(file, patch) {
const exists = fs.existsSync(file);
const doc = exists ? YAML.parseDocument(fs.readFileSync(file, 'utf8')) : new YAML.Document({});
if (doc.contents == null) doc.contents = doc.createNode({});
for (const [key, value] of Object.entries(patch)) {
if (!EDITABLE.includes(key)) throw new Error(`not an editable setting: ${key}`);
const p = key.split('.');
if (value === null || value === '' || (Array.isArray(value) && value.length === 0)) doc.deleteIn(p);
else doc.setIn(p, value);
}
fs.mkdirSync(path.dirname(file), { recursive: true });
const tmp = file + '.tmp';
fs.writeFileSync(tmp, doc.toString(), { mode: 0o600 });
fs.renameSync(tmp, file);
return readConfig(file);
}
// A fresh config the daemon can start with: admin API on, random password.
export function ensureConfig(file) {
if (fs.existsSync(file)) {
const cfg = readConfig(file);
if (cfg.data.adminAddress && cfg.data.adminPassword) return cfg;
// s3d refuses to start without both, and the GUI needs them for stats.
return writeConfig(file, {
adminAddress: cfg.data.adminAddress || DEFAULTS.adminAddress,
adminPassword: cfg.data.adminPassword || randomPassword(),
});
}
return writeConfig(file, {
apiAddress: DEFAULTS.apiAddress,
adminAddress: DEFAULTS.adminAddress,
adminPassword: randomPassword(),
});
}
export function randomPassword() {
return crypto.randomBytes(18).toString('base64url');
}