sliftutils 1.7.70 → 1.7.72

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 (91) hide show
  1. package/dist/treeSummary.ts.cache +2 -2
  2. package/index.d.ts +505 -257
  3. package/misc/dist/environment.ts.cache +2 -2
  4. package/misc/dist/getSecret.ts.cache +2 -2
  5. package/misc/dist/strings.ts.cache +2 -2
  6. package/misc/dist/zip.ts.cache +2 -2
  7. package/misc/https/dist/certs.ts.cache +2 -2
  8. package/misc/https/dist/hostServer.ts.cache +5 -2
  9. package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
  10. package/package.json +1 -1
  11. package/render-utils/dist/observer.tsx.cache +2 -2
  12. package/spec.txt +10 -0
  13. package/storage/ArchivesDisk.d.ts +4 -13
  14. package/storage/ArchivesDisk.ts +6 -4
  15. package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +2 -2
  16. package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +2 -2
  17. package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +2 -2
  18. package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +2 -2
  19. package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +2 -2
  20. package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +2 -2
  21. package/storage/BulkDatabase2/dist/blockCache.ts.cache +2 -2
  22. package/storage/BulkDatabase2/dist/mergeLock.ts.cache +2 -2
  23. package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +2 -2
  24. package/storage/BulkDatabase2/dist/streamLog.ts.cache +2 -2
  25. package/storage/BulkDatabase2/dist/syncClient.ts.cache +2 -2
  26. package/storage/IArchives.d.ts +14 -10
  27. package/storage/IArchives.ts +16 -4
  28. package/storage/backblaze.d.ts +3 -9
  29. package/storage/backblaze.ts +22 -19
  30. package/storage/dist/ArchivesDisk.ts.cache +6 -3
  31. package/storage/dist/FileFolderAPI.tsx.cache +2 -2
  32. package/storage/dist/IArchives.ts.cache +18 -4
  33. package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
  34. package/storage/dist/JSONStorage.ts.cache +2 -2
  35. package/storage/dist/PendingManager.tsx.cache +2 -2
  36. package/storage/dist/TransactionStorage.ts.cache +2 -2
  37. package/storage/dist/backblaze.ts.cache +36 -27
  38. package/storage/dist/fileSystemPointer.ts.cache +2 -2
  39. package/storage/dist/remoteFileStorage.ts.cache +2 -2
  40. package/storage/remoteStorage/ArchivesRemote.d.ts +5 -9
  41. package/storage/remoteStorage/ArchivesRemote.ts +25 -20
  42. package/storage/remoteStorage/ArchivesUrl.d.ts +3 -9
  43. package/storage/remoteStorage/ArchivesUrl.ts +7 -5
  44. package/storage/remoteStorage/accessPage.tsx +4 -4
  45. package/storage/remoteStorage/accessStats.d.ts +2 -0
  46. package/storage/remoteStorage/accessStats.ts +32 -0
  47. package/storage/remoteStorage/blobStore.d.ts +193 -88
  48. package/storage/remoteStorage/blobStore.ts +535 -400
  49. package/storage/remoteStorage/bucketDisk.d.ts +18 -0
  50. package/storage/remoteStorage/bucketDisk.ts +66 -0
  51. package/storage/remoteStorage/createArchives.d.ts +4 -10
  52. package/storage/remoteStorage/createArchives.ts +102 -63
  53. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +29 -20
  54. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +8 -5
  55. package/storage/remoteStorage/dist/accessPage.tsx.cache +6 -6
  56. package/storage/remoteStorage/dist/accessStats.ts.cache +39 -4
  57. package/storage/remoteStorage/dist/blobStore.ts.cache +517 -375
  58. package/storage/remoteStorage/dist/bucketDisk.ts.cache +72 -0
  59. package/storage/remoteStorage/dist/cliArgs.ts.cache +2 -2
  60. package/storage/remoteStorage/dist/createArchives.ts.cache +112 -58
  61. package/storage/remoteStorage/dist/deployTakeover.ts.cache +2 -2
  62. package/storage/remoteStorage/dist/intermediateSources.ts.cache +13 -13
  63. package/storage/remoteStorage/dist/remoteConfig.ts.cache +19 -3
  64. package/storage/remoteStorage/dist/serverConfig.ts.cache +116 -0
  65. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +7 -6
  66. package/storage/remoteStorage/dist/sourcesList.ts.cache +6 -3
  67. package/storage/remoteStorage/dist/storageClientController.ts.cache +2 -2
  68. package/storage/remoteStorage/dist/storageController.ts.cache +224 -191
  69. package/storage/remoteStorage/dist/storageServer.ts.cache +9 -9
  70. package/storage/remoteStorage/dist/storageServerState.ts.cache +330 -567
  71. package/storage/remoteStorage/dist/storePlan.ts.cache +149 -0
  72. package/storage/remoteStorage/dist/validation.ts.cache +40 -0
  73. package/storage/remoteStorage/grantAccessCli.ts +1 -1
  74. package/storage/remoteStorage/intermediateSources.d.ts +3 -5
  75. package/storage/remoteStorage/intermediateSources.ts +14 -16
  76. package/storage/remoteStorage/remoteConfig.d.ts +2 -2
  77. package/storage/remoteStorage/remoteConfig.ts +19 -3
  78. package/storage/remoteStorage/serverConfig.d.ts +27 -0
  79. package/storage/remoteStorage/serverConfig.ts +109 -0
  80. package/storage/remoteStorage/sourceWrapper.d.ts +3 -3
  81. package/storage/remoteStorage/sourceWrapper.ts +7 -6
  82. package/storage/remoteStorage/sourcesList.ts +3 -0
  83. package/storage/remoteStorage/storageController.d.ts +118 -36
  84. package/storage/remoteStorage/storageController.ts +166 -168
  85. package/storage/remoteStorage/storageServer.ts +1 -1
  86. package/storage/remoteStorage/storageServerState.d.ts +52 -63
  87. package/storage/remoteStorage/storageServerState.ts +340 -584
  88. package/storage/remoteStorage/storePlan.d.ts +34 -0
  89. package/storage/remoteStorage/storePlan.ts +166 -0
  90. package/storage/remoteStorage/validation.d.ts +4 -0
  91. package/storage/remoteStorage/validation.ts +31 -0
