sliftutils 1.7.9 → 1.7.10

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 (41) hide show
  1. package/bin/storageserver.js +4 -0
  2. package/{teststorage → examplestorage}/browser.tsx +18 -22
  3. package/{teststorage/server.ts → examplestorage/exampleserver.ts} +5 -5
  4. package/index.d.ts +528 -78
  5. package/misc/https/dns.d.ts +7 -3
  6. package/misc/https/dns.ts +4 -9
  7. package/misc/https/hostServer.d.ts +6 -3
  8. package/misc/https/hostServer.ts +6 -6
  9. package/package.json +2 -1
  10. package/spec.txt +77 -42
  11. package/storage/ArchivesDisk.d.ts +65 -0
  12. package/storage/ArchivesDisk.ts +365 -0
  13. package/storage/IArchives.d.ts +95 -1
  14. package/storage/IArchives.ts +146 -1
  15. package/storage/backblaze.d.ts +14 -2
  16. package/storage/backblaze.ts +18 -2
  17. package/storage/remoteStorage/ArchivesRemote.d.ts +29 -17
  18. package/storage/remoteStorage/ArchivesRemote.ts +63 -63
  19. package/storage/remoteStorage/ArchivesUrl.d.ts +46 -0
  20. package/storage/remoteStorage/ArchivesUrl.ts +74 -0
  21. package/storage/remoteStorage/accessPage.tsx +111 -28
  22. package/storage/remoteStorage/blobStore.d.ts +79 -25
  23. package/storage/remoteStorage/blobStore.ts +441 -287
  24. package/storage/remoteStorage/cliArgs.d.ts +1 -0
  25. package/storage/remoteStorage/cliArgs.ts +9 -0
  26. package/storage/remoteStorage/createArchives.d.ts +64 -0
  27. package/storage/remoteStorage/createArchives.ts +395 -0
  28. package/storage/remoteStorage/grantAccess.js +4 -0
  29. package/storage/remoteStorage/grantAccessCli.d.ts +1 -0
  30. package/storage/remoteStorage/grantAccessCli.ts +27 -0
  31. package/storage/remoteStorage/remoteConfig.d.ts +21 -0
  32. package/storage/remoteStorage/remoteConfig.ts +94 -0
  33. package/storage/remoteStorage/storageController.d.ts +19 -27
  34. package/storage/remoteStorage/storageController.ts +205 -136
  35. package/storage/remoteStorage/storageServer.d.ts +11 -0
  36. package/storage/remoteStorage/storageServer.ts +80 -93
  37. package/storage/remoteStorage/storageServerCli.d.ts +1 -0
  38. package/storage/remoteStorage/storageServerCli.ts +41 -0
  39. package/storage/remoteStorage/storageServerState.d.ts +37 -0
  40. package/storage/remoteStorage/storageServerState.ts +358 -0
  41. package/testsite/server.ts +1 -1
@@ -1,8 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
- import { ArchiveFileInfo } from "../IArchives";
4
- import type { BlobStore } from "./blobStore";
5
- import type { IStorage } from "../IStorage";
3
+ import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
6
4
  export declare const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
7
5
  export declare const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
8
6
  export declare const STORAGE_NOT_AUTHENTICATED = "REMOTE_STORAGE_NOT_AUTHENTICATED_cf2f7b1e";
@@ -25,33 +23,13 @@ export type TrustRecord = {
25
23
  ip: string;
26
24
  time: number;
27
25
  };
28
- export type BucketConfig = {
29
- public?: boolean;
30
- fast?: boolean;
31
- writeDelay?: number;
32
- };
33
26
  export type AccessState = {
34
27
  machineId: string;
35
28
  ip: string;
36
29
  hasAccess: boolean;
37
- listAccessCommand: string;
38
30
  grantAccessCommand?: string;
39
- machines?: (AccessRequest & {
40
- trusted: boolean;
41
- })[];
42
- };
43
- export type StorageServerState = {
44
- domain: string;
45
- port: number;
46
- rootDomain: string;
47
- sshTarget: string;
48
- serverCommand: string;
49
- blobStore: BlobStore;
50
- trust: IStorage<TrustRecord>;
51
- requests: IStorage<AccessRequest[]>;
52
- buckets: IStorage<BucketConfig>;
31
+ trustedMachines?: TrustRecord[];
53
32
  };
