// 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 '../vendor/yaml/lib/index.js'; 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'); }