@@ -6,16 +6,20 @@ import { performLocalCall } from "socket-function/src/callManager";
6
6
  import { RequireController } from "socket-function/require/RequireController";
7
7
  import { timeInMinute } from "socket-function/src/misc";
8
8
  import { getCommonName, getPublicIdentifier, getOwnMachineId, verify, verifyMachineIdForPublicKey } from "../../misc/https/certs";
9
- import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, IMMUTABLE_CACHE_TIME } from "../IArchives";
10
- import { ROUTING_FILE } from "./remoteConfig";
9
+ import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, FindConfig, SourceConfig, IMMUTABLE_CACHE_TIME } from "../IArchives";
10
+ import { ROUTING_FILE, getRoute, routeContains } from "./remoteConfig";
11
11
  import {
12
- getStorageServerConfig, getTrust, getRequests, getLoadedBucket, writeBucketFile,
13
- deleteBucketFile, assertWritesAllowed, assertMutable, LoadedBucket,
14
- getBucketConfig, listAccountBuckets, ServerBucketInfo, clearAccountWriteStats,
15
- getActiveBucket, activateBucket, ActiveBucketInfo, getActiveBucketKeys,
12
+ getLoadedBucket, requireBucket, findBucketStore, readBucketInternal, queueRoutingConfigWrite,
13
+ getBucketConfig, bucketSyncStatus, bucketIndexTotals, LoadedStore, ActiveBucketInfo,
14
+ listAccountBuckets, ServerBucketInfo, clearAccountWriteStats,
15
+ getActiveBucket, activateBucket, getActiveBucketKeys,
16
16
  } from "./storageServerState";
17
+ import { getStorageServerConfig, getTrust, getRequests, assertWritesAllowed } from "./serverConfig";
18
+ import { IBucketStore } from "./blobStore";
19
+ import { getRoutingFileResult } from "./bucketDisk";
17
20
  import { StorageClientController } from "./storageClientController";
18
- import { trackAccess, getAccessTotals, readAccessSummaries, clearAccountAccessStats, AccessTotals, AccessSummaryState } from "./accessStats";
21
+ import { trackAccess, trackAccessCall, getAccessTotals, readAccessSummaries, clearAccountAccessStats, AccessTotals, AccessSummaryState } from "./accessStats";
22
+ import { assertValidName, assertValidPath, assertValidArgs } from "./validation";
19
23
  import type { SummaryEntry } from "../../treeSummary";
20
24
 
21
25
  export const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
@@ -69,23 +73,6 @@ const CONTENT_TYPES: { [ext: string]: string } = {
69
73
  mp3: "audio/mpeg", wav: "audio/wav", pdf: "application/pdf",
70
74
  };
71
75
 
72
- function assertValidName(value: string, kind: string) {
73
- if (!/^[\w-]{1,64}$/.test(value)) {
74
- throw new Error(`Invalid ${kind} ${JSON.stringify(value)}, expected 1-64 characters of letters/numbers/underscore/dash`);
75
- }
76
- }
77
- function assertValidPath(path: string) {
78
- if (Buffer.from(path, "utf8").length > 1000) {
79
- throw new Error(`Path too long: ${path.length} characters > 1000. Path: ${path.slice(0, 200)}`);
80
- }
81
- if (!path || path.startsWith("/") || path.endsWith("/") || path.includes("//") || path.includes("\\") || path.includes("\x00")) {
82
- throw new Error(`Invalid path ${JSON.stringify(path.slice(0, 200))}, paths cannot be empty, start or end with /, or contain //, backslashes, or null characters`);
83
- }
84
- if (path.split("/").some(part => part === "." || part === "..")) {
85
- throw new Error(`Invalid path ${JSON.stringify(path.slice(0, 200))}, paths cannot contain . or .. segments`);
86
- }
87
- }
88
-
89
76
  const connectedClients = new Set<string>();