54
- export declare function setStorageServerState(state: StorageServerState): void;
55
33
  export declare const RemoteStorageController: import("socket-function/SocketFunctionTypes").SocketRegistered<{
56
34
  authenticate: (token: AuthToken) => Promise<{
57
35
  machineId: string;
@@ -64,14 +42,22 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
64
42
  grantAccessCommand: string;
65
43
  }>;
66
44
  getAccessState: (account: string) => Promise<AccessState>;
45
+ listRequestsForIP: (account: string, ip: string) => Promise<AccessRequest[]>;
46
+ grantAccess: (requestId: string) => Promise<TrustRecord>;
67
47
  adminListRequests: (ip: string) => Promise<AccessRequest[]>;
68
48
  adminGrantAccess: (requestId: string) => Promise<TrustRecord>;
69
- ensureBucket: (account: string, bucketName: string, config: BucketConfig) => Promise<void>;
70
49
  get: (account: string, bucketName: string, path: string, range?: {
71
50
  start: number;
72
51
  end: number;
73
52
  }) => Promise<Buffer | undefined>;
74
- set: (account: string, bucketName: string, path: string, data: Buffer) => Promise<void>;
53
+ get2: (account: string, bucketName: string, path: string, range?: {
54
+ start: number;
55
+ end: number;
56
+ }) => Promise<{
57
+ data: Buffer;
58
+ writeTime: number;
59
+ } | undefined>;
60
+ set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number) => Promise<void>;
75
61
  del: (account: string, bucketName: string, path: string) => Promise<void>;
76
62
  getInfo: (account: string, bucketName: string, path: string) => Promise<{
77
63
  writeTime: number;
@@ -81,9 +67,15 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
81
67
  shallow?: boolean;
82
68
  type?: "files" | "folders";
83
69
  }) => Promise<ArchiveFileInfo[]>;
70
+ getChangesAfter: (account: string, bucketName: string, time: number) => Promise<ArchiveFileInfo[]>;
71
+ getArchivesConfig: (account: string, bucketName: string) => Promise<ArchivesConfig>;
72
+ getSyncStatus: (account: string, bucketName: string) => Promise<ArchivesSyncStatus>;
84
73
  startLargeFile: (account: string, bucketName: string, path: string) => Promise<string>;
85
74
  uploadPart: (uploadId: string, data: Buffer) => Promise<void>;
86
75
  finishLargeFile: (uploadId: string) => Promise<void>;
87
76
  cancelLargeFile: (uploadId: string) => Promise<void>;
88
- getPublicFile: (account: string, bucketName: string, path: string) => Promise<Buffer>;
77
+ httpEntry: (config?: {
78
+ requireCalls?: string[];
79
+ cacheTime?: number;
80
+ }) => Promise<Buffer>;
89
81
  }>;
@@ -2,18 +2,27 @@ module.allowclient = true;
2
2
 
3
3
  import { SocketFunction } from "socket-function/SocketFunction";
4
4
  import { getNodeIdIP } from "socket-function/src/nodeCache";
5
- import { setHTTPResultHeaders } from "socket-function/src/callHTTPHandler";
5
+ import { setHTTPResultHeaders, getCurrentHTTPRequest } from "socket-function/src/callHTTPHandler";
6
+ import { performLocalCall } from "socket-function/src/callManager";
7
+ import { RequireController } from "socket-function/require/RequireController";
6
8
  import { timeInMinute } from "socket-function/src/misc";
7
9
  import { getCommonName, getPublicIdentifier, getOwnMachineId, verify, verifyMachineIdForPublicKey } from "../../misc/https/certs";
8
- import { ArchiveFileInfo } from "../IArchives";
9
- import type { BlobStore, WriteConfig } from "./blobStore";
10
- import type { IStorage } from "../IStorage";
10
+ import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
11
+ import { ROUTING_FILE } from "./remoteConfig";
12
+ import {
13
+ getStorageServerConfig, getTrust, getRequests, getLoadedBucket, writeBucketFile,
14
+ deleteBucketFile, assertWritesAllowed, assertMutable, LoadedBucket,
15
+ } from "./storageServerState";
11
16
 
12
17
  // The remote storage server's API. Authentication uses certs.ts machine identities: a client
13
18
  // proves it owns its machine key by signing a timestamped token (bound to this server, so tokens
14
19
  // can't be replayed elsewhere), and the server then trusts that connection as that machineId.
15
20
  // Access to an account is granted to specific machineIds, via a command line command run on the
16
21
  // storage machine (see storageServer.ts).
