sliftutils 1.7.29 → 1.7.33

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 (116) hide show
  1. package/builders/extensionBuild.ts +1 -1
  2. package/bundler/bundleEntry.ts +1 -3
  3. package/bundler/bundler.ts +2 -4
  4. package/bundler/sourceMaps.ts +1 -2
  5. package/examplestorage/browser.tsx +1 -2
  6. package/examplestorage/exampleserver.ts +1 -2
  7. package/index.d.ts +106 -99
  8. package/misc/dist/environment.ts.cache +2 -2
  9. package/misc/dist/getSecret.ts.cache +2 -2
  10. package/misc/dist/strings.ts.cache +2 -2
  11. package/misc/dist/zip.ts.cache +2 -2
  12. package/misc/https/dist/certs.ts.cache +2 -2
  13. package/misc/https/dist/dns.ts.cache +7 -16
  14. package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
  15. package/misc/https/dns.d.ts +1 -4
  16. package/misc/https/dns.ts +5 -14
  17. package/misc/https/hostServer.d.ts +2 -0
  18. package/misc/https/hostServer.ts +6 -7
  19. package/misc/https/httpsCerts.ts +4 -10
  20. package/misc/openrouter.ts +1 -2
  21. package/package.json +1 -1
  22. package/render-utils/FullscreenModal.tsx +1 -4
  23. package/render-utils/GenericFormat.tsx +2 -8
  24. package/render-utils/Input.tsx +5 -13
  25. package/render-utils/InputLabel.tsx +4 -6
  26. package/render-utils/InputPicker.tsx +1 -2
  27. package/render-utils/URLParam.ts +2 -3
  28. package/render-utils/dist/observer.tsx.cache +2 -2
  29. package/render-utils/mobxTyped.ts +2 -3
  30. package/render-utils/niceStringify.ts +2 -4
  31. package/storage/ArchivesDisk.ts +4 -14
  32. package/storage/BulkDatabase2/BulkDatabase2.d.ts +1 -2
  33. package/storage/BulkDatabase2/BulkDatabase2.ts +4 -11
  34. package/storage/BulkDatabase2/BulkDatabaseBase.ts +42 -114
  35. package/storage/BulkDatabase2/BulkDatabaseFormat.ts +31 -91
  36. package/storage/BulkDatabase2/BulkDatabaseMerge.ts +19 -57
  37. package/storage/BulkDatabase2/BulkDatabaseReader.ts +4 -12
  38. package/storage/BulkDatabase2/LoadedIndex.ts +6 -18
  39. package/storage/BulkDatabase2/WriteOverlay.ts +3 -8
  40. package/storage/BulkDatabase2/blockCache.ts +12 -28
  41. package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +40 -103
  42. package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +19 -50
  43. package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +18 -50
  44. package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +7 -15
  45. package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +9 -21
  46. package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +6 -11
  47. package/storage/BulkDatabase2/dist/blockCache.ts.cache +15 -31
  48. package/storage/BulkDatabase2/dist/mergeLock.ts.cache +10 -29
  49. package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +10 -18
  50. package/storage/BulkDatabase2/dist/streamLog.ts.cache +17 -43
  51. package/storage/BulkDatabase2/dist/syncClient.ts.cache +11 -27
  52. package/storage/BulkDatabase2/mergeLock.ts +7 -26
  53. package/storage/BulkDatabase2/mergeMarkers.ts +7 -15
  54. package/storage/BulkDatabase2/streamLog.ts +14 -40
  55. package/storage/BulkDatabase2/syncClient.ts +8 -24
  56. package/storage/FileFolderAPI.tsx +28 -75
  57. package/storage/IArchives.d.ts +3 -3
  58. package/storage/IArchives.ts +21 -50
  59. package/storage/IStorage.ts +2 -4
  60. package/storage/PendingManager.tsx +2 -3
  61. package/storage/TransactionStorage.ts +11 -24
  62. package/storage/backblaze.ts +12 -43
  63. package/storage/dist/ArchivesDisk.ts.cache +7 -17
  64. package/storage/dist/FileFolderAPI.tsx.cache +25 -63
  65. package/storage/dist/IArchives.ts.cache +10 -20
  66. package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
  67. package/storage/dist/JSONStorage.ts.cache +2 -2
  68. package/storage/dist/PendingManager.tsx.cache +5 -6
  69. package/storage/dist/TransactionStorage.ts.cache +14 -27
  70. package/storage/dist/backblaze.ts.cache +15 -46
  71. package/storage/dist/embeddingFormats.ts.cache +11 -24
  72. package/storage/dist/fileSystemPointer.ts.cache +4 -7
  73. package/storage/dist/remoteFileStorage.ts.cache +17 -36
  74. package/storage/embeddingFormats.ts +11 -27
  75. package/storage/fileSystemPointer.ts +2 -7
  76. package/storage/proxydatabase/dist/ivfEmbeddingDatabase.ts.cache +8 -18
  77. package/storage/proxydatabase/dist/transactionSet.ts.cache +4 -7
  78. package/storage/proxydatabase/inMemoryDatabase.ts +1 -3
  79. package/storage/proxydatabase/ivfEmbeddingDatabase.ts +8 -23
  80. package/storage/proxydatabase/transactionSet.ts +2 -5
  81. package/storage/remoteFileServer.ts +20 -48
  82. package/storage/remoteFileStorage.ts +14 -33
  83. package/storage/remoteStorage/ArchivesRemote.d.ts +1 -4
  84. package/storage/remoteStorage/ArchivesRemote.ts +11 -26
  85. package/storage/remoteStorage/ArchivesUrl.ts +2 -4
  86. package/storage/remoteStorage/accessPage.tsx +1 -5
  87. package/storage/remoteStorage/blobStore.d.ts +5 -13
  88. package/storage/remoteStorage/blobStore.ts +52 -130
  89. package/storage/remoteStorage/createArchives.d.ts +9 -13
  90. package/storage/remoteStorage/createArchives.ts +43 -187
  91. package/storage/remoteStorage/deployTakeover.d.ts +18 -32
  92. package/storage/remoteStorage/deployTakeover.ts +70 -384
  93. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +12 -24
  94. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +5 -7
  95. package/storage/remoteStorage/dist/blobStore.ts.cache +47 -117
  96. package/storage/remoteStorage/dist/cliArgs.ts.cache +2 -2
  97. package/storage/remoteStorage/dist/createArchives.ts.cache +54 -186
  98. package/storage/remoteStorage/dist/deployTakeover.ts.cache +81 -387
  99. package/storage/remoteStorage/dist/intermediateSources.ts.cache +119 -0
  100. package/storage/remoteStorage/dist/remoteConfig.ts.cache +36 -24
  101. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +20 -40
  102. package/storage/remoteStorage/dist/storageClientController.ts.cache +32 -0
  103. package/storage/remoteStorage/dist/storageController.ts.cache +30 -64
  104. package/storage/remoteStorage/dist/storageServerState.ts.cache +269 -186
  105. package/storage/remoteStorage/grantAccessCli.ts +1 -3
  106. package/storage/remoteStorage/intermediateSources.d.ts +21 -0
  107. package/storage/remoteStorage/intermediateSources.ts +117 -0
  108. package/storage/remoteStorage/remoteConfig.ts +33 -20
  109. package/storage/remoteStorage/sourceWrapper.d.ts +8 -19
  110. package/storage/remoteStorage/sourceWrapper.ts +19 -41
  111. package/storage/remoteStorage/storageClientController.ts +1 -3
  112. package/storage/remoteStorage/storageController.d.ts +5 -3
  113. package/storage/remoteStorage/storageController.ts +12 -83
  114. package/storage/remoteStorage/storageServer.ts +20 -34
  115. package/storage/remoteStorage/storageServerState.d.ts +28 -6
  116. package/storage/remoteStorage/storageServerState.ts +275 -221