90
77
  function trackCaller(): void {
91
78
  let nodeId = SocketFunction.getCaller().nodeId;
@@ -142,12 +129,6 @@ function getGrantAccessCommand(requestId: string): string {
142
129
  return `ssh ${sshTarget} '${serverCommand} --requestId ${requestId}'`;
143
130
  }
144
131
 
145
- async function getBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined> {
146
- assertValidName(account, "account");
147
- assertValidName(bucketName, "bucket name");
148
- return await getLoadedBucket(account, bucketName);
149
- }
150
-
151
132
  class RemoteStorageControllerBase {
152
133
  async ping(): Promise<{}> {
153
134
  trackCaller();
@@ -182,8 +163,9 @@ class RemoteStorageControllerBase {
182
163
  return { machineId, ip: getCallerIP() };
183
164
  }
184
165
 
185
- async requestAccess(account: string): Promise<{ machineId: string; ip: string; requestId: string; grantAccessCommand: string }> {
186
- assertValidName(account, "account");
166
+ @assertValidArgs
167
+ async requestAccess(config: { account: string }): Promise<{ machineId: string; ip: string; requestId: string; grantAccessCommand: string }> {
168
+ let account = config.account;
187
169
  let machineId = getCallerMachineId();
188
170
  let ip = getCallerIP();
189
171
  let requestsStorage = await getRequests();
@@ -206,8 +188,9 @@ class RemoteStorageControllerBase {
206
188
  return { machineId, ip, requestId: existing.requestId, grantAccessCommand: getGrantAccessCommand(existing.requestId) };
207
189
  }
208
190
 
209
- async getAccessState(account: string): Promise<AccessState> {
210
- assertValidName(account, "account");
191
+ @assertValidArgs
192
+ async getAccessState(config: { account: string }): Promise<AccessState> {
193
+ let account = config.account;
211
194
  let machineId = getCallerMachineId();
212
195
  let ip = getCallerIP();
213
196
  let trust = await getTrust();
@@ -232,12 +215,13 @@ class RemoteStorageControllerBase {
232
215
  return result;
233
216
  }
234
217
 
235
- async listRequestsForIP(account: string, ip: string): Promise<AccessRequest[]> {
218
+ async listRequestsForIP(config: { account: string; ip: string }): Promise<AccessRequest[]> {
236
219
  let requests = await getRequests();
237
- return (await requests.get(ip) || []).filter(x => x.account === account);
220
+ return (await requests.get(config.ip) || []).filter(x => x.account === config.account);
238
221
  }
239
222
 
240
- async grantAccess(requestId: string): Promise<TrustRecord> {
223
+ async grantAccess(config: { requestId: string }): Promise<TrustRecord> {
224
+ let requestId = config.requestId;
241
225
  let callerMachineId = getCallerMachineId();
242
226
  let trust = await getTrust();
243
227
  let requests = await getRequests();
@@ -264,11 +248,12 @@ class RemoteStorageControllerBase {
264
248
  async adminListActiveBuckets(): Promise<{ account: string; bucketName: string }[]> {
265
249
  return getActiveBucketKeys();
266
250
  }
267
- async adminListRequests(ip: string): Promise<AccessRequest[]> {
251
+ async adminListRequests(config: { ip: string }): Promise<AccessRequest[]> {
268
252
  let requests = await getRequests();
269
- return await requests.get(ip) || [];
253
+ return await requests.get(config.ip) || [];
270
254
  }
271
- async adminGrantAccess(requestId: string): Promise<TrustRecord> {
255
+ async adminGrantAccess(config: { requestId: string }): Promise<TrustRecord> {
256
+ let requestId = config.requestId;
272
257
  let trust = await getTrust();
273
258
  let requests = await getRequests();
274
259
  for (let ip of await requests.getKeys()) {
@@ -287,159 +272,139 @@ class RemoteStorageControllerBase {
287
272
  throw new Error(`No access request found with id ${JSON.stringify(requestId)}. It may have already been granted or expired.`);
288
273
  }
289
274
 
290
- async get(account: string, bucketName: string, path: string, range?: { start: number; end: number }): Promise<Buffer | undefined> {
291
- let result = await this.get2(account, bucketName, path, range);
292
- return result && result.data || undefined;
293
- }
294
- async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }, internal?: boolean): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
295
- assertValidPath(path);
296
- let bucket = await getBucket(account, bucketName);
297
- let result: { data: Buffer; writeTime: number; size: number } | undefined;
298
- if (bucket) {
299
- if (internal && bucket.store.getInternal2) {
300
- result = await bucket.store.getInternal2(path, { range });
301
- } else {
302
- result = await bucket.store.get2(path, { range });
303
- }
275
+ @assertValidArgs
276
+ @trackAccessCall("get")
277
+ async get2(config: { account: string; bucketName: string; path: string; sourceConfig: SourceConfig; range?: { start: number; end: number }; internal?: boolean; includeTombstones?: boolean }): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
278
+ if (config.path === ROUTING_FILE) {
279
+ // The routing file lives outside every store and even outside the bucket (it is what CREATES it), so it is read straight off the disk - absent means undefined, exactly like any file read
280
+ return await getRoutingFileResult(config.account, config.bucketName);
304
281
  }
305
- trackAccess({ account, operation: "get", path: `${bucketName}/${path}`, size: result && result.data.length || 0 });
306
- return result;
307
- }
308
- async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean, internal?: boolean): Promise<void> {
309
- assertValidName(bucketName, "bucket name");
310
- assertValidPath(path);
311
- if (!data.length) {
312
- throw new Error(`set was called with an empty buffer for ${JSON.stringify(path)} in bucket ${account}/${bucketName}: an empty file IS a deletion in this system and would read back as missing - call del instead`);
282
+ if (config.internal) {
283
+ return await readBucketInternal(config.account, config.bucketName, config);
313
284
  }
314
- trackAccess({ account, operation: "set", path: `${bucketName}/${path}`, size: data.length });
315
- await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified, forceSetImmutable, internal });
285
+ return await withStore(config, store => store.get2(config));
316
286
  }
317
- async del(account: string, bucketName: string, path: string, lastModified?: number, internal?: boolean): Promise<void> {
318
- assertValidName(bucketName, "bucket name");
319
- assertValidPath(path);
320
- trackAccess({ account, operation: "del", path: `${bucketName}/${path}` });
321
- await deleteBucketFile(account, bucketName, path, { lastModified, internal });
322
- }
323
- async getInfo(account: string, bucketName: string, path: string, includeTombstones?: boolean): Promise<{ writeTime: number; size: number } | undefined> {
324
- assertValidPath(path);
325
- trackAccess({ account, operation: "getInfo", path: `${bucketName}/${path}` });
326
- let bucket = await getBucket(account, bucketName);
327
- if (!bucket) return undefined;
328
- return await bucket.store.getInfo(path, { includeTombstones });
329
- }
330
- async findInfo(account: string, bucketName: string, prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
331
- let bucket = await getBucket(account, bucketName);
332
- let results = bucket && await bucket.store.findInfo(prefix, config) || [];
333
- // The paths are only known once the results are in; an empty result still counts as one access at the prefix.
334
- if (results.length) {
335
- for (let info of results) {
336
- trackAccess({ account, operation: "findInfo", path: `${bucketName}/${info.path}` });
337
- }
338
- } else {
339
- trackAccess({ account, operation: "findInfo", path: `${bucketName}/${prefix}` });
287
+ @assertValidArgs
288
+ @trackAccessCall("set")
289
+ async set(config: { account: string; bucketName: string; path: string; data: Buffer; sourceConfig: SourceConfig; lastModified?: number; forceSetImmutable?: boolean; internal?: boolean }): Promise<void> {
290
+ assertWritesAllowed();
291
+ // Copied because the wire hands us a plain Uint8Array view, not a real Buffer
292
+ let data = Buffer.from(config.data);
293
+ if (config.path === ROUTING_FILE) {
294
+ return await queueRoutingConfigWrite(config.account, config.bucketName, data, config);
340
295
  }
341
- return results;
342
- }
343
- async getChangesAfter2(account: string, bucketName: string, config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
344
- let bucket = await getBucket(account, bucketName);
345
- if (!bucket) return [];
346
- let results = await bucket.store.getChangesAfter2(config);
347
- for (let info of results) {
348
- trackAccess({ account, operation: "getChangesAfter", path: `${bucketName}/${info.path}` });
296
+ await withStore(config, store => store.set({ ...config, data }));
297
+ }
298
+ @assertValidArgs
299
+ @trackAccessCall("del")
300
+ async del(config: { account: string; bucketName: string; path: string; sourceConfig: SourceConfig; lastModified?: number; internal?: boolean }): Promise<void> {
301
+ assertWritesAllowed();
302
+ await withStore(config, store => store.del(config));
303
+ }
304
+ @assertValidArgs
305
+ @trackAccessCall("getInfo")
306
+ async getInfo(config: { account: string; bucketName: string; path: string; sourceConfig: SourceConfig; includeTombstones?: boolean }): Promise<{ writeTime: number; size: number } | undefined> {
307
+ if (config.path === ROUTING_FILE) {
308
+ let result = await getRoutingFileResult(config.account, config.bucketName);
309
+ return result && { writeTime: result.writeTime, size: result.size } || undefined;
349
310
  }
350
- return results;
311
+ return await withStore(config, store => store.getInfo(config));
351
312
  }
352
- async getArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
353
- let bucket = await getBucket(account, bucketName);
354
- if (!bucket) return { supportsChangesAfter: true };
355
- return getBucketConfig(bucket);
313
+ @assertValidArgs
314
+ @trackAccessCall("findInfo")
315
+ async findInfo(config: FindConfig & { account: string; bucketName: string; prefix: string; sourceConfig: SourceConfig }): Promise<ArchiveFileInfo[]> {
316
+ return await withStore(config, store => store.findInfo(config));
356
317
  }
357
- async listBuckets(account: string): Promise<ServerBucketInfo[]> {
318
+ @assertValidArgs
319
+ @trackAccessCall("getChangesAfter")
320
+ async getChangesAfter2(config: { account: string; bucketName: string; sourceConfig: SourceConfig; time: number; routes?: [number, number][] }): Promise<ArchiveFileInfo[]> {
321
+ return await withStore(config, store => store.getChangesAfter2(config));
322
+ }
323
+ @assertValidArgs
324
+ async getArchivesConfig(config: { account: string; bucketName: string }): Promise<ArchivesConfig> {
325
+ return getBucketConfig(await requireBucket(config.account, config.bucketName));
326
+ }
327
+ @assertValidArgs
328
+ async listBuckets(config: { account: string }): Promise<ServerBucketInfo[]> {
358
329
  let start = Date.now();
359
330
  try {
360
- return await listAccountBuckets(account);
331
+ return await listAccountBuckets(config.account);
361
332
  } finally {
362
333
  // The access hook (and the storage it initializes) runs before this, so a large gap between this and the caller's timing is the hook
363
- console.log(`listBuckets(${account}) call body took ${Date.now() - start}ms`);
334
+ console.log(`listBuckets(${config.account}) call body took ${Date.now() - start}ms`);
364
335
  }
365
336
  }
366
337
  /** The live, in-memory state of one bucket, or a string saying why it is unavailable. Answered without touching the disk, so it is cheap - but only works while the bucket is loaded here. */
367
- async getActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
368
- assertValidName(bucketName, "bucket name");
369
- return await getActiveBucket(account, bucketName);
338
+ @assertValidArgs
339
+ async getActiveBucket(config: { account: string; bucketName: string }): Promise<ActiveBucketInfo | string> {
340
+ return await getActiveBucket(config.account, config.bucketName);
370
341
  }
371
342
  /** Loads a bucket that exists on this server into memory (starting its synchronization) and returns its live state, or a string saying why it could not be loaded. */
372
- async activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
373
- assertValidName(bucketName, "bucket name");
374
- return await activateBucket(account, bucketName);
343
+ @assertValidArgs
344
+ async activateBucket(config: { account: string; bucketName: string }): Promise<ActiveBucketInfo | string> {
345
+ return await activateBucket(config.account, config.bucketName);
375
346
  }
376
347
  /** Zeroes the write statistics listBuckets reports and the in-memory access statistics, for every bucket in the account. */
377
- async clearWriteStats(account: string): Promise<{ clearedBuckets: number }> {
378
- assertValidName(account, "account");
379
- clearAccountAccessStats(account);
380
- return { clearedBuckets: clearAccountWriteStats(account) };
348
+ @assertValidArgs
349
+ async clearWriteStats(config: { account: string }): Promise<{ clearedBuckets: number }> {
350
+ clearAccountAccessStats(config.account);
351
+ return { clearedBuckets: clearAccountWriteStats(config.account) };
381
352
  }
382
353
  /** In-memory totals per operation type since startup (or the last clearWriteStats). */
383
- async getAccessStats(account: string): Promise<AccessTotals> {
384
- assertValidName(account, "account");
385
- return getAccessTotals(account);
354
+ @assertValidArgs
355
+ async getAccessStats(config: { account: string }): Promise<AccessTotals> {
356
+ return getAccessTotals(config.account);
386
357
  }
387
358
  /** A path breakdown of one operation's accesses (operation names come from getAccessStats). maxCount is passed straight to TreeSummary.getSummaries. weightBySize is ignored for count-only operations, which return their count breakdown. */
388
- async getAccessSummaries(account: string, config: { operation: string; maxCount: number; weightBySize?: boolean }): Promise<SummaryEntry<AccessSummaryState>[]> {
389
- assertValidName(account, "account");
390
- return readAccessSummaries({ account, ...config });
391
- }
392
- async getIndexInfo(account: string, bucketName: string): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] } | undefined> {
393
- let bucket = await getBucket(account, bucketName);
394
- if (!bucket || !bucket.store.computeIndexTotals) return undefined;
395
- return await bucket.store.computeIndexTotals();
396
- }
397
- async getSyncStatus(account: string, bucketName: string): Promise<ArchivesSyncStatus> {
398
- let bucket = await getBucket(account, bucketName);
399
- if (!bucket) return { allScansComplete: true, indexSize: 0, sources: [] };
400
- if (!bucket.store.getSyncStatus) {
401
- throw new Error(`Bucket ${account}/${bucketName} does not support getSyncStatus (rawDisk buckets have no synchronization)`);
402
- }
403
- return await bucket.store.getSyncStatus();
359
+ @assertValidArgs
360
+ async getAccessSummaries(config: { account: string; operation: string; maxCount: number; weightBySize?: boolean }): Promise<SummaryEntry<AccessSummaryState>[]> {
361
+ return readAccessSummaries(config);
362
+ }
363
+ @assertValidArgs
364
+ async getIndexInfo(config: { account: string; bucketName: string }): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] } | undefined> {
365
+ return await bucketIndexTotals(await requireBucket(config.account, config.bucketName));
366
+ }
367
+ @assertValidArgs
368
+ async getSyncStatus(config: { account: string; bucketName: string }): Promise<ArchivesSyncStatus> {
369
+ return await bucketSyncStatus(await requireBucket(config.account, config.bucketName));
404
370
  }
405
371
 
406
- async startLargeFile(account: string, bucketName: string, path: string, lastModified?: number): Promise<string> {
372
+ @assertValidArgs
373
+ async startLargeFile(config: { account: string; bucketName: string; path: string; sourceConfig: SourceConfig; lastModified?: number }): Promise<string> {
407
374
  assertWritesAllowed();
408
- assertValidPath(path);
409
- let bucket = await getBucket(account, bucketName);
410
- if (!bucket) {
411
- throw new Error(`Bucket ${account}/${bucketName} does not exist. Write its routing config to ${JSON.stringify(ROUTING_FILE)} to create it.`);
412
- }
413
- await assertMutable(bucket, path, lastModified || Date.now());
414
- let id = await bucket.store.startLargeUpload();
415
- largeUploadInfo.set(id, { account, bucketName, path, lastModified });
375
+ let target = await findBucketStore(config.account, config.bucketName, config.sourceConfig);
376
+ let id = await target.store.startLargeUpload({ path: config.path, lastModified: config.lastModified });
377
+ // routeKey pins the upload's parts and finish to the same store the start picked (in-flight uploads survive a bucket rebuild: the part data lives in the store's folder, which a rebuilt store still sees)
378
+ largeUploadInfo.set(id, { account: config.account, bucketName: config.bucketName, path: config.path, lastModified: config.lastModified, routeKey: target.routeKey });
416
379
  return id;
417
380
  }
418
- async uploadPart(uploadId: string, data: Buffer): Promise<void> {
381
+ async uploadPart(config: { uploadId: string; data: Buffer }): Promise<void> {
419
382
  assertWritesAllowed();
420
- let info = largeUploadInfo.get(uploadId);
421
- if (!info) throw new Error(`Unknown large upload ${uploadId}`);
422
- trackAccess({ account: info.account, operation: "uploadPart", path: `${info.bucketName}/${info.path}`, size: data.length });
423
- let bucket = await getBucket(info.account, info.bucketName);
424
- if (!bucket) throw new Error(`Bucket ${info.account}/${info.bucketName} no longer exists`);
425
- await bucket.store.appendLargeUpload(uploadId, Buffer.from(data));
426
- }
427
- async finishLargeFile(uploadId: string): Promise<void> {
383
+ let info = largeUploadInfo.get(config.uploadId);
384
+ if (!info) throw new Error(`Unknown large upload ${config.uploadId}`);
385
+ trackAccess({ account: info.account, operation: "uploadPart", path: `${info.bucketName}/${info.path}`, size: config.data.length });
386
+ let target = await findUploadStore(info);
387
+ await target.store.appendLargeUpload({ id: config.uploadId, data: Buffer.from(config.data) });
388
+ }
389
+ async finishLargeFile(config: { uploadId: string }): Promise<void> {
428
390
  assertWritesAllowed();
429
- let info = largeUploadInfo.get(uploadId);
430
- if (!info) throw new Error(`Unknown large upload ${uploadId}`);
431
- largeUploadInfo.delete(uploadId);
432
- let bucket = await getBucket(info.account, info.bucketName);
433
- if (!bucket) throw new Error(`Bucket ${info.account}/${info.bucketName} no longer exists`);
434
- await bucket.store.finishLargeUpload(uploadId, info.path, info.lastModified);
435
- }
436
- async cancelLargeFile(uploadId: string): Promise<void> {
437
- let info = largeUploadInfo.get(uploadId);
391
+ let info = largeUploadInfo.get(config.uploadId);
392
+ if (!info) throw new Error(`Unknown large upload ${config.uploadId}`);
393
+ largeUploadInfo.delete(config.uploadId);
394
+ let target = await findUploadStore(info);
395
+ await target.store.finishLargeUpload({ id: config.uploadId, path: info.path, lastModified: info.lastModified });
396
+ }
397
+ /** Best-effort cleanup: an upload whose bucket or store has since vanished has nothing left to cancel. */
398
+ async cancelLargeFile(config: { uploadId: string }): Promise<void> {
399
+ let info = largeUploadInfo.get(config.uploadId);
438
400
  if (!info) return;
439
- largeUploadInfo.delete(uploadId);
440
- let bucket = await getBucket(info.account, info.bucketName);
401
+ largeUploadInfo.delete(config.uploadId);
402
+ let bucket = await getLoadedBucket(info.account, info.bucketName);
441
403
  if (!bucket) return;
442
- await bucket.store.cancelLargeUpload(uploadId);
404
+ const routeKey = info.routeKey;
405
+ let target = bucket.stores.find(x => x.routeKey === routeKey);
406
+ if (!target) return;
407
+ await target.store.cancelLargeUpload({ id: config.uploadId });
443
408
  }
444
409
 
445
410
  // IMPORTANT: We can never expose enumeration (listing, prefix search, changes feeds) over this public HTTP endpoint - only exact-key reads. Enumeration would be a massive security risk (public buckets rely on unguessable keys staying unguessable), and could also crash the client by sending them too much data. Listings exist only on the authenticated API (findInfo etc, behind accountAccess).
@@ -472,7 +437,16 @@ class RemoteStorageControllerBase {
472
437
  if (!bucket.self?.public) {
473
438
  throw new Error(`Bucket ${account}/${bucketName} is not public, so its files cannot be read over plain URLs`);
474
439
  }
475
- let info = await bucket.store.getInfo(filePath);
440
+ // Anonymous URL reads carry no source selection - the file's route picks the store, and a route no store here covers is simply not found (the routing file itself lives outside every store)
441
+ let httpStore: LoadedStore | undefined;
442
+ let info: { writeTime: number; size: number } | undefined;
443
+ if (filePath !== ROUTING_FILE) {
444
+ let route = getRoute(filePath);
445
+ httpStore = bucket.stores.find(s => routeContains(s.route, route));
446
+ info = httpStore && await httpStore.store.getInfo({ path: filePath }) || undefined;
447
+ } else {
448
+ info = await getRoutingFileResult(account, bucketName);
449
+ }
476
450
  if (!info || !info.size) {
477
451
  trackAccess({ account, operation: "httpGet", path: `${bucketName}/${filePath}`, size: 0 });
478
452
  return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
@@ -514,7 +488,15 @@ class RemoteStorageControllerBase {
514
488
  range = { start, end: endInclusive + 1 };
515
489
  }
516
490
  }
517
- let result = await bucket.store.get2(filePath, { range });
491
+ let result: { data: Buffer; writeTime: number; size: number } | undefined;
492
+ if (httpStore) {
493
+ result = await httpStore.store.get2({ path: filePath, range });
494
+ } else {
495
+ result = await getRoutingFileResult(account, bucketName);
496
+ if (result && range) {
497
+ result = { ...result, data: result.data.subarray(Math.min(range.start, result.data.length), Math.min(range.end, result.data.length)) };
498
+ }
499
+ }
518
500
  trackAccess({ account, operation: "httpGet", path: `${bucketName}/${filePath}`, size: result && result.data.length || 0 });
519
501
  if (!result) {
520
502
  return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
@@ -526,18 +508,35 @@ class RemoteStorageControllerBase {
526
508
  }
527
509
  }
528
510
 
529
- const largeUploadInfo = new Map<string, { account: string; bucketName: string; path: string; lastModified?: number }>();
511
+ /** The one resolution every data call does: the client's selected sourceConfig maps to exactly one store (loading the bucket - and so instantiating its stores - if needed), and fn runs on it. findBucketStore throws for missing buckets and unmatched configs. */
512
+ async function withStore<T>(config: { account: string; bucketName: string; sourceConfig: SourceConfig }, fn: (store: IBucketStore) => Promise<T>): Promise<T> {
513
+ let target = await findBucketStore(config.account, config.bucketName, config.sourceConfig);
514
+ return await fn(target.store);
515
+ }
516
+
517
+ const largeUploadInfo = new Map<string, { account: string; bucketName: string; path: string; lastModified?: number; routeKey: string }>();
518
+
519
+ async function findUploadStore(info: { account: string; bucketName: string; routeKey: string }): Promise<LoadedStore> {
520
+ let bucket = await requireBucket(info.account, info.bucketName);
521
+ let target = bucket.stores.find(x => x.routeKey === info.routeKey);
522
+ if (!target) {
523
+ throw new Error(`The store (route ${info.routeKey}) this upload targets no longer exists on bucket ${info.account}/${info.bucketName} (available: ${JSON.stringify(bucket.stores.map(x => x.routeKey))})`);
524
+ }
525
+ return target;
526
+ }
530
527
 
531
528
  const accountAccess: SocketFunctionHook = async (context) => {
532
529
  let start = Date.now();
533
- await requireAccess(String(context.call.args[0]));
530
+ let config = context.call.args[0] as { account?: string } | undefined;
531
+ await requireAccess(String(config?.account));
534
532
  let duration = Date.now() - start;
535
533
  if (duration > ACCESS_CHECK_SLOW_TIME) {
536
534
  console.log(`Access check for ${context.call.functionName} took ${duration}ms`);
537
535
  }
538
536
  };
539
537
  const uploadAccess: SocketFunctionHook = async (context) => {
540
- let info = largeUploadInfo.get(String(context.call.args[0]));
538
+ let config = context.call.args[0] as { uploadId?: string } | undefined;
539
+ let info = largeUploadInfo.get(String(config?.uploadId));
541
540
  if (!info) return;
542
541
  await requireAccess(info.account);
543
542
  };
@@ -558,7 +557,6 @@ export const RemoteStorageController = SocketFunction.register(
558
557
  adminListRequests: { hooks: [adminAccess] },
559
558
  adminListActiveBuckets: { hooks: [adminAccess] },
560
559
  adminGrantAccess: { hooks: [adminAccess] },
561
- get: { hooks: [accountAccess] },
562
560
  get2: { hooks: [accountAccess] },
563
561
  set: { hooks: [accountAccess] },
564
562
  del: { hooks: [accountAccess] },
@@ -8,7 +8,7 @@ import { getExternalIP } from "socket-function/src/networking";
8
8
  import { RequireController } from "socket-function/require/RequireController";
9
9
  import { hostServer } from "../../misc/https/hostServer";
10
10
  import { RemoteStorageController } from "./storageController";
11
- import { setStorageServerConfig, setWritesRejectedReason, addExtraListenPort, removeExtraListenPort } from "./storageServerState";
11
+ import { setStorageServerConfig, setWritesRejectedReason, addExtraListenPort, removeExtraListenPort } from "./serverConfig";
12
12
  import { detectDeployTakeover, setAltPort, getAltPortListenEnd, getMainPortAcquireDelay } from "./deployTakeover";
13
13
  import { parseStorageUrl } from "./ArchivesRemote";
14
14
  // Import browser code, so it is allowed to be required by the client
@@ -1,73 +1,61 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
3
  import { IBucketStore } from "./blobStore";
4
- import { RemoteConfig, HostedConfig, IArchives, ArchivesConfig } from "../IArchives";
5
- import type { IStorage } from "../IStorage";
6
- import type { AccessRequest, TrustRecord } from "./storageController";
7
- export type StorageServerConfig = {
8
- domain: string;
9
- port: number;
10
- rootDomain: string;
11
- sshTarget: string;
12
- serverCommand: string;
4
+ import { RemoteConfig, HostedConfig, SourceConfig, IArchives, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
5
+ import { BucketDiskInfo } from "./bucketDisk";
6
+ import { SelfSummary } from "./storePlan";
7
+ export type LoadedStore = {
8
+ routeKey: string;
9
+ route?: [number, number];
10
+ entries: HostedConfig[];
13
11
  folder: string;
12
+ store: IBucketStore;
14
13
  };
15
- export declare function setStorageServerConfig(value: StorageServerConfig): void;
16
- export declare function getStorageServerConfig(): StorageServerConfig;
17
- export declare function getStorageServerConfigOptional(): StorageServerConfig | undefined;
18
- export declare function setWritesRejectedReason(reason: string | undefined): void;
19
- export declare function getWritesRejectedReason(): string | undefined;
20
- export declare function assertWritesAllowed(): void;
21
- export declare function getTrust(): Promise<IStorage<TrustRecord>>;
22
- export declare function getRequests(): Promise<IStorage<AccessRequest[]>>;
23
- export type BucketWriteStats = {
24
- /** Every set call the bucket accepted */
25
- originalWrites: number;
26
- originalBytes: number;
27
- /** What actually reached the sources. Fast writes coalesce repeated writes to the same key, so this is lower than the original counts (and is what the disk actually did). */
28
- flushedWrites: number;
29
- flushedBytes: number;
30
- };
31
- /** Zeroes the write statistics of every bucket in the account. */
32
- export declare function clearAccountWriteStats(account: string): number;
33
- export declare function setTrustedMachines(config: {
34
- account: string;
35
- machineIds: string[];
36
- }): Promise<void>;
37
- export type LoadedBucket = {
14
+ export type BucketState = {
38
15
  account: string;
39
16
  bucketName: string;
40
17
  routing: RemoteConfig;
41
18
  routingJSON: string;
42
19
  selfEntries: HostedConfig[];
43
20
  self: SelfSummary | undefined;
44
- store: IBucketStore;
21
+ stores: LoadedStore[];
45
22
  structureKey: string;
46
23
  };
47
- export declare function addExtraListenPort(port: number): void;
48
- export declare function removeExtraListenPort(port: number): void;
49
- /** Whether address:port is this server process. The ONE self test - findSelfIndexes, createApiArchives, and SourceWrapper all consult it, so "is this me" cannot disagree between the routing plan and connection building: a URL that is us on an extra listen port must never become a network client to ourselves, which is how infinite self-request loops form. */
50
- export declare function isOwnAddress(address: string, port: number): boolean;
24
+ /** The loaded bucket, loading it (which instantiates its stores and starts their synchronization) if needed. A bucket that does not exist on this server throws - callers never see undefined buckets. */
25
+ export declare function requireBucket(account: string, bucketName: string): Promise<BucketState>;
26
+ /** The store serving a request: the exact config entry the CLIENT selected, matched by equality (key order ignored) against the bucket's own entries. A match is honored even when its window has passed - the selection never validates, the store's own validation throws instead. Throws when nothing matches, listing what is available. */
27
+ export declare function findBucketStore(account: string, bucketName: string, sourceConfig: SourceConfig | undefined): Promise<LoadedStore>;
28
+ /** Internal (store-to-store) reads skip store selection entirely: the caller is another store whose index says this MACHINE holds the bytes - the persisted holder identity is just a URL, which cannot name a store. Whichever store's folder has the newest copy answers. */
29
+ export declare function readBucketInternal(account: string, bucketName: string, config: {
30
+ path: string;
31
+ range?: {
32
+ start: number;
33
+ end: number;
34
+ };
35
+ includeTombstones?: boolean;
36
+ }): Promise<{
37
+ data: Buffer;
38
+ writeTime: number;
39
+ size: number;
40
+ } | undefined>;
41
+ export declare function getBucketConfig(bucket: BucketState): ArchivesConfig;
42
+ export declare function bucketSyncStatus(bucket: BucketState): Promise<ArchivesSyncStatus>;
43
+ export declare function bucketIndexTotals(bucket: BucketState): Promise<{
44
+ fileCount: number;
45
+ byteCount: number;
46
+ sources: {
47
+ debugName: string;
48
+ fileCount: number;
49
+ byteCount: number;
50
+ }[];
51
+ } | undefined>;
51
52
  /** A cached IArchives for a persisted source identity: a routing URL (hosted/backblaze) or a disk folder path - the form BlobStore's sources list stores. Configuration (valid windows, routes) decides WHEN a source should be used; for reading bytes the index says a source holds, the URL alone is enough - even for sources no longer in any config. */
52
53
  export declare function resolveSourceArchives(url: string): IArchives;
53
- /** Our role in a bucket's routing config, summarized across ALL currently-valid self entries. Stored instead of a single representative HostedConfig, so nothing can accidentally use one entry's route or flags where the union is required - the standard config has the same URL twice: a routed write-shard entry plus an unrouted read-everything entry. */
54
- export type SelfSummary = {
55
- /** The union of the current entries' routes, with overlapping/adjacent ranges combined - which commonly collapses to a single full range, making matching trivial. */
56
- routes: [number, number][];
57
- public: boolean;
58
- immutable: boolean;
59
- noFullSync: boolean;
60
- rawDisk: boolean;
61
- readerDiskLimit?: number;
62
- };
63
- export declare function getLoadedBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined>;
64
- export declare function assertMutable(bucket: LoadedBucket, filePath: string, writeTime: number): Promise<void>;
65
- export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
54
+ export declare function getLoadedBucket(account: string, bucketName: string): Promise<BucketState | undefined>;
55
+ /** The routing-config write path - the ONE write that cannot go through a store (it is what CREATES the bucket and its stores). Serialized per bucket: concurrent config writes would race the version check. */
56
+ export declare function queueRoutingConfigWrite(account: string, bucketName: string, data: Buffer, config?: {
66
57
  lastModified?: number;
67
- forceSetImmutable?: boolean;
68
- internal?: boolean;
69
58
  }): Promise<void>;
70
- export declare function getBucketConfig(bucket: LoadedBucket): ArchivesConfig;
71
59
  /** Which buckets this process currently has loaded - what a deploy successor asks its predecessor for, so it activates exactly the buckets that are actually in use. */
72
60
  export declare function getActiveBucketKeys(): {
73
61
  account: string;
@@ -80,11 +68,6 @@ export declare const startIntermediateMaintenance: {
80
68
  reset(): void;
81
69
  set(newValue: void): void;
82
70
  };
83
- export type BucketDiskInfo = {
84
- totalBytes: number;
85
- freeBytes: number;
86
- usedBytes: number;
87
- };
88
71
  export type ServerBucketInfo = {
89
72
  bucketName: string;
90
73
  active: boolean;
@@ -111,8 +94,14 @@ export declare function getActiveBucket(account: string, bucketName: string): Pr
111
94
  /** Loads a bucket that exists on this server's disk into memory, which starts its synchronization and window timers, and returns its live state. Nothing is written and no other server is contacted - unlike building an ArchivesChain for it, which would probe every source and could write the routing config. Already-loaded buckets just return their state. */
112
95
  export declare function activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
113
96
  export declare function listAccountBuckets(account: string): Promise<ServerBucketInfo[]>;
114
- export declare function deleteBucketFile(account: string, bucketName: string, filePath: string, config?: {
115
- lastModified?: number;
116
- internal?: boolean;
117
- }): Promise<void>;
118
- export declare function getLocalArchives(account: string, bucketName: string): IArchives;
97
+ export type BucketWriteStats = {
98
+ /** Every set call the bucket accepted */
99
+ originalWrites: number;
100
+ originalBytes: number;
101
+ /** What actually reached the sources. Fast writes coalesce repeated writes to the same key, so this is lower than the original counts (and is what the disk actually did). */
102
+ flushedWrites: number;
103
+ flushedBytes: number;
104
+ };
105
+ /** Zeroes the write statistics of every bucket in the account. */
106
+ export declare function clearAccountWriteStats(account: string): number;
107
+ export declare function getLocalArchives(account: string, bucketName: string, sourceConfig: SourceConfig): IArchives;