Skip to content

Commit 06c38e7

Browse files
authored
Improve FileStore read/write safety (#491)
1 parent 3fadf0b commit 06c38e7

3 files changed

Lines changed: 350 additions & 199 deletions

File tree

src/datastore/FileStore.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class FileStoreFactory implements DataStoreFactory {
1919

2020
private readonly metricsInterval: NodeJS.Timeout;
2121
private readonly timeout: NodeJS.Timeout;
22+
private closed = false;
2223

2324
constructor(
2425
rootDir: string,
@@ -60,12 +61,16 @@ export class FileStoreFactory implements DataStoreFactory {
6061
}
6162

6263
close(): Promise<void> {
64+
if (this.closed) return Promise.resolve();
65+
this.closed = true;
6366
clearTimeout(this.timeout);
6467
clearInterval(this.metricsInterval);
6568
return Promise.resolve();
6669
}
6770

6871
private emitMetrics(): void {
72+
if (this.closed) return;
73+
6974
this.telemetry.histogram('version', VersionNumber);
7075
this.telemetry.histogram('env.entries', this.stores.size);
7176

@@ -84,6 +89,8 @@ export class FileStoreFactory implements DataStoreFactory {
8489
}
8590

8691
private cleanupOldVersions(): void {
92+
if (this.closed || !existsSync(this.fileDbRoot)) return;
93+
8794
const entries = readdirSync(this.fileDbRoot, { withFileTypes: true });
8895
for (const entry of entries) {
8996
try {

src/datastore/file/EncryptedFileStore.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { existsSync, readFileSync, statSync, writeFileSync } from 'fs'; // eslint-disable-line no-restricted-imports -- files being checked
2-
import { writeFile } from 'fs/promises';
1+
import { existsSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs'; // eslint-disable-line no-restricted-imports -- files being checked
2+
import { rename, writeFile } from 'fs/promises';
33
import { join } from 'path';
44
import { Logger } from 'pino';
55
import { lock, LockOptions, lockSync } from 'proper-lockfile';
@@ -28,18 +28,15 @@ export class EncryptedFileStore implements DataStore {
2828
this.telemetry = TelemetryService.instance.get(`FileStore.${name}`);
2929

3030
if (existsSync(this.file)) {
31+
const release = lockSync(this.file, LOCK_OPTIONS_SYNC);
3132
try {
3233
this.content = this.readFile();
3334
} catch (error) {
3435
this.log.error(error, 'Failed to decrypt file store, recreating store');
3536
this.telemetry.count('filestore.recreate', 1);
36-
37-
const release = lockSync(this.file, LOCK_OPTIONS_SYNC);
38-
try {
39-
this.saveSync();
40-
} finally {
41-
release();
42-
}
37+
this.saveSync();
38+
} finally {
39+
release();
4340
}
4441
} else {
4542
this.saveSync();
@@ -113,11 +110,15 @@ export class EncryptedFileStore implements DataStore {
113110
}
114111

115112
private saveSync() {
116-
writeFileSync(this.file, encrypt(this.KEY, JSON.stringify(this.content)));
113+
const tmp = `${this.file}.${process.pid}.tmp`;
114+
writeFileSync(tmp, encrypt(this.KEY, JSON.stringify(this.content)));
115+
renameSync(tmp, this.file);
117116
}
118117

119118
private async save() {
120-
await writeFile(this.file, encrypt(this.KEY, JSON.stringify(this.content)));
119+
const tmp = `${this.file}.${process.pid}.tmp`;
120+
await writeFile(tmp, encrypt(this.KEY, JSON.stringify(this.content)));
121+
await rename(tmp, this.file);
121122
}
122123
}
123124

0 commit comments

Comments
 (0)