sliftutils 1.7.57 → 1.7.59
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/index.d.ts +16 -3
- package/package.json +1 -1
- package/storage/backblaze.ts +5 -3
- package/storage/dist/IArchives.ts.cache +2 -2
- package/storage/dist/backblaze.ts.cache +6 -5
- package/storage/remoteStorage/createArchives.d.ts +3 -0
- package/storage/remoteStorage/createArchives.ts +30 -18
- package/storage/remoteStorage/dist/blobStore.ts.cache +2 -2
- package/storage/remoteStorage/dist/createArchives.ts.cache +33 -25
- package/storage/remoteStorage/dist/storageController.ts.cache +3 -2
- package/storage/remoteStorage/dist/storageServerState.ts.cache +52 -13
- package/storage/remoteStorage/storageController.ts +1 -0
- package/storage/remoteStorage/storageServerState.d.ts +13 -3
- package/storage/remoteStorage/storageServerState.ts +64 -13
|
@@ -402,6 +402,7 @@ class RemoteStorageControllerBase {
|
|
|
402
402
|
await bucket.store.cancelLargeUpload(uploadId);
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
// 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).
|
|
405
406
|
async httpEntry(config?: { requireCalls?: string[]; cacheTime?: number }): Promise<Buffer> {
|
|
406
407
|
let caller = SocketFunction.getCaller();
|
|
407
408
|
let request = getCurrentHTTPRequest();
|
|
@@ -40,7 +40,7 @@ export type LoadedBucket = {
|
|
|
40
40
|
routing: RemoteConfig;
|
|
41
41
|
routingJSON: string;
|
|
42
42
|
selfEntries: HostedConfig[];
|
|
43
|
-
self:
|
|
43
|
+
self: SelfSummary | undefined;
|
|
44
44
|
store: IBucketStore;
|
|
45
45
|
structureKey: string;
|
|
46
46
|
};
|
|
@@ -48,6 +48,16 @@ export declare function addExtraListenPort(port: number): void;
|
|
|
48
48
|
export declare function removeExtraListenPort(port: number): void;
|
|
49
49
|
/** 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. */
|
|
50
50
|
export declare function resolveSourceArchives(url: string): IArchives;
|
|
51
|
+
/** Our role in a bucket's routing config, summarized across ALL currently-valid self entries. Stored instead of a single representative HostedConfig, so nothing can accidentally use one entry's route or flags where the union is required - the standard config has the same URL twice: a routed write-shard entry plus an unrouted read-everything entry. */
|
|
52
|
+
export type SelfSummary = {
|
|
53
|
+
/** The union of the current entries' routes, with overlapping/adjacent ranges combined - which commonly collapses to a single full range, making matching trivial. */
|
|
54
|
+
routes: [number, number][];
|
|
55
|
+
public: boolean;
|
|
56
|
+
immutable: boolean;
|
|
57
|
+
noFullSync: boolean;
|
|
58
|
+
rawDisk: boolean;
|
|
59
|
+
readerDiskLimit?: number;
|
|
60
|
+
};
|
|
51
61
|
export declare function getLoadedBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined>;
|
|
52
62
|
export declare function assertMutable(bucket: LoadedBucket, filePath: string, writeTime: number): Promise<void>;
|
|
53
63
|
export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
|
|
@@ -88,9 +98,9 @@ export type ActiveBucketInfo = {
|
|
|
88
98
|
folder: string;
|
|
89
99
|
/** The routing config the bucket is RUNNING on, straight from memory - including switchover windows written since it loaded */
|
|
90
100
|
routing: RemoteConfig;
|
|
91
|
-
/** Our own entries in that config, and
|
|
101
|
+
/** Our own entries in that config, and their summarized current role (routes union + flags) */
|
|
92
102
|
selfEntries: HostedConfig[];
|
|
93
|
-
self?:
|
|
103
|
+
self?: SelfSummary;
|
|
94
104
|
config: ArchivesConfig;
|
|
95
105
|
};
|
|
96
106
|
/** 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. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import { lazy } from "socket-function/src/caching";
|
|
4
|
+
import { sort } from "socket-function/src/misc";
|
|
4
5
|
import { runInfinitePoll } from "socket-function/src/batching";
|
|
5
6
|
import { getFileStorageNested2 } from "../FileFolderAPI";
|
|
6
7
|
import { TransactionStorage } from "../TransactionStorage";
|
|
@@ -201,7 +202,7 @@ export type LoadedBucket = {
|
|
|
201
202
|
routing: RemoteConfig;
|
|
202
203
|
routingJSON: string;
|
|
203
204
|
selfEntries: HostedConfig[];
|
|
204
|
-
self:
|
|
205
|
+
self: SelfSummary | undefined;
|
|
205
206
|
store: IBucketStore;
|
|
206
207
|
structureKey: string;
|
|
207
208
|
};
|
|
@@ -417,7 +418,7 @@ async function runBoundaryScan(bucketKey: string, windowStart: number, offset: n
|
|
|
417
418
|
|
|
418
419
|
type StorePlan = {
|
|
419
420
|
selfEntries: HostedConfig[];
|
|
420
|
-
self:
|
|
421
|
+
self: SelfSummary | undefined;
|
|
421
422
|
rawDisk: boolean;
|
|
422
423
|
sourceSpecs: { sourceConfig?: HostedConfig | BackblazeConfig; validWindow: [number, number]; route?: [number, number]; noFullSync?: boolean }[];
|
|
423
424
|
readerDiskLimit?: number;
|
|
@@ -444,17 +445,64 @@ function sourceIdentity(sourceConfig: HostedConfig | BackblazeConfig | undefined
|
|
|
444
445
|
return JSON.stringify({ ...sourceConfig, validWindow: undefined, route: undefined });
|
|
445
446
|
}
|
|
446
447
|
|
|
448
|
+
/** Our role in a bucket's routing config, summarized across ALL currently-valid self entries. Stored instead of a single representative HostedConfig, so nothing can accidentally use one entry's route or flags where the union is required - the standard config has the same URL twice: a routed write-shard entry plus an unrouted read-everything entry. */
|
|
449
|
+
export type SelfSummary = {
|
|
450
|
+
/** The union of the current entries' routes, with overlapping/adjacent ranges combined - which commonly collapses to a single full range, making matching trivial. */
|
|
451
|
+
routes: [number, number][];
|
|
452
|
+
public: boolean;
|
|
453
|
+
immutable: boolean;
|
|
454
|
+
noFullSync: boolean;
|
|
455
|
+
rawDisk: boolean;
|
|
456
|
+
readerDiskLimit?: number;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
function mergeRoutes(routes: ([number, number] | undefined)[]): [number, number][] {
|
|
460
|
+
let list = routes.map(x => x || FULL_ROUTE).map(x => [x[0], x[1]] as [number, number]);
|
|
461
|
+
sort(list, x => x[0]);
|
|
462
|
+
let merged: [number, number][] = [];
|
|
463
|
+
for (let route of list) {
|
|
464
|
+
let last = merged[merged.length - 1];
|
|
465
|
+
if (last && route[0] <= last[1]) {
|
|
466
|
+
last[1] = Math.max(last[1], route[1]);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
merged.push(route);
|
|
470
|
+
}
|
|
471
|
+
return merged;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** anchor is the first currently-valid entry (nearest-window when none contains now, matching selectEntryAt) - the deterministic representative used only for positional things: the sync-topology cut (selfIndex) and the diskWindow seed. Everything behavioral comes from the summary, which spans every entry the anchor's group represents. */
|
|
475
|
+
function summarizeSelf(selfEntries: HostedConfig[], now: number): { summary: SelfSummary | undefined; anchor: HostedConfig | undefined } {
|
|
476
|
+
let current = selfEntries.filter(x => x.validWindow[0] <= now && now < x.validWindow[1]);
|
|
477
|
+
if (!current.length) {
|
|
478
|
+
let nearest = selectEntryAt(selfEntries, now);
|
|
479
|
+
current = nearest && [nearest] || [];
|
|
480
|
+
}
|
|
481
|
+
if (!current.length) {
|
|
482
|
+
return { summary: undefined, anchor: undefined };
|
|
483
|
+
}
|
|
484
|
+
let summary: SelfSummary = {
|
|
485
|
+
routes: mergeRoutes(current.map(x => x.route)),
|
|
486
|
+
public: current.some(x => x.public),
|
|
487
|
+
immutable: current.some(x => x.immutable),
|
|
488
|
+
noFullSync: current.some(x => x.noFullSync),
|
|
489
|
+
rawDisk: current.some(x => x.rawDisk),
|
|
490
|
+
readerDiskLimit: current.find(x => x.readerDiskLimit !== undefined)?.readerDiskLimit,
|
|
491
|
+
};
|
|
492
|
+
return { summary, anchor: current[0] };
|
|
493
|
+
}
|
|
494
|
+
|
|
447
495
|
function computeStorePlan(account: string, bucketName: string, routing: RemoteConfig): StorePlan {
|
|
448
496
|
let selfIndexes = findSelfIndexes(routing, account, bucketName);
|
|
449
497
|
let selfEntries = selfIndexes.map(i => routing.sources[i] as HostedConfig);
|
|
450
|
-
let self =
|
|
498
|
+
let { summary: self, anchor } = summarizeSelf(selfEntries, Date.now());
|
|
451
499
|
let selfIndex = -1;
|
|
452
|
-
if (
|
|
453
|
-
selfIndex = routing.sources.indexOf(
|
|
500
|
+
if (anchor) {
|
|
501
|
+
selfIndex = routing.sources.indexOf(anchor);
|
|
454
502
|
}
|
|
455
503
|
let diskWindow: [number, number] = [0, 0];
|
|
456
|
-
if (
|
|
457
|
-
let [start, end] =
|
|
504
|
+
if (anchor) {
|
|
505
|
+
let [start, end] = anchor.validWindow;
|
|
458
506
|
let merged = true;
|
|
459
507
|
while (merged) {
|
|
460
508
|
merged = false;
|
|
@@ -474,13 +522,16 @@ function computeStorePlan(account: string, bucketName: string, routing: RemoteCo
|
|
|
474
522
|
let sourceSpecs: StorePlan["sourceSpecs"] = [{
|
|
475
523
|
validWindow: diskWindow,
|
|
476
524
|
}];
|
|
477
|
-
if (selfIndex !== -1) {
|
|
525
|
+
if (self && selfIndex !== -1) {
|
|
478
526
|
for (let i = selfIndex + 1; i < routing.sources.length; i++) {
|
|
479
527
|
let source = routing.sources[i];
|
|
480
528
|
if (typeof source === "string" || ownIndexes.has(i)) continue;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
529
|
+
// One spec per intersection segment: a peer's route can overlap several of our (disjoint) route slices, and each slice becomes its own sync-source slot (same-URL multi-slot is already the supported shape)
|
|
530
|
+
for (let selfRoute of self.routes) {
|
|
531
|
+
let sharedRoute = routeIntersection(selfRoute, source.route);
|
|
532
|
+
if (!sharedRoute) continue;
|
|
533
|
+
sourceSpecs.push({ sourceConfig: source, validWindow: source.validWindow, route: sharedRoute, noFullSync: source.noFullSync || self.noFullSync });
|
|
534
|
+
}
|
|
484
535
|
}
|
|
485
536
|
}
|
|
486
537
|
let rawDisk = !!self?.rawDisk;
|
|
@@ -914,9 +965,9 @@ export type ActiveBucketInfo = {
|
|
|
914
965
|
folder: string;
|
|
915
966
|
/** The routing config the bucket is RUNNING on, straight from memory - including switchover windows written since it loaded */
|
|
916
967
|
routing: RemoteConfig;
|
|
917
|
-
/** Our own entries in that config, and
|
|
968
|
+
/** Our own entries in that config, and their summarized current role (routes union + flags) */
|
|
918
969
|
selfEntries: HostedConfig[];
|
|
919
|
-
self?:
|
|
970
|
+
self?: SelfSummary;
|
|
920
971
|
config: ArchivesConfig;
|
|
921
972
|
};
|
|
922
973
|
|