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.
- 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 +106 -99
- 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 +9 -13
- package/storage/remoteStorage/createArchives.ts +43 -187
- 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 +54 -186
- 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 +32 -0
- package/storage/remoteStorage/dist/storageController.ts.cache +30 -64
- package/storage/remoteStorage/dist/storageServerState.ts.cache +269 -186
- 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 +5 -3
- package/storage/remoteStorage/storageController.ts +12 -83
- package/storage/remoteStorage/storageServer.ts +20 -34
- package/storage/remoteStorage/storageServerState.d.ts +28 -6
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
/**
|
|
394
|
-
|
|
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 {
|
|
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,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;
|