22
+ //
23
+ // There is no bucket-creation API: a bucket exists iff its routing config (ROUTING_FILE) exists,
24
+ // and writing that file creates or reconfigures the bucket (see storageServerState.ts). Reads of
25
+ // nonexistent buckets return undefined / empty, same as reads of nonexistent files.
17
26
 
18
27
  export const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
19
28
  export const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
@@ -43,47 +52,20 @@ export type TrustRecord = {
43
52
  ip: string;
44
53
  time: number;
45
54
  };
46
- export type BucketConfig = {
47
- public?: boolean;
48
- fast?: boolean;
49
- writeDelay?: number;
50
- };
51
55
  export type AccessState = {
52
56
  machineId: string;
53
57
  ip: string;
54
58
  hasAccess: boolean;
55
- // Single ssh commands, runnable from anywhere, that run the admin CLI on the storage machine
56
- // (shown when the caller has no access). grantAccessCommand grants the caller's own request.
57
- listAccessCommand: string;
59
+ // A single ssh command, runnable from anywhere, that runs the grantAccess CLI on the storage
60
+ // machine to grant the caller's own pending request. Only set when the caller has a pending
61
+ // request (so an already-trusted caller has no need for it).
58
62
  grantAccessCommand?: string;
59
- // Only provided when the caller has access
60
- machines?: (AccessRequest & { trusted: boolean })[];
61
- };
62
-
63
- export type StorageServerState = {
64
- domain: string;
65
- port: number;
66
- rootDomain: string;
67
- // user@externalIp of the storage machine, for generated ssh commands
68
- sshTarget: string;
69
- // Absolute command that runs storageServer.ts on the storage machine (admin args appended)
70
- serverCommand: string;
71
- blobStore: BlobStore;
72
- trust: IStorage<TrustRecord>;
73
- requests: IStorage<AccessRequest[]>;
74
- buckets: IStorage<BucketConfig>;
63
+ // Only the machines that ALREADY have access. Pending requests are NEVER listed here — showing
64
+ // them would let a trusted user accidentally approve a random request. Callers see pending
65
+ // requests only by explicitly typing an IP into listRequestsForIP.
66
+ trustedMachines?: TrustRecord[];
75
67
  };
76
68
 
77
- let serverState: StorageServerState | undefined;
78
- export function setStorageServerState(state: StorageServerState) {
79
- serverState = state;
80
- }
81
- function getState(): StorageServerState {
82
- let state = serverState;
83
- if (!state) throw new Error(`Storage server is not initialized (this API only works on the storage server)`);
84
- return state;
85
- }
86
-
87
69
  // callerNodeId -> authenticated machineId. Connections are long-lived websockets, so a session
88
70
  // lasts until the connection drops (clients re-authenticate on reconnect).
89
71
  const sessions = new Map<string, string>();
@@ -107,6 +89,10 @@ function assertValidPath(path: string) {
107
89
  if (!path || path.startsWith("/") || path.endsWith("/") || path.includes("//") || path.includes("\\") || path.includes("\x00")) {
108
90
  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`);
109
91
  }
92
+ // Paths are one-to-one with files on disk, so . and .. segments would escape the store folder
93
+ if (path.split("/").some(part => part === "." || part === "..")) {
94
+ throw new Error(`Invalid path ${JSON.stringify(path.slice(0, 200))}, paths cannot contain . or .. segments`);
95
+ }
110
96
  }
111
97
 
112
98
  function getCallerMachineId(): string {
@@ -121,8 +107,7 @@ function getCallerIP(): string {
121
107
  return getNodeIdIP(SocketFunction.getCaller().nodeId);
122
108
  }
123
109
  function isAdmin(machineId: string): boolean {
124
- let state = getState();
125
- return machineId === getOwnMachineId(state.rootDomain);
110
+ return machineId === getOwnMachineId(getStorageServerConfig().rootDomain);
126
111
  }
127
112
  function requireAdmin(): string {
128
113
  let machineId = getCallerMachineId();
@@ -133,46 +118,35 @@ function requireAdmin(): string {
133
118
  }
134
119
  async function requireAccess(account: string): Promise<string> {
135
120
  assertValidName(account, "account");
136
- let state = getState();
137
121
  let machineId = getCallerMachineId();
138
122
  if (isAdmin(machineId)) return machineId;
139
- let trusted = await state.trust.get(`${account}|${machineId}`);
123
+ let trust = await getTrust();
124
+ let trusted = await trust.get(`${account}|${machineId}`);
140
125
  if (!trusted) {
141
- throw new Error(`${STORAGE_ACCESS_DENIED} Machine ${machineId} has no access to account ${JSON.stringify(account)}. Visit https://${state.domain}:${state.port}/${account} for access instructions.`);
126
+ let { domain, port } = getStorageServerConfig();
127
+ throw new Error(`${STORAGE_ACCESS_DENIED} Machine ${machineId} has no access to account ${JSON.stringify(account)}. Visit https://${domain}:${port}/${account} for access instructions.`);
142
128
  }
143
129
  return machineId;
144
130
  }
