sliftutils 1.7.32 → 1.7.34
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/builders/extensionBuild.ts +1 -1
- package/bundler/bundleEntry.ts +1 -3
- package/bundler/bundler.ts +2 -4
- package/bundler/sourceMaps.ts +1 -2
- package/examplestorage/browser.tsx +1 -2
- package/examplestorage/exampleserver.ts +1 -2
- package/index.d.ts +134 -102
- package/misc/dist/environment.ts.cache +2 -2
- package/misc/dist/getSecret.ts.cache +2 -2
- package/misc/dist/strings.ts.cache +2 -2
- package/misc/dist/zip.ts.cache +2 -2
- package/misc/https/dist/certs.ts.cache +2 -2
- package/misc/https/dist/dns.ts.cache +7 -16
- package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
- package/misc/https/dns.d.ts +1 -4
- package/misc/https/dns.ts +5 -14
- package/misc/https/hostServer.d.ts +2 -0
- package/misc/https/hostServer.ts +6 -7
- package/misc/https/httpsCerts.ts +4 -10
- package/misc/openrouter.ts +1 -2
- package/package.json +1 -1
- package/render-utils/FullscreenModal.tsx +1 -4
- package/render-utils/GenericFormat.tsx +2 -8
- package/render-utils/Input.tsx +5 -13
- package/render-utils/InputLabel.tsx +4 -6
- package/render-utils/InputPicker.tsx +1 -2
- package/render-utils/URLParam.ts +2 -3
- package/render-utils/dist/observer.tsx.cache +2 -2
- package/render-utils/mobxTyped.ts +2 -3
- package/render-utils/niceStringify.ts +2 -4
- package/storage/ArchivesDisk.ts +4 -14
- package/storage/BulkDatabase2/BulkDatabase2.d.ts +1 -2
- package/storage/BulkDatabase2/BulkDatabase2.ts +4 -11
- package/storage/BulkDatabase2/BulkDatabaseBase.ts +42 -114
- package/storage/BulkDatabase2/BulkDatabaseFormat.ts +31 -91
- package/storage/BulkDatabase2/BulkDatabaseMerge.ts +19 -57
- package/storage/BulkDatabase2/BulkDatabaseReader.ts +4 -12
- package/storage/BulkDatabase2/LoadedIndex.ts +6 -18
- package/storage/BulkDatabase2/WriteOverlay.ts +3 -8
- package/storage/BulkDatabase2/blockCache.ts +12 -28
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +40 -103
- package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +19 -50
- package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +18 -50
- package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +7 -15
- package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +9 -21
- package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +6 -11
- package/storage/BulkDatabase2/dist/blockCache.ts.cache +15 -31
- package/storage/BulkDatabase2/dist/mergeLock.ts.cache +10 -29
- package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +10 -18
- package/storage/BulkDatabase2/dist/streamLog.ts.cache +17 -43
- package/storage/BulkDatabase2/dist/syncClient.ts.cache +11 -27
- package/storage/BulkDatabase2/mergeLock.ts +7 -26
- package/storage/BulkDatabase2/mergeMarkers.ts +7 -15
- package/storage/BulkDatabase2/streamLog.ts +14 -40
- package/storage/BulkDatabase2/syncClient.ts +8 -24
- package/storage/FileFolderAPI.tsx +28 -75
- package/storage/IArchives.d.ts +3 -3
- package/storage/IArchives.ts +21 -50
- package/storage/IStorage.ts +2 -4
- package/storage/PendingManager.tsx +2 -3
- package/storage/TransactionStorage.ts +11 -24
- package/storage/backblaze.ts +12 -43
- package/storage/dist/ArchivesDisk.ts.cache +7 -17
- package/storage/dist/FileFolderAPI.tsx.cache +25 -63
- package/storage/dist/IArchives.ts.cache +10 -20
- package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
- package/storage/dist/JSONStorage.ts.cache +2 -2
- package/storage/dist/PendingManager.tsx.cache +5 -6
- package/storage/dist/TransactionStorage.ts.cache +14 -27
- package/storage/dist/backblaze.ts.cache +15 -46
- package/storage/dist/embeddingFormats.ts.cache +11 -24
- package/storage/dist/fileSystemPointer.ts.cache +4 -7
- package/storage/dist/remoteFileStorage.ts.cache +17 -36
- package/storage/embeddingFormats.ts +11 -27
- package/storage/fileSystemPointer.ts +2 -7
- package/storage/proxydatabase/dist/ivfEmbeddingDatabase.ts.cache +8 -18
- package/storage/proxydatabase/dist/transactionSet.ts.cache +4 -7
- package/storage/proxydatabase/inMemoryDatabase.ts +1 -3
- package/storage/proxydatabase/ivfEmbeddingDatabase.ts +8 -23
- package/storage/proxydatabase/transactionSet.ts +2 -5
- package/storage/remoteFileServer.ts +20 -48
- package/storage/remoteFileStorage.ts +14 -33
- package/storage/remoteStorage/ArchivesRemote.d.ts +1 -4
- package/storage/remoteStorage/ArchivesRemote.ts +11 -26
- package/storage/remoteStorage/ArchivesUrl.ts +2 -4
- package/storage/remoteStorage/accessPage.tsx +1 -5
- package/storage/remoteStorage/blobStore.d.ts +5 -13
- package/storage/remoteStorage/blobStore.ts +52 -130
- package/storage/remoteStorage/createArchives.d.ts +22 -14
- package/storage/remoteStorage/createArchives.ts +54 -188
- package/storage/remoteStorage/deployTakeover.d.ts +18 -32
- package/storage/remoteStorage/deployTakeover.ts +70 -384
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +12 -24
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +5 -7
- package/storage/remoteStorage/dist/blobStore.ts.cache +47 -117
- package/storage/remoteStorage/dist/cliArgs.ts.cache +2 -2
- package/storage/remoteStorage/dist/createArchives.ts.cache +58 -189
- package/storage/remoteStorage/dist/deployTakeover.ts.cache +81 -387
- package/storage/remoteStorage/dist/intermediateSources.ts.cache +119 -0
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +36 -24
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +20 -40
- package/storage/remoteStorage/dist/storageClientController.ts.cache +4 -6
- package/storage/remoteStorage/dist/storageController.ts.cache +37 -74
- package/storage/remoteStorage/dist/storageServerState.ts.cache +345 -188
- package/storage/remoteStorage/grantAccessCli.ts +1 -3
- package/storage/remoteStorage/intermediateSources.d.ts +21 -0
- package/storage/remoteStorage/intermediateSources.ts +117 -0
- package/storage/remoteStorage/remoteConfig.ts +33 -20
- package/storage/remoteStorage/sourceWrapper.d.ts +8 -19
- package/storage/remoteStorage/sourceWrapper.ts +19 -41
- package/storage/remoteStorage/storageClientController.ts +1 -3
- package/storage/remoteStorage/storageController.d.ts +7 -6
- package/storage/remoteStorage/storageController.ts +36 -82
- package/storage/remoteStorage/storageServer.ts +20 -34
- package/storage/remoteStorage/storageServerState.d.ts +41 -5
- package/storage/remoteStorage/storageServerState.ts +355 -216
|
@@ -10,39 +10,27 @@ 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,
|
|
16
|
+
getBucketConfig, listAccountBuckets, ServerBucketInfo, clearAccountWriteStats,
|
|
17
|
+
getActiveBucket, activateBucket, ActiveBucketInfo,
|
|
18
18
|
} from "./storageServerState";
|
|
19
19
|
import { StorageClientController } from "./storageClientController";
|
|
20
20
|
|
|
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
21
|
export const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
|
|
32
22
|
export const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
|
|
33
|
-
// Error markers, so clients can identify these failures inside error messages
|
|
34
23
|
export const STORAGE_NOT_AUTHENTICATED = "REMOTE_STORAGE_NOT_AUTHENTICATED_cf2f7b1e";
|
|
35
24
|
export const STORAGE_ACCESS_DENIED = "REMOTE_STORAGE_ACCESS_DENIED_9d81a4c0";
|
|
36
25
|
|
|
37
26
|
const AUTH_TIME_WINDOW = timeInMinute * 10;
|
|
27
|
+
const ACCESS_CHECK_SLOW_TIME = 50;
|
|
38
28
|
const MAX_SESSIONS = 100 * 1000;
|
|
39
29
|
const MAX_REQUESTS_PER_IP = 50;
|
|
40
30
|
|
|
41
31
|
export type AuthTokenData = {
|
|
42
32
|
purpose: string;
|
|
43
33
|
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
34
|
server: string;
|
|
47
35
|
};
|
|
48
36
|
export type AuthToken = {
|
|
@@ -67,18 +55,10 @@ export type AccessState = {
|
|
|
67
55
|
machineId: string;
|
|
68
56
|
ip: string;
|
|
69
57
|
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
58
|
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
59
|
trustedMachines?: TrustRecord[];
|
|
78
60
|
};
|
|
79
61
|
|
|
80
|
-
// callerNodeId -> authenticated machineId. Connections are long-lived websockets, so a session
|
|
81
|
-
// lasts until the connection drops (clients re-authenticate on reconnect).
|
|
82
62
|
const sessions = new Map<string, string>();
|
|
83
63
|
|
|
84
64
|
const CONTENT_TYPES: { [ext: string]: string } = {
|
|
@@ -100,15 +80,11 @@ function assertValidPath(path: string) {
|
|
|
100
80
|
if (!path || path.startsWith("/") || path.endsWith("/") || path.includes("//") || path.includes("\\") || path.includes("\x00")) {
|
|
101
81
|
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
82
|
}
|
|
103
|
-
// Paths are one-to-one with files on disk, so . and .. segments would escape the store folder
|
|
104
83
|
if (path.split("/").some(part => part === "." || part === "..")) {
|
|
105
84
|
throw new Error(`Invalid path ${JSON.stringify(path.slice(0, 200))}, paths cannot contain . or .. segments`);
|
|
106
85
|
}
|
|
107
86
|
}
|
|
108
87
|
|
|
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
88
|
const connectedClients = new Set<string>();
|
|
113
89
|
function trackCaller(): void {
|
|
114
90
|
let nodeId = SocketFunction.getCaller().nodeId;
|
|
@@ -119,15 +95,10 @@ function trackCaller(): void {
|
|
|
119
95
|
});
|
|
120
96
|
}
|
|
121
97
|
|
|
122
|
-
/** Called by storageServerState the moment any routing config is applied - clients must never
|
|
123
|
-
* have to wait for a poll to learn the topology changed. */
|
|
124
98
|
export function broadcastRoutingChanged(): void {
|
|
125
99
|
console.log(`Broadcasting routing config change to ${connectedClients.size} connected clients`);
|
|
126
100
|
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
|
-
});
|
|
101
|
+
void StorageClientController.nodes[nodeId].routingConfigChanged().catch(() => { });
|
|
131
102
|
}
|
|
132
103
|
}
|
|
133
104
|
|
|
@@ -165,8 +136,6 @@ async function requireAccess(account: string): Promise<string> {
|
|
|
165
136
|
return machineId;
|
|
166
137
|
}
|
|
167
138
|
|
|
168
|
-
// A single command, runnable from anywhere, that sshes into the storage machine and runs the
|
|
169
|
-
// grantAccess CLI there
|
|
170
139
|
function getGrantAccessCommand(requestId: string): string {
|
|
171
140
|
let { sshTarget, serverCommand } = getStorageServerConfig();
|
|
172
141
|
return `ssh ${sshTarget} '${serverCommand} --requestId ${requestId}'`;
|
|
@@ -179,23 +148,14 @@ async function getBucket(account: string, bucketName: string): Promise<LoadedBuc
|
|
|
179
148
|
}
|
|
180
149
|
|
|
181
150
|
class RemoteStorageControllerBase {
|
|
182
|
-
|
|
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 }> {
|
|
151
|
+
async ping(): Promise<{}> {
|
|
187
152
|
trackCaller();
|
|
188
|
-
return {
|
|
153
|
+
return {};
|
|
189
154
|
}
|
|
190
155
|
|
|
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
156
|
async authenticate(token: AuthToken): Promise<{ machineId: string; ip: string }> {
|
|
194
157
|
let { domain, port } = getStorageServerConfig();
|
|
195
158
|
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
159
|
verify(token.certPem, token.signature, token.data);
|
|
200
160
|
let { purpose, time, server } = token.data;
|
|
201
161
|
if (purpose !== STORAGE_AUTH_PURPOSE) {
|
|
@@ -204,8 +164,6 @@ class RemoteStorageControllerBase {
|
|
|
204
164
|
if (Math.abs(Date.now() - time) > AUTH_TIME_WINDOW) {
|
|
205
165
|
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
166
|
}
|
|
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
167
|
let tokenDomain = server.split(":")[0];
|
|
210
168
|
if (tokenDomain !== domain) {
|
|
211
169
|
throw new Error(`Auth token is for server ${JSON.stringify(server)}, but this server is ${JSON.stringify(`${domain}:${port}`)}`);
|
|
@@ -223,8 +181,6 @@ class RemoteStorageControllerBase {
|
|
|
223
181
|
return { machineId, ip: getCallerIP() };
|
|
224
182
|
}
|
|
225
183
|
|
|
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
184
|
async requestAccess(account: string): Promise<{ machineId: string; ip: string; requestId: string; grantAccessCommand: string }> {
|
|
229
185
|
assertValidName(account, "account");
|
|
230
186
|
let machineId = getCallerMachineId();
|
|
@@ -275,20 +231,12 @@ class RemoteStorageControllerBase {
|
|
|
275
231
|
return result;
|
|
276
232
|
}
|
|
277
233
|
|
|
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
234
|
async listRequestsForIP(account: string, ip: string): Promise<AccessRequest[]> {
|
|
283
235
|
let requests = await getRequests();
|
|
284
236
|
return (await requests.get(ip) || []).filter(x => x.account === account);
|
|
285
237
|
}
|
|
286
238
|
|
|
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
239
|
async grantAccess(requestId: string): Promise<TrustRecord> {
|
|
291
|
-
// Must capture in the synchronous phase — SocketFunction.getCaller() only works before any await.
|
|
292
240
|
let callerMachineId = getCallerMachineId();
|
|
293
241
|
let trust = await getTrust();
|
|
294
242
|
let requests = await getRequests();
|
|
@@ -311,9 +259,6 @@ class RemoteStorageControllerBase {
|
|
|
311
259
|
throw new Error(`No access request found with id ${JSON.stringify(requestId)}. It may have already been granted or expired.`);
|
|
312
260
|
}
|
|
313
261
|
|
|
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
262
|
async adminListRequests(ip: string): Promise<AccessRequest[]> {
|
|
318
263
|
let requests = await getRequests();
|
|
319
264
|
return await requests.get(ip) || [];
|
|
@@ -350,8 +295,6 @@ class RemoteStorageControllerBase {
|
|
|
350
295
|
async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number): Promise<void> {
|
|
351
296
|
assertValidName(bucketName, "bucket name");
|
|
352
297
|
assertValidPath(path);
|
|
353
|
-
// Handles bucket creation (writes of ROUTING_FILE), reconfiguration, fast mode, and
|
|
354
|
-
// immutability — see storageServerState.ts
|
|
355
298
|
await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified });
|
|
356
299
|
}
|
|
357
300
|
async del(account: string, bucketName: string, path: string): Promise<void> {
|
|
@@ -370,7 +313,6 @@ class RemoteStorageControllerBase {
|
|
|
370
313
|
if (!bucket) return [];
|
|
371
314
|
return await bucket.store.findInfo(prefix, config);
|
|
372
315
|
}
|
|
373
|
-
// Fast (served from the store's BulkDatabase2 index, not a scan) — see IArchives.getChangesAfter
|
|
374
316
|
async getChangesAfter(account: string, bucketName: string, time: number): Promise<ArchiveFileInfo[]> {
|
|
375
317
|
let bucket = await getBucket(account, bucketName);
|
|
376
318
|
if (!bucket) return [];
|
|
@@ -381,17 +323,33 @@ class RemoteStorageControllerBase {
|
|
|
381
323
|
}
|
|
382
324
|
async getArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
|
|
383
325
|
let bucket = await getBucket(account, bucketName);
|
|
384
|
-
// Missing buckets say true, matching what they become once created (the default store type)
|
|
385
326
|
if (!bucket) return { supportsChangesAfter: true };
|
|
386
327
|
return getBucketConfig(bucket);
|
|
387
328
|
}
|
|
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
329
|
async listBuckets(account: string): Promise<ServerBucketInfo[]> {
|
|
391
|
-
|
|
330
|
+
let start = Date.now();
|
|
331
|
+
try {
|
|
332
|
+
return await listAccountBuckets(account);
|
|
333
|
+
} finally {
|
|
334
|
+
// 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
|
|
335
|
+
console.log(`listBuckets(${account}) call body took ${Date.now() - start}ms`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** 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. */
|
|
339
|
+
async getActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
340
|
+
assertValidName(bucketName, "bucket name");
|
|
341
|
+
return await getActiveBucket(account, bucketName);
|
|
342
|
+
}
|
|
343
|
+
/** 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. */
|
|
344
|
+
async activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
345
|
+
assertValidName(bucketName, "bucket name");
|
|
346
|
+
return await activateBucket(account, bucketName);
|
|
347
|
+
}
|
|
348
|
+
/** Zeroes the write statistics listBuckets reports, for every bucket in the account. */
|
|
349
|
+
async clearWriteStats(account: string): Promise<{ clearedBuckets: number }> {
|
|
350
|
+
assertValidName(account, "account");
|
|
351
|
+
return { clearedBuckets: await clearAccountWriteStats(account) };
|
|
392
352
|
}
|
|
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. */
|
|
395
353
|
async getIndexInfo(account: string, bucketName: string): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] } | undefined> {
|
|
396
354
|
let bucket = await getBucket(account, bucketName);
|
|
397
355
|
if (!bucket || !bucket.store.computeIndexTotals) return undefined;
|
|
@@ -408,7 +366,6 @@ class RemoteStorageControllerBase {
|
|
|
408
366
|
|
|
409
367
|
async startLargeFile(account: string, bucketName: string, path: string): Promise<string> {
|
|
410
368
|
assertWritesAllowed();
|
|
411
|
-
// Validates now, so the upload doesn't fail at the end
|
|
412
369
|
assertValidPath(path);
|
|
413
370
|
let bucket = await getBucket(account, bucketName);
|
|
414
371
|
if (!bucket) {
|
|
@@ -445,11 +402,7 @@ class RemoteStorageControllerBase {
|
|
|
445
402
|
await bucket.store.cancelLargeUpload(uploadId);
|
|
446
403
|
}
|
|
447
404
|
|
|
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
405
|
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
406
|
let caller = SocketFunction.getCaller();
|
|
454
407
|
let request = getCurrentHTTPRequest();
|
|
455
408
|
let pathname = new URL(request?.url || "/", "https://localhost").pathname;
|
|
@@ -476,8 +429,6 @@ class RemoteStorageControllerBase {
|
|
|
476
429
|
if (!bucket.self?.public) {
|
|
477
430
|
throw new Error(`Bucket ${account}/${bucketName} is not public, so its files cannot be read over plain URLs`);
|
|
478
431
|
}
|
|
479
|
-
// The index answers existence + write time + size without touching any data, so
|
|
480
|
-
// If-Modified-Since and range validation cost nothing
|
|
481
432
|
let info = await bucket.store.getInfo(filePath);
|
|
482
433
|
if (!info || !info.size) {
|
|
483
434
|
return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
|
|
@@ -494,7 +445,6 @@ class RemoteStorageControllerBase {
|
|
|
494
445
|
let ifModifiedSince = request?.headers["if-modified-since"];
|
|
495
446
|
if (typeof ifModifiedSince === "string") {
|
|
496
447
|
let since = new Date(ifModifiedSince).getTime();
|
|
497
|
-
// Last-Modified is served at 1 second resolution, so compare at that resolution
|
|
498
448
|
if (since && Math.floor(info.writeTime / 1000) * 1000 <= since) {
|
|
499
449
|
return setHTTPResultHeaders(Buffer.from(""), { ...headers, status: "304" });
|
|
500
450
|
}
|
|
@@ -502,7 +452,6 @@ class RemoteStorageControllerBase {
|
|
|
502
452
|
let range: { start: number; end: number } | undefined;
|
|
503
453
|
let rangeHeader = request?.headers["range"];
|
|
504
454
|
if (typeof rangeHeader === "string") {
|
|
505
|
-
// Single-range form only (bytes=start-end / start- / -suffix); anything else serves the full file
|
|
506
455
|
let match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
|
|
507
456
|
if (match && (match[1] || match[2])) {
|
|
508
457
|
let start: number;
|
|
@@ -534,14 +483,16 @@ class RemoteStorageControllerBase {
|
|
|
534
483
|
|
|
535
484
|
const largeUploadInfo = new Map<string, { account: string; bucketName: string; path: string }>();
|
|
536
485
|
|
|
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
486
|
const accountAccess: SocketFunctionHook = async (context) => {
|
|
487
|
+
let start = Date.now();
|
|
540
488
|
await requireAccess(String(context.call.args[0]));
|
|
489
|
+
let duration = Date.now() - start;
|
|
490
|
+
if (duration > ACCESS_CHECK_SLOW_TIME) {
|
|
491
|
+
console.log(`Access check for ${context.call.functionName} took ${duration}ms`);
|
|
492
|
+
}
|
|
541
493
|
};
|
|
542
494
|
const uploadAccess: SocketFunctionHook = async (context) => {
|
|
543
495
|
let info = largeUploadInfo.get(String(context.call.args[0]));
|
|
544
|
-
// Unknown upload ids are handled by the methods themselves (throw / no-op)
|
|
545
496
|
if (!info) return;
|
|
546
497
|
await requireAccess(info.account);
|
|
547
498
|
};
|
|
@@ -571,6 +522,9 @@ export const RemoteStorageController = SocketFunction.register(
|
|
|
571
522
|
getArchivesConfig: { hooks: [accountAccess] },
|
|
572
523
|
getIndexInfo: { hooks: [accountAccess] },
|
|
573
524
|
listBuckets: { hooks: [accountAccess] },
|
|
525
|
+
getActiveBucket: { hooks: [accountAccess] },
|
|
526
|
+
activateBucket: { hooks: [accountAccess] },
|
|
527
|
+
clearWriteStats: { hooks: [accountAccess] },
|
|
574
528
|
getSyncStatus: { hooks: [accountAccess] },
|
|
575
529
|
startLargeFile: { hooks: [accountAccess] },
|
|
576
530
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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,6 +45,7 @@ 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
50
|
export declare function assertMutable(bucket: LoadedBucket, filePath: string, writeTime: number): Promise<void>;
|
|
41
51
|
export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
|
|
@@ -43,16 +53,42 @@ export declare function writeBucketFile(account: string, bucketName: string, fil
|
|
|
43
53
|
}): Promise<void>;
|
|
44
54
|
export declare function getBucketConfig(bucket: LoadedBucket): ArchivesConfig;
|
|
45
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
|
+
};
|
|
46
67
|
export type ServerBucketInfo = {
|
|
47
68
|
bucketName: string;
|
|
48
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;
|
|
49
76
|
config?: ArchivesConfig;
|
|
50
77
|
error?: string;
|
|
51
78
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
export type ActiveBucketInfo = {
|
|
80
|
+
folder: string;
|
|
81
|
+
/** The routing config the bucket is RUNNING on, straight from memory - including switchover windows written since it loaded */
|
|
82
|
+
routing: RemoteConfig;
|
|
83
|
+
/** Our own entries in that config, and the one currently valid */
|
|
84
|
+
selfEntries: HostedConfig[];
|
|
85
|
+
self?: HostedConfig;
|
|
86
|
+
config: ArchivesConfig;
|
|
87
|
+
};
|
|
88
|
+
/** The live in-memory state of ONE bucket, answered without touching the disk (no routing file read, no statfs, no stored write stats). Returns an error string when the bucket is not loaded here, which is the normal state for a bucket nothing has accessed since startup. */
|
|
89
|
+
export declare function getActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
|
|
90
|
+
/** 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. */
|
|
91
|
+
export declare function activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
|
|
56
92
|
export declare function listAccountBuckets(account: string): Promise<ServerBucketInfo[]>;
|
|
57
93
|
export declare function deleteBucketFile(account: string, bucketName: string, filePath: string): Promise<void>;
|
|
58
94
|
export declare function getLocalArchives(account: string, bucketName: string): IArchives;
|