sliftutils 1.7.71 → 1.7.73

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.
Files changed (62) hide show
  1. package/index.d.ts +506 -257
  2. package/misc/https/dist/hostServer.ts.cache +5 -2
  3. package/package.json +1 -1
  4. package/spec.txt +10 -0
  5. package/storage/ArchivesDisk.d.ts +4 -13
  6. package/storage/ArchivesDisk.ts +6 -4
  7. package/storage/IArchives.d.ts +15 -10
  8. package/storage/IArchives.ts +19 -6
  9. package/storage/backblaze.d.ts +3 -9
  10. package/storage/backblaze.ts +22 -19
  11. package/storage/dist/ArchivesDisk.ts.cache +5 -2
  12. package/storage/dist/IArchives.ts.cache +17 -3
  13. package/storage/dist/backblaze.ts.cache +35 -26
  14. package/storage/remoteStorage/ArchivesRemote.d.ts +5 -9
  15. package/storage/remoteStorage/ArchivesRemote.ts +25 -20
  16. package/storage/remoteStorage/ArchivesUrl.d.ts +3 -9
  17. package/storage/remoteStorage/ArchivesUrl.ts +7 -5
  18. package/storage/remoteStorage/accessPage.tsx +4 -4
  19. package/storage/remoteStorage/accessStats.d.ts +2 -0
  20. package/storage/remoteStorage/accessStats.ts +32 -0
  21. package/storage/remoteStorage/blobStore.d.ts +193 -88
  22. package/storage/remoteStorage/blobStore.ts +535 -400
  23. package/storage/remoteStorage/bucketDisk.d.ts +18 -0
  24. package/storage/remoteStorage/bucketDisk.ts +66 -0
  25. package/storage/remoteStorage/createArchives.d.ts +4 -10
  26. package/storage/remoteStorage/createArchives.ts +103 -62
  27. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +28 -19
  28. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +7 -4
  29. package/storage/remoteStorage/dist/accessPage.tsx.cache +6 -6
  30. package/storage/remoteStorage/dist/accessStats.ts.cache +38 -3
  31. package/storage/remoteStorage/dist/blobStore.ts.cache +516 -374
  32. package/storage/remoteStorage/dist/bucketDisk.ts.cache +72 -0
  33. package/storage/remoteStorage/dist/createArchives.ts.cache +114 -57
  34. package/storage/remoteStorage/dist/intermediateSources.ts.cache +12 -12
  35. package/storage/remoteStorage/dist/remoteConfig.ts.cache +18 -2
  36. package/storage/remoteStorage/dist/serverConfig.ts.cache +116 -0
  37. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +6 -5
  38. package/storage/remoteStorage/dist/sourcesList.ts.cache +5 -2
  39. package/storage/remoteStorage/dist/storageController.ts.cache +223 -190
  40. package/storage/remoteStorage/dist/storageServer.ts.cache +9 -9
  41. package/storage/remoteStorage/dist/storageServerState.ts.cache +329 -566
  42. package/storage/remoteStorage/dist/storePlan.ts.cache +149 -0
  43. package/storage/remoteStorage/dist/validation.ts.cache +40 -0
  44. package/storage/remoteStorage/grantAccessCli.ts +1 -1
  45. package/storage/remoteStorage/intermediateSources.d.ts +3 -5
  46. package/storage/remoteStorage/intermediateSources.ts +14 -16
  47. package/storage/remoteStorage/remoteConfig.d.ts +2 -2
  48. package/storage/remoteStorage/remoteConfig.ts +19 -3
  49. package/storage/remoteStorage/serverConfig.d.ts +27 -0
  50. package/storage/remoteStorage/serverConfig.ts +109 -0
  51. package/storage/remoteStorage/sourceWrapper.d.ts +3 -3
  52. package/storage/remoteStorage/sourceWrapper.ts +7 -6
  53. package/storage/remoteStorage/sourcesList.ts +3 -0
  54. package/storage/remoteStorage/storageController.d.ts +118 -36
  55. package/storage/remoteStorage/storageController.ts +166 -168
  56. package/storage/remoteStorage/storageServer.ts +1 -1
  57. package/storage/remoteStorage/storageServerState.d.ts +52 -63
  58. package/storage/remoteStorage/storageServerState.ts +340 -584
  59. package/storage/remoteStorage/storePlan.d.ts +34 -0
  60. package/storage/remoteStorage/storePlan.ts +166 -0
  61. package/storage/remoteStorage/validation.d.ts +4 -0
  62. package/storage/remoteStorage/validation.ts +31 -0
@@ -1,9 +1,11 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
- import { IArchives, ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, DelConfig, GetConfig, GetInfoConfig, SetConfig } from "../IArchives";
3
+ import { IArchives, ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, SourceConfig, SetConfig } from "../IArchives";
4
4
  export type ArchivesRemoteConfig = {
5
5
  url: string;
6
6
  waitForAccess?: boolean;
7
+ /** The exact routing-config entry this connection represents, sent with every call so the server picks the matching per-route store (one server hosts one store per route). Instances built from a bare URL fabricate one - it will never match, which only works for calls that don't select a store (internal reads, ROUTING_FILE, getConfig). */
8
+ sourceConfig: SourceConfig;
7
9
  };