@@ -10,27 +10,15 @@ import { timeInMinute } from "socket-function/src/misc";
10
10
  import { getCommonName, getPublicIdentifier, getOwnMachineId, verify, verifyMachineIdForPublicKey } from "../../misc/https/certs";
11
11
  import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, IMMUTABLE_CACHE_TIME } from "../IArchives";
12
12
  import { ROUTING_FILE } from "./remoteConfig";
13
- import { getTakeoverStamp } from "./deployTakeover";
14
13
  import {
15
14
  getStorageServerConfig, getTrust, getRequests, getLoadedBucket, writeBucketFile,
16
15
  deleteBucketFile, assertWritesAllowed, assertMutable, LoadedBucket,
17
- getBucketConfig, listAccountBuckets, ServerBucketInfo, setRoutingChangedBroadcaster,
16
+ getBucketConfig, listAccountBuckets, ServerBucketInfo, clearAccountWriteStats,
18
17
  } from "./storageServerState";
19
18
  import { StorageClientController } from "./storageClientController";
20
19
 
21
- // The remote storage server's API. Authentication uses certs.ts machine identities: a client
22
- // proves it owns its machine key by signing a timestamped token (bound to this server, so tokens
23
- // can't be replayed elsewhere), and the server then trusts that connection as that machineId.
24
- // Access to an account is granted to specific machineIds, via a command line command run on the
25
- // storage machine (see storageServer.ts).
26
- //
27
- // There is no bucket-creation API: a bucket exists iff its routing config (ROUTING_FILE) exists,
28
- // and writing that file creates or reconfigures the bucket (see storageServerState.ts). Reads of
29
- // nonexistent buckets return undefined / empty, same as reads of nonexistent files.
30
-
31
20
  export const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
