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.
@@ -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: HostedConfig | undefined;
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 the one currently valid */
101
+ /** Our own entries in that config, and their summarized current role (routes union + flags) */
92
102
  selfEntries: HostedConfig[];
93
- self?: HostedConfig;
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: HostedConfig | undefined;
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: HostedConfig | undefined;
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 = selectEntryAt(selfEntries, Date.now());
498
+ let { summary: self, anchor } = summarizeSelf(selfEntries, Date.now());
451
499
  let selfIndex = -1;
452
- if (self) {
453
- selfIndex = routing.sources.indexOf(self);
500
+ if (anchor) {
501
+ selfIndex = routing.sources.indexOf(anchor);
454
502
  }
455
503
  let diskWindow: [number, number] = [0, 0];
456
- if (self) {
457
- let [start, end] = self.validWindow;
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
- let sharedRoute = routeIntersection(self?.route, source.route);
482
- if (!sharedRoute) continue;
483
- sourceSpecs.push({ sourceConfig: source, validWindow: source.validWindow, route: sharedRoute, noFullSync: source.noFullSync || self?.noFullSync });
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 the one currently valid */
968
+ /** Our own entries in that config, and their summarized current role (routes union + flags) */
918
969
  selfEntries: HostedConfig[];
919
- self?: HostedConfig;
970
+ self?: SelfSummary;
920
971
  config: ArchivesConfig;
921
972
  };
922
973