sliftutils 1.7.10 → 1.7.11
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/examplestorage/exampleserver.ts +0 -2
- package/index.d.ts +290 -54
- package/misc/dist/environment.ts.cache +3 -3
- package/misc/dist/getSecret.ts.cache +139 -0
- package/misc/dist/strings.ts.cache +13 -0
- package/misc/dist/zip.ts.cache +9 -0
- package/misc/getSecret.ts +38 -16
- package/misc/https/cloudflareHelpers.d.ts +19 -0
- package/misc/https/cloudflareHelpers.ts +69 -0
- package/misc/https/dist/certs.ts.cache +115 -179
- package/misc/https/dist/cloudflareHelpers.ts.cache +68 -0
- package/misc/https/dist/dns.ts.cache +112 -100
- package/misc/https/dist/hostServer.ts.cache +130 -0
- package/misc/https/dist/httpsCerts.ts.cache +24 -5
- package/misc/https/dist/persistentLocalStorage.ts.cache +3 -2
- package/misc/https/dns.d.ts +41 -12
- package/misc/https/dns.ts +104 -101
- package/misc/https/hostServer.d.ts +0 -6
- package/misc/https/hostServer.ts +9 -5
- package/package.json +2 -2
- package/render-utils/dist/observer.tsx.cache +3 -2
- package/spec.txt +0 -90
- package/storage/ArchivesDisk.d.ts +2 -0
- package/storage/ArchivesDisk.ts +7 -3
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +1242 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +513 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +352 -0
- package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +306 -0
- package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +435 -0
- package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +58 -0
- package/storage/BulkDatabase2/dist/blockCache.ts.cache +179 -0
- package/storage/BulkDatabase2/dist/mergeLock.ts.cache +149 -0
- package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +101 -0
- package/storage/BulkDatabase2/dist/streamLog.ts.cache +227 -0
- package/storage/BulkDatabase2/dist/syncClient.ts.cache +104 -0
- package/storage/IArchives.d.ts +47 -15
- package/storage/IArchives.ts +86 -31
- package/storage/backblaze.d.ts +15 -0
- package/storage/backblaze.ts +199 -79
- package/storage/dist/ArchivesDisk.ts.cache +373 -0
- package/storage/dist/FileFolderAPI.tsx.cache +508 -65
- package/storage/dist/IArchives.ts.cache +44 -0
- package/storage/dist/IndexedDBFileFolderAPI.ts.cache +8 -2
- package/storage/dist/JSONStorage.ts.cache +6 -2
- package/storage/dist/PendingManager.tsx.cache +1 -1
- package/storage/dist/TransactionStorage.ts.cache +231 -92
- package/storage/dist/backblaze.ts.cache +797 -0
- package/storage/dist/fileSystemPointer.ts.cache +36 -3
- package/storage/dist/remoteFileStorage.ts.cache +495 -0
- package/storage/remoteStorage/ArchivesRemote.d.ts +5 -1
- package/storage/remoteStorage/ArchivesRemote.ts +28 -8
- package/storage/remoteStorage/ArchivesUrl.d.ts +2 -0
- package/storage/remoteStorage/ArchivesUrl.ts +16 -4
- package/storage/remoteStorage/blobStore.d.ts +52 -3
- package/storage/remoteStorage/blobStore.ts +414 -101
- package/storage/remoteStorage/createArchives.d.ts +31 -10
- package/storage/remoteStorage/createArchives.ts +470 -253
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +197 -0
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +85 -0
- package/storage/remoteStorage/dist/accessPage.tsx.cache +222 -0
- package/storage/remoteStorage/dist/blobStore.ts.cache +837 -0
- package/storage/remoteStorage/dist/cliArgs.ts.cache +16 -0
- package/storage/remoteStorage/dist/createArchives.ts.cache +610 -0
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +206 -0
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +223 -0
- package/storage/remoteStorage/dist/storageController.ts.cache +500 -0
- package/storage/remoteStorage/dist/storageServer.ts.cache +106 -0
- package/storage/remoteStorage/dist/storageServerCli.ts.cache +39 -0
- package/storage/remoteStorage/dist/storageServerState.ts.cache +518 -0
- package/storage/remoteStorage/remoteConfig.d.ts +8 -0
- package/storage/remoteStorage/remoteConfig.ts +105 -5
- package/storage/remoteStorage/sourceWrapper.d.ts +41 -0
- package/storage/remoteStorage/sourceWrapper.ts +223 -0
- package/storage/remoteStorage/spec.md +31 -0
- package/storage/remoteStorage/storageController.d.ts +11 -0
- package/storage/remoteStorage/storageController.ts +109 -40
- package/storage/remoteStorage/storageServer.d.ts +0 -5
- package/storage/remoteStorage/storageServer.ts +17 -10
- package/storage/remoteStorage/storageServerCli.ts +0 -3
- package/storage/remoteStorage/storageServerState.d.ts +7 -1
- package/storage/remoteStorage/storageServerState.ts +230 -48
- package/testsite/server.ts +0 -2
- package/yarn.lock +11 -6
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
module.allowclient = true;
|
|
2
2
|
|
|
3
3
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
4
|
+
import { SocketFunctionHook } from "socket-function/SocketFunctionTypes";
|
|
4
5
|
import { getNodeIdIP } from "socket-function/src/nodeCache";
|
|
5
6
|
import { setHTTPResultHeaders, getCurrentHTTPRequest } from "socket-function/src/callHTTPHandler";
|
|
6
7
|
import { performLocalCall } from "socket-function/src/callManager";
|
|
7
8
|
import { RequireController } from "socket-function/require/RequireController";
|
|
8
9
|
import { timeInMinute } from "socket-function/src/misc";
|
|
9
10
|
import { getCommonName, getPublicIdentifier, getOwnMachineId, verify, verifyMachineIdForPublicKey } from "../../misc/https/certs";
|
|
10
|
-
import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
|
|
11
|
+
import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, IMMUTABLE_CACHE_TIME } from "../IArchives";
|
|
11
12
|
import { ROUTING_FILE } from "./remoteConfig";
|
|
12
13
|
import {
|
|
13
14
|
getStorageServerConfig, getTrust, getRequests, getLoadedBucket, writeBucketFile,
|
|
@@ -143,6 +144,9 @@ async function getBucket(account: string, bucketName: string): Promise<LoadedBuc
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
class RemoteStorageControllerBase {
|
|
147
|
+
// Latency measurement (see SourceWrapper's pinging); no auth, it measures the transport
|
|
148
|
+
async ping(): Promise<void> { }
|
|
149
|
+
|
|
146
150
|
// Proves the caller owns the machine key for the machineId in its certificate. The signature
|
|
147
151
|
// must be fresh and bound to this server, so it cannot be replayed to (or from) other servers.
|
|
148
152
|
async authenticate(token: AuthToken): Promise<{ machineId: string; ip: string }> {
|
|
@@ -226,7 +230,6 @@ class RemoteStorageControllerBase {
|
|
|
226
230
|
// volunteers a list of requesting IPs, so a trusted user can't accidentally approve a random
|
|
227
231
|
// request from a machine they didn't mean to trust.
|
|
228
232
|
async listRequestsForIP(account: string, ip: string): Promise<AccessRequest[]> {
|
|
229
|
-
await requireAccess(account);
|
|
230
233
|
let requests = await getRequests();
|
|
231
234
|
return (await requests.get(ip) || []).filter(x => x.account === account);
|
|
232
235
|
}
|
|
@@ -262,12 +265,10 @@ class RemoteStorageControllerBase {
|
|
|
262
265
|
// Only returns requests for the given IP, so you cannot accidentally grant a request from an
|
|
263
266
|
// IP you didn't explicitly type in.
|
|
264
267
|
async adminListRequests(ip: string): Promise<AccessRequest[]> {
|
|
265
|
-
requireAdmin();
|
|
266
268
|
let requests = await getRequests();
|
|
267
269
|
return await requests.get(ip) || [];
|
|
268
270
|
}
|
|
269
271
|
async adminGrantAccess(requestId: string): Promise<TrustRecord> {
|
|
270
|
-
requireAdmin();
|
|
271
272
|
let trust = await getTrust();
|
|
272
273
|
let requests = await getRequests();
|
|
273
274
|
for (let ip of await requests.getKeys()) {
|
|
@@ -290,15 +291,13 @@ class RemoteStorageControllerBase {
|
|
|
290
291
|
let result = await this.get2(account, bucketName, path, range);
|
|
291
292
|
return result && result.data || undefined;
|
|
292
293
|
}
|
|
293
|
-
async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }): Promise<{ data: Buffer; writeTime: number } | undefined> {
|
|
294
|
-
await requireAccess(account);
|
|
294
|
+
async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
|
|
295
295
|
assertValidPath(path);
|
|
296
296
|
let bucket = await getBucket(account, bucketName);
|
|
297
297
|
if (!bucket) return undefined;
|
|
298
298
|
return await bucket.store.get2(path, { range });
|
|
299
299
|
}
|
|
300
300
|
async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number): Promise<void> {
|
|
301
|
-
await requireAccess(account);
|
|
302
301
|
assertValidName(bucketName, "bucket name");
|
|
303
302
|
assertValidPath(path);
|
|
304
303
|
// Handles bucket creation (writes of ROUTING_FILE), reconfiguration, fast mode, and
|
|
@@ -306,27 +305,23 @@ class RemoteStorageControllerBase {
|
|
|
306
305
|
await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified });
|
|
307
306
|
}
|
|
308
307
|
async del(account: string, bucketName: string, path: string): Promise<void> {
|
|
309
|
-
await requireAccess(account);
|
|
310
308
|
assertValidName(bucketName, "bucket name");
|
|
311
309
|
assertValidPath(path);
|
|
312
310
|
await deleteBucketFile(account, bucketName, path);
|
|
313
311
|
}
|
|
314
312
|
async getInfo(account: string, bucketName: string, path: string): Promise<{ writeTime: number; size: number } | undefined> {
|
|
315
|
-
await requireAccess(account);
|
|
316
313
|
assertValidPath(path);
|
|
317
314
|
let bucket = await getBucket(account, bucketName);
|
|
318
315
|
if (!bucket) return undefined;
|
|
319
316
|
return await bucket.store.getInfo(path);
|
|
320
317
|
}
|
|
321
318
|
async findInfo(account: string, bucketName: string, prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
|
|
322
|
-
await requireAccess(account);
|
|
323
319
|
let bucket = await getBucket(account, bucketName);
|
|
324
320
|
if (!bucket) return [];
|
|
325
321
|
return await bucket.store.findInfo(prefix, config);
|
|
326
322
|
}
|
|
327
323
|
// Fast (served from the store's BulkDatabase2 index, not a scan) — see IArchives.getChangesAfter
|
|
328
324
|
async getChangesAfter(account: string, bucketName: string, time: number): Promise<ArchiveFileInfo[]> {
|
|
329
|
-
await requireAccess(account);
|
|
330
325
|
let bucket = await getBucket(account, bucketName);
|
|
331
326
|
if (!bucket) return [];
|
|
332
327
|
if (!bucket.store.getChangesAfter) {
|
|
@@ -335,13 +330,26 @@ class RemoteStorageControllerBase {
|
|
|
335
330
|
return await bucket.store.getChangesAfter(time);
|
|
336
331
|
}
|
|
337
332
|
async getArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
|
|
338
|
-
await requireAccess(account);
|
|
339
333
|
let bucket = await getBucket(account, bucketName);
|
|
340
334
|
// Missing buckets say true, matching what they become once created (the default store type)
|
|
341
|
-
|
|
335
|
+
let progress = bucket?.store.getSyncProgress?.();
|
|
336
|
+
return {
|
|
337
|
+
supportsChangesAfter: !bucket || !!bucket.store.getChangesAfter,
|
|
338
|
+
remoteConfig: bucket?.routing,
|
|
339
|
+
index: progress?.index,
|
|
340
|
+
indexSources: progress?.sources,
|
|
341
|
+
readerDiskLimit: progress?.readerDiskLimit,
|
|
342
|
+
syncing: progress?.syncing,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/** Walks the whole index for exact totals (overall and per holding source) - more expensive
|
|
346
|
+
* than the maintained counters that getArchivesConfig returns, but immune to counter drift. */
|
|
347
|
+
async getIndexInfo(account: string, bucketName: string): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] } | undefined> {
|
|
348
|
+
let bucket = await getBucket(account, bucketName);
|
|
349
|
+
if (!bucket || !bucket.store.computeIndexTotals) return undefined;
|
|
350
|
+
return await bucket.store.computeIndexTotals();
|
|
342
351
|
}
|
|
343
352
|
async getSyncStatus(account: string, bucketName: string): Promise<ArchivesSyncStatus> {
|
|
344
|
-
await requireAccess(account);
|
|
345
353
|
let bucket = await getBucket(account, bucketName);
|
|
346
354
|
if (!bucket) return { allScansComplete: true, indexSize: 0, sources: [] };
|
|
347
355
|
if (!bucket.store.getSyncStatus) {
|
|
@@ -352,14 +360,13 @@ class RemoteStorageControllerBase {
|
|
|
352
360
|
|
|
353
361
|
async startLargeFile(account: string, bucketName: string, path: string): Promise<string> {
|
|
354
362
|
assertWritesAllowed();
|
|
355
|
-
await requireAccess(account);
|
|
356
363
|
// Validates now, so the upload doesn't fail at the end
|
|
357
364
|
assertValidPath(path);
|
|
358
365
|
let bucket = await getBucket(account, bucketName);
|
|
359
366
|
if (!bucket) {
|
|
360
367
|
throw new Error(`Bucket ${account}/${bucketName} does not exist. Write its routing config to ${JSON.stringify(ROUTING_FILE)} to create it.`);
|
|
361
368
|
}
|
|
362
|
-
await assertMutable(bucket, path);
|
|
369
|
+
await assertMutable(bucket, path, Date.now());
|
|
363
370
|
let id = await bucket.store.startLargeUpload();
|
|
364
371
|
largeUploadInfo.set(id, { account, bucketName, path });
|
|
365
372
|
return id;
|
|
@@ -368,7 +375,6 @@ class RemoteStorageControllerBase {
|
|
|
368
375
|
assertWritesAllowed();
|
|
369
376
|
let info = largeUploadInfo.get(uploadId);
|
|
370
377
|
if (!info) throw new Error(`Unknown large upload ${uploadId}`);
|
|
371
|
-
await requireAccess(info.account);
|
|
372
378
|
let bucket = await getBucket(info.account, info.bucketName);
|
|
373
379
|
if (!bucket) throw new Error(`Bucket ${info.account}/${info.bucketName} no longer exists`);
|
|
374
380
|
await bucket.store.appendLargeUpload(uploadId, Buffer.from(data));
|
|
@@ -377,7 +383,6 @@ class RemoteStorageControllerBase {
|
|
|
377
383
|
assertWritesAllowed();
|
|
378
384
|
let info = largeUploadInfo.get(uploadId);
|
|
379
385
|
if (!info) throw new Error(`Unknown large upload ${uploadId}`);
|
|
380
|
-
await requireAccess(info.account);
|
|
381
386
|
largeUploadInfo.delete(uploadId);
|
|
382
387
|
let bucket = await getBucket(info.account, info.bucketName);
|
|
383
388
|
if (!bucket) throw new Error(`Bucket ${info.account}/${info.bucketName} no longer exists`);
|
|
@@ -386,7 +391,6 @@ class RemoteStorageControllerBase {
|
|
|
386
391
|
async cancelLargeFile(uploadId: string): Promise<void> {
|
|
387
392
|
let info = largeUploadInfo.get(uploadId);
|
|
388
393
|
if (!info) return;
|
|
389
|
-
await requireAccess(info.account);
|
|
390
394
|
largeUploadInfo.delete(uploadId);
|
|
391
395
|
let bucket = await getBucket(info.account, info.bucketName);
|
|
392
396
|
if (!bucket) return;
|
|
@@ -424,44 +428,109 @@ class RemoteStorageControllerBase {
|
|
|
424
428
|
if (!bucket.self?.public) {
|
|
425
429
|
throw new Error(`Bucket ${account}/${bucketName} is not public, so its files cannot be read over plain URLs`);
|
|
426
430
|
}
|
|
427
|
-
|
|
428
|
-
|
|
431
|
+
// The index answers existence + write time + size without touching any data, so
|
|
432
|
+
// If-Modified-Since and range validation cost nothing
|
|
433
|
+
let info = await bucket.store.getInfo(filePath);
|
|
434
|
+
if (!info || !info.size) {
|
|
429
435
|
return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
|
|
430
436
|
}
|
|
431
437
|
let ext = filePath.split(".").pop() || "";
|
|
432
|
-
|
|
438
|
+
let headers: { [header: string]: string } = {
|
|
433
439
|
"Content-Type": CONTENT_TYPES[ext.toLowerCase()] || "application/octet-stream",
|
|
434
|
-
"Last-Modified": new Date(
|
|
435
|
-
|
|
440
|
+
"Last-Modified": new Date(info.writeTime).toUTCString(),
|
|
441
|
+
"Accept-Ranges": "bytes",
|
|
442
|
+
};
|
|
443
|
+
if (bucket.self?.immutable) {
|
|
444
|
+
headers["Cache-Control"] = `max-age=${IMMUTABLE_CACHE_TIME / 1000}`;
|
|
445
|
+
}
|
|
446
|
+
let ifModifiedSince = request?.headers["if-modified-since"];
|
|
447
|
+
if (typeof ifModifiedSince === "string") {
|
|
448
|
+
let since = new Date(ifModifiedSince).getTime();
|
|
449
|
+
// Last-Modified is served at 1 second resolution, so compare at that resolution
|
|
450
|
+
if (since && Math.floor(info.writeTime / 1000) * 1000 <= since) {
|
|
451
|
+
return setHTTPResultHeaders(Buffer.from(""), { ...headers, status: "304" });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
let range: { start: number; end: number } | undefined;
|
|
455
|
+
let rangeHeader = request?.headers["range"];
|
|
456
|
+
if (typeof rangeHeader === "string") {
|
|
457
|
+
// Single-range form only (bytes=start-end / start- / -suffix); anything else serves the full file
|
|
458
|
+
let match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
|
|
459
|
+
if (match && (match[1] || match[2])) {
|
|
460
|
+
let start: number;
|
|
461
|
+
let endInclusive = info.size - 1;
|
|
462
|
+
if (!match[1]) {
|
|
463
|
+
start = Math.max(0, info.size - +match[2]);
|
|
464
|
+
} else {
|
|
465
|
+
start = +match[1];
|
|
466
|
+
if (match[2]) {
|
|
467
|
+
endInclusive = Math.min(+match[2], info.size - 1);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (start >= info.size || start > endInclusive) {
|
|
471
|
+
return setHTTPResultHeaders(Buffer.from(""), { ...headers, "Content-Range": `bytes */${info.size}`, status: "416" });
|
|
472
|
+
}
|
|
473
|
+
range = { start, end: endInclusive + 1 };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
let result = await bucket.store.get2(filePath, { range });
|
|
477
|
+
if (!result) {
|
|
478
|
+
return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
|
|
479
|
+
}
|
|
480
|
+
if (range) {
|
|
481
|
+
return setHTTPResultHeaders(result.data, { ...headers, "Content-Range": `bytes ${range.start}-${range.end - 1}/${info.size}`, status: "206" });
|
|
482
|
+
}
|
|
483
|
+
return setHTTPResultHeaders(result.data, headers);
|
|
436
484
|
}
|
|
437
485
|
}
|
|
438
486
|
|
|
439
487
|
const largeUploadInfo = new Map<string, { account: string; bucketName: string; path: string }>();
|
|
440
488
|
|
|
489
|
+
// Access checks run as hooks on the register shape below, keyed off the call's arguments, so the
|
|
490
|
+
// method bodies don't each repeat them
|
|
491
|
+
const accountAccess: SocketFunctionHook = async (context) => {
|
|
492
|
+
await requireAccess(String(context.call.args[0]));
|
|
493
|
+
};
|
|
494
|
+
const uploadAccess: SocketFunctionHook = async (context) => {
|
|
495
|
+
let info = largeUploadInfo.get(String(context.call.args[0]));
|
|
496
|
+
// Unknown upload ids are handled by the methods themselves (throw / no-op)
|
|
497
|
+
if (!info) return;
|
|
498
|
+
await requireAccess(info.account);
|
|
499
|
+
};
|
|
500
|
+
const adminAccess: SocketFunctionHook = async () => {
|
|
501
|
+
requireAdmin();
|
|
502
|
+
};
|
|
503
|
+
|
|
441
504
|
export const RemoteStorageController = SocketFunction.register(
|
|
442
505
|
REMOTE_STORAGE_CLASS_GUID,
|
|
443
506
|
new RemoteStorageControllerBase(),
|
|
444
507
|
() => ({
|
|
508
|
+
ping: {},
|
|
445
509
|
authenticate: {},
|
|
446
510
|
requestAccess: {},
|
|
447
511
|
getAccessState: {},
|
|
448
|
-
listRequestsForIP: {},
|
|
512
|
+
listRequestsForIP: { hooks: [accountAccess] },
|
|
449
513
|
grantAccess: {},
|
|
450
|
-
adminListRequests: {},
|
|
451
|
-
adminGrantAccess: {},
|
|
452
|
-
get: {},
|
|
453
|
-
get2: {},
|
|
454
|
-
set: {},
|
|
455
|
-
del: {},
|
|
456
|
-
getInfo: {},
|
|
457
|
-
findInfo: {},
|
|
458
|
-
getChangesAfter: {},
|
|
459
|
-
getArchivesConfig: {},
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
514
|
+
adminListRequests: { hooks: [adminAccess] },
|
|
515
|
+
adminGrantAccess: { hooks: [adminAccess] },
|
|
516
|
+
get: { hooks: [accountAccess] },
|
|
517
|
+
get2: { hooks: [accountAccess] },
|
|
518
|
+
set: { hooks: [accountAccess] },
|
|
519
|
+
del: { hooks: [accountAccess] },
|
|
520
|
+
getInfo: { hooks: [accountAccess] },
|
|
521
|
+
findInfo: { hooks: [accountAccess] },
|
|
522
|
+
getChangesAfter: { hooks: [accountAccess] },
|
|
523
|
+
getArchivesConfig: { hooks: [accountAccess] },
|
|
524
|
+
getIndexInfo: { hooks: [accountAccess] },
|
|
525
|
+
getSyncStatus: { hooks: [accountAccess] },
|
|
526
|
+
startLargeFile: { hooks: [accountAccess] },
|
|
527
|
+
uploadPart: { hooks: [uploadAccess] },
|
|
528
|
+
finishLargeFile: { hooks: [uploadAccess] },
|
|
529
|
+
cancelLargeFile: { hooks: [uploadAccess] },
|
|
465
530
|
httpEntry: {},
|
|
531
|
+
}),
|
|
532
|
+
() => ({
|
|
533
|
+
noClientHooks: true,
|
|
534
|
+
noDefaultHooks: true,
|
|
466
535
|
})
|
|
467
536
|
);
|
|
@@ -2,11 +2,6 @@ import "./accessPage";
|
|
|
2
2
|
export type HostStorageServerConfig = {
|
|
3
3
|
url: string;
|
|
4
4
|
folder: string;
|
|
5
|
-
cloudflareApiToken?: {
|
|
6
|
-
key: string;
|
|
7
|
-
} | {
|
|
8
|
-
path: string;
|
|
9
|
-
};
|
|
10
5
|
lowSpaceThresholdBytes?: number;
|
|
11
6
|
};
|
|
12
7
|
export declare function hostStorageServer(config: HostStorageServerConfig): Promise<void>;
|
|
@@ -5,7 +5,6 @@ import { SocketFunction } from "socket-function/SocketFunction";
|
|
|
5
5
|
import { getExternalIP } from "socket-function/src/networking";
|
|
6
6
|
import { RequireController } from "socket-function/require/RequireController";
|
|
7
7
|
import { hostServer } from "../../misc/https/hostServer";
|
|
8
|
-
import { getSecret } from "../../misc/getSecret";
|
|
9
8
|
import { RemoteStorageController } from "./storageController";
|
|
10
9
|
import { setStorageServerConfig, setWritesRejectedReason } from "./storageServerState";
|
|
11
10
|
import { parseStorageUrl } from "./ArchivesRemote";
|
|
@@ -23,14 +22,13 @@ const HARD_REJECT_FRACTION = 0.1;
|
|
|
23
22
|
// grantAccess.js bootstrap (next to this file) is what the access page's shown SSH command points at.
|
|
24
23
|
|
|
25
24
|
export type HostStorageServerConfig = {
|
|
26
|
-
// Full URL of this storage server, e.g. "https://
|
|
27
|
-
//
|
|
28
|
-
//
|
|
25
|
+
// Full URL of this storage server, e.g. "https://1-2-3-4.example.com:4444". The subdomain must
|
|
26
|
+
// be an ip domain - this machine's external IP with dashes, or 127-0-0-1 for local testing
|
|
27
|
+
// (see the validation in hostStorageServer). The domain and port are extracted from it (bucket
|
|
28
|
+
// routing URLs clients use look like
|
|
29
|
+
// https://1-2-3-4.example.com:4444/file/<account>/<bucketName>/storage/storagerouting.json).
|
|
29
30
|
url: string;
|
|
30
31
|
folder: string;
|
|
31
|
-
// Set hostServer.ts:HostServerConfig:cloudflareApiToken. Defaults to getSecret("cloudflare.json")
|
|
32
|
-
// (~/cloudflare.json, { key: string }).
|
|
33
|
-
cloudflareApiToken?: { key: string } | { path: string };
|
|
34
32
|
// When free space on the folder's drive drops below this many bytes, the server console.errors
|
|
35
33
|
// every 15 minutes. Below 10% of it, the server also rejects write operations (creating files,
|
|
36
34
|
// large uploads, new buckets) — reads, findInfo, and deletes still work so the user can free
|
|
@@ -79,12 +77,22 @@ function getGrantAccessCliPath(): string {
|
|
|
79
77
|
export async function hostStorageServer(config: HostStorageServerConfig): Promise<void> {
|
|
80
78
|
let { url, folder } = config;
|
|
81
79
|
let { address: domain, port } = parseStorageUrl(url);
|
|
80
|
+
let rootDomain = domain.split(".").slice(-2).join(".");
|
|
81
|
+
let externalIP = (await getExternalIP()).trim();
|
|
82
|
+
// The subdomain must be an ip domain: the domain's A record points at exactly one machine, so a
|
|
83
|
+
// dynamic name would let the same script run on two servers and silently fight over it. Encoding
|
|
84
|
+
// the IP makes that mistake fail loudly - the wrong machine's domain just doesn't match.
|
|
85
|
+
let allowedDomains = [`127-0-0-1.${rootDomain}`, `${externalIP.replaceAll(".", "-")}.${rootDomain}`];
|
|
86
|
+
if (!allowedDomains.includes(domain)) {
|
|
87
|
+
throw new Error(`The storage server domain is based on the machine's IP (the subdomain is the IP with dots replaced by dashes). Expected ${allowedDomains.join(" or ")}, was ${domain}. Your external IP is ${externalIP}.`);
|
|
88
|
+
}
|
|
89
|
+
await fsp.mkdir(folder, { recursive: true });
|
|
82
90
|
let lowSpaceThreshold = config.lowSpaceThresholdBytes ?? DEFAULT_LOW_SPACE_THRESHOLD_BYTES;
|
|
83
91
|
setStorageServerConfig({
|
|
84
92
|
domain,
|
|
85
93
|
port,
|
|
86
|
-
rootDomain
|
|
87
|
-
sshTarget: `${os.userInfo().username}@${
|
|
94
|
+
rootDomain,
|
|
95
|
+
sshTarget: `${os.userInfo().username}@${externalIP}`,
|
|
88
96
|
serverCommand: `node ${getGrantAccessCliPath()} --url ${url}`,
|
|
89
97
|
folder: path.resolve(folder),
|
|
90
98
|
});
|
|
@@ -113,7 +121,6 @@ export async function hostStorageServer(config: HostStorageServerConfig): Promis
|
|
|
113
121
|
await hostServer({
|
|
114
122
|
domain,
|
|
115
123
|
port,
|
|
116
|
-
cloudflareApiToken: config.cloudflareApiToken || { key: await getSecret("cloudflare.json.key") },
|
|
117
124
|
setDNSRecord: true,
|
|
118
125
|
});
|
|
119
126
|
}
|
|
@@ -16,8 +16,6 @@ async function main() {
|
|
|
16
16
|
if (!url) throw new Error(`--url is required (ex: --url https://storage.example.com:4444)`);
|
|
17
17
|
let folder = getArg("folder");
|
|
18
18
|
if (!folder) throw new Error(`--folder is required (the folder all data is stored in)`);
|
|
19
|
-
// Optional: hostStorageServer falls back to ~/cloudflare.json
|
|
20
|
-
let cloudflareApiToken = getArg("cloudflareApiToken");
|
|
21
19
|
let lowSpaceThresholdBytes: number | undefined;
|
|
22
20
|
let lowSpaceThreshold = getArg("lowSpaceThreshold");
|
|
23
21
|
if (lowSpaceThreshold) {
|
|
@@ -30,7 +28,6 @@ async function main() {
|
|
|
30
28
|
await hostStorageServer({
|
|
31
29
|
url,
|
|
32
30
|
folder,
|
|
33
|
-
cloudflareApiToken: cloudflareApiToken && { path: cloudflareApiToken } || undefined,
|
|
34
31
|
lowSpaceThresholdBytes,
|
|
35
32
|
});
|
|
36
33
|
}
|
|
@@ -20,16 +20,22 @@ export declare function getWritesRejectedReason(): string | undefined;
|
|
|
20
20
|
export declare function assertWritesAllowed(): void;
|
|
21
21
|
export declare function getTrust(): Promise<IStorage<TrustRecord>>;
|
|
22
22
|
export declare function getRequests(): Promise<IStorage<AccessRequest[]>>;
|
|
23
|
+
/** Makes machineIds the complete trust list for the account: machines not in the list lose access, machines already trusted keep their existing record, and missing ones are added. */
|
|
24
|
+
export declare function setTrustedMachines(config: {
|
|
25
|
+
account: string;
|
|
26
|
+
machineIds: string[];
|
|
27
|
+
}): Promise<void>;
|
|
23
28
|
export type LoadedBucket = {
|
|
24
29
|
account: string;
|
|
25
30
|
bucketName: string;
|
|
26
31
|
routing: RemoteConfig;
|
|
27
32
|
routingJSON: string;
|
|
33
|
+
selfEntries: HostedConfig[];
|
|
28
34
|
self: HostedConfig | undefined;
|
|
29
35
|
store: IBucketStore;
|
|
30
36
|
};
|
|
31
37
|
export declare function getLoadedBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined>;
|
|
32
|
-
export declare function assertMutable(bucket: LoadedBucket, filePath: string): Promise<void>;
|
|
38
|
+
export declare function assertMutable(bucket: LoadedBucket, filePath: string, writeTime: number): Promise<void>;
|
|
33
39
|
export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
|
|
34
40
|
lastModified?: number;
|
|
35
41
|
}): Promise<void>;
|