/* * Copyright (C) 2021-2022 Kristóf Marussy * * This file is part of Sophie. * * Sophie is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * SPDX-License-Identifier: AGPL-3.0-only */ import { watch } from 'fs'; import { readFile, stat, writeFile } from 'fs/promises'; import JSON5 from 'json5'; import { throttle } from 'lodash'; import { IDisposer } from 'mobx-state-tree'; import { join } from 'path'; import type { ConfigSnapshotOut } from '../stores/Config'; export type ReadConfigResult = { found: true; data: unknown; } | { found: false; }; export class ConfigPersistenceService { readonly #userDataDir: string; readonly #configFileName: string; readonly #configFilePath: string; constructor( userDataDir: string, configFileName: string, ) { this.#userDataDir = userDataDir; this.#configFileName = configFileName; this.#configFilePath = join(this.#userDataDir, this.#configFileName); } async readConfig(): Promise { let configStr; try { configStr = await readFile(this.#configFilePath, 'utf8'); } catch (err) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') { return { found: false }; } throw err; } return { found: true, data: JSON5.parse(configStr), }; } async writeConfig(configSnapshot: ConfigSnapshotOut): Promise { const configJson = JSON5.stringify(configSnapshot, { space: 2, }); await writeFile(this.#configFilePath, configJson, 'utf8'); const { mtime } = await stat(this.#configFilePath); return mtime; } watchConfig(callback: (mtime: Date) => Promise, throttleMs: number): IDisposer { const configChanged = throttle(async () => { let mtime: Date; try { const stats = await stat(this.#configFilePath); mtime = stats.mtime; } catch (err) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') { return; } throw err; } return callback(mtime); }, throttleMs); const watcher = watch(this.#userDataDir, { persistent: false, }); watcher.on('change', (eventType, filename) => { if (eventType === 'change' && (filename === this.#configFileName || filename === null)) { configChanged()?.catch((err) => { console.log('Unhandled error while listening for config changes', err); }); } }); return () => watcher.close(); } }