querysub 0.433.0 → 0.436.0

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.
Files changed (74) hide show
  1. package/.eslintrc.js +50 -50
  2. package/bin/deploy.js +0 -0
  3. package/bin/function.js +0 -0
  4. package/bin/server.js +0 -0
  5. package/costsBenefits.txt +115 -115
  6. package/deploy.ts +2 -2
  7. package/package.json +1 -1
  8. package/spec.txt +1192 -1192
  9. package/src/-a-archives/archives.ts +202 -202
  10. package/src/-a-archives/archivesBackBlaze.ts +1 -0
  11. package/src/-a-archives/archivesDisk.ts +454 -454
  12. package/src/-a-auth/certs.ts +540 -540
  13. package/src/-a-auth/node-forge-ed25519.d.ts +16 -16
  14. package/src/-b-authorities/dnsAuthority.ts +138 -138
  15. package/src/-c-identity/IdentityController.ts +258 -258
  16. package/src/-d-trust/NetworkTrust2.ts +180 -180
  17. package/src/-e-certs/EdgeCertController.ts +252 -252
  18. package/src/-e-certs/certAuthority.ts +201 -201
  19. package/src/-f-node-discovery/NodeDiscovery.ts +640 -640
  20. package/src/-g-core-values/NodeCapabilities.ts +200 -200
  21. package/src/-h-path-value-serialize/stringSerializer.ts +175 -175
  22. package/src/0-path-value-core/PathValueCommitter.ts +468 -468
  23. package/src/0-path-value-core/pathValueCore.ts +2 -2
  24. package/src/2-proxy/PathValueProxyWatcher.ts +2542 -2542
  25. package/src/2-proxy/TransactionDelayer.ts +94 -94
  26. package/src/2-proxy/pathDatabaseProxyBase.ts +36 -36
  27. package/src/2-proxy/pathValueProxy.ts +159 -159
  28. package/src/3-path-functions/PathFunctionRunnerMain.ts +87 -87
  29. package/src/3-path-functions/pathFunctionLoader.ts +516 -516
  30. package/src/3-path-functions/tests/rejectTest.ts +76 -76
  31. package/src/4-deploy/deployCheck.ts +6 -6
  32. package/src/4-dom/css.tsx +29 -29
  33. package/src/4-dom/cssTypes.d.ts +211 -211
  34. package/src/4-dom/qreact.tsx +2799 -2799
  35. package/src/4-dom/qreactTest.tsx +410 -410
  36. package/src/4-querysub/permissions.ts +335 -335
  37. package/src/4-querysub/querysubPrediction.ts +483 -483
  38. package/src/5-diagnostics/qreactDebug.tsx +346 -346
  39. package/src/TestController.ts +34 -34
  40. package/src/bits.ts +104 -104
  41. package/src/buffers.ts +69 -69
  42. package/src/diagnostics/ActionsHistory.ts +57 -57
  43. package/src/diagnostics/listenOnDebugger.ts +71 -71
  44. package/src/diagnostics/periodic.ts +111 -111
  45. package/src/diagnostics/trackResources.ts +91 -91
  46. package/src/diagnostics/watchdog.ts +120 -120
  47. package/src/errors.ts +133 -133
  48. package/src/forceProduction.ts +2 -2
  49. package/src/fs.ts +80 -80
  50. package/src/functional/diff.ts +857 -857
  51. package/src/functional/promiseCache.ts +78 -78
  52. package/src/functional/random.ts +8 -8
  53. package/src/functional/stats.ts +60 -60
  54. package/src/heapDumps.ts +665 -665
  55. package/src/https.ts +1 -1
  56. package/src/library-components/AspectSizedComponent.tsx +87 -87
  57. package/src/library-components/ButtonSelector.tsx +64 -64
  58. package/src/library-components/DropdownCustom.tsx +150 -150
  59. package/src/library-components/DropdownSelector.tsx +31 -31
  60. package/src/library-components/InlinePopup.tsx +66 -66
  61. package/src/misc/color.ts +29 -29
  62. package/src/misc/hash.ts +83 -83
  63. package/src/misc/ipPong.js +13 -13
  64. package/src/misc/networking.ts +1 -1
  65. package/src/misc/random.ts +44 -44
  66. package/src/misc.ts +196 -196
  67. package/src/path.ts +255 -255
  68. package/src/persistentLocalStore.ts +41 -41
  69. package/src/promise.ts +14 -14
  70. package/src/storage/fileSystemPointer.ts +71 -71
  71. package/src/test/heapProcess.ts +35 -35
  72. package/src/zip.ts +15 -15
  73. package/tsconfig.json +26 -26
  74. package/yarnSpec.txt +56 -56
