snowbase 6.0.2 → 6.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -34
- package/LICENSE +7 -7
- package/README.md +229 -229
- package/bin/snowbase.js +286 -315
- package/package.json +2 -2
- package/src/Snowbase.js +403 -403
- package/src/helpers/crypto.js +69 -69
- package/src/helpers/utilsAsync.js +228 -228
- package/src/storages/FileStorage.js +170 -170
- package/src/storages/FileStorageAsync.js +167 -167
|
@@ -1,170 +1,170 @@
|
|
|
1
|
-
import {
|
|
2
|
-
writeFileSync,
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
copyFileSync,
|
|
6
|
-
statSync,
|
|
7
|
-
readdirSync,
|
|
8
|
-
unlinkSync,
|
|
9
|
-
} from "fs";
|
|
10
|
-
import { read, write, randomId5 } from "../helpers/utils.js";
|
|
11
|
-
import { encrypt, decrypt } from "../helpers/crypto.js";
|
|
12
|
-
import { resolve, join } from "path";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @fileoverview FileStorage is a storage class that stores data in a file.
|
|
16
|
-
* @class FileStorage
|
|
17
|
-
*/
|
|
18
|
-
export default class FileStorage {
|
|
19
|
-
/**
|
|
20
|
-
* @description Creates a new instance of FileStorage.
|
|
21
|
-
* @param {Object} options - Options for the FileStorage.
|
|
22
|
-
* @param {string} options.baseDir - The base directory for the database.
|
|
23
|
-
* @param {boolean} [options.encryption] - Whether to encrypt the data.
|
|
24
|
-
* @param {string} [options.secret] - The secret key for encryption.
|
|
25
|
-
* @param {boolean} [options.backup] - Whether to backup the data.
|
|
26
|
-
* @param {boolean} [options.logging] - Whether to log the data.
|
|
27
|
-
* @param {function(any): never} options.error - Function to handle errors.
|
|
28
|
-
* @param {function(...any): void} options.emit - Function to emit events.
|
|
29
|
-
* @param {function(string, any): void} options.log - Function to log the data.
|
|
30
|
-
* @memberof FileStorage
|
|
31
|
-
* @constructor
|
|
32
|
-
*/
|
|
33
|
-
constructor({
|
|
34
|
-
baseDir,
|
|
35
|
-
encryption = false,
|
|
36
|
-
secret = "",
|
|
37
|
-
backup = false,
|
|
38
|
-
logging = false,
|
|
39
|
-
error,
|
|
40
|
-
emit,
|
|
41
|
-
log,
|
|
42
|
-
}) {
|
|
43
|
-
this.baseDir = resolve(baseDir) || resolve("./snowbase");
|
|
44
|
-
this.encryption = encryption;
|
|
45
|
-
this.secret = secret;
|
|
46
|
-
this.backup = backup;
|
|
47
|
-
this.logging = logging;
|
|
48
|
-
this._error = error;
|
|
49
|
-
this.emit = emit;
|
|
50
|
-
this._log = log;
|
|
51
|
-
this.path = join(this.baseDir, "snowbase.json");
|
|
52
|
-
|
|
53
|
-
if (this.logging) {
|
|
54
|
-
this.logsDir = join(this.baseDir, "logs");
|
|
55
|
-
}
|
|
56
|
-
if (this.backup) {
|
|
57
|
-
this.backupDir = join(this.baseDir, "backups");
|
|
58
|
-
}
|
|
59
|
-
this.path = join(this.baseDir, "snowbase.json");
|
|
60
|
-
if (this.logsDir) this.logsPath = join(this.logsDir, "snowbase.log");
|
|
61
|
-
|
|
62
|
-
//Creating directories if they don't exist
|
|
63
|
-
mkdirSync(this.baseDir, { recursive: true });
|
|
64
|
-
if (this.backup && this.backupDir)
|
|
65
|
-
mkdirSync(this.backupDir, { recursive: true }); //only if the user wants backup
|
|
66
|
-
if (this.logging && this.logsDir)
|
|
67
|
-
mkdirSync(this.logsDir, { recursive: true }); //only if the user wants logging
|
|
68
|
-
|
|
69
|
-
//Creating the database file if it doesn't exist
|
|
70
|
-
if (!existsSync(this.path)) {
|
|
71
|
-
const initialData = this.encryption
|
|
72
|
-
? encrypt({}, this.secret)
|
|
73
|
-
: JSON.stringify({});
|
|
74
|
-
writeFileSync(this.path, initialData, "utf8");
|
|
75
|
-
/** @type {Record<string, any>|null} */
|
|
76
|
-
this._cache = {};
|
|
77
|
-
/** @type {import("node:fs").Stats} */
|
|
78
|
-
const stat = statSync(this.path);
|
|
79
|
-
/** @type {number} Last known modification time of the database file */
|
|
80
|
-
this._cacheMtime = stat.mtimeMs;
|
|
81
|
-
/** @type {number} Last known size of the database file */
|
|
82
|
-
this._cacheSize = stat.size;
|
|
83
|
-
} else {
|
|
84
|
-
/** @type {Record<string, any>|null} */
|
|
85
|
-
this._cache = null;
|
|
86
|
-
/** @type {number} */
|
|
87
|
-
this._cacheMtime = 0;
|
|
88
|
-
}
|
|
89
|
-
if (this.logging && this.logsPath && !existsSync(this.logsPath))
|
|
90
|
-
writeFileSync(this.logsPath, "", "utf8");
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* @returns {Record<string, any>} The data from the database
|
|
94
|
-
*/
|
|
95
|
-
read() {
|
|
96
|
-
// Validate cache freshness by comparing file mtime
|
|
97
|
-
const stat = statSync(this.path);
|
|
98
|
-
const currentMtime = stat.mtimeMs;
|
|
99
|
-
if (
|
|
100
|
-
this._cache !== null &&
|
|
101
|
-
currentMtime === this._cacheMtime &&
|
|
102
|
-
stat.size === this._cacheSize
|
|
103
|
-
) {
|
|
104
|
-
return this._cache;
|
|
105
|
-
}
|
|
106
|
-
const data = read(
|
|
107
|
-
this.path,
|
|
108
|
-
this._error.bind(this),
|
|
109
|
-
this.encryption,
|
|
110
|
-
this.secret || "",
|
|
111
|
-
);
|
|
112
|
-
if (data === null || Array.isArray(data) || typeof data !== "object") {
|
|
113
|
-
this._error("[INVALID_DATABASE] The database root must be an object.");
|
|
114
|
-
}
|
|
115
|
-
this._cache = data;
|
|
116
|
-
this._cacheMtime = currentMtime;
|
|
117
|
-
this._cacheSize = stat.size;
|
|
118
|
-
return data;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* @param {Object} data - Data to write
|
|
123
|
-
* @returns {void}
|
|
124
|
-
*/
|
|
125
|
-
write(data) {
|
|
126
|
-
write(
|
|
127
|
-
this.path,
|
|
128
|
-
data,
|
|
129
|
-
this._error.bind(this),
|
|
130
|
-
this.emit.bind(this),
|
|
131
|
-
this.encryption,
|
|
132
|
-
this.secret,
|
|
133
|
-
);
|
|
134
|
-
this.createBackup();
|
|
135
|
-
this._cache = data;
|
|
136
|
-
this._cacheMtime = statSync(this.path).mtimeMs;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
createBackup() {
|
|
140
|
-
if (!this.backup || !this.backupDir) return false;
|
|
141
|
-
let maxBackups = 20;
|
|
142
|
-
const backupDir = this.backupDir || join(this.baseDir, "backups");
|
|
143
|
-
mkdirSync(backupDir, { recursive: true });
|
|
144
|
-
//Getting all backup files
|
|
145
|
-
const files = readdirSync(backupDir)
|
|
146
|
-
.filter((f) => f.startsWith("backup_"))
|
|
147
|
-
.map((file) => {
|
|
148
|
-
const fullPath = join(backupDir, file);
|
|
149
|
-
const stats = statSync(fullPath);
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
path: fullPath,
|
|
153
|
-
time: stats.mtimeMs,
|
|
154
|
-
};
|
|
155
|
-
})
|
|
156
|
-
.sort((a, b) => a.time - b.time);
|
|
157
|
-
|
|
158
|
-
if (files.length - maxBackups > 0) {
|
|
159
|
-
for (let i = 0; i < files.length - maxBackups; i++) {
|
|
160
|
-
unlinkSync(files[i].path);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
//Creates a new backup
|
|
164
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
165
|
-
const backupFile = join(backupDir, `backup_${stamp}-${randomId5()}.json`);
|
|
166
|
-
copyFileSync(this.path, backupFile);
|
|
167
|
-
this._log("create backup", { backupFile });
|
|
168
|
-
return backupFile;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
writeFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
copyFileSync,
|
|
6
|
+
statSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
} from "fs";
|
|
10
|
+
import { read, write, randomId5 } from "../helpers/utils.js";
|
|
11
|
+
import { encrypt, decrypt } from "../helpers/crypto.js";
|
|
12
|
+
import { resolve, join } from "path";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @fileoverview FileStorage is a storage class that stores data in a file.
|
|
16
|
+
* @class FileStorage
|
|
17
|
+
*/
|
|
18
|
+
export default class FileStorage {
|
|
19
|
+
/**
|
|
20
|
+
* @description Creates a new instance of FileStorage.
|
|
21
|
+
* @param {Object} options - Options for the FileStorage.
|
|
22
|
+
* @param {string} options.baseDir - The base directory for the database.
|
|
23
|
+
* @param {boolean} [options.encryption] - Whether to encrypt the data.
|
|
24
|
+
* @param {string} [options.secret] - The secret key for encryption.
|
|
25
|
+
* @param {boolean} [options.backup] - Whether to backup the data.
|
|
26
|
+
* @param {boolean} [options.logging] - Whether to log the data.
|
|
27
|
+
* @param {function(any): never} options.error - Function to handle errors.
|
|
28
|
+
* @param {function(...any): void} options.emit - Function to emit events.
|
|
29
|
+
* @param {function(string, any): void} options.log - Function to log the data.
|
|
30
|
+
* @memberof FileStorage
|
|
31
|
+
* @constructor
|
|
32
|
+
*/
|
|
33
|
+
constructor({
|
|
34
|
+
baseDir,
|
|
35
|
+
encryption = false,
|
|
36
|
+
secret = "",
|
|
37
|
+
backup = false,
|
|
38
|
+
logging = false,
|
|
39
|
+
error,
|
|
40
|
+
emit,
|
|
41
|
+
log,
|
|
42
|
+
}) {
|
|
43
|
+
this.baseDir = resolve(baseDir) || resolve("./snowbase");
|
|
44
|
+
this.encryption = encryption;
|
|
45
|
+
this.secret = secret;
|
|
46
|
+
this.backup = backup;
|
|
47
|
+
this.logging = logging;
|
|
48
|
+
this._error = error;
|
|
49
|
+
this.emit = emit;
|
|
50
|
+
this._log = log;
|
|
51
|
+
this.path = join(this.baseDir, "snowbase.json");
|
|
52
|
+
|
|
53
|
+
if (this.logging) {
|
|
54
|
+
this.logsDir = join(this.baseDir, "logs");
|
|
55
|
+
}
|
|
56
|
+
if (this.backup) {
|
|
57
|
+
this.backupDir = join(this.baseDir, "backups");
|
|
58
|
+
}
|
|
59
|
+
this.path = join(this.baseDir, "snowbase.json");
|
|
60
|
+
if (this.logsDir) this.logsPath = join(this.logsDir, "snowbase.log");
|
|
61
|
+
|
|
62
|
+
//Creating directories if they don't exist
|
|
63
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
64
|
+
if (this.backup && this.backupDir)
|
|
65
|
+
mkdirSync(this.backupDir, { recursive: true }); //only if the user wants backup
|
|
66
|
+
if (this.logging && this.logsDir)
|
|
67
|
+
mkdirSync(this.logsDir, { recursive: true }); //only if the user wants logging
|
|
68
|
+
|
|
69
|
+
//Creating the database file if it doesn't exist
|
|
70
|
+
if (!existsSync(this.path)) {
|
|
71
|
+
const initialData = this.encryption
|
|
72
|
+
? encrypt({}, this.secret)
|
|
73
|
+
: JSON.stringify({});
|
|
74
|
+
writeFileSync(this.path, initialData, "utf8");
|
|
75
|
+
/** @type {Record<string, any>|null} */
|
|
76
|
+
this._cache = {};
|
|
77
|
+
/** @type {import("node:fs").Stats} */
|
|
78
|
+
const stat = statSync(this.path);
|
|
79
|
+
/** @type {number} Last known modification time of the database file */
|
|
80
|
+
this._cacheMtime = stat.mtimeMs;
|
|
81
|
+
/** @type {number} Last known size of the database file */
|
|
82
|
+
this._cacheSize = stat.size;
|
|
83
|
+
} else {
|
|
84
|
+
/** @type {Record<string, any>|null} */
|
|
85
|
+
this._cache = null;
|
|
86
|
+
/** @type {number} */
|
|
87
|
+
this._cacheMtime = 0;
|
|
88
|
+
}
|
|
89
|
+
if (this.logging && this.logsPath && !existsSync(this.logsPath))
|
|
90
|
+
writeFileSync(this.logsPath, "", "utf8");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* @returns {Record<string, any>} The data from the database
|
|
94
|
+
*/
|
|
95
|
+
read() {
|
|
96
|
+
// Validate cache freshness by comparing file mtime
|
|
97
|
+
const stat = statSync(this.path);
|
|
98
|
+
const currentMtime = stat.mtimeMs;
|
|
99
|
+
if (
|
|
100
|
+
this._cache !== null &&
|
|
101
|
+
currentMtime === this._cacheMtime &&
|
|
102
|
+
stat.size === this._cacheSize
|
|
103
|
+
) {
|
|
104
|
+
return this._cache;
|
|
105
|
+
}
|
|
106
|
+
const data = read(
|
|
107
|
+
this.path,
|
|
108
|
+
this._error.bind(this),
|
|
109
|
+
this.encryption,
|
|
110
|
+
this.secret || "",
|
|
111
|
+
);
|
|
112
|
+
if (data === null || Array.isArray(data) || typeof data !== "object") {
|
|
113
|
+
this._error("[INVALID_DATABASE] The database root must be an object.");
|
|
114
|
+
}
|
|
115
|
+
this._cache = data;
|
|
116
|
+
this._cacheMtime = currentMtime;
|
|
117
|
+
this._cacheSize = stat.size;
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {Object} data - Data to write
|
|
123
|
+
* @returns {void}
|
|
124
|
+
*/
|
|
125
|
+
write(data) {
|
|
126
|
+
write(
|
|
127
|
+
this.path,
|
|
128
|
+
data,
|
|
129
|
+
this._error.bind(this),
|
|
130
|
+
this.emit.bind(this),
|
|
131
|
+
this.encryption,
|
|
132
|
+
this.secret,
|
|
133
|
+
);
|
|
134
|
+
this.createBackup();
|
|
135
|
+
this._cache = data;
|
|
136
|
+
this._cacheMtime = statSync(this.path).mtimeMs;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
createBackup() {
|
|
140
|
+
if (!this.backup || !this.backupDir) return false;
|
|
141
|
+
let maxBackups = 20;
|
|
142
|
+
const backupDir = this.backupDir || join(this.baseDir, "backups");
|
|
143
|
+
mkdirSync(backupDir, { recursive: true });
|
|
144
|
+
//Getting all backup files
|
|
145
|
+
const files = readdirSync(backupDir)
|
|
146
|
+
.filter((f) => f.startsWith("backup_"))
|
|
147
|
+
.map((file) => {
|
|
148
|
+
const fullPath = join(backupDir, file);
|
|
149
|
+
const stats = statSync(fullPath);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
path: fullPath,
|
|
153
|
+
time: stats.mtimeMs,
|
|
154
|
+
};
|
|
155
|
+
})
|
|
156
|
+
.sort((a, b) => a.time - b.time);
|
|
157
|
+
|
|
158
|
+
if (files.length - maxBackups > 0) {
|
|
159
|
+
for (let i = 0; i < files.length - maxBackups; i++) {
|
|
160
|
+
unlinkSync(files[i].path);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//Creates a new backup
|
|
164
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
165
|
+
const backupFile = join(backupDir, `backup_${stamp}-${randomId5()}.json`);
|
|
166
|
+
copyFileSync(this.path, backupFile);
|
|
167
|
+
this._log("create backup", { backupFile });
|
|
168
|
+
return backupFile;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
import { writeFileSync, existsSync, mkdirSync, statSync } from "fs";
|
|
2
|
-
import { stat, mkdir, readdir, unlink, copyFile } from "node:fs/promises";
|
|
3
|
-
import { read, write, randomId5 } from "../helpers/utilsAsync.js";
|
|
4
|
-
import { encrypt, decrypt } from "../helpers/crypto.js";
|
|
5
|
-
import { resolve, join } from "path";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @fileoverview FileStorage is a storage class that stores data in a file.
|
|
9
|
-
* @class FileStorage
|
|
10
|
-
*/
|
|
11
|
-
export default class FileStorageAsync {
|
|
12
|
-
/**
|
|
13
|
-
* @description Creates a new instance of FileStorage.
|
|
14
|
-
* @param {Object} options - Options for the FileStorage.
|
|
15
|
-
* @param {string} options.baseDir - The base directory for the database.
|
|
16
|
-
* @param {boolean} [options.encryption] - Whether to encrypt the data.
|
|
17
|
-
* @param {string} [options.secret] - The secret key for encryption.
|
|
18
|
-
* @param {boolean} [options.backup] - Whether to backup the data.
|
|
19
|
-
* @param {boolean} [options.logging] - Whether to log the data.
|
|
20
|
-
* @param {*} options.error - Function to handle errors.
|
|
21
|
-
* @param {function(...any): void} options.emit - Function to emit events.
|
|
22
|
-
* @param {function(string, any): void} options.log - Function to log the data.
|
|
23
|
-
* @memberof FileStorage
|
|
24
|
-
* @constructor
|
|
25
|
-
*/
|
|
26
|
-
constructor({
|
|
27
|
-
baseDir,
|
|
28
|
-
encryption = false,
|
|
29
|
-
secret = "",
|
|
30
|
-
backup = false,
|
|
31
|
-
logging = false,
|
|
32
|
-
error,
|
|
33
|
-
emit,
|
|
34
|
-
log,
|
|
35
|
-
}) {
|
|
36
|
-
this.baseDir = resolve(baseDir) || resolve("./snowbase");
|
|
37
|
-
this.encryption = encryption;
|
|
38
|
-
this.secret = secret;
|
|
39
|
-
this.backup = backup;
|
|
40
|
-
this.logging = logging;
|
|
41
|
-
this._error = error;
|
|
42
|
-
this.emit = emit;
|
|
43
|
-
this._log = log;
|
|
44
|
-
this.path = join(this.baseDir, "snowbase.json");
|
|
45
|
-
|
|
46
|
-
if (this.logging) {
|
|
47
|
-
this.logsDir = join(this.baseDir, "logs");
|
|
48
|
-
}
|
|
49
|
-
if (this.backup) {
|
|
50
|
-
this.backupDir = join(this.baseDir, "backups");
|
|
51
|
-
}
|
|
52
|
-
this.path = join(this.baseDir, "snowbase.json");
|
|
53
|
-
if (this.logsDir) this.logsPath = join(this.logsDir, "snowbase.log");
|
|
54
|
-
|
|
55
|
-
//Creating directories if they don't exist
|
|
56
|
-
mkdirSync(this.baseDir, { recursive: true });
|
|
57
|
-
if (this.backup && this.backupDir)
|
|
58
|
-
mkdirSync(this.backupDir, { recursive: true }); //only if the user wants backup
|
|
59
|
-
if (this.logging && this.logsDir)
|
|
60
|
-
mkdirSync(this.logsDir, { recursive: true }); //only if the user wants logging
|
|
61
|
-
|
|
62
|
-
//Creating the database file if it doesn't exist
|
|
63
|
-
if (!existsSync(this.path)) {
|
|
64
|
-
const initialData = this.encryption
|
|
65
|
-
? encrypt({}, this.secret)
|
|
66
|
-
: JSON.stringify({});
|
|
67
|
-
writeFileSync(this.path, initialData, "utf8");
|
|
68
|
-
/** @type {Record<string, any>|null} */
|
|
69
|
-
this._cache = {};
|
|
70
|
-
/** @type {import("node:fs").Stats} */
|
|
71
|
-
const stat = statSync(this.path);
|
|
72
|
-
/** @type {number} Last known modification time of the database file */
|
|
73
|
-
this._cacheMtime = stat.mtimeMs;
|
|
74
|
-
/** @type {number} Last known size of the database file */
|
|
75
|
-
this._cacheSize = stat.size;
|
|
76
|
-
} else {
|
|
77
|
-
/** @type {Record<string, any>|null} */
|
|
78
|
-
this._cache = null;
|
|
79
|
-
/** @type {number} */
|
|
80
|
-
this._cacheMtime = 0;
|
|
81
|
-
}
|
|
82
|
-
if (this.logging && this.logsPath && !existsSync(this.logsPath))
|
|
83
|
-
writeFileSync(this.logsPath, "", "utf8");
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* @returns {Promise<Record<string, any>>} The data from the database
|
|
87
|
-
*/
|
|
88
|
-
async readAsync() {
|
|
89
|
-
// Validate cache freshness by comparing file mtime
|
|
90
|
-
const fileStat = await stat(this.path);
|
|
91
|
-
const currentMtime = fileStat.mtimeMs;
|
|
92
|
-
if (
|
|
93
|
-
this._cache !== null &&
|
|
94
|
-
currentMtime === this._cacheMtime &&
|
|
95
|
-
fileStat.size === this._cacheSize
|
|
96
|
-
) {
|
|
97
|
-
return this._cache;
|
|
98
|
-
}
|
|
99
|
-
const data = await read(
|
|
100
|
-
this.path,
|
|
101
|
-
this._error.bind(this),
|
|
102
|
-
this.encryption,
|
|
103
|
-
this.secret || "",
|
|
104
|
-
);
|
|
105
|
-
if (data === null || Array.isArray(data) || typeof data !== "object") {
|
|
106
|
-
this._error("[INVALID_DATABASE] The database root must be an object.");
|
|
107
|
-
}
|
|
108
|
-
this._cache = data;
|
|
109
|
-
this._cacheMtime = currentMtime;
|
|
110
|
-
this._cacheSize = fileStat.size;
|
|
111
|
-
return data;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* @param {Object} data - Data to write
|
|
116
|
-
* @returns {Promise<void>}
|
|
117
|
-
*/
|
|
118
|
-
async writeAsync(data) {
|
|
119
|
-
await write(
|
|
120
|
-
this.path,
|
|
121
|
-
data,
|
|
122
|
-
this._error.bind(this),
|
|
123
|
-
this.emit.bind(this),
|
|
124
|
-
this.encryption,
|
|
125
|
-
this.secret,
|
|
126
|
-
);
|
|
127
|
-
await this.createBackupAsync();
|
|
128
|
-
this._cache = data;
|
|
129
|
-
this._cacheMtime = (await stat(this.path)).mtimeMs;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async createBackupAsync() {
|
|
133
|
-
if (!this.backup || !this.backupDir) return;
|
|
134
|
-
const maxBackups = 20;
|
|
135
|
-
const backupDir = this.backupDir || join(this.baseDir, "backups");
|
|
136
|
-
await mkdir(backupDir, { recursive: true });
|
|
137
|
-
//Getting all backup files
|
|
138
|
-
const files = (
|
|
139
|
-
await Promise.all(
|
|
140
|
-
(await readdir(backupDir))
|
|
141
|
-
.filter((f) => f.startsWith("backup_"))
|
|
142
|
-
.map(async (file) => {
|
|
143
|
-
const fullPath = join(backupDir, file);
|
|
144
|
-
const stats = await stat(fullPath);
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
path: fullPath,
|
|
148
|
-
time: stats.mtimeMs,
|
|
149
|
-
};
|
|
150
|
-
}),
|
|
151
|
-
)
|
|
152
|
-
).sort((a, b) => a.time - b.time);
|
|
153
|
-
|
|
154
|
-
if (files.length - maxBackups > 0) {
|
|
155
|
-
for (let i = 0; i < files.length - maxBackups; i++) {
|
|
156
|
-
await unlink(files[i].path);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
//Creates a new backup
|
|
160
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
161
|
-
const backupFile = join(backupDir, `backup_${stamp}-${randomId5()}.json`);
|
|
162
|
-
|
|
163
|
-
await copyFile(this.path, backupFile);
|
|
164
|
-
this._log("create backup", { backupFile });
|
|
165
|
-
return backupFile;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
1
|
+
import { writeFileSync, existsSync, mkdirSync, statSync } from "fs";
|
|
2
|
+
import { stat, mkdir, readdir, unlink, copyFile } from "node:fs/promises";
|
|
3
|
+
import { read, write, randomId5 } from "../helpers/utilsAsync.js";
|
|
4
|
+
import { encrypt, decrypt } from "../helpers/crypto.js";
|
|
5
|
+
import { resolve, join } from "path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview FileStorage is a storage class that stores data in a file.
|
|
9
|
+
* @class FileStorage
|
|
10
|
+
*/
|
|
11
|
+
export default class FileStorageAsync {
|
|
12
|
+
/**
|
|
13
|
+
* @description Creates a new instance of FileStorage.
|
|
14
|
+
* @param {Object} options - Options for the FileStorage.
|
|
15
|
+
* @param {string} options.baseDir - The base directory for the database.
|
|
16
|
+
* @param {boolean} [options.encryption] - Whether to encrypt the data.
|
|
17
|
+
* @param {string} [options.secret] - The secret key for encryption.
|
|
18
|
+
* @param {boolean} [options.backup] - Whether to backup the data.
|
|
19
|
+
* @param {boolean} [options.logging] - Whether to log the data.
|
|
20
|
+
* @param {*} options.error - Function to handle errors.
|
|
21
|
+
* @param {function(...any): void} options.emit - Function to emit events.
|
|
22
|
+
* @param {function(string, any): void} options.log - Function to log the data.
|
|
23
|
+
* @memberof FileStorage
|
|
24
|
+
* @constructor
|
|
25
|
+
*/
|
|
26
|
+
constructor({
|
|
27
|
+
baseDir,
|
|
28
|
+
encryption = false,
|
|
29
|
+
secret = "",
|
|
30
|
+
backup = false,
|
|
31
|
+
logging = false,
|
|
32
|
+
error,
|
|
33
|
+
emit,
|
|
34
|
+
log,
|
|
35
|
+
}) {
|
|
36
|
+
this.baseDir = resolve(baseDir) || resolve("./snowbase");
|
|
37
|
+
this.encryption = encryption;
|
|
38
|
+
this.secret = secret;
|
|
39
|
+
this.backup = backup;
|
|
40
|
+
this.logging = logging;
|
|
41
|
+
this._error = error;
|
|
42
|
+
this.emit = emit;
|
|
43
|
+
this._log = log;
|
|
44
|
+
this.path = join(this.baseDir, "snowbase.json");
|
|
45
|
+
|
|
46
|
+
if (this.logging) {
|
|
47
|
+
this.logsDir = join(this.baseDir, "logs");
|
|
48
|
+
}
|
|
49
|
+
if (this.backup) {
|
|
50
|
+
this.backupDir = join(this.baseDir, "backups");
|
|
51
|
+
}
|
|
52
|
+
this.path = join(this.baseDir, "snowbase.json");
|
|
53
|
+
if (this.logsDir) this.logsPath = join(this.logsDir, "snowbase.log");
|
|
54
|
+
|
|
55
|
+
//Creating directories if they don't exist
|
|
56
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
57
|
+
if (this.backup && this.backupDir)
|
|
58
|
+
mkdirSync(this.backupDir, { recursive: true }); //only if the user wants backup
|
|
59
|
+
if (this.logging && this.logsDir)
|
|
60
|
+
mkdirSync(this.logsDir, { recursive: true }); //only if the user wants logging
|
|
61
|
+
|
|
62
|
+
//Creating the database file if it doesn't exist
|
|
63
|
+
if (!existsSync(this.path)) {
|
|
64
|
+
const initialData = this.encryption
|
|
65
|
+
? encrypt({}, this.secret)
|
|
66
|
+
: JSON.stringify({});
|
|
67
|
+
writeFileSync(this.path, initialData, "utf8");
|
|
68
|
+
/** @type {Record<string, any>|null} */
|
|
69
|
+
this._cache = {};
|
|
70
|
+
/** @type {import("node:fs").Stats} */
|
|
71
|
+
const stat = statSync(this.path);
|
|
72
|
+
/** @type {number} Last known modification time of the database file */
|
|
73
|
+
this._cacheMtime = stat.mtimeMs;
|
|
74
|
+
/** @type {number} Last known size of the database file */
|
|
75
|
+
this._cacheSize = stat.size;
|
|
76
|
+
} else {
|
|
77
|
+
/** @type {Record<string, any>|null} */
|
|
78
|
+
this._cache = null;
|
|
79
|
+
/** @type {number} */
|
|
80
|
+
this._cacheMtime = 0;
|
|
81
|
+
}
|
|
82
|
+
if (this.logging && this.logsPath && !existsSync(this.logsPath))
|
|
83
|
+
writeFileSync(this.logsPath, "", "utf8");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* @returns {Promise<Record<string, any>>} The data from the database
|
|
87
|
+
*/
|
|
88
|
+
async readAsync() {
|
|
89
|
+
// Validate cache freshness by comparing file mtime
|
|
90
|
+
const fileStat = await stat(this.path);
|
|
91
|
+
const currentMtime = fileStat.mtimeMs;
|
|
92
|
+
if (
|
|
93
|
+
this._cache !== null &&
|
|
94
|
+
currentMtime === this._cacheMtime &&
|
|
95
|
+
fileStat.size === this._cacheSize
|
|
96
|
+
) {
|
|
97
|
+
return this._cache;
|
|
98
|
+
}
|
|
99
|
+
const data = await read(
|
|
100
|
+
this.path,
|
|
101
|
+
this._error.bind(this),
|
|
102
|
+
this.encryption,
|
|
103
|
+
this.secret || "",
|
|
104
|
+
);
|
|
105
|
+
if (data === null || Array.isArray(data) || typeof data !== "object") {
|
|
106
|
+
this._error("[INVALID_DATABASE] The database root must be an object.");
|
|
107
|
+
}
|
|
108
|
+
this._cache = data;
|
|
109
|
+
this._cacheMtime = currentMtime;
|
|
110
|
+
this._cacheSize = fileStat.size;
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {Object} data - Data to write
|
|
116
|
+
* @returns {Promise<void>}
|
|
117
|
+
*/
|
|
118
|
+
async writeAsync(data) {
|
|
119
|
+
await write(
|
|
120
|
+
this.path,
|
|
121
|
+
data,
|
|
122
|
+
this._error.bind(this),
|
|
123
|
+
this.emit.bind(this),
|
|
124
|
+
this.encryption,
|
|
125
|
+
this.secret,
|
|
126
|
+
);
|
|
127
|
+
await this.createBackupAsync();
|
|
128
|
+
this._cache = data;
|
|
129
|
+
this._cacheMtime = (await stat(this.path)).mtimeMs;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async createBackupAsync() {
|
|
133
|
+
if (!this.backup || !this.backupDir) return;
|
|
134
|
+
const maxBackups = 20;
|
|
135
|
+
const backupDir = this.backupDir || join(this.baseDir, "backups");
|
|
136
|
+
await mkdir(backupDir, { recursive: true });
|
|
137
|
+
//Getting all backup files
|
|
138
|
+
const files = (
|
|
139
|
+
await Promise.all(
|
|
140
|
+
(await readdir(backupDir))
|
|
141
|
+
.filter((f) => f.startsWith("backup_"))
|
|
142
|
+
.map(async (file) => {
|
|
143
|
+
const fullPath = join(backupDir, file);
|
|
144
|
+
const stats = await stat(fullPath);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
path: fullPath,
|
|
148
|
+
time: stats.mtimeMs,
|
|
149
|
+
};
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
).sort((a, b) => a.time - b.time);
|
|
153
|
+
|
|
154
|
+
if (files.length - maxBackups > 0) {
|
|
155
|
+
for (let i = 0; i < files.length - maxBackups; i++) {
|
|
156
|
+
await unlink(files[i].path);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//Creates a new backup
|
|
160
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
161
|
+
const backupFile = join(backupDir, `backup_${stamp}-${randomId5()}.json`);
|
|
162
|
+
|
|
163
|
+
await copyFile(this.path, backupFile);
|
|
164
|
+
this._log("create backup", { backupFile });
|
|
165
|
+
return backupFile;
|
|
166
|
+
}
|
|
167
|
+
}
|