aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/infrastructure/config/impl/ConfigFile.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main/src/infrastructure/config/impl/ConfigFile.ts')
-rw-r--r--packages/main/src/infrastructure/config/impl/ConfigFile.ts141
1 files changed, 141 insertions, 0 deletions
diff --git a/packages/main/src/infrastructure/config/impl/ConfigFile.ts b/packages/main/src/infrastructure/config/impl/ConfigFile.ts
new file mode 100644
index 0000000..90ee187
--- /dev/null
+++ b/packages/main/src/infrastructure/config/impl/ConfigFile.ts
@@ -0,0 +1,141 @@
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/config/Config';
29import type Disposer from '../../../utils/Disposer';
30import { getLogger } from '../../../utils/log';
31import type ConfigRepository from '../ConfigRepository';
32import type ReadConfigResult from '../ReadConfigResult';
33
34const log = getLogger('ConfigFile');
35
36export default class ConfigFile implements ConfigRepository {
37 readonly #userDataDir: string;
38
39 readonly #configFileName: string;
40
41 readonly #configFilePath: string;
42
43 #writingConfig = false;
44
45 #timeLastWritten: Date | undefined;
46
47 constructor(userDataDir: string, configFileName = 'config.json5') {
48 this.#userDataDir = userDataDir;
49 this.#configFileName = configFileName;
50 this.#configFilePath = path.join(userDataDir, configFileName);
51 }
52
53 async readConfig(): Promise<ReadConfigResult> {
54 let configStr: string;
55 try {
56 configStr = await readFile(this.#configFilePath, 'utf8');
57 } catch (error) {
58 if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
59 log.debug('Config file', this.#configFilePath, 'was not found');
60 return { found: false };
61 }
62 throw error;
63 }
64 log.info('Read config file', this.#configFilePath);
65 return {
66 found: true,
67 data: JSON5.parse(configStr),
68 };
69 }
70
71 async writeConfig(configSnapshot: Config): Promise<void> {
72 const configJson = JSON5.stringify(configSnapshot, {
73 space: 2,
74 });
75 this.#writingConfig = true;
76 try {
77 await writeFile(this.#configFilePath, configJson, 'utf8');
78 const { mtime } = await stat(this.#configFilePath);
79 log.trace('Config file', this.#configFilePath, 'last written at', mtime);
80 this.#timeLastWritten = mtime;
81 } finally {
82 this.#writingConfig = false;
83 }
84 log.info('Wrote config file', this.#configFilePath);
85 }
86
87 watchConfig(callback: () => Promise<void>, throttleMs: number): Disposer {
88 log.debug('Installing watcher for', this.#userDataDir);
89
90 const configChanged = throttle(async () => {
91 let mtime: Date;
92 try {
93 const stats = await stat(this.#configFilePath);
94 mtime = stats.mtime;
95 log.trace('Config file last modified at', mtime);
96 } catch (error) {
97 if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
98 log.debug(
99 'Config file',
100 this.#configFilePath,
101 'was deleted after being changed',
102 );
103 return;
104 }
105 throw error;
106 }
107 if (
108 !this.#writingConfig &&
109 (this.#timeLastWritten === undefined || mtime > this.#timeLastWritten)
110 ) {
111 log.debug(
112 'Found a config file modified at',
113 mtime,
114 'whish is newer than last written',
115 this.#timeLastWritten,
116 );
117 await callback();
118 }
119 }, throttleMs);
120
121 const watcher = watch(this.#userDataDir, {
122 persistent: false,
123 });
124
125 watcher.on('change', (eventType, filename) => {
126 if (
127 eventType === 'change' &&
128 (filename === this.#configFileName || filename === null)
129 ) {
130 configChanged()?.catch((err) => {
131 log.error('Unhandled error while listening for config changes', err);
132 });
133 }
134 });
135
136 return () => {
137 log.trace('Removing watcher for', this.#configFilePath);
138 watcher.close();
139 };
140 }
141}