querysub 0.591.0 → 0.593.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/.claude/settings.local.json +5 -1
- package/bin/stop-machine.js +4 -0
- package/package.json +6 -3
- package/spec.txt +12 -0
- package/src/-a-archives/archiveCache.ts +28 -7
- package/src/-a-archives/archiveCache2.ts +4 -1
- package/src/-a-archives/archives.ts +2 -63
- package/src/-a-archives/archives2.ts +21 -17
- package/src/-a-archives/archivesDisk.ts +2 -13
- package/src/-a-archives/archivesMemoryCache.ts +27 -17
- package/src/-a-archives/archivesMemoryCache2.ts +23 -0
- package/src/-d-trust/NetworkTrust2.ts +3 -3
- package/src/-e-certs/certAuthority.ts +2 -2
- package/src/-f-node-discovery/NodeDiscovery.ts +3 -3
- package/src/0-path-value-core/AuthorityLookup.ts +0 -4
- package/src/0-path-value-core/PathRouter.ts +17 -16
- package/src/0-path-value-core/ShardPrefixes.ts +2 -4
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +133 -38
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +0 -1
- package/src/0-path-value-core/pathValueArchives.ts +9 -5
- package/src/2-proxy/archiveMoveHarness.ts +1 -1
- package/src/4-deploy/edgeBootstrap.ts +50 -11
- package/src/4-deploy/edgeNodes.ts +7 -13
- package/src/4-querysub/Querysub.ts +2 -2
- package/src/archiveapps/archiveJoinEntry.ts +1 -1
- package/src/config2.ts +1 -7
- package/src/deployManager/components/ServicesListPage.tsx +27 -8
- package/src/deployManager/machineDaemonShared.ts +2 -0
- package/src/deployManager/machineSchema.ts +5 -6
- package/src/deployManager/setupMachineMain.ts +1 -2
- package/src/deployManager/stopMachineMain.ts +45 -0
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +34 -21
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +3 -3
- package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +11 -2
- package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +3 -4
- package/src/diagnostics/logs/errorTickets/tickets.ts +3 -4
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +2 -4
- package/src/storageSetup.ts +1 -1
- package/src/-a-archives/archivesBackBlaze.ts +0 -156
- package/src/-a-archives/archivesCborT.ts +0 -52
- package/src/-a-archives/archivesLimitedCache.ts +0 -307
- package/src/-a-archives/archivesPrivateFileSystem.ts +0 -326
- package/src/-a-archives/copyLocalToBackblaze.ts +0 -24
- package/src/-b-authorities/cdnAuthority.ts +0 -53
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
2
|
-
import { Archives } from "./archives";
|
|
3
|
-
import { cache } from "socket-function/src/caching";
|
|
4
|
-
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
5
|
-
import { batchFunction, runInfinitePoll } from "socket-function/src/batching";
|
|
6
|
-
import { timeInHour } from "socket-function/src/misc";
|
|
7
|
-
|
|
8
|
-
interface FileInfo {
|
|
9
|
-
writeTime: number;
|
|
10
|
-
accessTime: number;
|
|
11
|
-
size: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface IndexData {
|
|
15
|
-
files: Record<string, FileInfo>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
class ArchivesLimitedCache {
|
|
19
|
-
private baseArchives: Archives;
|
|
20
|
-
private maxFiles: number;
|
|
21
|
-
private maxSize: number;
|
|
22
|
-
private cache = new Map<string, FileInfo>();
|
|
23
|
-
private initialized = false;
|
|
24
|
-
private readonly indexPath = ".cache-index.json";
|
|
25
|
-
|
|
26
|
-
constructor(baseArchives: Archives, config: { maxFiles: number; maxSize: number }) {
|
|
27
|
-
this.baseArchives = baseArchives;
|
|
28
|
-
this.maxFiles = config.maxFiles;
|
|
29
|
-
this.maxSize = config.maxSize;
|
|
30
|
-
this.initOptionalMethods();
|
|
31
|
-
this.initPeriodicSync();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
public getDebugName(): string {
|
|
35
|
-
return `limitedCache(${this.maxFiles}files,${Math.round(this.maxSize / (1024 * 1024))}MB)/${this.baseArchives.getDebugName()}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
@measureFnc
|
|
39
|
-
private async rebuildCacheFromFiles(): Promise<void> {
|
|
40
|
-
const allFiles = await this.baseArchives.findInfo("", { type: "files" });
|
|
41
|
-
const newCache = new Map<string, FileInfo>();
|
|
42
|
-
|
|
43
|
-
// Build map of actual files (excluding index file)
|
|
44
|
-
for (const file of allFiles) {
|
|
45
|
-
if (file.path === this.indexPath) continue;
|
|
46
|
-
|
|
47
|
-
newCache.set(file.path, {
|
|
48
|
-
writeTime: file.createTime,
|
|
49
|
-
accessTime: this.cache.get(file.path)?.accessTime ?? file.createTime,
|
|
50
|
-
size: file.size,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Update cache with actual files
|
|
55
|
-
this.cache.clear();
|
|
56
|
-
for (const [path, info] of newCache.entries()) {
|
|
57
|
-
this.cache.set(path, info);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
@measureFnc
|
|
62
|
-
private async ensureInitialized(): Promise<void> {
|
|
63
|
-
if (this.initialized) return;
|
|
64
|
-
|
|
65
|
-
// First try to load from index file
|
|
66
|
-
await this.loadIndex();
|
|
67
|
-
|
|
68
|
-
// If index is empty or missing, fall back to scanning all files
|
|
69
|
-
if (this.cache.size === 0) {
|
|
70
|
-
console.log("Index file missing or empty, scanning all files...");
|
|
71
|
-
await this.rebuildCacheFromFiles();
|
|
72
|
-
// Save the newly built index
|
|
73
|
-
await this.saveIndex();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
this.initialized = true;
|
|
77
|
-
|
|
78
|
-
// Cleanup if we're already over limits
|
|
79
|
-
await this.applyLimits();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private async applyLimits(): Promise<void> {
|
|
83
|
-
const files = Array.from(this.cache.entries()).map(([path, info]) => ({
|
|
84
|
-
path,
|
|
85
|
-
...info,
|
|
86
|
-
}));
|
|
87
|
-
|
|
88
|
-
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
89
|
-
const totalFiles = files.length;
|
|
90
|
-
|
|
91
|
-
if (totalFiles <= this.maxFiles && totalSize <= this.maxSize) {
|
|
92
|
-
return; // No cleanup needed
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Sort by access time (oldest first)
|
|
96
|
-
files.sort((a, b) => a.accessTime - b.accessTime);
|
|
97
|
-
|
|
98
|
-
let currentSize = totalSize;
|
|
99
|
-
let currentFiles = totalFiles;
|
|
100
|
-
|
|
101
|
-
// Remove files until we're under both limits
|
|
102
|
-
for (const file of files) {
|
|
103
|
-
if (currentFiles <= this.maxFiles && currentSize <= this.maxSize) {
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
console.warn(`Deleting file ${file.path} to maintain limits. Currently have ${formatNumber(currentFiles)} files and ${formatNumber(currentSize)}B`);
|
|
108
|
-
try {
|
|
109
|
-
await this.baseArchives.del(file.path);
|
|
110
|
-
this.cache.delete(file.path);
|
|
111
|
-
currentSize -= file.size;
|
|
112
|
-
currentFiles--;
|
|
113
|
-
} catch (error) {
|
|
114
|
-
// File might already be deleted, continue
|
|
115
|
-
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
116
|
-
this.cache.delete(file.path); // Remove from cache anyway
|
|
117
|
-
currentSize -= file.size;
|
|
118
|
-
currentFiles--;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Trigger index flush if any files were deleted
|
|
123
|
-
if (currentFiles < totalFiles) {
|
|
124
|
-
this.triggerIndexFlush();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private updateAccessTime(path: string): void {
|
|
129
|
-
const info = this.cache.get(path);
|
|
130
|
-
if (info) {
|
|
131
|
-
info.accessTime = Date.now();
|
|
132
|
-
this.triggerIndexFlush();
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
public async get(path: string, config?: { range?: { start: number; end: number; }; retryCount?: number }): Promise<Buffer | undefined> {
|
|
137
|
-
await this.ensureInitialized();
|
|
138
|
-
this.updateAccessTime(path);
|
|
139
|
-
return this.baseArchives.get(path, config);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
public async set(path: string, data: Buffer): Promise<void> {
|
|
143
|
-
await this.ensureInitialized();
|
|
144
|
-
// Limit before, not after, so we don't remove the file we just added
|
|
145
|
-
await this.applyLimits();
|
|
146
|
-
await this.baseArchives.set(path, data);
|
|
147
|
-
const now = Date.now();
|
|
148
|
-
this.cache.set(path, {
|
|
149
|
-
writeTime: now,
|
|
150
|
-
accessTime: now,
|
|
151
|
-
size: data.length,
|
|
152
|
-
});
|
|
153
|
-
this.triggerIndexFlush();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
public async append(path: string, data: Buffer): Promise<void> {
|
|
157
|
-
await this.ensureInitialized();
|
|
158
|
-
await this.baseArchives.append(path, data);
|
|
159
|
-
// Update the cache info - increment size and update times
|
|
160
|
-
const info = this.cache.get(path);
|
|
161
|
-
const now = Date.now();
|
|
162
|
-
if (info) {
|
|
163
|
-
info.size += data.length;
|
|
164
|
-
info.writeTime = now;
|
|
165
|
-
info.accessTime = now;
|
|
166
|
-
} else {
|
|
167
|
-
// File didn't exist in cache, add it
|
|
168
|
-
this.cache.set(path, {
|
|
169
|
-
writeTime: now,
|
|
170
|
-
accessTime: now,
|
|
171
|
-
size: data.length,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
this.triggerIndexFlush();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
public async del(path: string): Promise<void> {
|
|
178
|
-
await this.ensureInitialized();
|
|
179
|
-
await this.baseArchives.del(path);
|
|
180
|
-
this.cache.delete(path);
|
|
181
|
-
this.triggerIndexFlush();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
public async getInfo(path: string): Promise<{ writeTime: number; size: number; } | undefined> {
|
|
185
|
-
await this.ensureInitialized();
|
|
186
|
-
this.updateAccessTime(path);
|
|
187
|
-
return this.baseArchives.getInfo(path);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined>; }): Promise<void> {
|
|
191
|
-
throw new Error(`setLargeFile in archivesLimitedCache is not supported`);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
public async find(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<string[]> {
|
|
195
|
-
await this.ensureInitialized();
|
|
196
|
-
return this.baseArchives.find(prefix, config);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<{ path: string; createTime: number; size: number; }[]> {
|
|
200
|
-
await this.ensureInitialized();
|
|
201
|
-
return this.baseArchives.findInfo(prefix, config);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
public async move(config: { path: string; target: Archives; targetPath: string }): Promise<void> {
|
|
205
|
-
throw new Error(`Move in archivesLimitedCache is not supported`);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
public async copy(config: { path: string; target: Archives; targetPath: string }): Promise<void> {
|
|
209
|
-
throw new Error(`Copy in archivesLimitedCache is not supported`);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
public enableLogging(): void {
|
|
213
|
-
this.baseArchives.enableLogging();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
public async assertPathValid(path: string): Promise<void> {
|
|
217
|
-
await this.baseArchives.assertPathValid(path);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
public getBaseArchives?: () => { parentPath: string; archives: Archives; } | undefined = () => {
|
|
221
|
-
const base = this.baseArchives.getBaseArchives?.();
|
|
222
|
-
if (base) {
|
|
223
|
-
return base;
|
|
224
|
-
}
|
|
225
|
-
return { parentPath: "", archives: this.baseArchives };
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
public getURL?: (path: string) => Promise<string>;
|
|
229
|
-
public getDownloadAuthorization?: (config: { validDurationInSeconds: number }) => Promise<{ bucketId: string; fileNamePrefix: string; authorizationToken: string; }>;
|
|
230
|
-
|
|
231
|
-
private batchedFlushIndex = batchFunction(
|
|
232
|
-
{ delay: 2000 }, // Flush index changes every 2 seconds
|
|
233
|
-
async () => {
|
|
234
|
-
// We don't actually need the batched values, just flush the index
|
|
235
|
-
await this.saveIndex();
|
|
236
|
-
}
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
private async loadIndex(): Promise<void> {
|
|
240
|
-
try {
|
|
241
|
-
const indexBuffer = await this.baseArchives.get(this.indexPath);
|
|
242
|
-
if (!indexBuffer) {
|
|
243
|
-
// No existing index, will need to build it
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const indexData: IndexData = JSON.parse(indexBuffer.toString("utf8"));
|
|
248
|
-
|
|
249
|
-
// Load the cache from the index
|
|
250
|
-
this.cache.clear();
|
|
251
|
-
for (const [path, info] of Object.entries(indexData.files)) {
|
|
252
|
-
this.cache.set(path, info);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
console.log(`Loaded index with ${this.cache.size} files`);
|
|
256
|
-
} catch (error) {
|
|
257
|
-
console.warn(`Failed to load index file:`, error);
|
|
258
|
-
// Continue without index, will rebuild
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
private async saveIndex(): Promise<void> {
|
|
263
|
-
try {
|
|
264
|
-
const indexData: IndexData = {
|
|
265
|
-
files: Object.fromEntries(this.cache.entries())
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const indexBuffer = Buffer.from(JSON.stringify(indexData), "utf8");
|
|
269
|
-
await this.baseArchives.set(this.indexPath, indexBuffer);
|
|
270
|
-
} catch (error) {
|
|
271
|
-
console.warn(`Failed to save index file:`, error);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
private triggerIndexFlush(): void {
|
|
276
|
-
void this.batchedFlushIndex(undefined);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
private initPeriodicSync(): void {
|
|
280
|
-
// Sync index with actual files every hour to prevent drift
|
|
281
|
-
runInfinitePoll(timeInHour, async () => {
|
|
282
|
-
await this.syncIndexWithFiles();
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
private async syncIndexWithFiles(): Promise<void> {
|
|
287
|
-
try {
|
|
288
|
-
console.log("Syncing index with actual files...");
|
|
289
|
-
await this.rebuildCacheFromFiles();
|
|
290
|
-
|
|
291
|
-
// Save updated index
|
|
292
|
-
await this.saveIndex();
|
|
293
|
-
console.log(`Index synced with ${this.cache.size} actual files`);
|
|
294
|
-
} catch (error) {
|
|
295
|
-
console.warn("Failed to sync index with files:", error);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private initOptionalMethods(): void {
|
|
300
|
-
this.getURL = this.baseArchives.getURL;
|
|
301
|
-
this.getDownloadAuthorization = this.baseArchives.getDownloadAuthorization;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
export function createArchivesLimitedCache(baseArchives: Archives, config: { maxFiles: number; maxSize: number }): Archives {
|
|
306
|
-
return new ArchivesLimitedCache(baseArchives, config);
|
|
307
|
-
}
|
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
/// <reference path="../src.d.ts" />
|
|
2
|
-
|
|
3
|
-
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
4
|
-
import { blue } from "socket-function/src/formatting/logColors";
|
|
5
|
-
import { Archives } from "./archives";
|
|
6
|
-
import { cache, lazy } from "socket-function/src/caching";
|
|
7
|
-
import { delay } from "socket-function/src/batching";
|
|
8
|
-
import { isDefined } from "../misc";
|
|
9
|
-
import { formatTime } from "socket-function/src/formatting/format";
|
|
10
|
-
|
|
11
|
-
let getRootDirectory = lazy(async () => {
|
|
12
|
-
await navigator.storage.persist();
|
|
13
|
-
return navigator.storage.getDirectory();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
class ArchivesPrivateFileSystem {
|
|
17
|
-
public LAG = 0;
|
|
18
|
-
private rootDir: string;
|
|
19
|
-
|
|
20
|
-
private logging = false;
|
|
21
|
-
public enableLogging() {
|
|
22
|
-
this.logging = true;
|
|
23
|
-
}
|
|
24
|
-
private log(text: string) {
|
|
25
|
-
if (!this.logging) return;
|
|
26
|
-
console.log(text);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
constructor(rootDir: string) {
|
|
30
|
-
this.rootDir = rootDir;
|
|
31
|
-
// Ensure rootDir ends with "/" for consistency
|
|
32
|
-
if (!this.rootDir.endsWith("/")) {
|
|
33
|
-
this.rootDir = this.rootDir + "/";
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
public getDebugName() {
|
|
38
|
-
return "privateFS/" + this.rootDir;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
private async getOrCreateDirectory(path: string): Promise<FileSystemDirectoryHandle> {
|
|
42
|
-
const root = await getRootDirectory();
|
|
43
|
-
const pathParts = (this.rootDir + path).split("/").filter(part => part.length > 0);
|
|
44
|
-
|
|
45
|
-
let currentDir = root;
|
|
46
|
-
for (const part of pathParts) {
|
|
47
|
-
currentDir = await currentDir.getDirectoryHandle(part, { create: true });
|
|
48
|
-
}
|
|
49
|
-
return currentDir;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private async ensureDirsExist(path: string) {
|
|
53
|
-
const pathParts = path.split("/");
|
|
54
|
-
// Create directories up to the parent of the file (don't include the file itself)
|
|
55
|
-
if (pathParts.length > 1) {
|
|
56
|
-
const dirPath = pathParts.slice(0, -1).join("/");
|
|
57
|
-
await this.getOrCreateDirectory(dirPath);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
@measureFnc
|
|
62
|
-
public async set(fileName: string, data: Buffer): Promise<void> {
|
|
63
|
-
this.log(blue(`Setting file ${fileName} = ${data.length} bytes`));
|
|
64
|
-
|
|
65
|
-
await this.ensureDirsExist(fileName);
|
|
66
|
-
|
|
67
|
-
const pathParts = fileName.split("/");
|
|
68
|
-
const filename = pathParts[pathParts.length - 1];
|
|
69
|
-
const dirPath = pathParts.length > 1 ? pathParts.slice(0, -1).join("/") : "";
|
|
70
|
-
|
|
71
|
-
const directory = await this.getOrCreateDirectory(dirPath);
|
|
72
|
-
const fileHandle = await directory.getFileHandle(filename, { create: true });
|
|
73
|
-
const writable = await fileHandle.createWritable();
|
|
74
|
-
await writable.write(data);
|
|
75
|
-
await writable.close();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
@measureFnc
|
|
79
|
-
public async append(fileName: string, data: Buffer): Promise<void> {
|
|
80
|
-
this.log(blue(`Appending to file ${fileName} += ${data.length} bytes`));
|
|
81
|
-
|
|
82
|
-
await this.ensureDirsExist(fileName);
|
|
83
|
-
|
|
84
|
-
const pathParts = fileName.split("/");
|
|
85
|
-
const filename = pathParts[pathParts.length - 1];
|
|
86
|
-
const dirPath = pathParts.length > 1 ? pathParts.slice(0, -1).join("/") : "";
|
|
87
|
-
|
|
88
|
-
const directory = await this.getOrCreateDirectory(dirPath);
|
|
89
|
-
const fileHandle = await directory.getFileHandle(filename, { create: true });
|
|
90
|
-
|
|
91
|
-
// Get current file to find the size for appending at the end
|
|
92
|
-
const file = await fileHandle.getFile();
|
|
93
|
-
const currentSize = file.size;
|
|
94
|
-
|
|
95
|
-
const writable = await fileHandle.createWritable({ keepExistingData: true });
|
|
96
|
-
await writable.write({ type: "write", position: currentSize, data });
|
|
97
|
-
await writable.close();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
@measureFnc
|
|
101
|
-
public async del(fileName: string): Promise<void> {
|
|
102
|
-
this.log(blue(`Deleting file ${fileName}`));
|
|
103
|
-
|
|
104
|
-
const pathParts = fileName.split("/");
|
|
105
|
-
const filename = pathParts[pathParts.length - 1];
|
|
106
|
-
const dirPath = pathParts.length > 1 ? pathParts.slice(0, -1).join("/") : "";
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
const directory = await this.getOrCreateDirectory(dirPath);
|
|
110
|
-
await directory.removeEntry(filename);
|
|
111
|
-
} catch (e: any) {
|
|
112
|
-
// File might not exist, which is fine for delete operations
|
|
113
|
-
if (e.name !== "NotFoundError") {
|
|
114
|
-
throw e;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined>; }): Promise<void> {
|
|
120
|
-
let { path } = config;
|
|
121
|
-
await this.ensureDirsExist(path);
|
|
122
|
-
|
|
123
|
-
const pathParts = path.split("/");
|
|
124
|
-
const filename = pathParts[pathParts.length - 1];
|
|
125
|
-
const dirPath = pathParts.length > 1 ? pathParts.slice(0, -1).join("/") : "";
|
|
126
|
-
|
|
127
|
-
const directory = await this.getOrCreateDirectory(dirPath);
|
|
128
|
-
const fileHandle = await directory.getFileHandle(filename, { create: true });
|
|
129
|
-
const writable = await fileHandle.createWritable();
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
while (true) {
|
|
133
|
-
let data = await config.getNextData();
|
|
134
|
-
if (!data?.length) break;
|
|
135
|
-
await writable.write(data);
|
|
136
|
-
}
|
|
137
|
-
} finally {
|
|
138
|
-
await writable.close();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
@measureFnc
|
|
143
|
-
public async get(fileName: string, config?: { range?: { start: number; end: number; }; retryCount?: number }): Promise<Buffer | undefined> {
|
|
144
|
-
this.log(blue(`Start read file ${fileName}`));
|
|
145
|
-
|
|
146
|
-
const pathParts = fileName.split("/");
|
|
147
|
-
const filename = pathParts[pathParts.length - 1];
|
|
148
|
-
const dirPath = pathParts.length > 1 ? pathParts.slice(0, -1).join("/") : "";
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const directory = await this.getOrCreateDirectory(dirPath);
|
|
152
|
-
const fileHandle = await directory.getFileHandle(filename, { create: false });
|
|
153
|
-
const file = await fileHandle.getFile();
|
|
154
|
-
|
|
155
|
-
if (config?.range) {
|
|
156
|
-
const start = config.range.start;
|
|
157
|
-
const end = config.range.end;
|
|
158
|
-
const slice = file.slice(start, end);
|
|
159
|
-
const arrayBuffer = await slice.arrayBuffer();
|
|
160
|
-
return Buffer.from(arrayBuffer);
|
|
161
|
-
} else {
|
|
162
|
-
const arrayBuffer = await file.arrayBuffer();
|
|
163
|
-
return Buffer.from(arrayBuffer);
|
|
164
|
-
}
|
|
165
|
-
} catch (e: any) {
|
|
166
|
-
if (e.name === "NotFoundError") {
|
|
167
|
-
return undefined;
|
|
168
|
-
}
|
|
169
|
-
throw e;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
@measureFnc
|
|
174
|
-
public async find(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<string[]> {
|
|
175
|
-
this.log(blue(`findFileNames ${prefix}`));
|
|
176
|
-
|
|
177
|
-
let fileNames: string[] = [];
|
|
178
|
-
let folderNames: string[] = [];
|
|
179
|
-
|
|
180
|
-
async function readDir(directory: FileSystemDirectoryHandle, currentPath: string) {
|
|
181
|
-
for await (const [name, handle] of directory) {
|
|
182
|
-
const fullPath = currentPath + (currentPath ? "/" : "") + name;
|
|
183
|
-
|
|
184
|
-
if (handle.kind === "directory") {
|
|
185
|
-
folderNames.push(fullPath);
|
|
186
|
-
if (!config?.shallow) {
|
|
187
|
-
await readDir(handle, fullPath);
|
|
188
|
-
}
|
|
189
|
-
} else {
|
|
190
|
-
fileNames.push(fullPath);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
let readDirTime = Date.now();
|
|
197
|
-
// Start from the root directory of our namespace
|
|
198
|
-
const rootDirectory = await this.getOrCreateDirectory("");
|
|
199
|
-
await readDir(rootDirectory, "");
|
|
200
|
-
console.log(`readDir took ${formatTime(Date.now() - readDirTime)}`);
|
|
201
|
-
|
|
202
|
-
let results = config?.type === "folders" ? folderNames : fileNames;
|
|
203
|
-
results = results.filter(name => name.startsWith(prefix));
|
|
204
|
-
|
|
205
|
-
if (config?.shallow) {
|
|
206
|
-
let targetDepth = prefix.split("/").length;
|
|
207
|
-
results = results.filter(name => name.split("/").length === targetDepth);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return results;
|
|
211
|
-
} catch (e: any) {
|
|
212
|
-
if (e.name === "NotFoundError") {
|
|
213
|
-
return [];
|
|
214
|
-
}
|
|
215
|
-
throw e;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
@measureFnc
|
|
220
|
-
public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<{ path: string; createTime: number; size: number; }[]> {
|
|
221
|
-
let files = await this.find(prefix, config);
|
|
222
|
-
return (await Promise.all(files.map(async file => {
|
|
223
|
-
try {
|
|
224
|
-
const info = await this.getInfo(file);
|
|
225
|
-
if (!info) return undefined;
|
|
226
|
-
return { path: file, createTime: info.writeTime, size: info.size };
|
|
227
|
-
} catch {
|
|
228
|
-
return undefined;
|
|
229
|
-
}
|
|
230
|
-
}))).filter(isDefined);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
@measureFnc
|
|
234
|
-
public async getInfo(pathInput: string): Promise<{
|
|
235
|
-
writeTime: number;
|
|
236
|
-
size: number;
|
|
237
|
-
} | undefined> {
|
|
238
|
-
const pathParts = pathInput.split("/");
|
|
239
|
-
const filename = pathParts[pathParts.length - 1];
|
|
240
|
-
const dirPath = pathParts.length > 1 ? pathParts.slice(0, -1).join("/") : "";
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
const directory = await this.getOrCreateDirectory(dirPath);
|
|
244
|
-
const fileHandle = await directory.getFileHandle(filename, { create: false });
|
|
245
|
-
const file = await fileHandle.getFile();
|
|
246
|
-
|
|
247
|
-
return {
|
|
248
|
-
writeTime: file.lastModified,
|
|
249
|
-
size: file.size,
|
|
250
|
-
};
|
|
251
|
-
} catch (e: any) {
|
|
252
|
-
if (e.name === "NotFoundError") {
|
|
253
|
-
return undefined;
|
|
254
|
-
}
|
|
255
|
-
throw e;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
public async assertPathValid(path: string) {
|
|
260
|
-
// Private file system has fewer restrictions than regular file system
|
|
261
|
-
// Most characters are allowed, so we just check for obviously problematic patterns
|
|
262
|
-
if (path.includes("..")) {
|
|
263
|
-
throw new Error(`Invalid path contains '..': ${path}`);
|
|
264
|
-
}
|
|
265
|
-
if (path.startsWith("/")) {
|
|
266
|
-
throw new Error(`Invalid path starts with '/': ${path}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
@measureFnc
|
|
271
|
-
public async move(config: {
|
|
272
|
-
path: string;
|
|
273
|
-
target: Archives;
|
|
274
|
-
targetPath: string;
|
|
275
|
-
}) {
|
|
276
|
-
let { path, target, targetPath } = config;
|
|
277
|
-
|
|
278
|
-
// Unwrap any nested archives
|
|
279
|
-
while (true) {
|
|
280
|
-
let targetUnwrapped = target.getBaseArchives?.();
|
|
281
|
-
if (!targetUnwrapped) break;
|
|
282
|
-
target = targetUnwrapped.archives;
|
|
283
|
-
targetPath = targetUnwrapped.parentPath + targetPath;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// For moves, we can optimize if both source and target are private file system
|
|
287
|
-
if (target instanceof ArchivesPrivateFileSystem) {
|
|
288
|
-
// Private File System API doesn't have a native move operation
|
|
289
|
-
// So we fall back to copy + delete
|
|
290
|
-
await this.copy({ path, target, targetPath });
|
|
291
|
-
await this.del(path);
|
|
292
|
-
} else {
|
|
293
|
-
// Moving to a different archive type - copy then delete
|
|
294
|
-
let data = await this.get(path);
|
|
295
|
-
if (!data) throw new Error(`File not found to move: ${path}`);
|
|
296
|
-
await target.set(targetPath, data);
|
|
297
|
-
await this.del(path);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
@measureFnc
|
|
302
|
-
public async copy(config: {
|
|
303
|
-
path: string;
|
|
304
|
-
target: Archives;
|
|
305
|
-
targetPath: string;
|
|
306
|
-
}) {
|
|
307
|
-
let { path, target, targetPath } = config;
|
|
308
|
-
|
|
309
|
-
// Unwrap any nested archives
|
|
310
|
-
while (true) {
|
|
311
|
-
let targetUnwrapped = target.getBaseArchives?.();
|
|
312
|
-
if (!targetUnwrapped) break;
|
|
313
|
-
target = targetUnwrapped.archives;
|
|
314
|
-
targetPath = targetUnwrapped.parentPath + targetPath;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Get the data and set it in the target
|
|
318
|
-
let data = await this.get(path);
|
|
319
|
-
if (!data) throw new Error(`File not found to copy: ${path}`);
|
|
320
|
-
await target.set(targetPath, data);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export const getArchivesPrivateFileSystem = cache((rootDir: string): Archives => {
|
|
325
|
-
return new ArchivesPrivateFileSystem(rootDir);
|
|
326
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
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);
|