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