8
10
  export declare function parseStorageUrl(url: string): {
9
11
  address: string;
@@ -51,14 +53,8 @@ export declare class ArchivesRemote implements IArchives {
51
53
  writeTime: number;
52
54
  size: number;
53
55
  } | undefined>;
54
- findInfo(prefix: string, config?: {
55
- shallow?: boolean;
56
- type: "files" | "folders";
57
- }): Promise<ArchiveFileInfo[]>;
58
- find(prefix: string, config?: {
59
- shallow?: boolean;
60
- type: "files" | "folders";
61
- }): Promise<string[]>;
56
+ findInfo(prefix: string, config?: FindConfig): Promise<ArchiveFileInfo[]>;
57
+ find(prefix: string, config?: FindConfig): Promise<string[]>;
62
58
  getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
63
59
  getConfig(): Promise<ArchivesConfig>;
64
60
  getSyncStatus(): Promise<ArchivesSyncStatus>;
@@ -2,7 +2,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { timeInMinute } from "socket-function/src/misc";
3
3
  import { delay } from "socket-function/src/batching";
4
4
  import { getIdentityCA, loadIdentityCA, sign } from "../../misc/https/certs";
5
- import { IArchives, ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, DelConfig, GetConfig, GetInfoConfig, SetConfig, LARGE_SET_THRESHOLD, bufferChunkStream } from "../IArchives";
5
+ import { IArchives, ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, SourceConfig, SetConfig, LARGE_SET_THRESHOLD, bufferChunkStream } from "../IArchives";
6
6
  import { parseHostedUrl, getBucketBaseUrl, buildFileUrl } from "./remoteConfig";
7
7
  import {
8
8
  RemoteStorageController, STORAGE_AUTH_PURPOSE,
@@ -20,6 +20,8 @@ export type ArchivesRemoteConfig = {
20
20
  url: string;
21
21
  // false: access-denied calls throw immediately (the error includes the access page link) instead of requesting access and blocking until it is granted (the default).
22
22
  waitForAccess?: boolean;
23
+ /** The exact routing-config entry this connection represents, sent with every call so the server picks the matching per-route store (one server hosts one store per route). Instances built from a bare URL fabricate one - it will never match, which only works for calls that don't select a store (internal reads, ROUTING_FILE, getConfig). */
24
+ sourceConfig: SourceConfig;
23
25
  };
24
26
 
25
27
  export function parseStorageUrl(url: string): { address: string; port: number } {
@@ -88,9 +90,9 @@ export class ArchivesRemote implements IArchives {
88
90
 
89
91
  // Returns undefined if this machine has access to the account. Otherwise puts in an access request and returns our machineId + ip (so the caller can display them alongside the link, for the approver to match the incoming request) and the link to the grant page.
90
92
  public async waitingForAccess(): Promise<{ link: string; machineId: string; ip: string } | undefined> {
91
- let state = await this.callAuthed(() => this.controller.getAccessState(this.account));
93
+ let state = await this.callAuthed(() => this.controller.getAccessState({ account: this.account }));
92
94
  if (state.hasAccess) return undefined;
93
- let requested = await this.callAuthed(() => this.controller.requestAccess(this.account));
95
+ let requested = await this.callAuthed(() => this.controller.requestAccess({ account: this.account }));
94
96
  return {
95
97
  link: `https://${this.parsed.address}:${this.parsed.port}/${this.account}`,
96
98
  machineId: requested.machineId,
@@ -99,13 +101,13 @@ export class ArchivesRemote implements IArchives {
99
101
  }
100
102
 
101
103
  public async hasWriteAccess(): Promise<boolean> {
102
- let state = await this.callAuthed(() => this.controller.getAccessState(this.account));
104
+ let state = await this.callAuthed(() => this.controller.getAccessState({ account: this.account }));
103
105
  return !!state.hasAccess;
104
106
  }
105
107
 
106
108
  // Registers our access request server-side (so an admin has a requestId to grant) and logs the grant instructions, at most once a minute
107
109
  private async registerAccessRequest(): Promise<void> {
108
- let requested = await this.callAuthed(() => this.controller.requestAccess(this.account));
110
+ let requested = await this.callAuthed(() => this.controller.requestAccess({ account: this.account }));
109
111
  if (Date.now() - this.lastDeniedLog > timeInMinute) {
110
112
  this.lastDeniedLog = Date.now();
111
113
  console.log(`No access to storage account ${JSON.stringify(this.account)} on ${this.parsed.address}:${this.parsed.port} (our machine ${requested.machineId}, ip ${requested.ip}). See https://${this.parsed.address}:${this.parsed.port}/${this.account} - or grant it with: ${requested.grantAccessCommand}`);
@@ -142,56 +144,59 @@ export class ArchivesRemote implements IArchives {
142
144
  return result && result.data || undefined;
143
145
  }
144
146
  public async get2(fileName: string, config?: GetConfig): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
145
- let result = await this.call(() => this.controller.get2(this.account, this.bucketName, fileName, config?.range, config?.internal));
147
+ let result = await this.call(() => this.controller.get2({ account: this.account, bucketName: this.bucketName, path: fileName, sourceConfig: this.config.sourceConfig, range: config?.range, internal: config?.internal, includeTombstones: config?.includeTombstones }));
146
148
  return result && { data: Buffer.from(result.data), writeTime: result.writeTime, size: result.size } || undefined;
147
149
  }
148
150
  public async set(fileName: string, data: Buffer, config?: SetConfig): Promise<string> {
151
+ if (!data.length) {
152
+ throw new Error(`set was called with an empty buffer for ${JSON.stringify(fileName)} on ${this.getDebugName()}: an empty file IS a deletion in this system and would read back as missing - call del instead`);
153
+ }
149
154
  if (data.length > LARGE_SET_THRESHOLD) {
150
155
  // One giant message would exceed the wire limit and lag every other client sharing this connection - stream it instead. (The large-file path cannot carry forceSetImmutable/noChecks/internal; large pushes accept plain-write semantics, and reconciliation's per-file error handling absorbs the rare immutable rejection.)
151
156
  await this.setLargeFile({ path: fileName, lastModified: config?.lastModified, getNextData: bufferChunkStream(data) });
152
157
  return fileName;
153
158
  }
154
- await this.call(() => this.controller.set(this.account, this.bucketName, fileName, data, config?.lastModified, config?.forceSetImmutable, config?.internal));
159
+ await this.call(() => this.controller.set({ account: this.account, bucketName: this.bucketName, path: fileName, data, sourceConfig: this.config.sourceConfig, lastModified: config?.lastModified, forceSetImmutable: config?.forceSetImmutable, internal: config?.internal }));
155
160
  return fileName;
156
161
  }
157
162
  public async del(fileName: string, config?: DelConfig): Promise<void> {
158
- await this.call(() => this.controller.del(this.account, this.bucketName, fileName, config?.lastModified, config?.internal));
163
+ await this.call(() => this.controller.del({ account: this.account, bucketName: this.bucketName, path: fileName, sourceConfig: this.config.sourceConfig, lastModified: config?.lastModified, internal: config?.internal }));
159
164
  }
160
165
  public async getInfo(fileName: string, config?: GetInfoConfig): Promise<{ writeTime: number; size: number } | undefined> {
161
- return await this.call(() => this.controller.getInfo(this.account, this.bucketName, fileName, config?.includeTombstones));
166
+ return await this.call(() => this.controller.getInfo({ account: this.account, bucketName: this.bucketName, path: fileName, sourceConfig: this.config.sourceConfig, includeTombstones: config?.includeTombstones }));
162
167
  }
163
- public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
164
- return await this.call(() => this.controller.findInfo(this.account, this.bucketName, prefix, config));
168
+ public async findInfo(prefix: string, config?: FindConfig): Promise<ArchiveFileInfo[]> {
169
+ return await this.call(() => this.controller.findInfo({ account: this.account, bucketName: this.bucketName, prefix, sourceConfig: this.config.sourceConfig, shallow: config?.shallow, type: config?.type }));
165
170
  }
166
- public async find(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<string[]> {
171
+ public async find(prefix: string, config?: FindConfig): Promise<string[]> {
167
172
  return (await this.findInfo(prefix, config)).map(x => x.path);
168
173
  }
169
174
  public async getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
170
- return await this.call(() => this.controller.getChangesAfter2(this.account, this.bucketName, config));
175
+ return await this.call(() => this.controller.getChangesAfter2({ account: this.account, bucketName: this.bucketName, sourceConfig: this.config.sourceConfig, time: config.time, routes: config.routes }));
171
176
  }
172
177
  public async getConfig(): Promise<ArchivesConfig> {
173
- return await this.call(() => this.controller.getArchivesConfig(this.account, this.bucketName));
178
+ return await this.call(() => this.controller.getArchivesConfig({ account: this.account, bucketName: this.bucketName }));
174
179
  }
175
180
  public async getSyncStatus(): Promise<ArchivesSyncStatus> {
176
- return await this.call(() => this.controller.getSyncStatus(this.account, this.bucketName));
181
+ return await this.call(() => this.controller.getSyncStatus({ account: this.account, bucketName: this.bucketName }));
177
182
  }
178
183
 
179
184
  public async setLargeFile(config: { path: string; lastModified?: number; getNextData(): Promise<Buffer | undefined> }): Promise<void> {
180
185
  // Ensure we're authenticated with access BEFORE consuming any data (the stream cannot be rewound, so we can't use the retry loop around the actual upload)
181
- await this.call(() => this.controller.getInfo(this.account, this.bucketName, config.path));
182
- let uploadId = await this.controller.startLargeFile(this.account, this.bucketName, config.path, config.lastModified);
186
+ await this.call(() => this.controller.getInfo({ account: this.account, bucketName: this.bucketName, path: config.path, sourceConfig: this.config.sourceConfig }));
187
+ let uploadId = await this.controller.startLargeFile({ account: this.account, bucketName: this.bucketName, path: config.path, sourceConfig: this.config.sourceConfig, lastModified: config.lastModified });
183
188
  try {
184
189
  while (true) {
185
190
  let data = await config.getNextData();
186
191
  if (!data) break;
187
192
  for (let offset = 0; offset < data.length; offset += LARGE_FILE_PART_SIZE) {
188
- await this.controller.uploadPart(uploadId, data.subarray(offset, offset + LARGE_FILE_PART_SIZE));
193
+ await this.controller.uploadPart({ uploadId, data: data.subarray(offset, offset + LARGE_FILE_PART_SIZE) });
189
194
  }
190
195
  }
191
- await this.controller.finishLargeFile(uploadId);
196
+ await this.controller.finishLargeFile({ uploadId });
192
197
  } catch (e) {
193
198
  try {
194
- await this.controller.cancelLargeFile(uploadId);
199
+ await this.controller.cancelLargeFile({ uploadId });
195
200
  } catch { }
196
201
  throw e;
197
202
  }
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
- import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, GetConfig, GetInfoConfig } from "../IArchives";
3
+ import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, FindConfig, GetConfig, GetInfoConfig } from "../IArchives";
4
4
  export declare class ArchivesUrl implements IArchives {
5
5
  private base;
6
6
  constructor(base: string);
@@ -24,14 +24,8 @@ export declare class ArchivesUrl implements IArchives {
24
24
  path: string;
25
25
  getNextData(): Promise<Buffer | undefined>;
26
26
  }): Promise<void>;
27
- find(prefix: string, config?: {
28
- shallow?: boolean;
29
- type: "files" | "folders";
30
- }): Promise<string[]>;
31
- findInfo(prefix: string, config?: {
32
- shallow?: boolean;
33
- type: "files" | "folders";
34
- }): Promise<ArchiveFileInfo[]>;
27
+ find(prefix: string, config?: FindConfig): Promise<string[]>;
28
+ findInfo(prefix: string, config?: FindConfig): Promise<ArchiveFileInfo[]>;
35
29
  getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
36
30
  getURL(path: string): Promise<string>;
37
31
  getConfig(): Promise<ArchivesConfig>;
@@ -1,5 +1,5 @@
1
1
  import { httpsRequest, HttpsResponseInfo } from "socket-function/src/https";
2
- import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, GetConfig, GetInfoConfig } from "../IArchives";
2
+ import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, FindConfig, GetConfig, GetInfoConfig } from "../IArchives";
3
3
  import { buildFileUrl } from "./remoteConfig";
4
4
 
5
5
  // Read-only IArchives over a public bucket's plain-URL form (our storage server's
@@ -50,16 +50,18 @@ export class ArchivesUrl implements IArchives {
50
50
  }
51
51
  let lastModified = response.headers["last-modified"];
52
52
  let writeTime = lastModified && new Date(lastModified).getTime() || 0;
53
+ // A size-0 answer is a tombstone (backblaze friendly URLs serve deletions as real empty files; our own servers 404 them) - absent, unless the caller asked for tombstones. Ranged reads of a REAL file can legitimately return no bytes (range past EOF), which the total size distinguishes.
54
+ if (!size && !config?.includeTombstones) return undefined;
53
55
  return { data, writeTime, size };
54
56
  }
55
57
  public async getInfo(fileName: string, config?: GetInfoConfig): Promise<{ writeTime: number; size: number } | undefined> {
56
58
  // A 1-byte ranged read: Content-Range carries the full size and Last-Modified the write time, so metadata never downloads the file
57
59
  let result: { writeTime: number; size: number } | undefined;
58
60
  try {
59
- result = await this.get2(fileName, { range: { start: 0, end: 1 } });
61
+ result = await this.get2(fileName, { range: { start: 0, end: 1 }, includeTombstones: config?.includeTombstones });
60
62
  } catch {
61
63
  // Some servers reject ranged reads of empty files (416 instead of an empty 206); a full read is the fallback, and for an empty file it is free anyway
62
- result = await this.get2(fileName);
64
+ result = await this.get2(fileName, { includeTombstones: config?.includeTombstones });
63
65
  }
64
66
  if (!result) return undefined;
65
67
  if (!result.size && !config?.includeTombstones) return undefined;
@@ -75,10 +77,10 @@ export class ArchivesUrl implements IArchives {
75
77
  public async setLargeFile(config: { path: string; getNextData(): Promise<Buffer | undefined> }): Promise<void> {
76
78
  throw this.readOnlyError("setLargeFile");
77
79
  }
78
- public async find(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<string[]> {
80
+ public async find(prefix: string, config?: FindConfig): Promise<string[]> {
79
81
  throw this.readOnlyError("find (listing)");
80
82
  }
81
- public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
83
+ public async findInfo(prefix: string, config?: FindConfig): Promise<ArchiveFileInfo[]> {
82
84
  throw this.readOnlyError("findInfo (listing)");
83
85
  }
84
86
  public async getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
@@ -82,8 +82,8 @@ class AccessPage extends preact.Component {
82
82
  }
83
83
  private async refresh(account: string) {
84
84
  let controller = await this.controller();
85
- await controller.requestAccess(account);
86
- this.synced.state = await controller.getAccessState(account);
85
+ await controller.requestAccess({ account });
86
+ this.synced.state = await controller.getAccessState({ account });
87
87
  this.synced.error = "";
88
88
  }
89
89
 
@@ -94,7 +94,7 @@ class AccessPage extends preact.Component {
94
94
  this.synced.lookupError = "";
95
95
  try {
96
96
  let controller = await this.controller();
97
- let requests = await controller.listRequestsForIP(this.synced.account, ip);
97
+ let requests = await controller.listRequestsForIP({ account: this.synced.account, ip });
98
98
  this.synced.lookupResults = { ip, requests };
99
99
  } catch (e) {
100
100
  this.synced.lookupError = String(e);
@@ -107,7 +107,7 @@ class AccessPage extends preact.Component {
107
107
  this.synced.granting = request.requestId;
108
108
  try {
109
109
  let controller = await this.controller();
110
- await controller.grantAccess(request.requestId);
110
+ await controller.grantAccess({ requestId: request.requestId });
111
111
  if (this.synced.lookupResults) {
112
112
  this.synced.lookupResults.requests = this.synced.lookupResults.requests.filter(r => r.requestId !== request.requestId);
113
113
  }
@@ -15,6 +15,8 @@ export declare function trackAccess(config: {
15
15
  path: string;
16
16
  size?: number;
17
17
  }): void;
18
+ /** Method decorator factory, for API methods whose single config-object argument has account and bucketName: tracks the access (as `bucketName/path`) after the method succeeds. Sizes come from the config's data (writes) or the result's data (reads); operations without either are count-only. Array results (listings) track one access per returned path, or a single access at the prefix when empty. */
19
+ export declare function trackAccessCall(operation: string): (target: unknown, key: string, descriptor: PropertyDescriptor) => void;
18
20
  export declare function getAccessTotals(account: string): AccessTotals;
19
21
  export declare function readAccessSummaries(config: {
20
22
  account: string;
@@ -57,6 +57,38 @@ export function trackAccess(config: { account: string; operation: string; path:
57
57
  }
58
58
  }
59
59
 
60
+ /** Method decorator factory, for API methods whose single config-object argument has account and bucketName: tracks the access (as `bucketName/path`) after the method succeeds. Sizes come from the config's data (writes) or the result's data (reads); operations without either are count-only. Array results (listings) track one access per returned path, or a single access at the prefix when empty. */
61
+ export function trackAccessCall(operation: string) {
62
+ return function (target: unknown, key: string, descriptor: PropertyDescriptor): void {
63
+ let original = descriptor.value as (...args: unknown[]) => Promise<unknown>;
64
+ descriptor.value = async function (...args: unknown[]): Promise<unknown> {
65
+ let config = args[0] as { account: string; bucketName: string; path?: string; prefix?: string; data?: Buffer };
66
+ let result = await original.apply(this, args);
67
+ let base = `${config.bucketName}/`;
68
+ if (Array.isArray(result)) {
69
+ if (result.length) {
70
+ for (let entry of result as { path: string }[]) {
71
+ trackAccess({ account: config.account, operation, path: base + entry.path });
72
+ }
73
+ } else {
74
+ // The paths are only known once the results are in; an empty result still counts as one access at the prefix
75
+ trackAccess({ account: config.account, operation, path: base + (config.prefix || config.path || "") });
76
+ }
77
+ return result;
78
+ }
79
+ let size: number | undefined;
80
+ if (config.data) {
81
+ size = config.data.length;
82
+ } else {
83
+ let data = (result as { data?: Buffer } | undefined)?.data;
84
+ if (data) size = data.length;
85
+ }
86
+ trackAccess({ account: config.account, operation, path: base + config.path, size });
87
+ return result;
88
+ };
89
+ };
90
+ }
91
+
60
92
  export function getAccessTotals(account: string): AccessTotals {
61
93
  let result: AccessTotals = {};
62
94
  let operations = accounts.get(account);
@@ -1,37 +1,46 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
- import { IArchives, ArchiveFileInfo, ArchivesSource, ArchivesSyncStatus, ChangesAfterConfig, GetConfig, GetInfoConfig, SyncActivity } from "../IArchives";
3
+ import { IArchives, ArchiveFileInfo, ArchivesSource, ArchivesSyncStatus, ChangesAfterConfig, FindConfig, HostedConfig, SyncActivity } from "../IArchives";
4
+ import { ArchivesDisk } from "../ArchivesDisk";
4
5
  export declare const DEFAULT_FAST_WRITE_DELAY: number;
5
6
  export declare const WINDOW_END_FLUSH_MARGIN: number;
6
- export type WriteConfig = {
7
- fast?: boolean;
8
- writeDelay?: number;
9
- lastModified?: number;
10
- };
11
7
  export type IBucketStore = {
12
- get(fileName: string, config?: GetConfig): Promise<Buffer | undefined>;
13
- get2(fileName: string, config?: GetConfig): Promise<{
8
+ /** internal (store-to-store) reads answer purely from the local disk; see GetConfig.internal */
9
+ get2(config: {
10
+ path: string;
11
+ range?: {
12
+ start: number;
13
+ end: number;
14
+ };
15
+ internal?: boolean;
16
+ includeTombstones?: boolean;
17
+ }): Promise<{
14
18
  data: Buffer;
15
19
  writeTime: number;
16
20
  size: number;
17
21
  } | undefined>;
18
- getInternal2?(fileName: string, config?: GetConfig): Promise<{
22
+ /** internal (store-to-store) writes go to the local disk + index with no fan-out; see SetConfig.internal */
23
+ set(config: {
24
+ path: string;
19
25
  data: Buffer;
20
- writeTime: number;
21
- size: number;
22
- } | undefined>;
23
- setInternal?(fileName: string, data: Buffer, config: {
24
- lastModified: number;
26
+ lastModified?: number;
27
+ forceSetImmutable?: boolean;
28
+ internal?: boolean;
29
+ }): Promise<void>;
30
+ del(config: {
31
+ path: string;
32
+ lastModified?: number;
33
+ internal?: boolean;
25
34
  }): Promise<void>;
26
- set(fileName: string, data: Buffer, config?: WriteConfig): Promise<string>;
27
- del(fileName: string, config?: WriteConfig): Promise<void>;
28
- getInfo(fileName: string, config?: GetInfoConfig): Promise<{
35
+ getInfo(config: {
36
+ path: string;
37
+ includeTombstones?: boolean;
38
+ }): Promise<{
29
39
  writeTime: number;
30
40
  size: number;
31
41
  } | undefined>;
32
- findInfo(prefix: string, config?: {
33
- shallow?: boolean;
34
- type?: "files" | "folders";
42
+ findInfo(config: FindConfig & {
43
+ prefix: string;
35
44
  }): Promise<ArchiveFileInfo[]>;
36
45
  getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
37
46
  getSyncStatus?(): Promise<ArchivesSyncStatus>;
@@ -57,11 +66,78 @@ export type IBucketStore = {
57
66
  byteCount: number;
58
67
  }[];
59
68
  }>;
60
- startLargeUpload(): Promise<string>;
61
- appendLargeUpload(id: string, data: Buffer): Promise<void>;
62
- finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void>;
63
- cancelLargeUpload(id: string): Promise<void>;
69
+ /** path/lastModified let the store reject an upload into an immutable bucket before any bytes move */
70
+ startLargeUpload(config?: {
71
+ path?: string;
72
+ lastModified?: number;
73
+ }): Promise<string>;
74
+ appendLargeUpload(config: {
75
+ id: string;
76
+ data: Buffer;
77
+ }): Promise<void>;
78
+ finishLargeUpload(config: {
79
+ id: string;
80
+ path: string;
81
+ lastModified?: number;
82
+ }): Promise<void>;
83
+ cancelLargeUpload(config: {
84
+ id: string;
85
+ }): Promise<void>;
64
86
  };
87
+ /** rawDisk buckets: the disk IS the store. No index, no synchronization, no window/route/immutability validation. */
88
+ export declare class RawDiskStore implements IBucketStore {
89
+ private disk;
90
+ constructor(disk: ArchivesDisk);
91
+ get2(config: {
92
+ path: string;
93
+ range?: {
94
+ start: number;
95
+ end: number;
96
+ };
97
+ internal?: boolean;
98
+ includeTombstones?: boolean;
99
+ }): Promise<{
100
+ data: Buffer;
101
+ writeTime: number;
102
+ size: number;
103
+ } | undefined>;
104
+ set(config: {
105
+ path: string;
106
+ data: Buffer;
107
+ lastModified?: number;
108
+ forceSetImmutable?: boolean;
109
+ internal?: boolean;
110
+ }): Promise<void>;
111
+ del(config: {
112
+ path: string;
113
+ lastModified?: number;
114
+ internal?: boolean;
115
+ }): Promise<void>;
116
+ getInfo(config: {
117
+ path: string;
118
+ includeTombstones?: boolean;
119
+ }): Promise<{
120
+ writeTime: number;
121
+ size: number;
122
+ } | undefined>;
123
+ findInfo(config: FindConfig & {
124
+ prefix: string;
125
+ }): Promise<ArchiveFileInfo[]>;
126
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
127
+ startLargeUpload(): Promise<string>;
128
+ appendLargeUpload(config: {
129
+ id: string;
130
+ data: Buffer;
131
+ }): Promise<void>;
132
+ finishLargeUpload(config: {
133
+ id: string;
134
+ path: string;
135
+ lastModified?: number;
136
+ }): Promise<void>;
137
+ cancelLargeUpload(config: {
138
+ id: string;
139
+ }): Promise<void>;
140
+ }
65
141
  export type BlobSourceSpec = {
66
142
  identity: string;
67
143
  url: string;
@@ -80,47 +156,51 @@ export declare class BlobStore implements IBucketStore {
80
156
  readerDiskLimit?: number | undefined;
81
157
  onWriteCounted?: ((kind: "original" | "flushed", bytes: number) => void) | undefined;
82
158
  resolveSourceUrl?: ((url: string) => IArchives) | undefined;
159
+ entries?: HostedConfig[] | undefined;
83
160
  } | undefined);
84
- private stopped;
85
- private index;
86
- private mem;
87
- private indexFileCount;
88
- private indexByteCount;
89
- private sourceFileCounts;
90
- private sourceByteCounts;
91
- private syncActivities;
92
- private dirty;
93
- private overlay;
94
- private sourceStates;
95
- private syncStarted;
96
- private sourcesList;
97
- private slotSourcesListIndexes;
98
- private slotRegistrations;
99
- private isLive;
100
- private registerSlot;
101
- private sourcesListIndexOfSlot;
102
- private slotForSourcesListIndex;
103
- private getEntryHolder;
104
161
  init: {
105
162
  (): Promise<void>;
106
163
  reset(): void;
107
164
  set(newValue: Promise<void>): void;
108
165
  };
109
166
  dispose(): Promise<void>;
110
- private loadIndex;
111
- private countEntry;
112
- private setIndexEntry;
113
- private deleteIndexEntry;
114
- /** Applies a config change to the RUNNING store: windows/routes update in place, new sources are added (their sync starts immediately), and removed sources' slots go dead (their scans stop, their index entries drop). The store survives every routine config evolution - it is never destroyed for a source-list change, only for structural flips it cannot express (rawDisk). Pending fast writes are re-capped to the new flush deadline (flushing immediately when it has already passed). */
115
- updateSources(specs: BlobSourceSpec[]): void;
116
- private removeSource;
117
- /** Rescans our own disk's metadata into the index - used around valid window handoffs, where another process wrote files to the shared folder that our index hasn't seen. */
118
- rescanBase(): Promise<void>;
119
- /** A boundary scan of the node that owned (part of) our route in the valid window before ours, when that node is different storage (a disk rescan can't see its writes): just its changes since the boundary neighborhood, with matching values pulled onto our own disk. */
120
- boundaryScanRemote(source: IArchives, config: {
121
- since: number;
122
- route?: [number, number];
167
+ get2(config: {
168
+ path: string;
169
+ range?: {
170
+ start: number;
171
+ end: number;
172
+ };
173
+ internal?: boolean;
174
+ includeTombstones?: boolean;
175
+ }): Promise<{
176
+ data: Buffer;
177
+ writeTime: number;
178
+ size: number;
179
+ } | undefined>;
180
+ set(config: {
181
+ path: string;
182
+ data: Buffer;
183
+ lastModified?: number;
184
+ forceSetImmutable?: boolean;
185
+ internal?: boolean;
123
186
  }): Promise<void>;
187
+ del(config: {
188
+ path: string;
189
+ lastModified?: number;
190
+ internal?: boolean;
191
+ }): Promise<void>;
192
+ getInfo(config: {
193
+ path: string;
194
+ includeTombstones?: boolean;
195
+ }): Promise<{
196
+ writeTime: number;
197
+ size: number;
198
+ } | undefined>;
199
+ findInfo(config: FindConfig & {
200
+ prefix: string;
201
+ }): Promise<ArchiveFileInfo[]>;
202
+ getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
203
+ getSyncStatus(): Promise<ArchivesSyncStatus>;
124
204
  /** The cheap always-current totals plus any in-progress background synchronization. */
125
205
  getSyncProgress(): {
126
206
  index: {
@@ -145,7 +225,60 @@ export declare class BlobStore implements IBucketStore {
145
225
  byteCount: number;
146
226
  }[];
147
227
  }>;
228
+ /** Applies a config change to the RUNNING store: windows/routes update in place, new sources are added (their sync starts immediately), and removed sources' slots go dead (their scans stop, their index entries drop). The store survives every routine config evolution - it is never destroyed for a source-list change, only for structural flips it cannot express (rawDisk). Pending fast writes are re-capped to the new flush deadline (flushing immediately when it has already passed). */
229
+ updateSources(specs: BlobSourceSpec[], entries?: HostedConfig[]): void;
230
+ /** Rescans our own disk's metadata into the index - used around valid window handoffs, where another process wrote files to the shared folder that our index hasn't seen. */
231
+ rescanBase(): Promise<void>;
232
+ /** A boundary scan of the node that owned (part of) our route in the valid window before ours, when that node is different storage (a disk rescan can't see its writes): just its changes since the boundary neighborhood, with matching values pulled onto our own disk. */
233
+ boundaryScanRemote(source: IArchives, config: {
234
+ since: number;
235
+ route?: [number, number];
236
+ }): Promise<void>;
237
+ startLargeUpload(config?: {
238
+ path?: string;
239
+ lastModified?: number;
240
+ }): Promise<string>;
241
+ appendLargeUpload(config: {
242
+ id: string;
243
+ data: Buffer;
244
+ }): Promise<void>;
245
+ finishLargeUpload(config: {
246
+ id: string;
247
+ path: string;
248
+ lastModified?: number;
249
+ }): Promise<void>;
250
+ cancelLargeUpload(config: {
251
+ id: string;
252
+ }): Promise<void>;
253
+ private stopped;
254
+ private index;
255
+ private mem;
256
+ private indexFileCount;
257
+ private indexByteCount;
258
+ private sourceFileCounts;
259
+ private sourceByteCounts;
260
+ private syncActivities;
261
+ private dirty;
262
+ private overlay;
263
+ private sourceStates;
264
+ private syncStarted;
265
+ private entries;
266
+ private sourcesList;
267
+ private slotSourcesListIndexes;
268
+ private slotRegistrations;
269
+ private isLive;
270
+ private registerSlot;
271
+ private sourcesListIndexOfSlot;
272
+ private slotForSourcesListIndex;
273
+ private getEntryHolder;
274
+ private loadIndex;
275
+ private countEntry;
276
+ private setIndexEntry;
277
+ private deleteIndexEntry;
278
+ private removeSource;
148
279
  private flushIndex;
280
+ private assertMutable;
281
+ private assertInternalWriteAccepted;
149
282
  private runSourceSync;
150
283
  private isDeadIntermediate;
151
284
  private scanSource;
@@ -156,43 +289,15 @@ export declare class BlobStore implements IBucketStore {
156
289
  private waitForRequiredScans;
157
290
  private checkMissingKey;
158
291
  private getIndexEntry;
159
- get(key: string, config?: GetConfig): Promise<Buffer | undefined>;
160
- get2(key: string, config?: GetConfig): Promise<{
161
- data: Buffer;
162
- writeTime: number;
163
- size: number;
164
- } | undefined>;
165
292
  /** Internal (store-to-store) read: purely the local disk, completely short-circuiting the index and holder resolution - the caller is another store, and chasing OUR remote holders while answering it is how infinite get loops between stores form. No window or route checks: if the bytes are on our disk, the caller may have them. Note fast writes still sitting in the overlay are invisible here; the caller re-finds them after our flush. */
166
- getInternal2(key: string, config?: GetConfig): Promise<{
167
- data: Buffer;
168
- writeTime: number;
169
- size: number;
170
- } | undefined>;
171
- /** Internal (store-to-store) write: the local disk plus our index, with NO downstream fan-out - the pushing store owns propagation, and fanning its pushes back out is how write loops between stores form. Window/route acceptance is the caller's (writeBucketFile's) job; only-take-latest still applies here. */
172
- setInternal(key: string, data: Buffer, config: {
173
- lastModified: number;
174
- }): Promise<void>;
293
+ private getInternal2;
294
+ /** Internal (store-to-store) write: the local disk plus our index, with NO downstream fan-out - the pushing store owns propagation, and fanning its pushes back out is how write loops between stores form. Only-take-latest still applies here. */
295
+ private setInternal;
175
296
  private cacheRead;
176
- set(key: string, data: Buffer, config?: WriteConfig): Promise<string>;
177
- del(key: string, config?: WriteConfig): Promise<void>;
178
297
  private setOrDelete;
179
298
  private getWritableSources;
180
299
  private writeToSources;
181
- getInfo(key: string, config?: GetInfoConfig): Promise<{
182
- writeTime: number;
183
- size: number;
184
- } | undefined>;
185
- findInfo(prefix: string, config?: {
186
- shallow?: boolean;
187
- type?: "files" | "folders";
188
- }): Promise<ArchiveFileInfo[]>;
189
- getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
190
- getSyncStatus(): Promise<ArchivesSyncStatus>;
191
300
  private getDiskSource;
192
- startLargeUpload(): Promise<string>;
193
- appendLargeUpload(id: string, data: Buffer): Promise<void>;
194
- finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void>;
195
- cancelLargeUpload(id: string): Promise<void>;
196
301
  private flushOverlay;
197
302
  private evicting;
198
303
  private enforceDiskLimit;