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