aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/infrastructure/config/ConfigFile.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main/src/infrastructure/config/ConfigFile.ts')
-rw-r--r--packages/main/src/infrastructure/config/ConfigFile.ts142
1 files changed, 142 insertions, 0 deletions
diff --git a/packages/main/src/infrastructure/config/ConfigFile.ts b/packages/main/src/infrastructure/config/ConfigFile.ts
new file mode 100644
index 0000000..193a20d
--- /dev/null
+++ b/packages/main/src/infrastructure/config/ConfigFile.ts
@@ -0,0 +1,142 @@
1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import { watch } from 'node:fs';
22import { readFile, stat, writeFile } from 'node:fs/promises';
23import path from 'node:path';
24
25import JSON5 from 'json5';
26import { throttle } from 'lodash-es';
27
28import type { Config } from '../../stores/SharedStore';
29import type Disposer from '../../utils/Disposer';
30import { getLogger } from '../../utils/log';
31
32import type ConfigRepository from './ConfigRepository';
33import type ReadConfigResult from './ReadConfigResult';
34
35const log = getLogger('ConfigFile');
36
37export default class ConfigFile implements ConfigRepository {
38 readonly #userDataDir: string;
39
40 readonly #configFileName: string;
41
42 readonly #configFilePath: string;
43
44 #writingConfig = false;
45
46 #timeLastWritten: Date | undefined;
47
48 constructor(userDataDir: string, configFileName = 'config.json5') {
49 this.#userDataDir = userDataDir;
50 this.#configFileName = configFileName;
51 this.#configFilePath = path.join(userDataDir, configFileName);
52 }
53
54 async readConfig(): Promise<ReadConfigResult> {
55 let configStr: string;
56 try {
57 configStr = await readFile(this.#configFilePath, 'utf8');
58 } catch (error) {
59 if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
60 log.debug('Config file', this.#configFilePath, 'was not found');
61 return { found: false };
62 }
63 throw error;
64 }
65 log.info('Read config file', this.#configFilePath);
66 return {
67 found: true,
68 data: JSON5.parse(configStr),
69 };
70 }
71
72 async writeConfig(configSnapshot: Config): Promise<void> {
73 const configJson = JSON5.stringify(configSnapshot, {
74 space: 2,
75 });
76 this.#writingConfig = true;
77 try {
78 await writeFile(this.#configFilePath, configJson, 'utf8');
79 const { mtime } = await stat(this.#configFilePath);
80 log.trace('Config file', this.#configFilePath, 'last written at', mtime);
81 this.#timeLastWritten = mtime;
82 } finally {
83 this.#writingConfig = false;
84 }
85 log.info('Wrote config file', this.#configFilePath);
86 }
87
88 watchConfig(callback: () => Promise<void>, throttleMs: number): Disposer {
89 log.debug('Installing watcher for', this.#userDataDir);
90
91 const configChanged = throttle(async () => {
92 let mtime: Date;
93 try {
94 const stats = await stat(this.#configFilePath);
95 mtime = stats.mtime;
96 log.trace('Config file last modified at', mtime);
97 } catch (error) {
98 if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
99 log.debug(
100 'Config file',
101 this.#configFilePath,
102 'was deleted after being changed',
103 );
104 return;
105 }
106 throw error;
107 }
108 if (
109 !this.#writingConfig &&
110 (this.#timeLastWritten === undefined || mtime > this.#timeLastWritten)
111 ) {
112 log.debug(
113 'Found a config file modified at',
114 mtime,
115 'whish is newer than last written',
116 this.#timeLastWritten,
117 );
118 await callback();
119 }
120 }, throttleMs);
121
122 const watcher = watch(this.#userDataDir, {
123 persistent: false,
124 });
125
126 watcher.on('change', (eventType, filename) => {
127 if (
128 eventType === 'change' &&
129 (filename === this.#configFileName || filename === null)
130 ) {
131 configChanged()?.catch((err) => {
132 log.error('Unhandled error while listening for config changes', err);
133 });
134 }
135 });
136
137 return () => {
138 log.trace('Removing watcher for', this.#configFilePath);
139 watcher.close();
140 };
141 }
142}