107 lines
4.0 KiB
JavaScript
107 lines
4.0 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
// De staat op schijf: de allowlist en het opdrachtbestand van de statuspagina.
|
||
|
|
//
|
||
|
|
// Dit is de enige plek met bestanden erin. Het beleid zelf staat in `policy.js`
|
||
|
|
// en weet hier niets van.
|
||
|
|
//
|
||
|
|
// Waar het staat: onder `${APP_DATA_DIR}/data/`, náást de database van de relay
|
||
|
|
// en niet erin. De relay bezit zijn eigen SQLite en daar schrijven wij niet in.
|
||
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
||
|
|
import { dirname, join } from 'node:path';
|
||
|
|
|
||
|
|
import { createEmptyState, normalizeState } from './policy.js';
|
||
|
|
|
||
|
|
export const stateFileName = 'owners.json';
|
||
|
|
export const commandFileName = 'command.json';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Leest de allowlist.
|
||
|
|
*
|
||
|
|
* Drie uitkomsten, en de derde is de interessante:
|
||
|
|
* - `new`: er is nog niets, dus een verse staat met de leerstand aan;
|
||
|
|
* - `ok`: gelezen en begrepen;
|
||
|
|
* - `corrupt`: er stond iets wat we niet begrijpen. Dan schuiven we het bestand
|
||
|
|
* opzij en beginnen we met de leerstand **uit**.
|
||
|
|
*
|
||
|
|
* Die laatste keuze is bewust en volgt de regel die deze store al kent van
|
||
|
|
* Electrum Gate: bij twijfel weigert de app. Een onleesbaar bestand betekent dat
|
||
|
|
* we niet weten wie er toegelaten was; met de leerstand aan zou de eerstvolgende
|
||
|
|
* die verbindt stilzwijgend de nieuwe eigenaar worden, en dat hoeft je eigen
|
||
|
|
* apparaat niet te zijn. Dan liever een app die niemand toelaat en dat op de
|
||
|
|
* pagina toont.
|
||
|
|
*/
|
||
|
|
export const readState = (dataDir, now) => {
|
||
|
|
const file = join(dataDir, stateFileName);
|
||
|
|
if (!existsSync(file)) return { state: createEmptyState(), status: 'new' };
|
||
|
|
|
||
|
|
let parsed = null;
|
||
|
|
try {
|
||
|
|
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
||
|
|
} catch {
|
||
|
|
parsed = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const state = parsed === null ? null : normalizeState(parsed);
|
||
|
|
if (state !== null) return { state, status: 'ok' };
|
||
|
|
|
||
|
|
// Opzij schuiven en niet weggooien: als dit ooit gebeurt, wil je kunnen zien
|
||
|
|
// wat erin stond.
|
||
|
|
const aside = `${file}.corrupt-${now.replace(/[:.]/g, '-')}`;
|
||
|
|
try {
|
||
|
|
renameSync(file, aside);
|
||
|
|
} catch {
|
||
|
|
// Lukt het opzij schuiven niet, dan gaan we alsnog dicht. De melding hieronder
|
||
|
|
// is dan het enige dat de gebruiker heeft, en dat is nog steeds beter dan
|
||
|
|
// stilzwijgend iemand toelaten.
|
||
|
|
}
|
||
|
|
return { state: { ...createEmptyState(), learning: false }, status: 'corrupt' };
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Schrijft de allowlist. Eerst naar een tijdelijk bestand en dan hernoemen, zodat
|
||
|
|
* een onderbroken schrijfactie geen half bestand achterlaat: hernoemen binnen
|
||
|
|
* dezelfde map is atomair.
|
||
|
|
*/
|
||
|
|
export const writeState = (dataDir, state) => {
|
||
|
|
mkdirSync(dataDir, { recursive: true });
|
||
|
|
const file = join(dataDir, stateFileName);
|
||
|
|
const temporary = `${file}.tmp`;
|
||
|
|
writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
||
|
|
renameSync(temporary, file);
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Haalt een opdracht van de statuspagina op en verwijdert het bestand meteen.
|
||
|
|
*
|
||
|
|
* Verwijderen vóór het uitvoeren is met opzet: een opdracht die het proces laat
|
||
|
|
* struikelen mag niet bij elke ronde opnieuw worden geprobeerd.
|
||
|
|
*/
|
||
|
|
export const takeCommand = (dataDir) => {
|
||
|
|
const file = join(dataDir, commandFileName);
|
||
|
|
if (!existsSync(file)) return null;
|
||
|
|
|
||
|
|
let raw = null;
|
||
|
|
try {
|
||
|
|
raw = readFileSync(file, 'utf8');
|
||
|
|
} catch {
|
||
|
|
raw = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
unlinkSync(file);
|
||
|
|
} catch {
|
||
|
|
// Niets aan te doen; de volgende ronde probeert het opnieuw.
|
||
|
|
}
|
||
|
|
|
||
|
|
if (raw === null) return null;
|
||
|
|
try {
|
||
|
|
return JSON.parse(raw);
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/** De map waarin de staat hoort, afgeleid van het pad van de relay-database. */
|
||
|
|
export const dataDirectoryFor = (relayDatabaseFile) => dirname(relayDatabaseFile);
|