inibase 1.0.0-rc.4 → 1.0.0-rc.40
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/README.md +159 -91
- package/dist/config.d.ts +5 -0
- package/dist/config.js +5 -0
- package/dist/file.d.ts +176 -0
- package/dist/file.js +818 -0
- package/dist/file.thread.d.ts +1 -0
- package/dist/file.thread.js +5 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.js +1106 -0
- package/dist/index.thread.d.ts +1 -0
- package/dist/index.thread.js +6 -0
- package/dist/utils.d.ts +195 -0
- package/dist/utils.js +427 -0
- package/dist/utils.server.d.ts +103 -0
- package/dist/utils.server.js +122 -0
- package/package.json +69 -18
- package/file.ts +0 -327
- package/index.ts +0 -1280
- package/tsconfig.json +0 -6
- package/utils.ts +0 -110
package/dist/file.js
ADDED
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import { open, access, writeFile, readFile, constants as fsConstants, unlink, copyFile, appendFile, } from "node:fs/promises";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { Transform } from "node:stream";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
5
|
+
import { createGzip, createGunzip, gzip as gzipAsync, gunzip as gunzipAsync, } from "node:zlib";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { Worker } from "node:worker_threads";
|
|
9
|
+
import { detectFieldType, isArrayOfArrays, isNumber, isObject, isPassword, } from "./utils.js";
|
|
10
|
+
import { encodeID, comparePassword } from "./utils.server.js";
|
|
11
|
+
import Config from "./config.js";
|
|
12
|
+
const gzip = promisify(gzipAsync);
|
|
13
|
+
const gunzip = promisify(gunzipAsync);
|
|
14
|
+
export const lock = async (folderPath, prefix) => {
|
|
15
|
+
let lockFile, lockFilePath = join(folderPath, `${prefix ?? ""}.locked`);
|
|
16
|
+
try {
|
|
17
|
+
lockFile = await open(lockFilePath, "wx");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
catch ({ message }) {
|
|
21
|
+
if (message.split(":")[0] === "EEXIST")
|
|
22
|
+
return await new Promise((resolve, reject) => setTimeout(() => resolve(lock(folderPath, prefix)), 13));
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await lockFile?.close();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export const unlock = async (folderPath, prefix) => {
|
|
29
|
+
try {
|
|
30
|
+
await unlink(join(folderPath, `${prefix ?? ""}.locked`));
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
};
|
|
34
|
+
export const write = async (filePath, data, disableCompression = false) => {
|
|
35
|
+
await writeFile(filePath, Config.isCompressionEnabled && !disableCompression
|
|
36
|
+
? await gzip(String(data))
|
|
37
|
+
: String(data));
|
|
38
|
+
};
|
|
39
|
+
export const read = async (filePath, disableCompression = false) => {
|
|
40
|
+
return Config.isCompressionEnabled && !disableCompression
|
|
41
|
+
? (await gunzip(await readFile(filePath))).toString()
|
|
42
|
+
: (await readFile(filePath)).toString();
|
|
43
|
+
};
|
|
44
|
+
const _pipeline = async (rl, writeStream, transform) => {
|
|
45
|
+
if (Config.isCompressionEnabled)
|
|
46
|
+
await pipeline(rl, transform, createGzip(), writeStream);
|
|
47
|
+
else
|
|
48
|
+
await pipeline(rl, transform, writeStream);
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Creates a readline interface for a given file handle.
|
|
52
|
+
*
|
|
53
|
+
* @param fileHandle - The file handle from which to create a read stream.
|
|
54
|
+
* @returns A readline.Interface instance configured with the provided file stream.
|
|
55
|
+
*/
|
|
56
|
+
const readLineInternface = (fileHandle) => {
|
|
57
|
+
const [major, minor, patch] = process.versions.node.split(".").map(Number);
|
|
58
|
+
return major > 18 ||
|
|
59
|
+
(major === 18 && minor >= 11 && !Config.isCompressionEnabled)
|
|
60
|
+
? fileHandle.readLines()
|
|
61
|
+
: createInterface({
|
|
62
|
+
input: Config.isCompressionEnabled
|
|
63
|
+
? fileHandle.createReadStream().pipe(createGunzip())
|
|
64
|
+
: fileHandle.createReadStream(),
|
|
65
|
+
crlfDelay: Infinity,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Checks if a file or directory exists at the specified path.
|
|
70
|
+
*
|
|
71
|
+
* @param path - The path to the file or directory.
|
|
72
|
+
* @returns A Promise that resolves to true if the file/directory exists, false otherwise.
|
|
73
|
+
*/
|
|
74
|
+
export const isExists = async (path) => {
|
|
75
|
+
try {
|
|
76
|
+
await access(path, fsConstants.R_OK | fsConstants.W_OK);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const delimiters = [",", "|", "&", "$", "#", "@", "^", ":", "!", ";"];
|
|
84
|
+
/**
|
|
85
|
+
* Secures input by encoding/escaping characters.
|
|
86
|
+
*
|
|
87
|
+
* @param input - String, number, boolean, or null.
|
|
88
|
+
* @returns Encoded string for true/false, special characters in strings, or original input.
|
|
89
|
+
*/
|
|
90
|
+
const secureString = (input) => {
|
|
91
|
+
if (["true", "false"].includes(String(input)))
|
|
92
|
+
return input ? 1 : 0;
|
|
93
|
+
if (typeof input !== "string")
|
|
94
|
+
return input;
|
|
95
|
+
let decodedInput = null;
|
|
96
|
+
try {
|
|
97
|
+
decodedInput = decodeURIComponent(input);
|
|
98
|
+
}
|
|
99
|
+
catch (_error) {
|
|
100
|
+
decodedInput = decodeURIComponent(input.replace(/%(?![0-9][0-9a-fA-F]+)/g, ""));
|
|
101
|
+
}
|
|
102
|
+
const replacements = {
|
|
103
|
+
"<": "<",
|
|
104
|
+
">": ">",
|
|
105
|
+
",": "%2C",
|
|
106
|
+
"|": "%7C",
|
|
107
|
+
"&": "%26",
|
|
108
|
+
$: "%24",
|
|
109
|
+
"#": "%23",
|
|
110
|
+
"@": "%40",
|
|
111
|
+
"^": "%5E",
|
|
112
|
+
":": "%3A",
|
|
113
|
+
"!": "%21",
|
|
114
|
+
";": "%3B",
|
|
115
|
+
"\n": "\\n",
|
|
116
|
+
"\r": "\\r",
|
|
117
|
+
};
|
|
118
|
+
// Replace characters using a single regular expression.
|
|
119
|
+
return decodedInput.replace(/[<>,|&$#@^:!\n\r]/g, (match) => replacements[match]);
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Secures each element in an array or a single value using secureString.
|
|
123
|
+
*
|
|
124
|
+
* @param arr_str - An array or a single value of any type.
|
|
125
|
+
* @returns An array with each element secured, or a single secured value.
|
|
126
|
+
*/
|
|
127
|
+
const secureArray = (arr_str) => Array.isArray(arr_str) ? arr_str.map(secureArray) : secureString(arr_str);
|
|
128
|
+
/**
|
|
129
|
+
* Joins elements of a multidimensional array into a string.
|
|
130
|
+
*
|
|
131
|
+
* @param arr - A multidimensional array or a single level array.
|
|
132
|
+
* @param delimiter_index - Index for selecting delimiter, defaults to 0.
|
|
133
|
+
* @returns Joined string of array elements with appropriate delimiters.
|
|
134
|
+
*/
|
|
135
|
+
const joinMultidimensionalArray = (arr, delimiter_index = 0) => {
|
|
136
|
+
delimiter_index++;
|
|
137
|
+
if (isArrayOfArrays(arr))
|
|
138
|
+
arr = arr.map((ar) => joinMultidimensionalArray(ar, delimiter_index));
|
|
139
|
+
delimiter_index--;
|
|
140
|
+
return arr.join(delimiters[delimiter_index]);
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Encodes the input using 'secureString' and 'joinMultidimensionalArray' functions.
|
|
144
|
+
* If the input is an array, it is first secured and then joined into a string.
|
|
145
|
+
* If the input is a single value, it is directly secured.
|
|
146
|
+
*
|
|
147
|
+
* @param input - A value or array of values (string, number, boolean, null).
|
|
148
|
+
* @returns The secured and/or joined string.
|
|
149
|
+
*/
|
|
150
|
+
export const encode = (input) => {
|
|
151
|
+
// Use the optimized secureArray and joinMultidimensionalArray functions.
|
|
152
|
+
return Array.isArray(input)
|
|
153
|
+
? joinMultidimensionalArray(secureArray(input))
|
|
154
|
+
: secureString(input);
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Decodes each element in an array or a single value using unSecureString.
|
|
158
|
+
*
|
|
159
|
+
* @param arr_str - An array or a single value of any type.
|
|
160
|
+
* @returns An array with each element decoded, or a single decoded value.
|
|
161
|
+
*/
|
|
162
|
+
const unSecureArray = (arr_str) => Array.isArray(arr_str) ? arr_str.map(unSecureArray) : unSecureString(arr_str);
|
|
163
|
+
/**
|
|
164
|
+
* Reverses the encoding done by 'secureString'. Replaces encoded characters with their original symbols.
|
|
165
|
+
*
|
|
166
|
+
* @param input - Encoded string.
|
|
167
|
+
* @returns Decoded string or null if input is empty.
|
|
168
|
+
*/
|
|
169
|
+
const unSecureString = (input) => {
|
|
170
|
+
// Define a mapping of encoded characters to their original symbols.
|
|
171
|
+
const replacements = {
|
|
172
|
+
"<": "<",
|
|
173
|
+
"%2C": ",",
|
|
174
|
+
"%7C": "|",
|
|
175
|
+
"%26": "&",
|
|
176
|
+
"%24": "$",
|
|
177
|
+
"%23": "#",
|
|
178
|
+
"%40": "@",
|
|
179
|
+
"%5E": "^",
|
|
180
|
+
"%3A": ":",
|
|
181
|
+
"%21": "!",
|
|
182
|
+
"%3B": ";",
|
|
183
|
+
"\\n": "\n",
|
|
184
|
+
"\\r": "\r",
|
|
185
|
+
};
|
|
186
|
+
// Replace encoded characters with their original symbols using the defined mapping.
|
|
187
|
+
const decodedString = input.replace(/%(2C|7C|26|24|23|40|5E|3A|21|3B|\\n|\\r)/g, (match) => replacements[match]) || null;
|
|
188
|
+
if (decodedString === null)
|
|
189
|
+
return null;
|
|
190
|
+
return isNumber(decodedString) ? Number(decodedString) : decodedString;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Reverses the process of 'joinMultidimensionalArray', splitting a string back into a multidimensional array.
|
|
194
|
+
* It identifies delimiters used in the joined string and applies them recursively to reconstruct the original array structure.
|
|
195
|
+
*
|
|
196
|
+
* @param joinedString - A string, array, or multidimensional array.
|
|
197
|
+
* @returns Original array structure before joining, or the input if no delimiters are found.
|
|
198
|
+
*/
|
|
199
|
+
const reverseJoinMultidimensionalArray = (joinedString) => {
|
|
200
|
+
// Helper function for recursive array splitting based on delimiters.
|
|
201
|
+
const reverseJoinMultidimensionalArrayHelper = (arr, delimiter) => Array.isArray(arr)
|
|
202
|
+
? arr.map((ar) => reverseJoinMultidimensionalArrayHelper(ar, delimiter))
|
|
203
|
+
: arr.split(delimiter);
|
|
204
|
+
// Identify available delimiters in the input.
|
|
205
|
+
const availableDelimiters = delimiters.filter((delimiter) => joinedString.includes(delimiter));
|
|
206
|
+
// Apply delimiters recursively to reconstruct the original array structure.
|
|
207
|
+
return availableDelimiters.reduce((acc, delimiter) => reverseJoinMultidimensionalArrayHelper(acc, delimiter), joinedString);
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Decodes a value based on specified field types and optional secret key.
|
|
211
|
+
* Handles different data types and structures, including nested arrays.
|
|
212
|
+
*
|
|
213
|
+
* @param value - The value to be decoded, can be string, number, or array.
|
|
214
|
+
* @param fieldType - Optional type of the field to guide decoding (e.g., 'number', 'boolean').
|
|
215
|
+
* @param fieldChildrenType - Optional type for children elements, used for arrays.
|
|
216
|
+
* @param secretKey - Optional secret key for decoding, can be string or Buffer.
|
|
217
|
+
* @returns Decoded value, transformed according to the specified field type(s).
|
|
218
|
+
*/
|
|
219
|
+
const decodeHelper = (value, fieldType, fieldChildrenType, secretKey) => {
|
|
220
|
+
if (Array.isArray(value) && fieldType !== "array")
|
|
221
|
+
return value.map((v) => decodeHelper(v, fieldType, fieldChildrenType, secretKey));
|
|
222
|
+
switch (fieldType) {
|
|
223
|
+
case "json":
|
|
224
|
+
return JSON.parse(value);
|
|
225
|
+
case "number":
|
|
226
|
+
return isNumber(value) ? Number(value) : null;
|
|
227
|
+
case "boolean":
|
|
228
|
+
return typeof value === "string" ? value === "true" : Boolean(value);
|
|
229
|
+
case "array":
|
|
230
|
+
if (!Array.isArray(value))
|
|
231
|
+
return [value];
|
|
232
|
+
if (fieldChildrenType)
|
|
233
|
+
return fieldChildrenType
|
|
234
|
+
? value.map((v) => decode(v, Array.isArray(fieldChildrenType)
|
|
235
|
+
? detectFieldType(v, fieldChildrenType)
|
|
236
|
+
: fieldChildrenType, undefined, secretKey))
|
|
237
|
+
: value;
|
|
238
|
+
case "table":
|
|
239
|
+
case "id":
|
|
240
|
+
return isNumber(value) && secretKey
|
|
241
|
+
? encodeID(value, secretKey)
|
|
242
|
+
: value;
|
|
243
|
+
default:
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* Decodes the input based on the specified field type(s) and an optional secret key.
|
|
249
|
+
* Handles different formats of input, including strings, numbers, and their array representations.
|
|
250
|
+
*
|
|
251
|
+
* @param input - The input to be decoded, can be a string, number, or null.
|
|
252
|
+
* @param fieldType - Optional type of the field to guide decoding (e.g., 'number', 'boolean').
|
|
253
|
+
* @param fieldChildrenType - Optional type for child elements in array inputs.
|
|
254
|
+
* @param secretKey - Optional secret key for decoding, can be a string or Buffer.
|
|
255
|
+
* @returns Decoded value as a string, number, boolean, or array of these, or null if no fieldType or input is null/empty.
|
|
256
|
+
*/
|
|
257
|
+
export const decode = (input, fieldType, fieldChildrenType, secretKey) => {
|
|
258
|
+
if (!fieldType)
|
|
259
|
+
return null;
|
|
260
|
+
if (input === null || input === "")
|
|
261
|
+
return null;
|
|
262
|
+
// Detect the fieldType based on the input and the provided array of possible types.
|
|
263
|
+
if (Array.isArray(fieldType))
|
|
264
|
+
fieldType = detectFieldType(String(input), fieldType);
|
|
265
|
+
// Decode the input using the decodeHelper function.
|
|
266
|
+
return decodeHelper(typeof input === "string"
|
|
267
|
+
? input.includes(",")
|
|
268
|
+
? unSecureArray(reverseJoinMultidimensionalArray(input))
|
|
269
|
+
: unSecureString(input)
|
|
270
|
+
: input, fieldType, fieldChildrenType, secretKey);
|
|
271
|
+
};
|
|
272
|
+
export async function get(filePath, lineNumbers, fieldType, fieldChildrenType, secretKey, readWholeFile = false) {
|
|
273
|
+
let fileHandle, rl;
|
|
274
|
+
try {
|
|
275
|
+
fileHandle = await open(filePath, "r");
|
|
276
|
+
rl = readLineInternface(fileHandle);
|
|
277
|
+
let lines = {}, linesCount = 0;
|
|
278
|
+
if (!lineNumbers) {
|
|
279
|
+
for await (const line of rl)
|
|
280
|
+
linesCount++,
|
|
281
|
+
(lines[linesCount] = decode(line, fieldType, fieldChildrenType, secretKey));
|
|
282
|
+
}
|
|
283
|
+
else if (lineNumbers === -1) {
|
|
284
|
+
let lastLine = null;
|
|
285
|
+
for await (const line of rl)
|
|
286
|
+
linesCount++, (lastLine = line);
|
|
287
|
+
if (lastLine)
|
|
288
|
+
lines[linesCount] = decode(lastLine, fieldType, fieldChildrenType, secretKey);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
|
|
292
|
+
for await (const line of rl) {
|
|
293
|
+
linesCount++;
|
|
294
|
+
if (!lineNumbersArray.has(linesCount))
|
|
295
|
+
continue;
|
|
296
|
+
lines[linesCount] = decode(line, fieldType, fieldChildrenType, secretKey);
|
|
297
|
+
lineNumbersArray.delete(linesCount);
|
|
298
|
+
if (!lineNumbersArray.size && !readWholeFile)
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return readWholeFile ? [lines, linesCount] : lines;
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
// Ensure that file handles are closed, even if an error occurred
|
|
306
|
+
rl?.close();
|
|
307
|
+
await fileHandle?.close();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Asynchronously replaces specific lines in a file based on the provided replacements map or string.
|
|
312
|
+
*
|
|
313
|
+
* @param filePath - Path of the file to modify.
|
|
314
|
+
* @param replacements - Map of line numbers to replacement values, or a single replacement value for all lines.
|
|
315
|
+
* Can be a string, number, boolean, null, array of these types, or a Record/Map of line numbers to these types.
|
|
316
|
+
* @returns Promise<string[]>
|
|
317
|
+
*
|
|
318
|
+
* Note: If the file doesn't exist and replacements is an object, it creates a new file with the specified replacements.
|
|
319
|
+
*/
|
|
320
|
+
export const replace = async (filePath, replacements) => {
|
|
321
|
+
const fileTempPath = filePath.replace(/([^/]+)\/?$/, `.tmp/$1`);
|
|
322
|
+
if (await isExists(filePath)) {
|
|
323
|
+
let fileHandle, fileTempHandle, rl;
|
|
324
|
+
try {
|
|
325
|
+
let linesCount = 0;
|
|
326
|
+
fileHandle = await open(filePath, "r");
|
|
327
|
+
fileTempHandle = await open(fileTempPath, "w");
|
|
328
|
+
rl = readLineInternface(fileHandle);
|
|
329
|
+
await _pipeline(rl, fileTempHandle.createWriteStream(), new Transform({
|
|
330
|
+
transform(line, encoding, callback) {
|
|
331
|
+
linesCount++;
|
|
332
|
+
const replacement = isObject(replacements)
|
|
333
|
+
? replacements.hasOwnProperty(linesCount)
|
|
334
|
+
? replacements[linesCount]
|
|
335
|
+
: line
|
|
336
|
+
: replacements;
|
|
337
|
+
return callback(null, replacement + "\n");
|
|
338
|
+
},
|
|
339
|
+
}));
|
|
340
|
+
return [fileTempPath, filePath];
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
// Ensure that file handles are closed, even if an error occurred
|
|
344
|
+
rl?.close();
|
|
345
|
+
await fileHandle?.close();
|
|
346
|
+
await fileTempHandle?.close();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else if (isObject(replacements)) {
|
|
350
|
+
let replacementsKeys = Object.keys(replacements)
|
|
351
|
+
.map(Number)
|
|
352
|
+
.toSorted((a, b) => a - b);
|
|
353
|
+
await write(fileTempPath, "\n".repeat(replacementsKeys[0] - 1) +
|
|
354
|
+
replacementsKeys
|
|
355
|
+
.map((lineNumber, index) => index === 0 || lineNumber - replacementsKeys[index - 1] - 1 === 0
|
|
356
|
+
? replacements[lineNumber]
|
|
357
|
+
: "\n".repeat(lineNumber - replacementsKeys[index - 1] - 1) +
|
|
358
|
+
replacements[lineNumber])
|
|
359
|
+
.join("\n") +
|
|
360
|
+
"\n");
|
|
361
|
+
return [fileTempPath, filePath];
|
|
362
|
+
}
|
|
363
|
+
return [];
|
|
364
|
+
};
|
|
365
|
+
/**
|
|
366
|
+
* Asynchronously appends data to the beginning of a file.
|
|
367
|
+
*
|
|
368
|
+
* @param filePath - Path of the file to append to.
|
|
369
|
+
* @param data - Data to append. Can be a string, number, or an array of strings/numbers.
|
|
370
|
+
* @returns Promise<string[]>. Modifies the file by appending data.
|
|
371
|
+
*
|
|
372
|
+
*/
|
|
373
|
+
export const append = async (filePath, data) => {
|
|
374
|
+
const fileTempPath = filePath.replace(/([^/]+)\/?$/, `.tmp/$1`);
|
|
375
|
+
if (await isExists(filePath)) {
|
|
376
|
+
if (!Config.isReverseEnabled && !Config.isCompressionEnabled) {
|
|
377
|
+
await copyFile(filePath, fileTempPath);
|
|
378
|
+
await appendFile(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
let fileHandle, fileTempHandle, rl;
|
|
382
|
+
try {
|
|
383
|
+
fileHandle = await open(filePath, "r");
|
|
384
|
+
fileTempHandle = await open(fileTempPath, "w");
|
|
385
|
+
rl = readLineInternface(fileHandle);
|
|
386
|
+
let isAppended = false;
|
|
387
|
+
await _pipeline(rl, fileTempHandle.createWriteStream(), new Transform({
|
|
388
|
+
transform(line, encoding, callback) {
|
|
389
|
+
if (!isAppended) {
|
|
390
|
+
isAppended = true;
|
|
391
|
+
return callback(null, `${Array.isArray(data) ? data.join("\n") : data}\n` +
|
|
392
|
+
(line.length ? `${line}\n` : ""));
|
|
393
|
+
}
|
|
394
|
+
else
|
|
395
|
+
return callback(null, `${line}\n`);
|
|
396
|
+
},
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
// Ensure that file handles are closed, even if an error occurred
|
|
401
|
+
rl?.close();
|
|
402
|
+
await fileHandle?.close();
|
|
403
|
+
await fileTempHandle?.close();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else
|
|
408
|
+
await write(fileTempPath, `${Array.isArray(data) ? data.join("\n") : data}\n`, undefined);
|
|
409
|
+
return [fileTempPath, filePath];
|
|
410
|
+
};
|
|
411
|
+
/**
|
|
412
|
+
* Asynchronously removes specified lines from a file.
|
|
413
|
+
*
|
|
414
|
+
* @param filePath - Path of the file from which lines are to be removed.
|
|
415
|
+
* @param linesToDelete - A single line number or an array of line numbers to be deleted.
|
|
416
|
+
* @returns Promise<string[]>. Modifies the file by removing specified lines.
|
|
417
|
+
*
|
|
418
|
+
* Note: Creates a temporary file during the process and replaces the original file with it after removing lines.
|
|
419
|
+
*/
|
|
420
|
+
export const remove = async (filePath, linesToDelete) => {
|
|
421
|
+
let linesCount = 0, deletedCount = 0;
|
|
422
|
+
const fileHandle = await open(filePath, "r"), fileTempPath = filePath.replace(/([^/]+)\/?$/, `.tmp/$1`), fileTempHandle = await open(fileTempPath, "w"), linesToDeleteArray = new Set(Array.isArray(linesToDelete)
|
|
423
|
+
? linesToDelete.map(Number)
|
|
424
|
+
: [Number(linesToDelete)]), rl = readLineInternface(fileHandle);
|
|
425
|
+
await _pipeline(rl, fileTempHandle.createWriteStream(), new Transform({
|
|
426
|
+
transform(line, encoding, callback) {
|
|
427
|
+
linesCount++;
|
|
428
|
+
if (linesToDeleteArray.has(linesCount)) {
|
|
429
|
+
deletedCount++;
|
|
430
|
+
return callback();
|
|
431
|
+
}
|
|
432
|
+
else
|
|
433
|
+
return callback(null, `${line}\n`);
|
|
434
|
+
},
|
|
435
|
+
final(callback) {
|
|
436
|
+
if (deletedCount === linesCount)
|
|
437
|
+
this.push("\n");
|
|
438
|
+
return callback();
|
|
439
|
+
},
|
|
440
|
+
}));
|
|
441
|
+
await fileTempHandle.close();
|
|
442
|
+
await fileHandle.close();
|
|
443
|
+
return [fileTempPath, filePath];
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* Evaluates a comparison between two values based on a specified operator and field types.
|
|
447
|
+
*
|
|
448
|
+
* @param operator - The comparison operator (e.g., '=', '!=', '>', '<', '>=', '<=', '[]', '![]', '*', '!*').
|
|
449
|
+
* @param originalValue - The value to compare, can be a single value or an array of values.
|
|
450
|
+
* @param comparedAtValue - The value or values to compare against.
|
|
451
|
+
* @param fieldType - Optional type of the field to guide comparison (e.g., 'password', 'boolean').
|
|
452
|
+
* @param fieldChildrenType - Optional type for child elements in array inputs.
|
|
453
|
+
* @returns boolean - Result of the comparison operation.
|
|
454
|
+
*
|
|
455
|
+
* Note: Handles various data types and comparison logic, including special handling for passwords and regex patterns.
|
|
456
|
+
*/
|
|
457
|
+
const handleComparisonOperator = (operator, originalValue, comparedAtValue, fieldType, fieldChildrenType) => {
|
|
458
|
+
// Determine the field type if it's an array of potential types.
|
|
459
|
+
if (Array.isArray(fieldType)) {
|
|
460
|
+
fieldType = detectFieldType(String(originalValue), fieldType);
|
|
461
|
+
}
|
|
462
|
+
// Handle comparisons involving arrays.
|
|
463
|
+
if (Array.isArray(comparedAtValue) && !["[]", "![]"].includes(operator)) {
|
|
464
|
+
return comparedAtValue.some((comparedAtValueSingle) => handleComparisonOperator(operator, originalValue, comparedAtValueSingle, fieldType));
|
|
465
|
+
}
|
|
466
|
+
// Switch statement for different comparison operators.
|
|
467
|
+
switch (operator) {
|
|
468
|
+
// Equal (Case Insensitive for strings, specific handling for passwords and booleans).
|
|
469
|
+
case "=":
|
|
470
|
+
return isEqual(originalValue, comparedAtValue, fieldType);
|
|
471
|
+
// Not Equal.
|
|
472
|
+
case "!=":
|
|
473
|
+
return !isEqual(originalValue, comparedAtValue, fieldType);
|
|
474
|
+
// Greater Than.
|
|
475
|
+
case ">":
|
|
476
|
+
return (originalValue !== null &&
|
|
477
|
+
comparedAtValue !== null &&
|
|
478
|
+
originalValue > comparedAtValue);
|
|
479
|
+
// Less Than.
|
|
480
|
+
case "<":
|
|
481
|
+
return (originalValue !== null &&
|
|
482
|
+
comparedAtValue !== null &&
|
|
483
|
+
originalValue < comparedAtValue);
|
|
484
|
+
// Greater Than or Equal.
|
|
485
|
+
case ">=":
|
|
486
|
+
return (originalValue !== null &&
|
|
487
|
+
comparedAtValue !== null &&
|
|
488
|
+
originalValue >= comparedAtValue);
|
|
489
|
+
// Less Than or Equal.
|
|
490
|
+
case "<=":
|
|
491
|
+
return (originalValue !== null &&
|
|
492
|
+
comparedAtValue !== null &&
|
|
493
|
+
originalValue <= comparedAtValue);
|
|
494
|
+
// Array Contains (equality check for arrays).
|
|
495
|
+
case "[]":
|
|
496
|
+
return isArrayEqual(originalValue, comparedAtValue);
|
|
497
|
+
// Array Does Not Contain.
|
|
498
|
+
case "![]":
|
|
499
|
+
return !isArrayEqual(originalValue, comparedAtValue);
|
|
500
|
+
// Wildcard Match (using regex pattern).
|
|
501
|
+
case "*":
|
|
502
|
+
return isWildcardMatch(originalValue, comparedAtValue);
|
|
503
|
+
// Not Wildcard Match.
|
|
504
|
+
case "!*":
|
|
505
|
+
return !isWildcardMatch(originalValue, comparedAtValue);
|
|
506
|
+
// Unsupported operator.
|
|
507
|
+
default:
|
|
508
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
/**
|
|
512
|
+
* Helper function to check equality based on the field type.
|
|
513
|
+
*
|
|
514
|
+
* @param originalValue - The original value.
|
|
515
|
+
* @param comparedAtValue - The value to compare against.
|
|
516
|
+
* @param fieldType - Type of the field.
|
|
517
|
+
* @returns boolean - Result of the equality check.
|
|
518
|
+
*/
|
|
519
|
+
const isEqual = (originalValue, comparedAtValue, fieldType) => {
|
|
520
|
+
// Switch based on the field type for specific handling.
|
|
521
|
+
switch (fieldType) {
|
|
522
|
+
// Password comparison.
|
|
523
|
+
case "password":
|
|
524
|
+
return isPassword(originalValue) && typeof comparedAtValue === "string"
|
|
525
|
+
? comparePassword(originalValue, comparedAtValue)
|
|
526
|
+
: false;
|
|
527
|
+
// Boolean comparison.
|
|
528
|
+
case "boolean":
|
|
529
|
+
return Number(originalValue) === Number(comparedAtValue);
|
|
530
|
+
// Default comparison.
|
|
531
|
+
default:
|
|
532
|
+
return originalValue === comparedAtValue;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
/**
|
|
536
|
+
* Helper function to check array equality.
|
|
537
|
+
*
|
|
538
|
+
* @param originalValue - The original value.
|
|
539
|
+
* @param comparedAtValue - The value to compare against.
|
|
540
|
+
* @returns boolean - Result of the array equality check.
|
|
541
|
+
*/
|
|
542
|
+
const isArrayEqual = (originalValue, comparedAtValue) => {
|
|
543
|
+
return ((Array.isArray(originalValue) &&
|
|
544
|
+
Array.isArray(comparedAtValue) &&
|
|
545
|
+
originalValue.some((v) => comparedAtValue.includes(v))) ||
|
|
546
|
+
(Array.isArray(originalValue) &&
|
|
547
|
+
!Array.isArray(comparedAtValue) &&
|
|
548
|
+
originalValue.includes(comparedAtValue)) ||
|
|
549
|
+
(!Array.isArray(originalValue) &&
|
|
550
|
+
Array.isArray(comparedAtValue) &&
|
|
551
|
+
comparedAtValue.includes(originalValue)) ||
|
|
552
|
+
(!Array.isArray(originalValue) &&
|
|
553
|
+
!Array.isArray(comparedAtValue) &&
|
|
554
|
+
comparedAtValue === originalValue));
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Helper function to check wildcard pattern matching using regex.
|
|
558
|
+
*
|
|
559
|
+
* @param originalValue - The original value.
|
|
560
|
+
* @param comparedAtValue - The value with wildcard pattern.
|
|
561
|
+
* @returns boolean - Result of the wildcard pattern matching.
|
|
562
|
+
*/
|
|
563
|
+
const isWildcardMatch = (originalValue, comparedAtValue) => {
|
|
564
|
+
const wildcardPattern = `^${(String(comparedAtValue).includes("%")
|
|
565
|
+
? String(comparedAtValue)
|
|
566
|
+
: "%" + String(comparedAtValue) + "%").replace(/%/g, ".*")}$`;
|
|
567
|
+
return new RegExp(wildcardPattern, "i").test(String(originalValue));
|
|
568
|
+
};
|
|
569
|
+
/**
|
|
570
|
+
* Asynchronously searches a file for lines matching specified criteria, using comparison and logical operators.
|
|
571
|
+
*
|
|
572
|
+
* @param filePath - Path of the file to search.
|
|
573
|
+
* @param operator - Comparison operator(s) for evaluation (e.g., '=', '!=', '>', '<').
|
|
574
|
+
* @param comparedAtValue - Value(s) to compare each line against.
|
|
575
|
+
* @param logicalOperator - Optional logical operator ('and' or 'or') for combining multiple comparisons.
|
|
576
|
+
* @param fieldType - Optional type of the field to guide comparison.
|
|
577
|
+
* @param fieldChildrenType - Optional type for child elements in array inputs.
|
|
578
|
+
* @param limit - Optional limit on the number of results to return.
|
|
579
|
+
* @param offset - Optional offset to start returning results from.
|
|
580
|
+
* @param readWholeFile - Flag to indicate whether to continue reading the file after reaching the limit.
|
|
581
|
+
* @param secretKey - Optional secret key for decoding, can be a string or Buffer.
|
|
582
|
+
* @returns Promise resolving to a tuple:
|
|
583
|
+
* 1. Record of line numbers and their content that match the criteria or null if none.
|
|
584
|
+
* 2. The count of found items or processed items based on the 'readWholeFile' flag.
|
|
585
|
+
*
|
|
586
|
+
* Note: Decodes each line for comparison and can handle complex queries with multiple conditions.
|
|
587
|
+
*/
|
|
588
|
+
export const search = async (filePath, operator, comparedAtValue, logicalOperator, fieldType, fieldChildrenType, limit, offset, readWholeFile, secretKey) => {
|
|
589
|
+
// Initialize a Map to store the matching lines with their line numbers.
|
|
590
|
+
const matchingLines = {};
|
|
591
|
+
// Initialize counters for line number, found items, and processed items.
|
|
592
|
+
let linesCount = 0, foundItems = 0, linesNumbers = new Set();
|
|
593
|
+
let fileHandle, rl;
|
|
594
|
+
try {
|
|
595
|
+
// Open the file for reading.
|
|
596
|
+
fileHandle = await open(filePath, "r");
|
|
597
|
+
// Create a Readline interface to read the file line by line.
|
|
598
|
+
rl = readLineInternface(fileHandle);
|
|
599
|
+
// Iterate through each line in the file.
|
|
600
|
+
for await (const line of rl) {
|
|
601
|
+
// Increment the line count for each line.
|
|
602
|
+
linesCount++;
|
|
603
|
+
// Decode the line for comparison.
|
|
604
|
+
const decodedLine = decode(line, fieldType, fieldChildrenType, secretKey);
|
|
605
|
+
// Check if the line meets the specified conditions based on comparison and logical operators.
|
|
606
|
+
const meetsConditions = (Array.isArray(operator) &&
|
|
607
|
+
Array.isArray(comparedAtValue) &&
|
|
608
|
+
((logicalOperator === "or" &&
|
|
609
|
+
operator.some((single_operator, index) => handleComparisonOperator(single_operator, decodedLine, comparedAtValue[index], fieldType))) ||
|
|
610
|
+
operator.every((single_operator, index) => handleComparisonOperator(single_operator, decodedLine, comparedAtValue[index], fieldType)))) ||
|
|
611
|
+
(!Array.isArray(operator) &&
|
|
612
|
+
handleComparisonOperator(operator, decodedLine, comparedAtValue, fieldType));
|
|
613
|
+
// If the line meets the conditions, process it.
|
|
614
|
+
if (meetsConditions) {
|
|
615
|
+
// Increment the found items counter.
|
|
616
|
+
foundItems++;
|
|
617
|
+
linesNumbers.add(linesCount);
|
|
618
|
+
// Check if the line should be skipped based on the offset.
|
|
619
|
+
if (offset && foundItems < offset)
|
|
620
|
+
continue;
|
|
621
|
+
// Check if the limit has been reached.
|
|
622
|
+
if (limit && foundItems > limit)
|
|
623
|
+
if (readWholeFile)
|
|
624
|
+
continue;
|
|
625
|
+
else
|
|
626
|
+
break;
|
|
627
|
+
// Store the decoded line in the result object.
|
|
628
|
+
matchingLines[linesCount] = decodedLine;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Convert the Map to an object using Object.fromEntries and return the result.
|
|
632
|
+
return foundItems
|
|
633
|
+
? [
|
|
634
|
+
matchingLines,
|
|
635
|
+
readWholeFile ? foundItems : foundItems - 1,
|
|
636
|
+
linesNumbers.size ? linesNumbers : null,
|
|
637
|
+
]
|
|
638
|
+
: [null, 0, null];
|
|
639
|
+
}
|
|
640
|
+
finally {
|
|
641
|
+
// Close the file handle in the finally block to ensure it is closed even if an error occurs.
|
|
642
|
+
rl?.close();
|
|
643
|
+
await fileHandle?.close();
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
/**
|
|
647
|
+
* Asynchronously counts the number of lines in a file.
|
|
648
|
+
*
|
|
649
|
+
* @param filePath - Path of the file to count lines in.
|
|
650
|
+
* @returns Promise<number>. The number of lines in the file.
|
|
651
|
+
*
|
|
652
|
+
* Note: Reads through the file line by line to count the total number of lines.
|
|
653
|
+
*/
|
|
654
|
+
export const count = async (filePath) => {
|
|
655
|
+
// return Number((await exec(`wc -l < ${filePath}`)).stdout.trim());
|
|
656
|
+
let linesCount = 0;
|
|
657
|
+
if (await isExists(filePath)) {
|
|
658
|
+
let fileHandle, rl;
|
|
659
|
+
try {
|
|
660
|
+
(fileHandle = await open(filePath, "r")),
|
|
661
|
+
(rl = readLineInternface(fileHandle));
|
|
662
|
+
for await (const line of rl)
|
|
663
|
+
linesCount++;
|
|
664
|
+
}
|
|
665
|
+
finally {
|
|
666
|
+
rl?.close();
|
|
667
|
+
await fileHandle?.close();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return linesCount;
|
|
671
|
+
};
|
|
672
|
+
/**
|
|
673
|
+
* Asynchronously calculates the sum of numerical values from specified lines in a file.
|
|
674
|
+
*
|
|
675
|
+
* @param filePath - Path of the file to read.
|
|
676
|
+
* @param lineNumbers - Optional specific line number(s) to include in the sum. If not provided, sums all lines.
|
|
677
|
+
* @returns Promise<number>. The sum of numerical values from the specified lines.
|
|
678
|
+
*
|
|
679
|
+
* Note: Decodes each line as a number using the 'decode' function. Non-numeric lines contribute 0 to the sum.
|
|
680
|
+
*/
|
|
681
|
+
export const sum = async (filePath, lineNumbers) => {
|
|
682
|
+
let sum = 0;
|
|
683
|
+
const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
|
|
684
|
+
if (lineNumbers) {
|
|
685
|
+
let linesCount = 0;
|
|
686
|
+
let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
|
|
687
|
+
for await (const line of rl) {
|
|
688
|
+
linesCount++;
|
|
689
|
+
if (!lineNumbersArray.has(linesCount))
|
|
690
|
+
continue;
|
|
691
|
+
sum += +(decode(line, "number") ?? 0);
|
|
692
|
+
lineNumbersArray.delete(linesCount);
|
|
693
|
+
if (!lineNumbersArray.size)
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
else
|
|
698
|
+
for await (const line of rl)
|
|
699
|
+
sum += +(decode(line, "number") ?? 0);
|
|
700
|
+
await fileHandle.close();
|
|
701
|
+
return sum;
|
|
702
|
+
};
|
|
703
|
+
/**
|
|
704
|
+
* Asynchronously finds the maximum numerical value from specified lines in a file.
|
|
705
|
+
*
|
|
706
|
+
* @param filePath - Path of the file to read.
|
|
707
|
+
* @param lineNumbers - Optional specific line number(s) to consider for finding the maximum value. If not provided, considers all lines.
|
|
708
|
+
* @returns Promise<number>. The maximum numerical value found in the specified lines.
|
|
709
|
+
*
|
|
710
|
+
* Note: Decodes each line as a number using the 'decode' function. Considers only numerical values for determining the maximum.
|
|
711
|
+
*/
|
|
712
|
+
export const max = async (filePath, lineNumbers) => {
|
|
713
|
+
let max = 0;
|
|
714
|
+
const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
|
|
715
|
+
if (lineNumbers) {
|
|
716
|
+
let linesCount = 0;
|
|
717
|
+
let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
|
|
718
|
+
for await (const line of rl) {
|
|
719
|
+
linesCount++;
|
|
720
|
+
if (!lineNumbersArray.has(linesCount))
|
|
721
|
+
continue;
|
|
722
|
+
const lineContentNum = +(decode(line, "number") ?? 0);
|
|
723
|
+
if (lineContentNum > max)
|
|
724
|
+
max = lineContentNum;
|
|
725
|
+
lineNumbersArray.delete(linesCount);
|
|
726
|
+
if (!lineNumbersArray.size)
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
else
|
|
731
|
+
for await (const line of rl) {
|
|
732
|
+
const lineContentNum = +(decode(line, "number") ?? 0);
|
|
733
|
+
if (lineContentNum > max)
|
|
734
|
+
max = lineContentNum;
|
|
735
|
+
}
|
|
736
|
+
await fileHandle.close();
|
|
737
|
+
return max;
|
|
738
|
+
};
|
|
739
|
+
/**
|
|
740
|
+
* Asynchronously finds the minimum numerical value from specified lines in a file.
|
|
741
|
+
*
|
|
742
|
+
* @param filePath - Path of the file to read.
|
|
743
|
+
* @param lineNumbers - Optional specific line number(s) to consider for finding the minimum value. If not provided, considers all lines.
|
|
744
|
+
* @returns Promise<number>. The minimum numerical value found in the specified lines.
|
|
745
|
+
*
|
|
746
|
+
* Note: Decodes each line as a number using the 'decode' function. Considers only numerical values for determining the minimum.
|
|
747
|
+
*/
|
|
748
|
+
export const min = async (filePath, lineNumbers) => {
|
|
749
|
+
let min = 0;
|
|
750
|
+
const fileHandle = await open(filePath, "r"), rl = readLineInternface(fileHandle);
|
|
751
|
+
if (lineNumbers) {
|
|
752
|
+
let linesCount = 0;
|
|
753
|
+
let lineNumbersArray = new Set(Array.isArray(lineNumbers) ? lineNumbers : [lineNumbers]);
|
|
754
|
+
for await (const line of rl) {
|
|
755
|
+
linesCount++;
|
|
756
|
+
if (!lineNumbersArray.has(linesCount))
|
|
757
|
+
continue;
|
|
758
|
+
const lineContentNum = +(decode(line, "number") ?? 0);
|
|
759
|
+
if (lineContentNum < min)
|
|
760
|
+
min = lineContentNum;
|
|
761
|
+
lineNumbersArray.delete(linesCount);
|
|
762
|
+
if (!lineNumbersArray.size)
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
else
|
|
767
|
+
for await (const line of rl) {
|
|
768
|
+
const lineContentNum = +(decode(line, "number") ?? 0);
|
|
769
|
+
if (lineContentNum < min)
|
|
770
|
+
min = lineContentNum;
|
|
771
|
+
}
|
|
772
|
+
await fileHandle.close();
|
|
773
|
+
return min;
|
|
774
|
+
};
|
|
775
|
+
export function createWorker(functionName, arg) {
|
|
776
|
+
return new Promise(function (resolve, reject) {
|
|
777
|
+
const worker = new Worker("./dist/file.thread.js", {
|
|
778
|
+
workerData: { functionName, arg },
|
|
779
|
+
});
|
|
780
|
+
worker.on("message", (data) => {
|
|
781
|
+
resolve(data);
|
|
782
|
+
});
|
|
783
|
+
worker.on("error", (msg) => {
|
|
784
|
+
reject(`An error ocurred: ${msg}`);
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Asynchronously sorts the lines in a file in the specified direction.
|
|
790
|
+
*
|
|
791
|
+
* @param filePath - Path of the file to be sorted.
|
|
792
|
+
* @param sortDirection - Direction for sorting: 1 or 'asc' for ascending, -1 or 'desc' for descending.
|
|
793
|
+
* @param lineNumbers - Optional specific line numbers to sort. If not provided, sorts all lines.
|
|
794
|
+
* @param _lineNumbersPerChunk - Optional parameter for handling large files, specifying the number of lines per chunk.
|
|
795
|
+
* @returns Promise<void>. Modifies the file by sorting specified lines.
|
|
796
|
+
*
|
|
797
|
+
* Note: The sorting is applied either to the entire file or to the specified lines. Large files are handled in chunks.
|
|
798
|
+
*/
|
|
799
|
+
export const sort = async (filePath, sortDirection, lineNumbers, _lineNumbersPerChunk = 100000) => { };
|
|
800
|
+
export default class File {
|
|
801
|
+
static get = get;
|
|
802
|
+
static remove = remove;
|
|
803
|
+
static search = search;
|
|
804
|
+
static replace = replace;
|
|
805
|
+
static encode = encode;
|
|
806
|
+
static decode = decode;
|
|
807
|
+
static isExists = isExists;
|
|
808
|
+
static sum = sum;
|
|
809
|
+
static min = min;
|
|
810
|
+
static max = max;
|
|
811
|
+
static append = append;
|
|
812
|
+
static count = count;
|
|
813
|
+
static write = write;
|
|
814
|
+
static read = read;
|
|
815
|
+
static lock = lock;
|
|
816
|
+
static unlock = unlock;
|
|
817
|
+
static createWorker = createWorker;
|
|
818
|
+
}
|