@@ -1,455 +1,455 @@
1
- import { isNode } from "socket-function/src/misc";
2
- import os from "os";
3
- import { fsExistsAsync, getSubFolder } from "../fs";
4
- import fs from "fs";
5
- import { blue, red, yellow } from "socket-function/src/formatting/logColors";
6
- import { measureFnc } from "socket-function/src/profiling/measure";
7
- import { delay } from "socket-function/src/batching";
8
- import debugbreak from "debugbreak";
9
- import { Archives, getArchives } from "./archives";
10
- import { cache, lazy } from "socket-function/src/caching";
11
- import { isDefined } from "../misc";
12
-
13
- export let storageDisabled = false;
14
- /** Useful for tests */
15
- export function disableStorage() {
16
- storageDisabled = true;
17
- }
18
-
19
- let maxFileNameLength = 260;
20
- let maxFilePartLength = 255;
21
-
22
- class ArchivesDisk {
23
- public LAG = 0;
24
- // Must end with "/"
25
- public LOCAL_ARCHIVE_FOLDER = "";
26
-
27
- private logging = false;
28
- public enableLogging() {
29
- this.logging = true;
30
- }
31
- private log(text: string) {
32
- if (!this.logging) return;
33
- console.log(text);
34
- }
35
-
36
- public getDebugName() {
37
- return "disk/" + this.LOCAL_ARCHIVE_FOLDER;
38
- }
39
-
40
- private initFinished = false;
41
- // TODO: Now that init isn't async, call it directly in validate, instead of the callers to validate
42
- private init = lazy(() => {
43
- if (isNode() && process.platform === "win32") {
44
- try {
45
- let longPart = "LONG_FILE_PATH_TEST_" + Array(210 - "LONG_FILE_PATH_TEST_".length).fill("_").join("");
46
- let testPath = this.LOCAL_ARCHIVE_FOLDER + longPart + "/" + longPart + "/" + longPart;
47
- fs.mkdirSync(testPath, { recursive: true });
48
- maxFileNameLength = 600;
49
- } catch (e) {
50
- console.log(e);
51
- console.error(red(`Failed to set long file name. Google "windows enable long paths", and enable long paths to allow long file names.`));
52
- // TODO: We don't NEED to exit, but for now we will, because we can easily
53
- // setup our machine properly.
54
- console.log(`Exitting now`);
55
- process.exit();
56
- }
57
- } else {
58
- // 4096 for linux, but... 600 handles a lot more cases (backblaze, etc)
59
- maxFileNameLength = 600;
60
- }
61
- this.initFinished = true;
62
- });
63
-
64
- private async ensureDirsExist(path: string) {
65
- let fileNameParts = path.split("/");
66
- // Don't create the drive (and also only add up to the last path, via slicing (0, i)
67
- for (let i = 1; i < fileNameParts.length; i++) {
68
- let dir = this.LOCAL_ARCHIVE_FOLDER + fileNameParts.slice(0, i).join("/");
69
- try {
70
- if (!await fsExistsAsync(dir)) {
71
- await fs.promises.mkdir(dir, {
72
- recursive: true,
73
- });
74
- }
75
- } catch { }
76
- }
77
- }
78
-
79
- // We don't time the file system writes as it's almost never blocking, it's not stopping our processing. And if it is stopping our processing, then we actually want to know the parent function which is calling it, and then we want it to just void it instead of awaiting it (And the only way to know the parent caller is to not time these functions).
80
- public async set(fileName: string, data: Buffer): Promise<void> {
81
- await this.init();
82
-
83
- this.log(blue(`Setting file ${fileName} = ${data.length} bytes`));
84
- if (storageDisabled) return;
85
- fileName = escapeFileName(fileName);
86
- await this.simulateLag();
87
-
88
- await this.ensureDirsExist(fileName);
89
-
90
- await fs.promises.writeFile(this.LOCAL_ARCHIVE_FOLDER + fileName, data);
91
- }
92
- public async append(fileName: string, data: Buffer): Promise<void> {
93
- await this.init();
94
-
95
- this.log(blue(`Appending to file ${fileName} += ${data.length} bytes`));
96
- if (storageDisabled) return;
97
- fileName = escapeFileName(fileName);
98
- await this.simulateLag();
99
-
100
- await this.ensureDirsExist(fileName);
101
-
102
- await fs.promises.appendFile(this.LOCAL_ARCHIVE_FOLDER + fileName, data);
103
- }
104
- public async del(fileName: string): Promise<void> {
105
- await this.init();
106
- this.log(blue(`Deleting file ${fileName}`));
107
- if (storageDisabled) return;
108
- fileName = escapeFileName(fileName);
109
- await this.simulateLag();
110
- if (!await fsExistsAsync(this.LOCAL_ARCHIVE_FOLDER + fileName)) return;
111
- try {
112
- await fs.promises.rm(this.LOCAL_ARCHIVE_FOLDER + fileName, { recursive: true });
113
- } catch { }
114
- try {
115
- let dir = fileName;
116
- while (dir.length > 0) {
117
- dir = dir.replaceAll("\\", "/").split("/").slice(0, -1).join("/");
118
- if (dir.endsWith(":")) break;
119
- let files = await fs.promises.readdir(this.LOCAL_ARCHIVE_FOLDER + dir);
120
- if (files.length > 0) break;
121
- await fs.promises.rm(this.LOCAL_ARCHIVE_FOLDER + dir, { recursive: true });
122
- }
123
- } catch { }
124
- }
125
-
126
- public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined>; }): Promise<void> {
127
- await this.init();
128
- let { path } = config;
129
- path = escapeFileName(path);
130
- await this.ensureDirsExist(path);
131
- let handle: fs.promises.FileHandle | undefined;
132
- try {
133
- handle = await fs.promises.open(this.LOCAL_ARCHIVE_FOLDER + path, "w");
134
- let pos = 0;
135
- while (true) {
136
- let data = await config.getNextData();
137
- if (!data?.length) break;
138
- await handle.write(data, 0, data.length, pos);
139
- pos += data.length;
140
- }
141
- } finally {
142
- if (handle) {
143
- try {
144
- await handle.close();
145
- } catch { }
146
- }
147
- }
148
- }
149
-
150
- @measureFnc
151
- public async gcDir(dir: string) {
152
- await this.init();
153
- if (!dir.startsWith(this.LOCAL_ARCHIVE_FOLDER)) {
154
- dir = this.LOCAL_ARCHIVE_FOLDER + dir;
155
- }
156
- if (!dir.endsWith("/")) dir += "/";
157
- let files: string[] = [];
158
- try {
159
- files = await fs.promises.readdir(dir);
160
- } catch { }
161
- for (let file of files) {
162
- let isDir = false;
163
- let path = dir + file + "/";
164
- try {
165
- let stat = await fs.promises.stat(path);
166
- if (stat.isDirectory()) {
167
- isDir = true;
168
- }
169
- } catch { }
170
- if (isDir) {
171
- await this.gcDir(path);
172
- }
173
- }
174
- try {
175
- files = await fs.promises.readdir(dir);
176
- } catch { }
177
- if (files.length === 0) {
178
- try {
179
- await fs.promises.rm(dir, { recursive: true });
180
- } catch { }
181
- }
182
- }
183
-
184
- /**
185
- * NOTE: Generally we load data into memory and keep it there, so a streaming endpoint (instead of a buffer),
186
- * wouldn't be that useful (and would be a lot slower, as streaming data is a lot slower than reading
187
- * it all at once).
188
- * @fsErrorRetryCount Retries count for file system error (not user errors). Ex, too many open files, etc.
189
- */
190
- // NOTE: I've commented out the gate measuring so we can determine what is taking the time. We might want to remove all the timing from this file, as if we find any timing this file is slow, it doesn't help us optimize at all, because we then need to figure out what is calling it.
191
- //@measureFnc
192
- public async get(fileNameInput: string, config?: { range?: { start: number; end: number; }; retryCount?: number }): Promise<Buffer | undefined> {
193
- await this.init();
194
- this.log(blue(`Start read file ${fileNameInput}`));
195
- if (storageDisabled) return undefined;
196
- let fileName = escapeFileName(fileNameInput);
197
- await this.simulateLag();
198
- let handle: fs.promises.FileHandle | undefined;
199
- let retryCount = config?.retryCount ?? 3;
200
- config = config ?? {};
201
- try {
202
- handle = await fs.promises.open(this.LOCAL_ARCHIVE_FOLDER + fileName, "r");
203
- let stats = await handle.stat();
204
- let start = config.range?.start ?? 0;
205
- let end = config.range?.end ?? stats.size;
206
- let buffer = Buffer.alloc(end - start);
207
- let read = await handle.read(buffer, 0, end - start, start);
208
-
209
- let stats2 = await handle.stat();
210
- if (stats.mtimeMs !== stats2.mtimeMs) {
211
- if (retryCount > 0) {
212
- // Wait for the file to stop changing
213
- await delay(10);
214
- return this.get(fileNameInput, { ...config, retryCount: retryCount - 1 });
215
- }
216
- throw new Error(`File modified changed while reading file ${fileName}`);
217
- }
218
-
219
- // Make sure we only take as much data as we want in case we are given extra data.
220
- return buffer.slice(0, end - start);
221
- } catch (e: any) {
222
- if (e.code === "EMFILE") {
223
- if (retryCount > 0) {
224
- // Wait for some files to close.
225
- await delay(10);
226
- return this.get(fileNameInput, { ...config, retryCount: retryCount - 1 });
227
- } else {
228
- console.log(`Fixable error reading file, but we hit the retry limit. Returning undefined, even though the file likely exists. ${fileName}`, e);
229
- }
230
- }
231
- return undefined;
232
- } finally {
233
- if (handle) {
234
- try {
235
- await handle.close();
236
- } catch { }
237
- }
238
- }
239
- }
240
-
241
- @measureFnc
242
- public async find(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<string[]> {
243
- await this.init();
244
- prefix = escapeFileName(prefix);
245
- this.log(blue(`findFileNames ${prefix}`));
246
- if (storageDisabled) return [];
247
- await this.simulateLag();
248
- let fileNames: string[] = [];
249
- let folderNames: string[] = [];
250
- async function readDir(dir: string, depthToRead: number) {
251
- if (!await fsExistsAsync(dir)) {
252
- return;
253
- }
254
- try {
255
- let curFileNames = await fs.promises.readdir(dir, {
256
- withFileTypes: true
257
- });
258
- await Promise.all(curFileNames.map(async fileObj => {
259
- let fileName = fileObj.name;
260
- // try/catch so nested directory errors don't break everything
261
- // (I guess if the root directory is deleted we will still through though...)
262
- try {
263
- if (fileObj.isDirectory()) {
264
- folderNames.push(dir + fileName);
265
- await readDir(dir + fileName + "/", depthToRead - 1);
266
- } else {
267
- fileNames.push(dir + fileName);
268
- }
269
- } catch { }
270
- }));
271
- } catch { }
272
- }
273
-
274
- let rootDir = "";
275
- let pathParts = prefix.split("/");
276
- if (pathParts.length > 1) {
277
- rootDir = pathParts.slice(0, -1).join("/") + "/";
278
- }
279
- // We don't know if the prefix they gave us is the prefix of folders or if it is a folder itself. So we have to cut off the end and then read to a depth of 2 to make this work.
280
- await readDir(this.LOCAL_ARCHIVE_FOLDER + rootDir, config?.shallow ? 2 : Number.MAX_SAFE_INTEGER);
281
-
282
- let results = config?.type === "folders" ? folderNames : fileNames;
283
-
284
- results = results.map(name => name.slice(this.LOCAL_ARCHIVE_FOLDER.length));
285
- results = results.filter(name => name.startsWith(prefix));
286
-
287
- if (config?.shallow) {
288
- let targetDepth = prefix.split("/").length;
289
- results = results.filter(name => name.split("/").length === targetDepth);
290
- }
291
-
292
- results = results.map(name => unescapeFileName(name));
293
-
294
- return results;
295
- }
296
- @measureFnc
297
- public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<{ path: string; createTime: number; size: number; }[]> {
298
- // TODO: Isn't there any api to read file names and infos at the same time?
299
- let files = await this.find(prefix, config);
300
- return (await Promise.all(files.map(async file => {
301
- try {
302
- let stats = await fs.promises.stat(this.LOCAL_ARCHIVE_FOLDER + escapeFileName(file));
303
- return { path: file, createTime: stats.ctimeMs, size: stats.size };
304
- } catch {
305
- return undefined;
306
- }
307
- }))).filter(isDefined);
308
- }
309
-
310
-
311
- @measureFnc
312
- public async getInfo(pathInput: string): Promise<{
313
- writeTime: number;
314
- size: number;
315
- } | undefined> {
316
- let path = escapeFileName(pathInput);
317
- try {
318
- let stats = await fs.promises.stat(this.LOCAL_ARCHIVE_FOLDER + path);
319
- return {
320
- writeTime: stats.mtimeMs,
321
- size: stats.size,
322
- };
323
- } catch {
324
- return undefined;
325
- }
326
- }
327
-
328
- private simulateLag() {
329
- if (this.LAG === 0) return;
330
- return delay(this.LAG);
331
- }
332
-
333
- public async assertPathValid(path: string) {
334
- await this.init();
335
- escapeFileName(this.LOCAL_ARCHIVE_FOLDER + path);
336
- }
337
-
338
- @measureFnc
339
- public async move(config: {
340
- path: string;
341
- target: Archives;
342
- targetPath: string;
343
- }) {
344
- let { path, target, targetPath } = config;
345
- while (true) {
346
- let targetUnwrapped = target.getBaseArchives?.();
347
- if (!targetUnwrapped) break;
348
- target = targetUnwrapped.archives;
349
- targetPath = targetUnwrapped.parentPath + targetPath;
350
- }
351
-
352
- if (target instanceof ArchivesDisk) {
353
- path = escapeFileName(path);
354
- targetPath = escapeFileName(targetPath);
355
- await this.ensureDirsExist(path);
356
- await target.ensureDirsExist(targetPath);
357
- await fs.promises.rename(
358
- this.LOCAL_ARCHIVE_FOLDER + path,
359
- target.LOCAL_ARCHIVE_FOLDER + targetPath
360
- );
361
- } else {
362
- let data = await this.get(path);
363
- if (!data) throw new Error(`File not found to move: ${path}`);
364
- await target.set(targetPath, data);
365
- await this.del(path);
366
- }
367
- }
368
-
369
- @measureFnc
370
- public async copy(config: {
371
- path: string;
372
- target: Archives;
373
- targetPath: string;
374
- }) {
375
- let { path, target, targetPath } = config;
376
- while (true) {
377
- let targetUnwrapped = target.getBaseArchives?.();
378
- if (!targetUnwrapped) break;
379
- target = targetUnwrapped.archives;
380
- targetPath = targetUnwrapped.parentPath + targetPath;
381
- }
382
-
383
- if (target instanceof ArchivesDisk) {
384
- path = escapeFileName(path);
385
- targetPath = escapeFileName(targetPath);
386
- await this.ensureDirsExist(path);
387
- await target.ensureDirsExist(targetPath);
388
- await fs.promises.copyFile(
389
- this.LOCAL_ARCHIVE_FOLDER + path,
390
- target.LOCAL_ARCHIVE_FOLDER + targetPath
391
- );
392
- } else {
393
- let data = await this.get(path);
394
- if (!data) throw new Error(`File not found to move: ${path}`);
395
- await target.set(targetPath, data);
396
- }
397
- }
398
- }
399
-
400
- export const diskEscapeFileName = escapeFileName;
401
-
402
- function escapeFileName(fileName: string): string {
403
- fileName = fileName.replaceAll("_", "__");
404
- if (fileName[1] === ":" && fileName[2] === "/") {
405
- fileName = fileName.slice(0, 2) + fileName.slice(2).replaceAll(":", "_=");
406
- } else {
407
- fileName = fileName.replaceAll(":", "_=");
408
- }
409
- fileName = fileName.replaceAll(`"`, `_'`);
410
- // TODO: Better escaping for file system paths
411
- // TODO: Support longer filenames in some way? (backblaze can support longer names, but...
412
- // we're always going to want to support just using the local filesystem, as that is always
413
- // preferred during development).
414
- if (fileName.length > maxFileNameLength) {
415
- throw new Error(`File name too long, must be at most ${maxFileNameLength} characters long, was ${fileName.length}, file name ${JSON.stringify(fileName)}`);
416
- }
417
- fileName = fileName.replaceAll("\\", "/");
418
- let parts = fileName.split("/");
419
- for (let part of parts) {
420
- if (part.length > maxFilePartLength) {
421
- throw new Error(`File name part too long, must be at most ${maxFilePartLength} characters long, was ${part.length}, file name ${JSON.stringify(fileName)}`);
422
- }
423
- }
424
- if (fileName.startsWith("/")) {
425
- fileName = fileName.slice("/".length);
426
- }
427
- return fileName;
428
- }
429
- function unescapeFileName(fileName: string): string {
430
- fileName = fileName.replaceAll(`_'`, `"`);
431
- fileName = fileName.replaceAll("_=", ":");
432
- fileName = fileName.replaceAll("__", "_");
433
- return fileName;
434
- }
435
-
436
- export const getArchivesLocal = cache((domain: string): Archives => {
437
-
438
- const archivesLocal = new ArchivesDisk();
439
-
440
- if (isNode()) {
441
- archivesLocal.LOCAL_ARCHIVE_FOLDER = getSubFolder(domain).replaceAll("\\", "/");
442
- }
443
-
444
- return archivesLocal;
445
- });
446
-
447
- export const getArchivesHome = cache((domain: string): Archives => {
448
- const archivesLocal = new ArchivesDisk();
449
-
450
- if (isNode()) {
451
- archivesLocal.LOCAL_ARCHIVE_FOLDER = os.homedir() + "/querysub/" + domain + "/";
452
- }
453
-
454
- return archivesLocal;
1
+ import { isNode } from "socket-function/src/misc";
2
+ import os from "os";
3
+ import { fsExistsAsync, getSubFolder } from "../fs";
4
+ import fs from "fs";
5
+ import { blue, red, yellow } from "socket-function/src/formatting/logColors";
6
+ import { measureFnc } from "socket-function/src/profiling/measure";
7
+ import { delay } from "socket-function/src/batching";
8
+ import debugbreak from "debugbreak";
9
+ import { Archives, getArchives } from "./archives";
10
+ import { cache, lazy } from "socket-function/src/caching";
11
+ import { isDefined } from "../misc";
12
+
13
+ export let storageDisabled = false;
14
+ /** Useful for tests */
15
+ export function disableStorage() {
16
+ storageDisabled = true;
17
+ }
18
+
19
+ let maxFileNameLength = 260;
20
+ let maxFilePartLength = 255;
21
+
22
+ class ArchivesDisk {
23
+ public LAG = 0;
24
+ // Must end with "/"
25
+ public LOCAL_ARCHIVE_FOLDER = "";
26
+
27
+ private logging = false;
28
+ public enableLogging() {
29
+ this.logging = true;
30
+ }
31
+ private log(text: string) {
32
+ if (!this.logging) return;
33
+ console.log(text);
34
+ }
35
+
36
+ public getDebugName() {
37
+ return "disk/" + this.LOCAL_ARCHIVE_FOLDER;
38
+ }
39
+
40
+ private initFinished = false;
41
+ // TODO: Now that init isn't async, call it directly in validate, instead of the callers to validate
42
+ private init = lazy(() => {
43
+ if (isNode() && process.platform === "win32") {
44
+ try {
45
+ let longPart = "LONG_FILE_PATH_TEST_" + Array(210 - "LONG_FILE_PATH_TEST_".length).fill("_").join("");
46
+ let testPath = this.LOCAL_ARCHIVE_FOLDER + longPart + "/" + longPart + "/" + longPart;
47
+ fs.mkdirSync(testPath, { recursive: true });
48
+ maxFileNameLength = 600;
49
+ } catch (e) {
50
+ console.log(e);
51
+ console.error(red(`Failed to set long file name. Google "windows enable long paths", and enable long paths to allow long file names.`));
52
+ // TODO: We don't NEED to exit, but for now we will, because we can easily
53
+ // setup our machine properly.
54
+ console.log(`Exitting now`);
55
+ process.exit();
56
+ }
57
+ } else {
58
+ // 4096 for linux, but... 600 handles a lot more cases (backblaze, etc)
59
+ maxFileNameLength = 600;
60
+ }
61
+ this.initFinished = true;
62
+ });
63
+
64
+ private async ensureDirsExist(path: string) {
65
+ let fileNameParts = path.split("/");
66
+ // Don't create the drive (and also only add up to the last path, via slicing (0, i)
67
+ for (let i = 1; i < fileNameParts.length; i++) {
68
+ let dir = this.LOCAL_ARCHIVE_FOLDER + fileNameParts.slice(0, i).join("/");
69
+ try {
70
+ if (!await fsExistsAsync(dir)) {
71
+ await fs.promises.mkdir(dir, {
72
+ recursive: true,
73
+ });
74
+ }
75
+ } catch { }
76
+ }
77
+ }
78
+
79
+ // We don't time the file system writes as it's almost never blocking, it's not stopping our processing. And if it is stopping our processing, then we actually want to know the parent function which is calling it, and then we want it to just void it instead of awaiting it (And the only way to know the parent caller is to not time these functions).
80
+ public async set(fileName: string, data: Buffer): Promise<void> {
81
+ await this.init();
82
+
83
+ this.log(blue(`Setting file ${fileName} = ${data.length} bytes`));
84
+ if (storageDisabled) return;
85
+ fileName = escapeFileName(fileName);
86
+ await this.simulateLag();
87
+
88
+ await this.ensureDirsExist(fileName);
89
+
90
+ await fs.promises.writeFile(this.LOCAL_ARCHIVE_FOLDER + fileName, data);
91
+ }
92
+ public async append(fileName: string, data: Buffer): Promise<void> {
93
+ await this.init();
94
+
95
+ this.log(blue(`Appending to file ${fileName} += ${data.length} bytes`));
96
+ if (storageDisabled) return;
97
+ fileName = escapeFileName(fileName);
98
+ await this.simulateLag();
99
+
100
+ await this.ensureDirsExist(fileName);
101
+
102
+ await fs.promises.appendFile(this.LOCAL_ARCHIVE_FOLDER + fileName, data);
103
+ }
104
+ public async del(fileName: string): Promise<void> {
105
+ await this.init();
106
+ this.log(blue(`Deleting file ${fileName}`));
107
+ if (storageDisabled) return;
108
+ fileName = escapeFileName(fileName);
109
+ await this.simulateLag();
110
+ if (!await fsExistsAsync(this.LOCAL_ARCHIVE_FOLDER + fileName)) return;
111
+ try {
112
+ await fs.promises.rm(this.LOCAL_ARCHIVE_FOLDER + fileName, { recursive: true });
113
+ } catch { }
114
+ try {
115
+ let dir = fileName;
116
+ while (dir.length > 0) {
117
+ dir = dir.replaceAll("\\", "/").split("/").slice(0, -1).join("/");
118
+ if (dir.endsWith(":")) break;
119
+ let files = await fs.promises.readdir(this.LOCAL_ARCHIVE_FOLDER + dir);
120
+ if (files.length > 0) break;
121
+ await fs.promises.rm(this.LOCAL_ARCHIVE_FOLDER + dir, { recursive: true });
122
+ }
123
+ } catch { }
124
+ }
125
+
126
+ public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined>; }): Promise<void> {
127
+ await this.init();
128
+ let { path } = config;
129
+ path = escapeFileName(path);
130
+ await this.ensureDirsExist(path);
131
+ let handle: fs.promises.FileHandle | undefined;
132
+ try {
133
+ handle = await fs.promises.open(this.LOCAL_ARCHIVE_FOLDER + path, "w");
134
+ let pos = 0;
135
+ while (true) {
136
+ let data = await config.getNextData();
137
+ if (!data?.length) break;
138
+ await handle.write(data, 0, data.length, pos);
139
+ pos += data.length;
140
+ }
141
+ } finally {
142
+ if (handle) {
143
+ try {
144
+ await handle.close();
145
+ } catch { }
146
+ }
147
+ }
148
+ }
149
+
150
+ @measureFnc
151
+ public async gcDir(dir: string) {
152
+ await this.init();
153
+ if (!dir.startsWith(this.LOCAL_ARCHIVE_FOLDER)) {
154
+ dir = this.LOCAL_ARCHIVE_FOLDER + dir;
155
+ }
156
+ if (!dir.endsWith("/")) dir += "/";
157
+ let files: string[] = [];
158
+ try {
159
+ files = await fs.promises.readdir(dir);
160
+ } catch { }
161
+ for (let file of files) {
162
+ let isDir = false;
163
+ let path = dir + file + "/";
164
+ try {
165
+ let stat = await fs.promises.stat(path);
166
+ if (stat.isDirectory()) {
167
+ isDir = true;
168
+ }
169
+ } catch { }
170
+ if (isDir) {
171
+ await this.gcDir(path);
172
+ }
173
+ }
174
+ try {
175
+ files = await fs.promises.readdir(dir);
176
+ } catch { }
177
+ if (files.length === 0) {
178
+ try {
179
+ await fs.promises.rm(dir, { recursive: true });
180
+ } catch { }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * NOTE: Generally we load data into memory and keep it there, so a streaming endpoint (instead of a buffer),
186
+ * wouldn't be that useful (and would be a lot slower, as streaming data is a lot slower than reading
187
+ * it all at once).
188
+ * @fsErrorRetryCount Retries count for file system error (not user errors). Ex, too many open files, etc.
189
+ */
190
+ // NOTE: I've commented out the gate measuring so we can determine what is taking the time. We might want to remove all the timing from this file, as if we find any timing this file is slow, it doesn't help us optimize at all, because we then need to figure out what is calling it.
191
+ //@measureFnc
192
+ public async get(fileNameInput: string, config?: { range?: { start: number; end: number; }; retryCount?: number }): Promise<Buffer | undefined> {
193
+ await this.init();
194
+ this.log(blue(`Start read file ${fileNameInput}`));
195
+ if (storageDisabled) return undefined;
196
+ let fileName = escapeFileName(fileNameInput);
197
+ await this.simulateLag();
198
+ let handle: fs.promises.FileHandle | undefined;
199
+ let retryCount = config?.retryCount ?? 3;
200
+ config = config ?? {};
201
+ try {
202
+ handle = await fs.promises.open(this.LOCAL_ARCHIVE_FOLDER + fileName, "r");
203
+ let stats = await handle.stat();
204
+ let start = config.range?.start ?? 0;
205
+ let end = config.range?.end ?? stats.size;
206
+ let buffer = Buffer.alloc(end - start);
207
+ let read = await handle.read(buffer, 0, end - start, start);
208
+
209
+ let stats2 = await handle.stat();
210
+ if (stats.mtimeMs !== stats2.mtimeMs) {
211
+ if (retryCount > 0) {
212
+ // Wait for the file to stop changing
213
+ await delay(10);
214
+ return this.get(fileNameInput, { ...config, retryCount: retryCount - 1 });
215
+ }
216
+ throw new Error(`File modified changed while reading file ${fileName}`);
217
+ }
218
+
219
+ // Make sure we only take as much data as we want in case we are given extra data.
220
+ return buffer.slice(0, end - start);
221
+ } catch (e: any) {
222
+ if (e.code === "EMFILE") {
223
+ if (retryCount > 0) {
224
+ // Wait for some files to close.
225
+ await delay(10);
226
+ return this.get(fileNameInput, { ...config, retryCount: retryCount - 1 });
227
+ } else {
228
+ console.log(`Fixable error reading file, but we hit the retry limit. Returning undefined, even though the file likely exists. ${fileName}`, e);
229
+ }
230
+ }
231
+ return undefined;
232
+ } finally {
233
+ if (handle) {
234
+ try {
235
+ await handle.close();
236
+ } catch { }
237
+ }
238
+ }
239
+ }
240
+
241
+ @measureFnc
242
+ public async find(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<string[]> {
243
+ await this.init();
244
+ prefix = escapeFileName(prefix);
245
+ this.log(blue(`findFileNames ${prefix}`));
246
+ if (storageDisabled) return [];
247
+ await this.simulateLag();
248
+ let fileNames: string[] = [];
249
+ let folderNames: string[] = [];
250
+ async function readDir(dir: string, depthToRead: number) {
251
+ if (!await fsExistsAsync(dir)) {
252
+ return;
253
+ }
254
+ try {
255
+ let curFileNames = await fs.promises.readdir(dir, {
256
+ withFileTypes: true
257
+ });
258
+ await Promise.all(curFileNames.map(async fileObj => {
259
+ let fileName = fileObj.name;
260
+ // try/catch so nested directory errors don't break everything
261
+ // (I guess if the root directory is deleted we will still through though...)
262
+ try {
263
+ if (fileObj.isDirectory()) {
264
+ folderNames.push(dir + fileName);
265
+ await readDir(dir + fileName + "/", depthToRead - 1);
266
+ } else {
267
+ fileNames.push(dir + fileName);
268
+ }
269
+ } catch { }
270
+ }));
271
+ } catch { }
272
+ }
273
+
274
+ let rootDir = "";
275
+ let pathParts = prefix.split("/");
276
+ if (pathParts.length > 1) {
277
+ rootDir = pathParts.slice(0, -1).join("/") + "/";
278
+ }
279
+ // We don't know if the prefix they gave us is the prefix of folders or if it is a folder itself. So we have to cut off the end and then read to a depth of 2 to make this work.
280
+ await readDir(this.LOCAL_ARCHIVE_FOLDER + rootDir, config?.shallow ? 2 : Number.MAX_SAFE_INTEGER);
281
+
282
+ let results = config?.type === "folders" ? folderNames : fileNames;
283
+
284
+ results = results.map(name => name.slice(this.LOCAL_ARCHIVE_FOLDER.length));
285
+ results = results.filter(name => name.startsWith(prefix));
286
+
287
+ if (config?.shallow) {
288
+ let targetDepth = prefix.split("/").length;
289
+ results = results.filter(name => name.split("/").length === targetDepth);
290
+ }
291
+
292
+ results = results.map(name => unescapeFileName(name));
293
+
294
+ return results;
295
+ }
296
+ @measureFnc
297
+ public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<{ path: string; createTime: number; size: number; }[]> {
298
+ // TODO: Isn't there any api to read file names and infos at the same time?
299
+ let files = await this.find(prefix, config);
300
+ return (await Promise.all(files.map(async file => {
301
+ try {
302
+ let stats = await fs.promises.stat(this.LOCAL_ARCHIVE_FOLDER + escapeFileName(file));
303
+ return { path: file, createTime: stats.ctimeMs, size: stats.size };
304
+ } catch {
305
+ return undefined;
306
+ }
307
+ }))).filter(isDefined);
308
+ }
309
+
310
+
311
+ @measureFnc
312
+ public async getInfo(pathInput: string): Promise<{
313
+ writeTime: number;
314
+ size: number;
315
+ } | undefined> {
316
+ let path = escapeFileName(pathInput);
317
+ try {
318
+ let stats = await fs.promises.stat(this.LOCAL_ARCHIVE_FOLDER + path);
319
+ return {
320
+ writeTime: stats.mtimeMs,
321
+ size: stats.size,
322
+ };
323
+ } catch {
324
+ return undefined;
325
+ }
326
+ }
327
+
328
+ private simulateLag() {
329
+ if (this.LAG === 0) return;
330
+ return delay(this.LAG);
331
+ }
332
+
333
+ public async assertPathValid(path: string) {
334
+ await this.init();
335
+ escapeFileName(this.LOCAL_ARCHIVE_FOLDER + path);
336
+ }
337
+
338
+ @measureFnc
339
+ public async move(config: {
340
+ path: string;
341
+ target: Archives;
342
+ targetPath: string;
343
+ }) {
344
+ let { path, target, targetPath } = config;
345
+ while (true) {
346
+ let targetUnwrapped = target.getBaseArchives?.();
347
+ if (!targetUnwrapped) break;
348
+ target = targetUnwrapped.archives;
349
+ targetPath = targetUnwrapped.parentPath + targetPath;
350
+ }
351
+
352
+ if (target instanceof ArchivesDisk) {
353
+ path = escapeFileName(path);
354
+ targetPath = escapeFileName(targetPath);
355
+ await this.ensureDirsExist(path);
356
+ await target.ensureDirsExist(targetPath);
357
+ await fs.promises.rename(
358
+ this.LOCAL_ARCHIVE_FOLDER + path,
359
+ target.LOCAL_ARCHIVE_FOLDER + targetPath
360
+ );
361
+ } else {
362
+ let data = await this.get(path);
363
+ if (!data) throw new Error(`File not found to move: ${path}`);
364
+ await target.set(targetPath, data);
365
+ await this.del(path);
366
+ }
367
+ }
368
+
369
+ @measureFnc
370
+ public async copy(config: {
371
+ path: string;
372
+ target: Archives;
373
+ targetPath: string;
374
+ }) {
375
+ let { path, target, targetPath } = config;
376
+ while (true) {
377
+ let targetUnwrapped = target.getBaseArchives?.();
378
+ if (!targetUnwrapped) break;
379
+ target = targetUnwrapped.archives;
380
+ targetPath = targetUnwrapped.parentPath + targetPath;
381
+ }
382
+
383
+ if (target instanceof ArchivesDisk) {
384
+ path = escapeFileName(path);
385
+ targetPath = escapeFileName(targetPath);
386
+ await this.ensureDirsExist(path);
387
+ await target.ensureDirsExist(targetPath);
388
+ await fs.promises.copyFile(
389
+ this.LOCAL_ARCHIVE_FOLDER + path,
390
+ target.LOCAL_ARCHIVE_FOLDER + targetPath
391
+ );
392
+ } else {
393
+ let data = await this.get(path);
394
+ if (!data) throw new Error(`File not found to move: ${path}`);
395
+ await target.set(targetPath, data);
396
+ }
397
+ }
398
+ }
399
+
400
+ export const diskEscapeFileName = escapeFileName;
401
+
402
+ function escapeFileName(fileName: string): string {
403
+ fileName = fileName.replaceAll("_", "__");
404
+ if (fileName[1] === ":" && fileName[2] === "/") {
405
+ fileName = fileName.slice(0, 2) + fileName.slice(2).replaceAll(":", "_=");
406
+ } else {
407
+ fileName = fileName.replaceAll(":", "_=");
408
+ }
409
+ fileName = fileName.replaceAll(`"`, `_'`);
410
+ // TODO: Better escaping for file system paths
411
+ // TODO: Support longer filenames in some way? (backblaze can support longer names, but...
412
+ // we're always going to want to support just using the local filesystem, as that is always
413
+ // preferred during development).
414
+ if (fileName.length > maxFileNameLength) {
415
+ throw new Error(`File name too long, must be at most ${maxFileNameLength} characters long, was ${fileName.length}, file name ${JSON.stringify(fileName)}`);
416
+ }
417
+ fileName = fileName.replaceAll("\\", "/");
418
+ let parts = fileName.split("/");
419
+ for (let part of parts) {
420
+ if (part.length > maxFilePartLength) {
421
+ throw new Error(`File name part too long, must be at most ${maxFilePartLength} characters long, was ${part.length}, file name ${JSON.stringify(fileName)}`);
422
+ }
423
+ }
424
+ if (fileName.startsWith("/")) {
425
+ fileName = fileName.slice("/".length);
426
+ }
427
+ return fileName;
428
+ }
429
+ function unescapeFileName(fileName: string): string {
430
+ fileName = fileName.replaceAll(`_'`, `"`);
431
+ fileName = fileName.replaceAll("_=", ":");
432
+ fileName = fileName.replaceAll("__", "_");
433
+ return fileName;
434
+ }
435
+
436
+ export const getArchivesLocal = cache((domain: string): Archives => {
437
+
438
+ const archivesLocal = new ArchivesDisk();
439
+
440
+ if (isNode()) {
441
+ archivesLocal.LOCAL_ARCHIVE_FOLDER = getSubFolder(domain).replaceAll("\\", "/");
442
+ }
443
+
444
+ return archivesLocal;
445
+ });
446
+
447
+ export const getArchivesHome = cache((domain: string): Archives => {
448
+ const archivesLocal = new ArchivesDisk();
449
+
450
+ if (isNode()) {
451
+ archivesLocal.LOCAL_ARCHIVE_FOLDER = os.homedir() + "/querysub/" + domain + "/";
452
+ }
453
+
454
+ return archivesLocal;
455
455
  });