32
21
  export const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
33
- // Error markers, so clients can identify these failures inside error messages
34
22
  export const STORAGE_NOT_AUTHENTICATED = "REMOTE_STORAGE_NOT_AUTHENTICATED_cf2f7b1e";
35
23
  export const STORAGE_ACCESS_DENIED = "REMOTE_STORAGE_ACCESS_DENIED_9d81a4c0";
36
24
 
@@ -41,8 +29,6 @@ const MAX_REQUESTS_PER_IP = 50;
41
29
  export type AuthTokenData = {
42
30
  purpose: string;
43
31
  time: number;
44
- // The address the client dialed. The port varies freely (deploy takeovers serve on alternate
45
- // ports); only the domain is security-relevant, as it is what the cert binds to.
46
32
  server: string;
47
33
  };
48
34
  export type AuthToken = {
@@ -67,18 +53,10 @@ export type AccessState = {
67
53
  machineId: string;
68
54
  ip: string;
69
55
  hasAccess: boolean;
70
- // A single ssh command, runnable from anywhere, that runs the grantAccess CLI on the storage
71
- // machine to grant the caller's own pending request. Only set when the caller has a pending
72
- // request (so an already-trusted caller has no need for it).
73
56
  grantAccessCommand?: string;
74
- // Only the machines that ALREADY have access. Pending requests are NEVER listed here — showing
75
- // them would let a trusted user accidentally approve a random request. Callers see pending
76
- // requests only by explicitly typing an IP into listRequestsForIP.
77
57
  trustedMachines?: TrustRecord[];
78
58
  };
79
59
 
80
- // callerNodeId -> authenticated machineId. Connections are long-lived websockets, so a session
81
- // lasts until the connection drops (clients re-authenticate on reconnect).
82
60
  const sessions = new Map<string, string>();
83
61
 
84
62
  const CONTENT_TYPES: { [ext: string]: string } = {
@@ -100,15 +78,11 @@ function assertValidPath(path: string) {
100
78
  if (!path || path.startsWith("/") || path.endsWith("/") || path.includes("//") || path.includes("\\") || path.includes("\x00")) {
101
79
  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`);
102
80
  }
103
- // Paths are one-to-one with files on disk, so . and .. segments would escape the store folder
104
81
  if (path.split("/").some(part => part === "." || part === "..")) {
105
82
  throw new Error(`Invalid path ${JSON.stringify(path.slice(0, 200))}, paths cannot contain . or .. segments`);
106
83
  }
107
84
  }
108
85
 
109
- // Every client pings us (immediately on connect, then continuously), so pings tell us exactly who
110
- // our currently-connected clients are. Client nodeIds never reconnect (a reconnect is a NEW
111
- // nodeId), so the disconnect callback fully cleans an entry up.
112
86
  const connectedClients = new Set<string>();
113
87
  function trackCaller(): void {
114
88
  let nodeId = SocketFunction.getCaller().nodeId;
@@ -119,17 +93,12 @@ function trackCaller(): void {
119
93
  });
120
94
  }
121
95
 
122
- // The moment any routing config changes on this server, every connected client is told - clients
123
- // must never have to wait for a poll to learn the topology changed
124
- setRoutingChangedBroadcaster(() => {
96
+ export function broadcastRoutingChanged(): void {
125
97
  console.log(`Broadcasting routing config change to ${connectedClients.size} connected clients`);
126
98
  for (let nodeId of [...connectedClients]) {
127
- void StorageClientController.nodes[nodeId].routingConfigChanged().catch(() => {
128
- // The client is gone (or too old to know the controller); the disconnect callback
129
- // cleans the registry, nothing to do here
130
- });
99
+ void StorageClientController.nodes[nodeId].routingConfigChanged().catch(() => { });
131
100
  }
132
- });
101
+ }
133
102
 
134
103
  function getCallerMachineId(): string {
135
104
  let caller = SocketFunction.getCaller();
@@ -165,8 +134,6 @@ async function requireAccess(account: string): Promise<string> {
165
134
  return machineId;
166
135
  }
167
136
 
168
- // A single command, runnable from anywhere, that sshes into the storage machine and runs the
169
- // grantAccess CLI there
170
137
  function getGrantAccessCommand(requestId: string): string {
171
138
  let { sshTarget, serverCommand } = getStorageServerConfig();
172
139
  return `ssh ${sshTarget} '${serverCommand} --requestId ${requestId}'`;
@@ -179,23 +146,14 @@ async function getBucket(account: string, bucketName: string): Promise<LoadedBuc
179
146
  }
180
147
 
181
148
  class RemoteStorageControllerBase {
182
- // Latency measurement (see SourceWrapper's pinging); no auth, it measures the transport. The
183
- // takeover stamp piggybacks on it so every connected client learns of a deploy takeover
184
- // within one ping interval. Pings also tell us who our clients ARE - the trackCaller
185
- // registry is what routing-change broadcasts push to.
186
- async ping(): Promise<{ takeover?: string }> {
149
+ async ping(): Promise<{}> {
187
150
  trackCaller();
188
- return { takeover: getTakeoverStamp() };
151
+ return {};
189
152
  }
190
153
 
191
- // Proves the caller owns the machine key for the machineId in its certificate. The signature
192
- // must be fresh and bound to this server, so it cannot be replayed to (or from) other servers.
193
154
  async authenticate(token: AuthToken): Promise<{ machineId: string; ip: string }> {
194
155
  let { domain, port } = getStorageServerConfig();
195
156
  let caller = SocketFunction.getCaller();
196
- // First establish that the signature signs the data exactly as the client sent it, THEN
197
- // validate the data's fields. The two steps must never be mixed - reconstructing the
198
- // payload from expectations conflates "is this signed" with "is this acceptable".
199
157
  verify(token.certPem, token.signature, token.data);
200
158
  let { purpose, time, server } = token.data;
201
159
  if (purpose !== STORAGE_AUTH_PURPOSE) {
@@ -204,8 +162,6 @@ class RemoteStorageControllerBase {
204
162
  if (Math.abs(Date.now() - time) > AUTH_TIME_WINDOW) {
205
163
  throw new Error(`Auth token time is too far from the server time (token ${time}, server ${Date.now()}, allowed drift ${AUTH_TIME_WINDOW}ms)`);
206
164
  }
207
- // Clients sign the address they dialed. The port varies freely (deploy takeovers serve on
208
- // alternate ports); the domain is the replay boundary, so only it must match us.
209
165
  let tokenDomain = server.split(":")[0];
210
166
  if (tokenDomain !== domain) {
211
167
  throw new Error(`Auth token is for server ${JSON.stringify(server)}, but this server is ${JSON.stringify(`${domain}:${port}`)}`);
@@ -223,8 +179,6 @@ class RemoteStorageControllerBase {
223
179
  return { machineId, ip: getCallerIP() };
224
180
  }
225
181
 
226
- // Records that the calling machine wants access to an account. Requests are kept per requesting
227
- // IP, so the storage machine's admin can list them with --listAccess <ip> and grant one.
228
182
  async requestAccess(account: string): Promise<{ machineId: string; ip: string; requestId: string; grantAccessCommand: string }> {
229
183
  assertValidName(account, "account");
230
184
  let machineId = getCallerMachineId();
@@ -275,20 +229,12 @@ class RemoteStorageControllerBase {
275
229
  return result;
276
230
  }
277
231
 
278
- // Callable by any machine that has access to `account`. Returns pending access requests for the
279
- // account that come from EXACTLY `ip`. Callers must type in an IP explicitly — the server never
280
- // volunteers a list of requesting IPs, so a trusted user can't accidentally approve a random
281
- // request from a machine they didn't mean to trust.
282
232
  async listRequestsForIP(account: string, ip: string): Promise<AccessRequest[]> {
283
233
  let requests = await getRequests();
284
234
  return (await requests.get(ip) || []).filter(x => x.account === account);
285
235
  }
286
236
 
287
- // Callable by any machine that has access to the request's account (or by the storage-machine
288
- // admin). Grants the requested access; the caller must supply the specific requestId, which they
289
- // only get by explicitly looking up requests for a specific IP.
290
237
  async grantAccess(requestId: string): Promise<TrustRecord> {
291
- // Must capture in the synchronous phase — SocketFunction.getCaller() only works before any await.
292
238
  let callerMachineId = getCallerMachineId();
293
239
  let trust = await getTrust();
294
240
  let requests = await getRequests();
@@ -311,9 +257,6 @@ class RemoteStorageControllerBase {
311
257
  throw new Error(`No access request found with id ${JSON.stringify(requestId)}. It may have already been granted or expired.`);
312
258
  }
313
259
 
314
- // Admin (must be run from the storage machine itself, which shares the server's machineId).
315
- // Only returns requests for the given IP, so you cannot accidentally grant a request from an
316
- // IP you didn't explicitly type in.
317
260
  async adminListRequests(ip: string): Promise<AccessRequest[]> {
318
261
  let requests = await getRequests();
319
262
  return await requests.get(ip) || [];
@@ -350,8 +293,6 @@ class RemoteStorageControllerBase {
350
293
  async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number): Promise<void> {
351
294
  assertValidName(bucketName, "bucket name");
352
295
  assertValidPath(path);
353
- // Handles bucket creation (writes of ROUTING_FILE), reconfiguration, fast mode, and
354
- // immutability — see storageServerState.ts
355
296
  await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified });
356
297
  }
357
298
  async del(account: string, bucketName: string, path: string): Promise<void> {
@@ -370,7 +311,6 @@ class RemoteStorageControllerBase {
370
311
  if (!bucket) return [];
371
312
  return await bucket.store.findInfo(prefix, config);
372
313
  }
373
- // Fast (served from the store's BulkDatabase2 index, not a scan) — see IArchives.getChangesAfter
374
314
  async getChangesAfter(account: string, bucketName: string, time: number): Promise<ArchiveFileInfo[]> {
375
315
  let bucket = await getBucket(account, bucketName);
376
316
  if (!bucket) return [];
@@ -381,17 +321,17 @@ class RemoteStorageControllerBase {
381
321
  }
382
322
  async getArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
383
323
  let bucket = await getBucket(account, bucketName);
384
- // Missing buckets say true, matching what they become once created (the default store type)
385
324
  if (!bucket) return { supportsChangesAfter: true };
386
325
  return getBucketConfig(bucket);
387
326
  }
388
- /** Every bucket the account has on this host - active and inactive - with each bucket's
389
- * configuration. Inactive buckets stay inactive (their routing is read straight off disk). */
390
327
  async listBuckets(account: string): Promise<ServerBucketInfo[]> {
391
328
  return await listAccountBuckets(account);
392
329
  }
393
- /** Walks the whole index for exact totals (overall and per holding source) - more expensive
394
- * than the maintained counters that getArchivesConfig returns, but immune to counter drift. */
330
+ /** Zeroes the write statistics listBuckets reports, for every bucket in the account. */
331
+ async clearWriteStats(account: string): Promise<{ clearedBuckets: number }> {
332
+ assertValidName(account, "account");
333
+ return { clearedBuckets: await clearAccountWriteStats(account) };
334
+ }
395
335
  async getIndexInfo(account: string, bucketName: string): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] } | undefined> {
396
336
  let bucket = await getBucket(account, bucketName);
397
337
  if (!bucket || !bucket.store.computeIndexTotals) return undefined;
@@ -408,7 +348,6 @@ class RemoteStorageControllerBase {
408
348
 
409
349
  async startLargeFile(account: string, bucketName: string, path: string): Promise<string> {
410
350
  assertWritesAllowed();
411
- // Validates now, so the upload doesn't fail at the end
412
351
  assertValidPath(path);
413
352
  let bucket = await getBucket(account, bucketName);
414
353
  if (!bucket) {
@@ -445,11 +384,7 @@ class RemoteStorageControllerBase {
445
384
  await bucket.store.cancelLargeUpload(uploadId);
446
385
  }
447
386
 
448
- // The server's single default HTTP route. /file/<account>/<bucketName>/<path> serves files from
449
- // public buckets over plain GETs (see IArchives.getURL); every other path serves the access
450
- // page (via RequireController.requireHTML — the path is the account name, see accessPage.tsx).
451
387
  async httpEntry(config?: { requireCalls?: string[]; cacheTime?: number }): Promise<Buffer> {
452
- // Both are keyed by the current call and must be captured synchronously, before any await
453
388
  let caller = SocketFunction.getCaller();
454
389
  let request = getCurrentHTTPRequest();
455
390
  let pathname = new URL(request?.url || "/", "https://localhost").pathname;
@@ -476,8 +411,6 @@ class RemoteStorageControllerBase {
476
411
  if (!bucket.self?.public) {
477
412
  throw new Error(`Bucket ${account}/${bucketName} is not public, so its files cannot be read over plain URLs`);
478
413
  }
479
- // The index answers existence + write time + size without touching any data, so
480
- // If-Modified-Since and range validation cost nothing
481
414
  let info = await bucket.store.getInfo(filePath);
482
415
  if (!info || !info.size) {
483
416
  return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
@@ -494,7 +427,6 @@ class RemoteStorageControllerBase {
494
427
  let ifModifiedSince = request?.headers["if-modified-since"];
495
428
  if (typeof ifModifiedSince === "string") {
496
429
  let since = new Date(ifModifiedSince).getTime();
497
- // Last-Modified is served at 1 second resolution, so compare at that resolution
498
430
  if (since && Math.floor(info.writeTime / 1000) * 1000 <= since) {
499
431
  return setHTTPResultHeaders(Buffer.from(""), { ...headers, status: "304" });
500
432
  }
@@ -502,7 +434,6 @@ class RemoteStorageControllerBase {
502
434
  let range: { start: number; end: number } | undefined;
503
435
  let rangeHeader = request?.headers["range"];
504
436
  if (typeof rangeHeader === "string") {
505
- // Single-range form only (bytes=start-end / start- / -suffix); anything else serves the full file
506
437
  let match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
507
438
  if (match && (match[1] || match[2])) {
508
439
  let start: number;
@@ -534,14 +465,11 @@ class RemoteStorageControllerBase {
534
465
 
535
466
  const largeUploadInfo = new Map<string, { account: string; bucketName: string; path: string }>();
536
467
 
537
- // Access checks run as hooks on the register shape below, keyed off the call's arguments, so the
538
- // method bodies don't each repeat them
539
468
  const accountAccess: SocketFunctionHook = async (context) => {
540
469
  await requireAccess(String(context.call.args[0]));
541
470
  };
542
471
  const uploadAccess: SocketFunctionHook = async (context) => {
543
472
  let info = largeUploadInfo.get(String(context.call.args[0]));
544
- // Unknown upload ids are handled by the methods themselves (throw / no-op)
545
473
  if (!info) return;
546
474
  await requireAccess(info.account);
547
475
  };
@@ -571,6 +499,7 @@ export const RemoteStorageController = SocketFunction.register(
571
499
  getArchivesConfig: { hooks: [accountAccess] },
572
500
  getIndexInfo: { hooks: [accountAccess] },
573
501
  listBuckets: { hooks: [accountAccess] },
502
+ clearWriteStats: { hooks: [accountAccess] },
574
503
  getSyncStatus: { hooks: [accountAccess] },
575
504
  startLargeFile: { hooks: [accountAccess] },
576
505
  uploadPart: { hooks: [uploadAccess] },
@@ -6,34 +6,25 @@ import { getExternalIP } from "socket-function/src/networking";
6
6
  import { RequireController } from "socket-function/require/RequireController";
7
7
  import { hostServer } from "../../misc/https/hostServer";
8
8
  import { RemoteStorageController } from "./storageController";
9
- import { setStorageServerConfig, setWritesRejectedReason, addExtraListenPort } from "./storageServerState";
10
- import { initDeployTakeover, registerAltPort, getMainPortAcquireDelay } from "./deployTakeover";
9
+ import { setStorageServerConfig, setWritesRejectedReason, addExtraListenPort, removeExtraListenPort } from "./storageServerState";
10
+ import { detectDeployTakeover, setAltPort, getAltPortListenEnd, getMainPortAcquireDelay } from "./deployTakeover";
11
11
  import { parseStorageUrl } from "./ArchivesRemote";
12
12
  // Import browser code, so it is allowed to be required by the client
13
13
  import "./accessPage";
14
14
 
15
15
  const DEFAULT_LOW_SPACE_THRESHOLD_BYTES = 25 * 1024 ** 3;
16
16
  const DISK_SPACE_CHECK_INTERVAL_MS = 15 * 60 * 1000;
17
- // Below this fraction of the warn threshold, we start rejecting writes so the server itself doesn't
18
- // tip the machine into instability. Reads/deletes still work so users can free space.
17
+ // Below this fraction of the warn threshold, we start rejecting writes so the server itself doesn't tip the machine into instability. Reads/deletes still work so users can free space.
19
18
  const HARD_REJECT_FRACTION = 0.1;
20
19
 
21
- // The remote storage server, as a library function: consumers call hostStorageServer() from their
22
- // own process to start hosting (or use the storageserver bin, see storageServerCli.ts). The
23
- // grantAccess.js bootstrap (next to this file) is what the access page's shown SSH command points at.
20
+ // The remote storage server, as a library function: consumers call hostStorageServer() from their own process to start hosting (or use the storageserver bin, see storageServerCli.ts). The grantAccess.js bootstrap (next to this file) is what the access page's shown SSH command points at.
24
21
 
25
22
  export type HostStorageServerConfig = {
26
- // Full URL of this storage server, e.g. "https://1-2-3-4.example.com:4444". The subdomain must
27
- // be an ip domain - this machine's external IP with dashes, or 127-0-0-1 for local testing
28
- // (see the validation in hostStorageServer). The domain and port are extracted from it (bucket
29
- // routing URLs clients use look like
23
+ // Full URL of this storage server, e.g. "https://1-2-3-4.example.com:4444". The subdomain must be an ip domain - this machine's external IP with dashes, or 127-0-0-1 for local testing (see the validation in hostStorageServer). The domain and port are extracted from it (bucket routing URLs clients use look like
30
24
  // https://1-2-3-4.example.com:4444/file/<account>/<bucketName>/storage/storagerouting.json).
31
25
  url: string;
32
26
  folder: string;
33
- // When free space on the folder's drive drops below this many bytes, the server console.errors
34
- // every 15 minutes. Below 10% of it, the server also rejects write operations (creating files,
35
- // large uploads, new buckets) — reads, findInfo, and deletes still work so the user can free
36
- // space. Default 25 GiB.
27
+ // When free space on the folder's drive drops below this many bytes, the server console.errors every 15 minutes. Below 10% of it, the server also rejects write operations (creating files, large uploads, new buckets) — reads, findInfo, and deletes still work so the user can free space. Default 25 GiB.
37
28
  lowSpaceThresholdBytes?: number;
38
29
  };
39
30
 
@@ -68,9 +59,7 @@ async function checkDiskSpace(config: { folder: string; threshold: number }): Pr
68
59
  }
69
60
  }
70
61
 
71
- // Full path to the grantAccess CLI bootstrap that lives next to this file. The SSH command shown on
72
- // the access page invokes it via `node <path> ...` (found through __dirname so consumers don't have
73
- // to know where our source lives).
62
+ // Full path to the grantAccess CLI bootstrap that lives next to this file. The SSH command shown on the access page invokes it via `node <path> ...` (found through __dirname so consumers don't have to know where our source lives).
74
63
  function getGrantAccessCliPath(): string {
75
64
  return path.join(__dirname, "grantAccess.js");
76
65
  }
@@ -80,9 +69,7 @@ export async function hostStorageServer(config: HostStorageServerConfig): Promis
80
69
  let { address: domain, port } = parseStorageUrl(url);
81
70
  let rootDomain = domain.split(".").slice(-2).join(".");
82
71
  let externalIP = (await getExternalIP()).trim();
83
- // The subdomain must be an ip domain: the domain's A record points at exactly one machine, so a
84
- // dynamic name would let the same script run on two servers and silently fight over it. Encoding
85
- // the IP makes that mistake fail loudly - the wrong machine's domain just doesn't match.
72
+ // The subdomain must be an ip domain: the domain's A record points at exactly one machine, so a dynamic name would let the same script run on two servers and silently fight over it. Encoding the IP makes that mistake fail loudly - the wrong machine's domain just doesn't match.
86
73
  let allowedDomains = [`127-0-0-1.${rootDomain}`, `${externalIP.replaceAll(".", "-")}.${rootDomain}`];
87
74
  if (!allowedDomains.includes(domain)) {
88
75
  throw new Error(`The storage server domain is based on the machine's IP (the subdomain is the IP with dots replaced by dashes). Expected ${allowedDomains.join(" or ")}, was ${domain}. Your external IP is ${externalIP}.`);
@@ -101,17 +88,12 @@ export async function hostStorageServer(config: HostStorageServerConfig): Promis
101
88
  RequireController.allowAllNodeModules();
102
89
  SocketFunction.expose(RequireController);
103
90
  SocketFunction.expose(RemoteStorageController);
104
- // Every HTTP path goes through httpEntry: /file/<account>/<bucketName>/... serves public
105
- // bucket files, everything else serves the access page (the path is the account name, see
106
- // accessPage.tsx).
107
- // A full URL, so the page resolves modules from the origin root even when served at
108
- // /accountName (a relative require would resolve inside the account path).
91
+ // Every HTTP path goes through httpEntry: /file/<account>/<bucketName>/... serves public bucket files, everything else serves the access page (the path is the account name, see accessPage.tsx). A full URL, so the page resolves modules from the origin root even when served at /accountName (a relative require would resolve inside the account path).
109
92
  SocketFunction.setDefaultHTTPCall(RemoteStorageController, "httpEntry", {
110
93
  requireCalls: [`https://${domain}:${port}/./storage/remoteStorage/accessPage.tsx`],
111
94
  });
112
95
 
113
- // Initial check so a server starting under-limit immediately rejects writes; then keep checking
114
- // every 15 minutes so recovery (freed disk space) is picked up automatically.
96
+ // Initial check so a server starting under-limit immediately rejects writes; then keep checking every 15 minutes so recovery (freed disk space) is picked up automatically.
115
97
  await checkDiskSpace({ folder, threshold: lowSpaceThreshold });
116
98
  let interval = setInterval(() => {
117
99
  void checkDiskSpace({ folder, threshold: lowSpaceThreshold })
@@ -119,21 +101,25 @@ export async function hostStorageServer(config: HostStorageServerConfig): Promis
119
101
  }, DISK_SPACE_CHECK_INTERVAL_MS);
120
102
  (interval as { unref?: () => void }).unref?.();
121
103
 
122
- // Port fallback is always on: a busy port means listening on an alternate, registering it, and
123
- // polling to take the real port. The deploy takeover machinery merely rides on top (tighter
124
- // polling around a scheduled switchover, and the valid-window remap once a successor is known).
125
- await initDeployTakeover({ domain, mainPort: port, storageFolder: path.resolve(folder) });
126
-
127
104
  await hostServer({
128
105
  domain,
129
106
  port,
130
107
  setDNSRecord: true,
131
108
  portFallback: {
132
109
  getAcquireDelay: getMainPortAcquireDelay,
110
+ onPortInUse: async () => {
111
+ await detectDeployTakeover();
112
+ },
133
113
  onListening: (listeningPort, isMainPort) => {
134
114
  if (isMainPort) return;
135
115
  addExtraListenPort(listeningPort);
136
- void registerAltPort(listeningPort).catch((e: Error) => console.error(`Registering alternate port ${listeningPort} failed: ${e.stack ?? e}`));
116
+ setAltPort(listeningPort);
117
+ let listenEnd = getAltPortListenEnd();
118
+ let timer = setTimeout(() => {
119
+ console.log(`Alternate port ${listeningPort} reached its end of life at ${new Date(listenEnd).toISOString()}; it no longer counts as one of our addresses (the socket itself stays bound, as the main-port relay forwards into it)`);
120
+ removeExtraListenPort(listeningPort);
121
+ }, Math.max(0, listenEnd - Date.now()));
122
+ (timer as { unref?: () => void }).unref?.();
137
123
  },
138
124
  },
139
125
  });
@@ -20,7 +20,16 @@ export declare function getWritesRejectedReason(): string | undefined;
20
20
  export declare function assertWritesAllowed(): void;
21
21
  export declare function getTrust(): Promise<IStorage<TrustRecord>>;
22
22
  export declare function getRequests(): Promise<IStorage<AccessRequest[]>>;
23
- /** Makes machineIds the complete trust list for the account: machines not in the list lose access, machines already trusted keep their existing record, and missing ones are added. */
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, including counts not yet flushed. */
32
+ export declare function clearAccountWriteStats(account: string): Promise<number>;
24
33
  export declare function setTrustedMachines(config: {
25
34
  account: string;
26
35
  machineIds: string[];
@@ -36,24 +45,37 @@ export type LoadedBucket = {
36
45
  structureKey: string;
37
46
  };
38
47
  export declare function addExtraListenPort(port: number): void;
48
+ export declare function removeExtraListenPort(port: number): void;
39
49
  export declare function getLoadedBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined>;
40
- export declare function setRoutingChangedBroadcaster(broadcaster: () => void): void;
41
50
  export declare function assertMutable(bucket: LoadedBucket, filePath: string, writeTime: number): Promise<void>;
42
51
  export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
43
52
  lastModified?: number;
44
53
  }): Promise<void>;
45
54
  export declare function getBucketConfig(bucket: LoadedBucket): ArchivesConfig;
46
55
  export declare function rebuildAllLoadedBuckets(): Promise<void>;
56
+ /** Started by deployTakeover once we are actually a deploy successor listening on an alternate port. Until then there are no switchover windows to write or expire, so nothing polls. */
57
+ export declare const startIntermediateMaintenance: {
58
+ (): void;
59
+ reset(): void;
60
+ set(newValue: void): void;
61
+ };
62
+ export type BucketDiskInfo = {
63
+ totalBytes: number;
64
+ freeBytes: number;
65
+ usedBytes: number;
66
+ };
47
67
  export type ServerBucketInfo = {
48
68
  bucketName: string;
49
69
  active: boolean;
70
+ /** Where the bucket's data lives on this server */
71
+ folder: string;
72
+ /** The drive that folder is on. Buckets sharing a drive report the same numbers. */
73
+ disk?: BucketDiskInfo;
74
+ diskError?: string;
75
+ writeStats?: BucketWriteStats;
50
76
  config?: ArchivesConfig;
51
77
  error?: string;
52
78
  };
53
- /** Every bucket the account has on this server, active or not, each with its configuration.
54
- * Inactive buckets are inspected straight from disk WITHOUT loading them - loading would start
55
- * their synchronization, and old invalid buckets must stay inert (their parse error is reported
56
- * instead). */
57
79
  export declare function listAccountBuckets(account: string): Promise<ServerBucketInfo[]>;
58
80
  export declare function deleteBucketFile(account: string, bucketName: string, filePath: string): Promise<void>;
59
81
  export declare function getLocalArchives(account: string, bucketName: string): IArchives;