querysub 0.2.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.
- package/.dependency-cruiser.js +304 -0
- package/.eslintrc.js +51 -0
- package/.github/copilot-instructions.md +1 -0
- package/.vscode/settings.json +25 -0
- package/bin/deploy.js +4 -0
- package/bin/function.js +4 -0
- package/bin/server.js +4 -0
- package/costsBenefits.txt +112 -0
- package/deploy.ts +3 -0
- package/inject.ts +1 -0
- package/package.json +60 -0
- package/prompts.txt +54 -0
- package/spec.txt +820 -0
- package/src/-a-archives/archiveCache.ts +913 -0
- package/src/-a-archives/archives.ts +148 -0
- package/src/-a-archives/archivesBackBlaze.ts +792 -0
- package/src/-a-archives/archivesDisk.ts +418 -0
- package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
- package/src/-a-auth/certs.ts +517 -0
- package/src/-a-auth/der.ts +122 -0
- package/src/-a-auth/ed25519.ts +1015 -0
- package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
- package/src/-b-authorities/dnsAuthority.ts +203 -0
- package/src/-b-authorities/emailAuthority.ts +57 -0
- package/src/-c-identity/IdentityController.ts +200 -0
- package/src/-d-trust/NetworkTrust2.ts +150 -0
- package/src/-e-certs/EdgeCertController.ts +288 -0
- package/src/-e-certs/certAuthority.ts +192 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
- package/src/-g-core-values/NodeCapabilities.ts +134 -0
- package/src/-g-core-values/oneTimeForward.ts +91 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
- package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
- package/src/0-path-value-core/LoggingClient.tsx +24 -0
- package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
- package/src/0-path-value-core/PathController.ts +1 -0
- package/src/0-path-value-core/PathValueCommitter.ts +565 -0
- package/src/0-path-value-core/PathValueController.ts +231 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
- package/src/0-path-value-core/debugLogs.ts +90 -0
- package/src/0-path-value-core/pathValueArchives.ts +483 -0
- package/src/0-path-value-core/pathValueCore.ts +2217 -0
- package/src/1-path-client/RemoteWatcher.ts +558 -0
- package/src/1-path-client/pathValueClientWatcher.ts +702 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
- package/src/2-proxy/archiveMoveHarness.ts +376 -0
- package/src/2-proxy/garbageCollection.ts +753 -0
- package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
- package/src/2-proxy/pathValueProxy.ts +139 -0
- package/src/2-proxy/schema2.ts +518 -0
- package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
- package/src/3-path-functions/PathFunctionRunner.ts +619 -0
- package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
- package/src/3-path-functions/deployBlock.ts +10 -0
- package/src/3-path-functions/deployCheck.ts +7 -0
- package/src/3-path-functions/deployMain.ts +160 -0
- package/src/3-path-functions/pathFunctionLoader.ts +282 -0
- package/src/3-path-functions/syncSchema.ts +475 -0
- package/src/3-path-functions/tests/functionsTest.ts +135 -0
- package/src/3-path-functions/tests/rejectTest.ts +77 -0
- package/src/4-dom/css.tsx +29 -0
- package/src/4-dom/cssTypes.d.ts +212 -0
- package/src/4-dom/qreact.tsx +2322 -0
- package/src/4-dom/qreactTest.tsx +417 -0
- package/src/4-querysub/Querysub.ts +877 -0
- package/src/4-querysub/QuerysubController.ts +620 -0
- package/src/4-querysub/copyEvent.ts +0 -0
- package/src/4-querysub/permissions.ts +289 -0
- package/src/4-querysub/permissionsShared.ts +1 -0
- package/src/4-querysub/querysubPrediction.ts +525 -0
- package/src/5-diagnostics/FullscreenModal.tsx +67 -0
- package/src/5-diagnostics/GenericFormat.tsx +165 -0
- package/src/5-diagnostics/Modal.tsx +79 -0
- package/src/5-diagnostics/Table.tsx +183 -0
- package/src/5-diagnostics/TimeGrouper.tsx +114 -0
- package/src/5-diagnostics/diskValueAudit.ts +216 -0
- package/src/5-diagnostics/memoryValueAudit.ts +442 -0
- package/src/5-diagnostics/nodeMetadata.ts +135 -0
- package/src/5-diagnostics/qreactDebug.tsx +309 -0
- package/src/5-diagnostics/shared.ts +26 -0
- package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
- package/src/TestController.ts +35 -0
- package/src/allowclient.flag +0 -0
- package/src/bits.ts +86 -0
- package/src/buffers.ts +69 -0
- package/src/config.ts +53 -0
- package/src/config2.ts +48 -0
- package/src/diagnostics/ActionsHistory.ts +56 -0
- package/src/diagnostics/NodeViewer.tsx +503 -0
- package/src/diagnostics/SizeLimiter.ts +62 -0
- package/src/diagnostics/TimeDebug.tsx +18 -0
- package/src/diagnostics/benchmark.ts +139 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
- package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
- package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
- package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
- package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
- package/src/diagnostics/heapTag.ts +13 -0
- package/src/diagnostics/listenOnDebugger.ts +77 -0
- package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
- package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
- package/src/diagnostics/logs/ansiFormat.ts +108 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
- package/src/diagnostics/logs/diskLogger.ts +305 -0
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
- package/src/diagnostics/logs/logGitHashes.ts +30 -0
- package/src/diagnostics/managementPages.tsx +289 -0
- package/src/diagnostics/periodic.ts +89 -0
- package/src/diagnostics/runSaturationTest.ts +416 -0
- package/src/diagnostics/satSchema.ts +64 -0
- package/src/diagnostics/trackResources.ts +82 -0
- package/src/diagnostics/watchdog.ts +55 -0
- package/src/errors.ts +132 -0
- package/src/forceProduction.ts +3 -0
- package/src/fs.ts +72 -0
- package/src/heapDumps.ts +666 -0
- package/src/https.ts +2 -0
- package/src/inject.ts +1 -0
- package/src/library-components/ATag.tsx +84 -0
- package/src/library-components/Button.tsx +344 -0
- package/src/library-components/ButtonSelector.tsx +64 -0
- package/src/library-components/DropdownCustom.tsx +151 -0
- package/src/library-components/DropdownSelector.tsx +32 -0
- package/src/library-components/Input.tsx +334 -0
- package/src/library-components/InputLabel.tsx +198 -0
- package/src/library-components/InputPicker.tsx +125 -0
- package/src/library-components/LazyComponent.tsx +62 -0
- package/src/library-components/MeasureHeightCSS.tsx +48 -0
- package/src/library-components/MeasuredDiv.tsx +47 -0
- package/src/library-components/ShowMore.tsx +51 -0
- package/src/library-components/SyncedController.ts +171 -0
- package/src/library-components/TimeRangeSelector.tsx +407 -0
- package/src/library-components/URLParam.ts +263 -0
- package/src/library-components/colors.tsx +14 -0
- package/src/library-components/drag.ts +114 -0
- package/src/library-components/icons.tsx +692 -0
- package/src/library-components/niceStringify.ts +50 -0
- package/src/library-components/renderToString.ts +52 -0
- package/src/misc/PromiseRace.ts +101 -0
- package/src/misc/color.ts +30 -0
- package/src/misc/getParentProcessId.cs +53 -0
- package/src/misc/getParentProcessId.ts +53 -0
- package/src/misc/hash.ts +83 -0
- package/src/misc/ipPong.js +13 -0
- package/src/misc/networking.ts +2 -0
- package/src/misc/random.ts +45 -0
- package/src/misc.ts +19 -0
- package/src/noserverhotreload.flag +0 -0
- package/src/path.ts +226 -0
- package/src/persistentLocalStore.ts +37 -0
- package/src/promise.ts +15 -0
- package/src/server.ts +73 -0
- package/src/src.d.ts +1 -0
- package/src/test/heapProcess.ts +36 -0
- package/src/test/mongoSatTest.tsx +55 -0
- package/src/test/satTest.ts +193 -0
- package/src/test/test.tsx +552 -0
- package/src/zip.ts +92 -0
- package/src/zipThreaded.ts +106 -0
- package/src/zipThreadedWorker.js +19 -0
- package/tsconfig.json +27 -0
- package/yarnSpec.txt +56 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { isNode } from "socket-function/src/misc";
|
|
2
|
+
import { getSubFolder } from "../fs";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { blue, red, yellow } from "socket-function/src/formatting/logColors";
|
|
5
|
+
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
6
|
+
import { delay } from "socket-function/src/batching";
|
|
7
|
+
import debugbreak from "debugbreak";
|
|
8
|
+
import { Archives, getArchives } from "./archives";
|
|
9
|
+
import { cache, lazy } from "socket-function/src/caching";
|
|
10
|
+
import { isDefined } from "../misc";
|
|
11
|
+
|
|
12
|
+
export let storageDisabled = false;
|
|
13
|
+
/** Useful for tests */
|
|
14
|
+
export function disableStorage() {
|
|
15
|
+
storageDisabled = true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let maxFileNameLength = 260;
|
|
19
|
+
let maxFilePartLength = 255;
|
|
20
|
+
|
|
21
|
+
class ArchivesDisk {
|
|
22
|
+
public LAG = 0;
|
|
23
|
+
// Must end with "/"
|
|
24
|
+
public LOCAL_ARCHIVE_FOLDER = "";
|
|
25
|
+
|
|
26
|
+
private logging = false;
|
|
27
|
+
public enableLogging() {
|
|
28
|
+
this.logging = true;
|
|
29
|
+
}
|
|
30
|
+
private log(text: string) {
|
|
31
|
+
if (!this.logging) return;
|
|
32
|
+
console.log(text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public getDebugName() {
|
|
36
|
+
return "disk/" + this.LOCAL_ARCHIVE_FOLDER;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private initFinished = false;
|
|
40
|
+
// TODO: Now that init isn't async, call it directly in validate, instead of the callers to validate
|
|
41
|
+
private init = lazy(() => {
|
|
42
|
+
if (isNode() && process.platform === "win32") {
|
|
43
|
+
try {
|
|
44
|
+
let longPart = "LONG_FILE_PATH_TEST_" + Array(210 - "LONG_FILE_PATH_TEST_".length).fill("_").join("");
|
|
45
|
+
let testPath = this.LOCAL_ARCHIVE_FOLDER + longPart + "/" + longPart + "/" + longPart;
|
|
46
|
+
fs.mkdirSync(testPath, { recursive: true });
|
|
47
|
+
maxFileNameLength = 600;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.log(e);
|
|
50
|
+
console.error(red(`Failed to set long file name. Google "windows enable long paths", and enable long paths to allow long file names.`));
|
|
51
|
+
// TODO: We don't NEED to exit, but for now we will, because we can easily
|
|
52
|
+
// setup our machine properly.
|
|
53
|
+
console.log(`Exitting now`);
|
|
54
|
+
process.exit();
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
// 4096 for linux, but... 600 handles a lot more cases (backblaze, etc)
|
|
58
|
+
maxFileNameLength = 600;
|
|
59
|
+
}
|
|
60
|
+
this.initFinished = true;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
private async ensureDirsExist(path: string) {
|
|
64
|
+
let fileNameParts = path.split("/");
|
|
65
|
+
// Don't create the drive (and also only add up to the last path, via slicing (0, i)
|
|
66
|
+
for (let i = 1; i < fileNameParts.length; i++) {
|
|
67
|
+
let dir = this.LOCAL_ARCHIVE_FOLDER + fileNameParts.slice(0, i).join("/");
|
|
68
|
+
if (!fs.existsSync(dir)) {
|
|
69
|
+
fs.mkdirSync(dir);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@measureFnc
|
|
75
|
+
public async set(fileName: string, data: Buffer): Promise<void> {
|
|
76
|
+
await this.init();
|
|
77
|
+
|
|
78
|
+
this.log(blue(`Setting file ${fileName} = ${data.length} bytes`));
|
|
79
|
+
if (storageDisabled) return;
|
|
80
|
+
fileName = escapeFileName(fileName);
|
|
81
|
+
await this.simulateLag();
|
|
82
|
+
|
|
83
|
+
await this.ensureDirsExist(fileName);
|
|
84
|
+
|
|
85
|
+
await fs.promises.writeFile(this.LOCAL_ARCHIVE_FOLDER + fileName, data);
|
|
86
|
+
}
|
|
87
|
+
@measureFnc
|
|
88
|
+
public async del(fileName: string): Promise<void> {
|
|
89
|
+
await this.init();
|
|
90
|
+
this.log(blue(`Deleting file ${fileName}`));
|
|
91
|
+
if (storageDisabled) return;
|
|
92
|
+
fileName = escapeFileName(fileName);
|
|
93
|
+
await this.simulateLag();
|
|
94
|
+
if (!fs.existsSync(this.LOCAL_ARCHIVE_FOLDER + fileName)) return;
|
|
95
|
+
try {
|
|
96
|
+
await fs.promises.unlink(this.LOCAL_ARCHIVE_FOLDER + fileName);
|
|
97
|
+
} catch { }
|
|
98
|
+
let dir = fileName.replaceAll("\\", "/").split("/").slice(0, -1).join("/");
|
|
99
|
+
await this.gcDir(dir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined>; }): Promise<void> {
|
|
103
|
+
await this.init();
|
|
104
|
+
let { path } = config;
|
|
105
|
+
path = escapeFileName(path);
|
|
106
|
+
await this.ensureDirsExist(path);
|
|
107
|
+
let handle: fs.promises.FileHandle | undefined;
|
|
108
|
+
try {
|
|
109
|
+
handle = await fs.promises.open(this.LOCAL_ARCHIVE_FOLDER + path, "w");
|
|
110
|
+
let pos = 0;
|
|
111
|
+
while (true) {
|
|
112
|
+
let data = await config.getNextData();
|
|
113
|
+
if (!data?.length) break;
|
|
114
|
+
await handle.write(data, 0, data.length, pos);
|
|
115
|
+
pos += data.length;
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
if (handle) {
|
|
119
|
+
try {
|
|
120
|
+
await handle.close();
|
|
121
|
+
} catch { }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@measureFnc
|
|
127
|
+
public async gcDir(dir: string) {
|
|
128
|
+
await this.init();
|
|
129
|
+
if (!dir.startsWith(this.LOCAL_ARCHIVE_FOLDER)) {
|
|
130
|
+
dir = this.LOCAL_ARCHIVE_FOLDER + dir;
|
|
131
|
+
}
|
|
132
|
+
if (!dir.endsWith("/")) dir += "/";
|
|
133
|
+
let files: string[] = [];
|
|
134
|
+
try {
|
|
135
|
+
files = await fs.promises.readdir(dir);
|
|
136
|
+
} catch { }
|
|
137
|
+
for (let file of files) {
|
|
138
|
+
let isDir = false;
|
|
139
|
+
let path = dir + file + "/";
|
|
140
|
+
try {
|
|
141
|
+
let stat = await fs.promises.stat(path);
|
|
142
|
+
if (stat.isDirectory()) {
|
|
143
|
+
isDir = true;
|
|
144
|
+
}
|
|
145
|
+
} catch { }
|
|
146
|
+
if (isDir) {
|
|
147
|
+
await this.gcDir(path);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
files = await fs.promises.readdir(dir);
|
|
152
|
+
} catch { }
|
|
153
|
+
if (files.length === 0) {
|
|
154
|
+
try {
|
|
155
|
+
await fs.promises.rmdir(dir);
|
|
156
|
+
} catch { }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* NOTE: Generally we load data into memory and keep it there, so a streaming endpoint (instead of a buffer),
|
|
162
|
+
* wouldn't be that useful (and would be a lot slower, as streaming data is a lot slower than reading
|
|
163
|
+
* it all at once).
|
|
164
|
+
* @fsErrorRetryCount Retries count for file system error (not user errors). Ex, too many open files, etc.
|
|
165
|
+
*/
|
|
166
|
+
@measureFnc
|
|
167
|
+
public async get(fileNameInput: string, config?: { range?: { start: number; end: number; }; retryCount?: number }): Promise<Buffer | undefined> {
|
|
168
|
+
await this.init();
|
|
169
|
+
this.log(blue(`Start read file ${fileNameInput}`));
|
|
170
|
+
if (storageDisabled) return undefined;
|
|
171
|
+
let fileName = escapeFileName(fileNameInput);
|
|
172
|
+
await this.simulateLag();
|
|
173
|
+
let handle: fs.promises.FileHandle | undefined;
|
|
174
|
+
let retryCount = config?.retryCount ?? 3;
|
|
175
|
+
config = config ?? {};
|
|
176
|
+
try {
|
|
177
|
+
handle = await fs.promises.open(this.LOCAL_ARCHIVE_FOLDER + fileName, "r");
|
|
178
|
+
let stats = await handle.stat();
|
|
179
|
+
let start = config.range?.start ?? 0;
|
|
180
|
+
let end = config.range?.end ?? stats.size;
|
|
181
|
+
let buffer = Buffer.alloc(end - start);
|
|
182
|
+
let read = await handle.read(buffer, 0, end - start, start);
|
|
183
|
+
|
|
184
|
+
let stats2 = await handle.stat();
|
|
185
|
+
if (stats.mtimeMs !== stats2.mtimeMs) {
|
|
186
|
+
if (retryCount > 0) {
|
|
187
|
+
// Wait for the file to stop changing
|
|
188
|
+
await delay(10);
|
|
189
|
+
return this.get(fileNameInput, { ...config, retryCount: retryCount - 1 });
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`File modified changed while reading file ${fileName}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return buffer.slice(0, read.bytesRead);
|
|
195
|
+
} catch (e: any) {
|
|
196
|
+
if (e.code === "EMFILE") {
|
|
197
|
+
if (retryCount > 0) {
|
|
198
|
+
// Wait for some files to close.
|
|
199
|
+
await delay(10);
|
|
200
|
+
return this.get(fileNameInput, { ...config, retryCount: retryCount - 1 });
|
|
201
|
+
} else {
|
|
202
|
+
console.log(`Fixable error reading file, but we hit the retry limit. Returning undefined, even though the file likely exists. ${fileName}`, e);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
} finally {
|
|
207
|
+
if (handle) {
|
|
208
|
+
try {
|
|
209
|
+
await handle.close();
|
|
210
|
+
} catch { }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@measureFnc
|
|
216
|
+
public async find(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<string[]> {
|
|
217
|
+
await this.init();
|
|
218
|
+
prefix = escapeFileName(prefix);
|
|
219
|
+
this.log(blue(`findFileNames ${prefix}`));
|
|
220
|
+
if (storageDisabled) return [];
|
|
221
|
+
await this.simulateLag();
|
|
222
|
+
let fileNames: string[] = [];
|
|
223
|
+
let folderNames: string[] = [];
|
|
224
|
+
async function readDir(dir: string) {
|
|
225
|
+
if (!fs.existsSync(dir)) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
let curFileNames = await fs.promises.readdir(dir, {
|
|
230
|
+
withFileTypes: true
|
|
231
|
+
});
|
|
232
|
+
await Promise.all(curFileNames.map(async fileObj => {
|
|
233
|
+
let fileName = fileObj.name;
|
|
234
|
+
// try/catch so nested directory errors don't break everything
|
|
235
|
+
// (I guess if the root directory is deleted we will still through though...)
|
|
236
|
+
try {
|
|
237
|
+
if (fileObj.isDirectory()) {
|
|
238
|
+
folderNames.push(dir + fileName);
|
|
239
|
+
await readDir(dir + fileName + "/");
|
|
240
|
+
} else {
|
|
241
|
+
fileNames.push(dir + fileName);
|
|
242
|
+
}
|
|
243
|
+
} catch { }
|
|
244
|
+
}));
|
|
245
|
+
} catch { }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let rootDir = "";
|
|
249
|
+
let pathParts = prefix.split("/");
|
|
250
|
+
if (pathParts.length > 1) {
|
|
251
|
+
rootDir = pathParts.slice(0, -1).join("/") + "/";
|
|
252
|
+
}
|
|
253
|
+
await readDir(this.LOCAL_ARCHIVE_FOLDER + rootDir);
|
|
254
|
+
|
|
255
|
+
let results = config?.type === "folders" ? folderNames : fileNames;
|
|
256
|
+
|
|
257
|
+
results = results.map(name => name.slice(this.LOCAL_ARCHIVE_FOLDER.length));
|
|
258
|
+
results = results.filter(name => name.startsWith(prefix));
|
|
259
|
+
|
|
260
|
+
if (config?.shallow) {
|
|
261
|
+
let targetDepth = prefix.split("/").length;
|
|
262
|
+
results = results.filter(name => name.split("/").length === targetDepth);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
results = results.map(name => unescapeFileName(name));
|
|
266
|
+
|
|
267
|
+
return results;
|
|
268
|
+
}
|
|
269
|
+
@measureFnc
|
|
270
|
+
public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<{ path: string; createTime: number; size: number; }[]> {
|
|
271
|
+
// TODO: Isn't there any api to read file names and infos at the same time?
|
|
272
|
+
let files = await this.find(prefix, config);
|
|
273
|
+
return (await Promise.all(files.map(async file => {
|
|
274
|
+
try {
|
|
275
|
+
let stats = await fs.promises.stat(this.LOCAL_ARCHIVE_FOLDER + escapeFileName(file));
|
|
276
|
+
return { path: file, createTime: stats.ctimeMs, size: stats.size };
|
|
277
|
+
} catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
}))).filter(isDefined);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@measureFnc
|
|
285
|
+
public async getInfo(pathInput: string): Promise<{
|
|
286
|
+
writeTime: number;
|
|
287
|
+
size: number;
|
|
288
|
+
} | undefined> {
|
|
289
|
+
let path = escapeFileName(pathInput);
|
|
290
|
+
try {
|
|
291
|
+
let stats = await fs.promises.stat(this.LOCAL_ARCHIVE_FOLDER + path);
|
|
292
|
+
return {
|
|
293
|
+
writeTime: stats.mtimeMs,
|
|
294
|
+
size: stats.size,
|
|
295
|
+
};
|
|
296
|
+
} catch {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private simulateLag() {
|
|
302
|
+
if (this.LAG === 0) return;
|
|
303
|
+
return delay(this.LAG);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
public async assertPathValid(path: string) {
|
|
307
|
+
await this.init();
|
|
308
|
+
escapeFileName(this.LOCAL_ARCHIVE_FOLDER + path);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
@measureFnc
|
|
312
|
+
public async move(config: {
|
|
313
|
+
path: string;
|
|
314
|
+
target: Archives;
|
|
315
|
+
targetPath: string;
|
|
316
|
+
}) {
|
|
317
|
+
let { path, target, targetPath } = config;
|
|
318
|
+
while (true) {
|
|
319
|
+
let targetUnwrapped = target.getBaseArchives?.();
|
|
320
|
+
if (!targetUnwrapped) break;
|
|
321
|
+
target = targetUnwrapped.archives;
|
|
322
|
+
targetPath = targetUnwrapped.parentPath + targetPath;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (target instanceof ArchivesDisk) {
|
|
326
|
+
path = escapeFileName(path);
|
|
327
|
+
targetPath = escapeFileName(targetPath);
|
|
328
|
+
await this.ensureDirsExist(path);
|
|
329
|
+
await target.ensureDirsExist(targetPath);
|
|
330
|
+
await fs.promises.rename(
|
|
331
|
+
this.LOCAL_ARCHIVE_FOLDER + path,
|
|
332
|
+
target.LOCAL_ARCHIVE_FOLDER + targetPath
|
|
333
|
+
);
|
|
334
|
+
} else {
|
|
335
|
+
let data = await this.get(path);
|
|
336
|
+
if (!data) throw new Error(`File not found to move: ${path}`);
|
|
337
|
+
await target.set(targetPath, data);
|
|
338
|
+
await this.del(path);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@measureFnc
|
|
343
|
+
public async copy(config: {
|
|
344
|
+
path: string;
|
|
345
|
+
target: Archives;
|
|
346
|
+
targetPath: string;
|
|
347
|
+
}) {
|
|
348
|
+
let { path, target, targetPath } = config;
|
|
349
|
+
while (true) {
|
|
350
|
+
let targetUnwrapped = target.getBaseArchives?.();
|
|
351
|
+
if (!targetUnwrapped) break;
|
|
352
|
+
target = targetUnwrapped.archives;
|
|
353
|
+
targetPath = targetUnwrapped.parentPath + targetPath;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (target instanceof ArchivesDisk) {
|
|
357
|
+
path = escapeFileName(path);
|
|
358
|
+
targetPath = escapeFileName(targetPath);
|
|
359
|
+
await this.ensureDirsExist(path);
|
|
360
|
+
await target.ensureDirsExist(targetPath);
|
|
361
|
+
await fs.promises.copyFile(
|
|
362
|
+
this.LOCAL_ARCHIVE_FOLDER + path,
|
|
363
|
+
target.LOCAL_ARCHIVE_FOLDER + targetPath
|
|
364
|
+
);
|
|
365
|
+
} else {
|
|
366
|
+
let data = await this.get(path);
|
|
367
|
+
if (!data) throw new Error(`File not found to move: ${path}`);
|
|
368
|
+
await target.set(targetPath, data);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export const diskEscapeFileName = escapeFileName;
|
|
374
|
+
|
|
375
|
+
function escapeFileName(fileName: string): string {
|
|
376
|
+
fileName = fileName.replaceAll("_", "__");
|
|
377
|
+
if (fileName[1] === ":" && fileName[2] === "/") {
|
|
378
|
+
fileName = fileName.slice(0, 2) + fileName.slice(2).replaceAll(":", "_=");
|
|
379
|
+
} else {
|
|
380
|
+
fileName = fileName.replaceAll(":", "_=");
|
|
381
|
+
}
|
|
382
|
+
fileName = fileName.replaceAll(`"`, `_'`);
|
|
383
|
+
// TODO: Better escaping for file system paths
|
|
384
|
+
// TODO: Support longer filenames in some way? (backblaze can support longer names, but...
|
|
385
|
+
// we're always going to want to support just using the local filesystem, as that is always
|
|
386
|
+
// preferred during development).
|
|
387
|
+
if (fileName.length > maxFileNameLength) {
|
|
388
|
+
throw new Error(`File name too long, must be at most ${maxFileNameLength} characters long, was ${fileName.length}, file name ${JSON.stringify(fileName)}`);
|
|
389
|
+
}
|
|
390
|
+
fileName = fileName.replaceAll("\\", "/");
|
|
391
|
+
let parts = fileName.split("/");
|
|
392
|
+
for (let part of parts) {
|
|
393
|
+
if (part.length > maxFilePartLength) {
|
|
394
|
+
throw new Error(`File name part too long, must be at most ${maxFilePartLength} characters long, was ${part.length}, file name ${JSON.stringify(fileName)}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (fileName.startsWith("/")) {
|
|
398
|
+
fileName = fileName.slice("/".length);
|
|
399
|
+
}
|
|
400
|
+
return fileName;
|
|
401
|
+
}
|
|
402
|
+
function unescapeFileName(fileName: string): string {
|
|
403
|
+
fileName = fileName.replaceAll(`_'`, `"`);
|
|
404
|
+
fileName = fileName.replaceAll("_=", ":");
|
|
405
|
+
fileName = fileName.replaceAll("__", "_");
|
|
406
|
+
return fileName;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export const getArchivesLocal = cache((domain: string): Archives => {
|
|
410
|
+
|
|
411
|
+
const archivesLocal = new ArchivesDisk();
|
|
412
|
+
|
|
413
|
+
if (isNode()) {
|
|
414
|
+
archivesLocal.LOCAL_ARCHIVE_FOLDER = getSubFolder(domain).replaceAll("\\", "/");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return archivesLocal;
|
|
418
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { formatNumber } from "socket-function/src/formatting/format";
|
|
2
|
+
import { getDomain } from "../config";
|
|
3
|
+
import { getArchivesBackblaze } from "./archivesBackBlaze";
|
|
4
|
+
import { getArchivesLocal } from "./archivesDisk";
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const domain = getDomain();
|
|
8
|
+
let local = getArchivesLocal(domain);
|
|
9
|
+
let remote = getArchivesBackblaze(domain);
|
|
10
|
+
|
|
11
|
+
let files = await local.find("");
|
|
12
|
+
let allRemoteFiles = new Set(await remote.find(""));
|
|
13
|
+
for (let file of files) {
|
|
14
|
+
if (allRemoteFiles.has(file)) continue;
|
|
15
|
+
let buffer = await local.get(file);
|
|
16
|
+
if (!buffer) continue;
|
|
17
|
+
console.log(`Copying ${file} size ${formatNumber(buffer.length)}B`);
|
|
18
|
+
await remote.set(file, buffer);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
main().catch(console.error).finally(() => process.exit(0));
|
|
22
|
+
|
|
23
|
+
// archives = getArchivesLocal(domain);
|
|
24
|
+
// archives = getArchivesBackblaze(domain);
|