145
131
 
146
132
  // A single command, runnable from anywhere, that sshes into the storage machine and runs the
147
- // admin CLI there
148
- function getAdminCommand(args: string): string {
149
- let state = getState();
150
- return `ssh ${state.sshTarget} '${state.serverCommand} ${args}'`;
151
- }
152
- function getListAccessCommand(ip: string): string {
153
- return getAdminCommand(`--listAccess ${ip}`);
154
- }
133
+ // grantAccess CLI there
155
134
  function getGrantAccessCommand(requestId: string): string {
156
- return getAdminCommand(`--grantAccess ${requestId}`);
135
+ let { sshTarget, serverCommand } = getStorageServerConfig();
136
+ return `ssh ${sshTarget} '${serverCommand} --requestId ${requestId}'`;
157
137
  }
158
138
 
159
- async function getBucketWriteConfig(account: string, bucketName: string): Promise<WriteConfig> {
160
- let state = getState();
161
- let config = await state.buckets.get(`${account}/${bucketName}`);
162
- return { fast: config?.fast, writeDelay: config?.writeDelay };
163
- }
164
- function fileKey(account: string, bucketName: string, path: string): string {
139
+ async function getBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined> {
165
140
  assertValidName(account, "account");
166
141
  assertValidName(bucketName, "bucket name");
167
- assertValidPath(path);
168
- return `${account}/${bucketName}/${path}`;
142
+ return await getLoadedBucket(account, bucketName);
169
143
  }
170
144
 
171
145
  class RemoteStorageControllerBase {
172
146
  // Proves the caller owns the machine key for the machineId in its certificate. The signature
173
147
  // must be fresh and bound to this server, so it cannot be replayed to (or from) other servers.
174
148
  async authenticate(token: AuthToken): Promise<{ machineId: string; ip: string }> {
175
- let state = getState();
149
+ let { domain, port } = getStorageServerConfig();
176
150
  let caller = SocketFunction.getCaller();
177
151
  if (Math.abs(Date.now() - token.time) > AUTH_TIME_WINDOW) {
178
152
  throw new Error(`Auth token time is too far from the server time (token ${token.time}, server ${Date.now()}, allowed drift ${AUTH_TIME_WINDOW}ms)`);
@@ -180,7 +154,7 @@ class RemoteStorageControllerBase {
180
154
  verify(token.certPem, token.signature, {
181
155
  purpose: STORAGE_AUTH_PURPOSE,
182
156
  time: token.time,
183
- server: `${state.domain}:${state.port}`,
157
+ server: `${domain}:${port}`,
184
158
  });
185
159
  let machineId = getCommonName(token.certPem).split(".")[0];
186
160
  if (!verifyMachineIdForPublicKey({ machineId, publicKey: getPublicIdentifier(token.certPem) })) {
@@ -199,10 +173,10 @@ class RemoteStorageControllerBase {
199
173
  // IP, so the storage machine's admin can list them with --listAccess <ip> and grant one.
200
174
  async requestAccess(account: string): Promise<{ machineId: string; ip: string; requestId: string; grantAccessCommand: string }> {
201
175
  assertValidName(account, "account");
202
- let state = getState();
203
176
  let machineId = getCallerMachineId();
204
177
  let ip = getCallerIP();
205
- let requests = await state.requests.get(ip) || [];
178
+ let requestsStorage = await getRequests();
179
+ let requests = await requestsStorage.get(ip) || [];
206
180
  let existing = requests.find(x => x.account === account && x.machineId === machineId);
207
181
  if (existing) {
208
182
  existing.time = Date.now();
@@ -217,63 +191,87 @@ class RemoteStorageControllerBase {
217
191
  requests.push(existing);
218
192
  }
219
193
  while (requests.length > MAX_REQUESTS_PER_IP) requests.shift();
220
- await state.requests.set(ip, requests);
194
+ await requestsStorage.set(ip, requests);
221
195
  return { machineId, ip, requestId: existing.requestId, grantAccessCommand: getGrantAccessCommand(existing.requestId) };
222
196
  }
223
197
 
224
198
  async getAccessState(account: string): Promise<AccessState> {
225
199
  assertValidName(account, "account");
226
- let state = getState();
227
200
  let machineId = getCallerMachineId();
228
201
  let ip = getCallerIP();
229
- let hasAccess = isAdmin(machineId) || !!await state.trust.get(`${account}|${machineId}`);
230
- let result: AccessState = { machineId, ip, hasAccess, listAccessCommand: getListAccessCommand(ip) };
202
+ let trust = await getTrust();
203
+ let hasAccess = isAdmin(machineId) || !!await trust.get(`${account}|${machineId}`);
204
+ let result: AccessState = { machineId, ip, hasAccess };
231
205
  if (!hasAccess) {
232
- let ownRequest = (await state.requests.get(ip) || []).find(x => x.account === account && x.machineId === machineId);
206
+ let requests = await getRequests();
207
+ let ownRequest = (await requests.get(ip) || []).find(x => x.account === account && x.machineId === machineId);
233
208
  if (ownRequest) {
234
209
  result.grantAccessCommand = getGrantAccessCommand(ownRequest.requestId);
235
210
  }
236
211
  return result;
237
212
  }
238
213
 
239
- let machines = new Map<string, AccessRequest & { trusted: boolean }>();
240
- for (let requestIp of await state.requests.getKeys()) {
241
- for (let request of await state.requests.get(requestIp) || []) {
242
- if (request.account !== account) continue;
243
- machines.set(request.machineId, { ...request, trusted: false });
244
- }
245
- }
246
- for (let key of await state.trust.getKeys()) {
214
+ let trustedMachines: TrustRecord[] = [];
215
+ for (let key of await trust.getKeys()) {
247
216
  if (!key.startsWith(`${account}|`)) continue;
248
- let record = await state.trust.get(key);
249
- if (!record) continue;
250
- let existing = machines.get(record.machineId);
251
- machines.set(record.machineId, {
252
- requestId: existing?.requestId || "",
253
- account,
254
- machineId: record.machineId,
255
- ip: record.ip,
256
- time: record.time,
257
- trusted: true,
258
- });
217
+ let record = await trust.get(key);
218
+ if (record) trustedMachines.push(record);
259
219
  }
260
- result.machines = Array.from(machines.values());
220
+ result.trustedMachines = trustedMachines;
261
221
  return result;
262
222
  }
263
223
 
224
+ // Callable by any machine that has access to `account`. Returns pending access requests for the
225
+ // account that come from EXACTLY `ip`. Callers must type in an IP explicitly — the server never
226
+ // volunteers a list of requesting IPs, so a trusted user can't accidentally approve a random
227
+ // request from a machine they didn't mean to trust.
228
+ async listRequestsForIP(account: string, ip: string): Promise<AccessRequest[]> {
229
+ await requireAccess(account);
230
+ let requests = await getRequests();
231
+ return (await requests.get(ip) || []).filter(x => x.account === account);
232
+ }
233
+
234
+ // Callable by any machine that has access to the request's account (or by the storage-machine
235
+ // admin). Grants the requested access; the caller must supply the specific requestId, which they
236
+ // only get by explicitly looking up requests for a specific IP.
237
+ async grantAccess(requestId: string): Promise<TrustRecord> {
238
+ // Must capture in the synchronous phase — SocketFunction.getCaller() only works before any await.
239
+ let callerMachineId = getCallerMachineId();
240
+ let trust = await getTrust();
241
+ let requests = await getRequests();
242
+ for (let ip of await requests.getKeys()) {
243
+ for (let request of await requests.get(ip) || []) {
244
+ if (request.requestId !== requestId) continue;
245
+ if (!isAdmin(callerMachineId) && !await trust.get(`${request.account}|${callerMachineId}`)) {
246
+ throw new Error(`${STORAGE_ACCESS_DENIED} Machine ${callerMachineId} has no access to account ${JSON.stringify(request.account)}`);
247
+ }
248
+ let record: TrustRecord = {
249
+ account: request.account,
250
+ machineId: request.machineId,
251
+ ip: request.ip,
252
+ time: Date.now(),
253
+ };
254
+ await trust.set(`${request.account}|${request.machineId}`, record);
255
+ return record;
256
+ }
257
+ }
258
+ throw new Error(`No access request found with id ${JSON.stringify(requestId)}. It may have already been granted or expired.`);
259
+ }
260
+
264
261
  // Admin (must be run from the storage machine itself, which shares the server's machineId).
265
262
  // Only returns requests for the given IP, so you cannot accidentally grant a request from an
266
263
  // IP you didn't explicitly type in.
267
264
  async adminListRequests(ip: string): Promise<AccessRequest[]> {
268
265
  requireAdmin();
269
- let state = getState();
270
- return await state.requests.get(ip) || [];
266
+ let requests = await getRequests();
267
+ return await requests.get(ip) || [];
271
268
  }
272
269
  async adminGrantAccess(requestId: string): Promise<TrustRecord> {
273
270
  requireAdmin();
274
- let state = getState();
275
- for (let ip of await state.requests.getKeys()) {
276
- for (let request of await state.requests.get(ip) || []) {
271
+ let trust = await getTrust();
272
+ let requests = await getRequests();
273
+ for (let ip of await requests.getKeys()) {
274
+ for (let request of await requests.get(ip) || []) {
277
275
  if (request.requestId !== requestId) continue;
278
276
  let record: TrustRecord = {
279
277
  account: request.account,
@@ -281,93 +279,159 @@ class RemoteStorageControllerBase {
281
279
  ip: request.ip,
282
280
  time: Date.now(),
283
281
  };
284
- await state.trust.set(`${request.account}|${request.machineId}`, record);
282
+ await trust.set(`${request.account}|${request.machineId}`, record);
285
283
  return record;
286
284
  }
287
285
  }
288
- throw new Error(`No access request found with id ${JSON.stringify(requestId)}. Run --listAccess <ip> to see request ids.`);
289
- }
290
-
291
- async ensureBucket(account: string, bucketName: string, config: BucketConfig): Promise<void> {
292
- await requireAccess(account);
293
- assertValidName(bucketName, "bucket name");
294
- let state = getState();
295
- let key = `${account}/${bucketName}`;
296
- let existing = await state.buckets.get(key);
297
- if (existing && JSON.stringify(existing) === JSON.stringify(config)) return;
298
- await state.buckets.set(key, config);
286
+ throw new Error(`No access request found with id ${JSON.stringify(requestId)}. It may have already been granted or expired.`);
299
287
  }
300
288
 
301
289
  async get(account: string, bucketName: string, path: string, range?: { start: number; end: number }): Promise<Buffer | undefined> {
290
+ let result = await this.get2(account, bucketName, path, range);
291
+ return result && result.data || undefined;
292
+ }
293
+ async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }): Promise<{ data: Buffer; writeTime: number } | undefined> {
302
294
  await requireAccess(account);
303
- return await getState().blobStore.get(fileKey(account, bucketName, path), range);
295
+ assertValidPath(path);
296
+ let bucket = await getBucket(account, bucketName);
297
+ if (!bucket) return undefined;
298
+ return await bucket.store.get2(path, { range });
304
299
  }
305
- async set(account: string, bucketName: string, path: string, data: Buffer): Promise<void> {
300
+ async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number): Promise<void> {
306
301
  await requireAccess(account);
307
- let writeConfig = await getBucketWriteConfig(account, bucketName);
308
- await getState().blobStore.set(fileKey(account, bucketName, path), Buffer.from(data), writeConfig);
302
+ assertValidName(bucketName, "bucket name");
303
+ assertValidPath(path);
304
+ // Handles bucket creation (writes of ROUTING_FILE), reconfiguration, fast mode, and
305
+ // immutability — see storageServerState.ts
306
+ await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified });
309
307
  }
310
308
  async del(account: string, bucketName: string, path: string): Promise<void> {
311
309
  await requireAccess(account);
312
- let writeConfig = await getBucketWriteConfig(account, bucketName);
313
- await getState().blobStore.del(fileKey(account, bucketName, path), writeConfig);
310
+ assertValidName(bucketName, "bucket name");
311
+ assertValidPath(path);
312
+ await deleteBucketFile(account, bucketName, path);
314
313
  }
315
314
  async getInfo(account: string, bucketName: string, path: string): Promise<{ writeTime: number; size: number } | undefined> {
316
315
  await requireAccess(account);
317
- return await getState().blobStore.getInfo(fileKey(account, bucketName, path));
316
+ assertValidPath(path);
317
+ let bucket = await getBucket(account, bucketName);
318
+ if (!bucket) return undefined;
319
+ return await bucket.store.getInfo(path);
318
320
  }
319
321
  async findInfo(account: string, bucketName: string, prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
320
322
  await requireAccess(account);
321
- assertValidName(bucketName, "bucket name");
322
- let bucketRoot = `${account}/${bucketName}/`;
323
- let infos = await getState().blobStore.findInfo(bucketRoot + prefix, config);
324
- return infos.map(info => ({ ...info, path: info.path.slice(bucketRoot.length) }));
323
+ let bucket = await getBucket(account, bucketName);
324
+ if (!bucket) return [];
325
+ return await bucket.store.findInfo(prefix, config);
326
+ }
327
+ // Fast (served from the store's BulkDatabase2 index, not a scan) — see IArchives.getChangesAfter
328
+ async getChangesAfter(account: string, bucketName: string, time: number): Promise<ArchiveFileInfo[]> {
329
+ await requireAccess(account);
330
+ let bucket = await getBucket(account, bucketName);
331
+ if (!bucket) return [];
332
+ if (!bucket.store.getChangesAfter) {
333
+ throw new Error(`Bucket ${account}/${bucketName} does not support getChangesAfter (rawDisk buckets have no index)`);
334
+ }
335
+ return await bucket.store.getChangesAfter(time);
336
+ }
337
+ async getArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
338
+ await requireAccess(account);
339
+ let bucket = await getBucket(account, bucketName);
340
+ // Missing buckets say true, matching what they become once created (the default store type)
341
+ return { supportsChangesAfter: !bucket || !!bucket.store.getChangesAfter };
342
+ }
343
+ async getSyncStatus(account: string, bucketName: string): Promise<ArchivesSyncStatus> {
344
+ await requireAccess(account);
345
+ let bucket = await getBucket(account, bucketName);
346
+ if (!bucket) return { allScansComplete: true, indexSize: 0, sources: [] };
347
+ if (!bucket.store.getSyncStatus) {
348
+ throw new Error(`Bucket ${account}/${bucketName} does not support getSyncStatus (rawDisk buckets have no synchronization)`);
349
+ }
350
+ return await bucket.store.getSyncStatus();
325
351
  }
326
352
 
327
353
  async startLargeFile(account: string, bucketName: string, path: string): Promise<string> {
354
+ assertWritesAllowed();
328
355
  await requireAccess(account);
329
356
  // Validates now, so the upload doesn't fail at the end
330
- fileKey(account, bucketName, path);
331
- let id = await getState().blobStore.startLargeUpload();
357
+ assertValidPath(path);
358
+ let bucket = await getBucket(account, bucketName);
359
+ if (!bucket) {
360
+ throw new Error(`Bucket ${account}/${bucketName} does not exist. Write its routing config to ${JSON.stringify(ROUTING_FILE)} to create it.`);
361
+ }
362
+ await assertMutable(bucket, path);
363
+ let id = await bucket.store.startLargeUpload();
332
364
  largeUploadInfo.set(id, { account, bucketName, path });
333
365
  return id;
334
366
  }
335
367
  async uploadPart(uploadId: string, data: Buffer): Promise<void> {
368
+ assertWritesAllowed();
336
369
  let info = largeUploadInfo.get(uploadId);
337
370
  if (!info) throw new Error(`Unknown large upload ${uploadId}`);
338
371
  await requireAccess(info.account);
339
- await getState().blobStore.appendLargeUpload(uploadId, Buffer.from(data));
372
+ let bucket = await getBucket(info.account, info.bucketName);
373
+ if (!bucket) throw new Error(`Bucket ${info.account}/${info.bucketName} no longer exists`);
374
+ await bucket.store.appendLargeUpload(uploadId, Buffer.from(data));
340
375
  }
341
376
  async finishLargeFile(uploadId: string): Promise<void> {
377
+ assertWritesAllowed();
342
378
  let info = largeUploadInfo.get(uploadId);
343
379
  if (!info) throw new Error(`Unknown large upload ${uploadId}`);
344
380
  await requireAccess(info.account);
345
381
  largeUploadInfo.delete(uploadId);
346
- await getState().blobStore.finishLargeUpload(uploadId, fileKey(info.account, info.bucketName, info.path));
382
+ let bucket = await getBucket(info.account, info.bucketName);
383
+ if (!bucket) throw new Error(`Bucket ${info.account}/${info.bucketName} no longer exists`);
384
+ await bucket.store.finishLargeUpload(uploadId, info.path);
347
385
  }
348
386
  async cancelLargeFile(uploadId: string): Promise<void> {
349
387
  let info = largeUploadInfo.get(uploadId);
350
388
  if (!info) return;
351
389
  await requireAccess(info.account);
352
390
  largeUploadInfo.delete(uploadId);
353
- await getState().blobStore.cancelLargeUpload(uploadId);
391
+ let bucket = await getBucket(info.account, info.bucketName);
392
+ if (!bucket) return;
393
+ await bucket.store.cancelLargeUpload(uploadId);
354
394
  }
355
395
 
356
- // Serves files from public buckets over plain HTTP GET (see IArchives getURL). No
357
- // authentication, which is what public means (private buckets are API-access only).
358
- async getPublicFile(account: string, bucketName: string, path: string): Promise<Buffer> {
359
- let state = getState();
360
- let bucket = await state.buckets.get(`${account}/${bucketName}`);
361
- if (!bucket?.public) {
362
- throw new Error(`Bucket ${account}/${bucketName} is not public`);
396
+ // The server's single default HTTP route. /file/<account>/<bucketName>/<path> serves files from
397
+ // public buckets over plain GETs (see IArchives.getURL); every other path serves the access
398
+ // page (via RequireController.requireHTML the path is the account name, see accessPage.tsx).
399
+ async httpEntry(config?: { requireCalls?: string[]; cacheTime?: number }): Promise<Buffer> {
400
+ // Both are keyed by the current call and must be captured synchronously, before any await
401
+ let caller = SocketFunction.getCaller();
402
+ let request = getCurrentHTTPRequest();
403
+ let pathname = new URL(request?.url || "/", "https://localhost").pathname;
404
+ if (!pathname.startsWith("/file/")) {
405
+ return await performLocalCall({
406
+ caller,
407
+ call: { nodeId: caller.nodeId, classGuid: RequireController._classGuid, functionName: "requireHTML", args: [config] },
408
+ }) as Buffer;
409
+ }
410
+ let parts = pathname.split("/").filter(x => x).map(decodeURIComponent);
411
+ let account = parts[1];
412
+ let bucketName = parts[2];
413
+ let filePath = parts.slice(3).join("/");
414
+ if (!account || !bucketName || !filePath) {
415
+ return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
416
+ }
417
+ assertValidName(account, "account");
418
+ assertValidName(bucketName, "bucket name");
419
+ assertValidPath(filePath);
420
+ let bucket = await getLoadedBucket(account, bucketName);
421
+ if (!bucket) {
422
+ return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
423
+ }
424
+ if (!bucket.self?.public) {
425
+ throw new Error(`Bucket ${account}/${bucketName} is not public, so its files cannot be read over plain URLs`);
363
426
  }
364
- let data = await state.blobStore.get(fileKey(account, bucketName, path));
365
- if (!data) {
366
- throw new Error(`File not found: ${path} in ${account}/${bucketName}`);
427
+ let result = await bucket.store.get2(filePath);
428
+ if (!result) {
429
+ return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
367
430
  }
368
- let ext = path.split(".").pop() || "";
369
- return setHTTPResultHeaders(data, {
431
+ let ext = filePath.split(".").pop() || "";
432
+ return setHTTPResultHeaders(result.data, {
370
433
  "Content-Type": CONTENT_TYPES[ext.toLowerCase()] || "application/octet-stream",
434
+ "Last-Modified": new Date(result.writeTime).toUTCString(),
371
435
  });
372
436
  }
373
437
  }
@@ -381,18 +445,23 @@ export const RemoteStorageController = SocketFunction.register(
381
445
  authenticate: {},
382
446
  requestAccess: {},
383
447
  getAccessState: {},
448
+ listRequestsForIP: {},
449
+ grantAccess: {},
384
450
  adminListRequests: {},
385
451
  adminGrantAccess: {},
386
- ensureBucket: {},
387
452
  get: {},
453
+ get2: {},
388
454
  set: {},
389
455
  del: {},
390
456
  getInfo: {},
391
457
  findInfo: {},
458
+ getChangesAfter: {},
459
+ getArchivesConfig: {},
460
+ getSyncStatus: {},
392
461
  startLargeFile: {},
393
462
  uploadPart: {},
394
463
  finishLargeFile: {},
395
464
  cancelLargeFile: {},
396
- getPublicFile: {},
465
+ httpEntry: {},
397
466
  })
398
467
  );
@@ -1 +1,12 @@
1
1
  import "./accessPage";
2
+ export type HostStorageServerConfig = {
3
+ url: string;
4
+ folder: string;
5
+ cloudflareApiToken?: {
6
+ key: string;
7
+ } | {
8
+ path: string;
9
+ };
10
+ lowSpaceThresholdBytes?: number;
11
+ };
12
+ export declare function hostStorageServer(config: HostStorageServerConfig): Promise<void>;