sliftutils 1.7.9 → 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.
Files changed (92) 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} +4 -6
  4. package/index.d.ts +770 -84
  5. package/misc/dist/environment.ts.cache +3 -3
  6. package/misc/dist/getSecret.ts.cache +139 -0
  7. package/misc/dist/strings.ts.cache +13 -0
  8. package/misc/dist/zip.ts.cache +9 -0
  9. package/misc/getSecret.ts +38 -16
  10. package/misc/https/cloudflareHelpers.d.ts +19 -0
  11. package/misc/https/cloudflareHelpers.ts +69 -0
  12. package/misc/https/dist/certs.ts.cache +115 -179
  13. package/misc/https/dist/cloudflareHelpers.ts.cache +68 -0
  14. package/misc/https/dist/dns.ts.cache +112 -100
  15. package/misc/https/dist/hostServer.ts.cache +130 -0
  16. package/misc/https/dist/httpsCerts.ts.cache +24 -5
  17. package/misc/https/dist/persistentLocalStorage.ts.cache +3 -2
  18. package/misc/https/dns.d.ts +41 -8
  19. package/misc/https/dns.ts +104 -106
  20. package/misc/https/hostServer.d.ts +0 -3
  21. package/misc/https/hostServer.ts +12 -8
  22. package/package.json +3 -2
  23. package/render-utils/dist/observer.tsx.cache +3 -2
  24. package/spec.txt +0 -55
  25. package/storage/ArchivesDisk.d.ts +67 -0
  26. package/storage/ArchivesDisk.ts +369 -0
  27. package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +1242 -0
  28. package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +513 -0
  29. package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +352 -0
  30. package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +306 -0
  31. package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +435 -0
  32. package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +58 -0
  33. package/storage/BulkDatabase2/dist/blockCache.ts.cache +179 -0
  34. package/storage/BulkDatabase2/dist/mergeLock.ts.cache +149 -0
  35. package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +101 -0
  36. package/storage/BulkDatabase2/dist/streamLog.ts.cache +227 -0
  37. package/storage/BulkDatabase2/dist/syncClient.ts.cache +104 -0
  38. package/storage/IArchives.d.ts +127 -1
  39. package/storage/IArchives.ts +201 -1
  40. package/storage/backblaze.d.ts +29 -2
  41. package/storage/backblaze.ts +214 -78
  42. package/storage/dist/ArchivesDisk.ts.cache +373 -0
  43. package/storage/dist/FileFolderAPI.tsx.cache +508 -65
  44. package/storage/dist/IArchives.ts.cache +44 -0
  45. package/storage/dist/IndexedDBFileFolderAPI.ts.cache +8 -2
  46. package/storage/dist/JSONStorage.ts.cache +6 -2
  47. package/storage/dist/PendingManager.tsx.cache +1 -1
  48. package/storage/dist/TransactionStorage.ts.cache +231 -92
  49. package/storage/dist/backblaze.ts.cache +797 -0
  50. package/storage/dist/fileSystemPointer.ts.cache +36 -3
  51. package/storage/dist/remoteFileStorage.ts.cache +495 -0
  52. package/storage/remoteStorage/ArchivesRemote.d.ts +34 -18
  53. package/storage/remoteStorage/ArchivesRemote.ts +85 -65
  54. package/storage/remoteStorage/ArchivesUrl.d.ts +48 -0
  55. package/storage/remoteStorage/ArchivesUrl.ts +86 -0
  56. package/storage/remoteStorage/accessPage.tsx +111 -28
  57. package/storage/remoteStorage/blobStore.d.ts +128 -25
  58. package/storage/remoteStorage/blobStore.ts +757 -290
  59. package/storage/remoteStorage/cliArgs.d.ts +1 -0
  60. package/storage/remoteStorage/cliArgs.ts +9 -0
  61. package/storage/remoteStorage/createArchives.d.ts +85 -0
  62. package/storage/remoteStorage/createArchives.ts +612 -0
  63. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +197 -0
  64. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +85 -0
  65. package/storage/remoteStorage/dist/accessPage.tsx.cache +222 -0
  66. package/storage/remoteStorage/dist/blobStore.ts.cache +837 -0
  67. package/storage/remoteStorage/dist/cliArgs.ts.cache +16 -0
  68. package/storage/remoteStorage/dist/createArchives.ts.cache +610 -0
  69. package/storage/remoteStorage/dist/remoteConfig.ts.cache +206 -0
  70. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +223 -0
  71. package/storage/remoteStorage/dist/storageController.ts.cache +500 -0
  72. package/storage/remoteStorage/dist/storageServer.ts.cache +106 -0
  73. package/storage/remoteStorage/dist/storageServerCli.ts.cache +39 -0
  74. package/storage/remoteStorage/dist/storageServerState.ts.cache +518 -0
  75. package/storage/remoteStorage/grantAccess.js +4 -0
  76. package/storage/remoteStorage/grantAccessCli.d.ts +1 -0
  77. package/storage/remoteStorage/grantAccessCli.ts +27 -0
  78. package/storage/remoteStorage/remoteConfig.d.ts +29 -0
  79. package/storage/remoteStorage/remoteConfig.ts +194 -0
  80. package/storage/remoteStorage/sourceWrapper.d.ts +41 -0
  81. package/storage/remoteStorage/sourceWrapper.ts +223 -0
  82. package/storage/remoteStorage/spec.md +31 -0
  83. package/storage/remoteStorage/storageController.d.ts +30 -27
  84. package/storage/remoteStorage/storageController.ts +297 -159
  85. package/storage/remoteStorage/storageServer.d.ts +6 -0
  86. package/storage/remoteStorage/storageServer.ts +88 -94
  87. package/storage/remoteStorage/storageServerCli.d.ts +1 -0
  88. package/storage/remoteStorage/storageServerCli.ts +38 -0
  89. package/storage/remoteStorage/storageServerState.d.ts +43 -0
  90. package/storage/remoteStorage/storageServerState.ts +540 -0
  91. package/testsite/server.ts +0 -2
  92. package/yarn.lock +11 -6
@@ -1,132 +1,126 @@
1
- process.env.NODE_ENV = "production";
2
1
  import os from "os";
3
2
  import path from "path";
3
+ import fsp from "fs/promises";
4
4
  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 { getFileStorageNested2 } from "../FileFolderAPI";
9
- import { TransactionStorage } from "../TransactionStorage";
10
- import { JSONStorage } from "../JSONStorage";
11
- import { BlobStore } from "./blobStore";
12
- import {
13
- RemoteStorageController, setStorageServerState,
14
- AccessRequest, TrustRecord, BucketConfig,
15
- } from "./storageController";
16
- import { authenticateStorage } from "./ArchivesRemote";
8
+ import { RemoteStorageController } from "./storageController";
9
+ import { setStorageServerConfig, setWritesRejectedReason } from "./storageServerState";
10
+ import { parseStorageUrl } from "./ArchivesRemote";
17
11
  // Import browser code, so it is allowed to be required by the client
18
12
  import "./accessPage";
19
13
 
20
- // The remote storage server. Run modes:
21
- // Host the server:
22
- // typenode storage/remoteStorage/storageServer.ts --domain storage.example.com --port 4444
23
- // --folder /storage/data --cloudflareApiTokenPath ~/example.com.key
24
- // Admin commands (run on the storage machine itself, against the running server):
25
- // --listAccess <ip> lists pending access requests from that IP (with requestIds)
26
- // --grantAccess <requestId> trusts the machine from that request for the requested account
14
+ const DEFAULT_LOW_SPACE_THRESHOLD_BYTES = 25 * 1024 ** 3;
15
+ const DISK_SPACE_CHECK_INTERVAL_MS = 15 * 60 * 1000;
16
+ // Below this fraction of the warn threshold, we start rejecting writes so the server itself doesn't
17
+ // tip the machine into instability. Reads/deletes still work so users can free space.
18
+ const HARD_REJECT_FRACTION = 0.1;
27
19
 
28
- process.on("unhandledRejection", (error) => {
29
- console.error("Unhandled promise rejection:", error);
30
- });
31
- process.on("uncaughtException", (error) => {
32
- console.error("Uncaught exception:", error);
33
- });
20
+ // The remote storage server, as a library function: consumers call hostStorageServer() from their
21
+ // own process to start hosting (or use the storageserver bin, see storageServerCli.ts). The
22
+ // grantAccess.js bootstrap (next to this file) is what the access page's shown SSH command points at.
34
23
 
35
- // The absolute command that runs this script (with the given args) on this machine, usable from
36
- // any working directory (ex: over ssh)
37
- function getServerCommand(args: string): string {
38
- return `${process.execPath} ${require.resolve("typenode/bootstrap.js")} ${__filename} ${args}`;
39
- }
24
+ export type HostStorageServerConfig = {
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).
30
+ url: string;
31
+ folder: string;
32
+ // When free space on the folder's drive drops below this many bytes, the server console.errors
33
+ // every 15 minutes. Below 10% of it, the server also rejects write operations (creating files,
34
+ // large uploads, new buckets) — reads, findInfo, and deletes still work so the user can free
35
+ // space. Default 25 GiB.
36
+ lowSpaceThresholdBytes?: number;
37
+ };
40
38
 
41
- function getArg(name: string): string | undefined {
42
- let index = process.argv.indexOf(`--${name}`);
43
- if (index < 0) return undefined;
44
- let value = process.argv[index + 1];
45
- if (!value || value.startsWith("--")) {
46
- throw new Error(`Missing value for --${name}`);
47
- }
48
- return value;
39
+ function formatBytes(bytes: number): string {
40
+ return `${(bytes / 1024 ** 3).toFixed(2)} GiB`;
49
41
  }
50
42
 
51
- async function runAdminCommand(config: { domain: string; port: number; listAccess?: string; grantAccess?: string }) {
52
- let nodeId = SocketFunction.connect({ address: config.domain, port: config.port });
53
- let controller = RemoteStorageController.nodes[nodeId];
54
- await authenticateStorage({ address: config.domain, port: config.port, nodeId });
55
- if (config.listAccess) {
56
- let requests = await controller.adminListRequests(config.listAccess);
57
- if (!requests.length) {
58
- console.log(`No access requests from ${config.listAccess}`);
59
- return;
60
- }
61
- console.log(`Access requests from ${config.listAccess}:`);
62
- for (let request of requests) {
63
- console.log(` --grantAccess ${request.requestId} (account ${request.account}, machine ${request.machineId}, requested ${new Date(request.time).toISOString()})`);
64
- }
65
- console.log(`Grant one with: ${getServerCommand(`--domain ${config.domain} --port ${config.port} --grantAccess <requestId>`)}`);
43
+ async function checkDiskSpace(config: { folder: string; threshold: number }): Promise<void> {
44
+ let { folder, threshold } = config;
45
+ let stats = await fsp.statfs(folder);
46
+ let free = Number(stats.bavail) * Number(stats.bsize);
47
+ let hardLimit = threshold * HARD_REJECT_FRACTION;
48
+ if (free >= threshold) {
49
+ setWritesRejectedReason(undefined);
66
50
  return;
67
51
  }
68
- if (config.grantAccess) {
69
- let record = await controller.adminGrantAccess(config.grantAccess);
70
- console.log(`Granted machine ${record.machineId} access to account ${JSON.stringify(record.account)}`);
52
+ let under = threshold - free;
53
+ let rejecting = free < hardLimit;
54
+ console.error(
55
+ `Storage folder ${folder} is low on disk: ${formatBytes(free)} free`
56
+ + ` (warn threshold ${formatBytes(threshold)}, ${formatBytes(under)} under;`
57
+ + ` hard-reject threshold ${formatBytes(hardLimit)}${rejecting ? ", REACHED — write ops now rejected" : ""}).`
58
+ );
59
+ if (rejecting) {
60
+ setWritesRejectedReason(
61
+ `Storage server is out of disk space: only ${formatBytes(free)} free on ${folder}`
62
+ + ` (hard-reject threshold ${formatBytes(hardLimit)}, warn threshold ${formatBytes(threshold)}).`
63
+ + ` Write operations (create/append/new bucket) are rejected; reads, findInfo, and deletes still work — please free space.`
64
+ );
65
+ } else {
66
+ setWritesRejectedReason(undefined);
71
67
  }
72
68
  }
73
69
 
74
- async function main() {
75
- let domain = getArg("domain");
76
- if (!domain) throw new Error(`--domain is required (ex: --domain storage.example.com)`);
77
- let port = +(getArg("port") || 443);
70
+ // Full path to the grantAccess CLI bootstrap that lives next to this file. The SSH command shown on
71
+ // the access page invokes it via `node <path> ...` (found through __dirname so consumers don't have
72
+ // to know where our source lives).
73
+ function getGrantAccessCliPath(): string {
74
+ return path.join(__dirname, "grantAccess.js");
75
+ }
78
76
 
79
- let listAccess = getArg("listAccess");
80
- let grantAccess = getArg("grantAccess");
81
- if (listAccess || grantAccess) {
82
- await runAdminCommand({ domain, port, listAccess, grantAccess });
83
- process.exit(0);
77
+ export async function hostStorageServer(config: HostStorageServerConfig): Promise<void> {
78
+ let { url, folder } = config;
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}.`);
84
88
  }
85
-
86
- let folder = getArg("folder");
87
- if (!folder) throw new Error(`--folder is required (where the storage server keeps its data)`);
88
- let cloudflareApiTokenPath = getArg("cloudflareApiTokenPath");
89
- if (!cloudflareApiTokenPath) throw new Error(`--cloudflareApiTokenPath is required (path to a Cloudflare API token file, for HTTPS certs)`);
90
-
91
- let root = await getFileStorageNested2(path.resolve(folder));
92
- let system = await root.folder.getStorage("system");
93
- let trust = new JSONStorage<TrustRecord>(new TransactionStorage(await system.folder.getStorage("trust"), "storageTrust"));
94
- let requests = new JSONStorage<AccessRequest[]>(new TransactionStorage(await system.folder.getStorage("requests"), "storageRequests"));
95
- let buckets = new JSONStorage<BucketConfig>(new TransactionStorage(await system.folder.getStorage("buckets"), "storageBuckets"));
96
-
97
- setStorageServerState({
89
+ await fsp.mkdir(folder, { recursive: true });
90
+ let lowSpaceThreshold = config.lowSpaceThresholdBytes ?? DEFAULT_LOW_SPACE_THRESHOLD_BYTES;
91
+ setStorageServerConfig({
98
92
  domain,
99
93
  port,
100
- rootDomain: domain.split(".").slice(-2).join("."),
101
- sshTarget: `${os.userInfo().username}@${await getExternalIP()}`,
102
- serverCommand: getServerCommand(`--domain ${domain} --port ${port}`),
103
- blobStore: new BlobStore(path.resolve(folder)),
104
- trust,
105
- requests,
106
- buckets,
94
+ rootDomain,
95
+ sshTarget: `${os.userInfo().username}@${externalIP}`,
96
+ serverCommand: `node ${getGrantAccessCliPath()} --url ${url}`,
97
+ folder: path.resolve(folder),
107
98
  });
108
99
 
109
100
  RequireController.allowAllNodeModules();
110
101
  SocketFunction.expose(RequireController);
111
102
  SocketFunction.expose(RemoteStorageController);
112
- // No static roots, so the access page HTML is served at every path (the path is the account
113
- // name, see accessPage.tsx)
103
+ // Every HTTP path goes through httpEntry: /file/<account>/<bucketName>/... serves public
104
+ // bucket files, everything else serves the access page (the path is the account name, see
105
+ // accessPage.tsx).
114
106
  // A full URL, so the page resolves modules from the origin root even when served at
115
- // /accountName (a relative require would resolve inside the account path)
116
- SocketFunction.setDefaultHTTPCall(RequireController, "requireHTML", {
107
+ // /accountName (a relative require would resolve inside the account path).
108
+ SocketFunction.setDefaultHTTPCall(RemoteStorageController, "httpEntry", {
117
109
  requireCalls: [`https://${domain}:${port}/./storage/remoteStorage/accessPage.tsx`],
118
110
  });
119
111
 
112
+ // Initial check so a server starting under-limit immediately rejects writes; then keep checking
113
+ // every 15 minutes so recovery (freed disk space) is picked up automatically.
114
+ await checkDiskSpace({ folder, threshold: lowSpaceThreshold });
115
+ let interval = setInterval(() => {
116
+ void checkDiskSpace({ folder, threshold: lowSpaceThreshold })
117
+ .catch(e => console.error(`Disk space check failed for ${folder}:`, e));
118
+ }, DISK_SPACE_CHECK_INTERVAL_MS);
119
+ (interval as { unref?: () => void }).unref?.();
120
+
120
121
  await hostServer({
121
122
  domain,
122
123
  port,
123
- cloudflareApiTokenPath,
124
124
  setDNSRecord: true,
125
125
  });
126
- console.log(`Storage server running at https://${domain}:${port}`);
127
126
  }
128
-
129
- main().catch(e => {
130
- console.error(e);
131
- process.exit(1);
132
- });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ process.env.NODE_ENV = "production";
2
+ import { hostStorageServer } from "./storageServer";
3
+ import { getArg } from "./cliArgs";
4
+
5
+ // Hosts a storage server from the command line (via the storageserver bin, see package.json).
6
+
7
+ process.on("unhandledRejection", (error) => {
8
+ console.error("Unhandled promise rejection:", error);
9
+ });
10
+ process.on("uncaughtException", (error) => {
11
+ console.error("Uncaught exception:", error);
12
+ });
13
+
14
+ async function main() {
15
+ let url = getArg("url");
16
+ if (!url) throw new Error(`--url is required (ex: --url https://storage.example.com:4444)`);
17
+ let folder = getArg("folder");
18
+ if (!folder) throw new Error(`--folder is required (the folder all data is stored in)`);
19
+ let lowSpaceThresholdBytes: number | undefined;
20
+ let lowSpaceThreshold = getArg("lowSpaceThreshold");
21
+ if (lowSpaceThreshold) {
22
+ lowSpaceThresholdBytes = +lowSpaceThreshold;
23
+ if (isNaN(lowSpaceThresholdBytes)) {
24
+ throw new Error(`--lowSpaceThreshold must be a number of bytes, was ${JSON.stringify(lowSpaceThreshold)}`);
25
+ }
26
+ }
27
+
28
+ await hostStorageServer({
29
+ url,
30
+ folder,
31
+ lowSpaceThresholdBytes,
32
+ });
33
+ }
34
+
35
+ main().catch(e => {
36
+ console.error(e);
37
+ process.exit(1);
38
+ });
@@ -0,0 +1,43 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import { IBucketStore } from "./blobStore";
4
+ import { RemoteConfig, HostedConfig, IArchives } from "../IArchives";
5
+ import type { IStorage } from "../IStorage";
6
+ import type { AccessRequest, TrustRecord } from "./storageController";
7
+ export type StorageServerConfig = {
8
+ domain: string;
9
+ port: number;
10
+ rootDomain: string;
11
+ sshTarget: string;
12
+ serverCommand: string;
13
+ folder: string;
14
+ };
15
+ export declare function setStorageServerConfig(value: StorageServerConfig): void;
16
+ export declare function getStorageServerConfig(): StorageServerConfig;
17
+ export declare function getStorageServerConfigOptional(): StorageServerConfig | undefined;
18
+ export declare function setWritesRejectedReason(reason: string | undefined): void;
19
+ export declare function getWritesRejectedReason(): string | undefined;
20
+ export declare function assertWritesAllowed(): void;
21
+ export declare function getTrust(): Promise<IStorage<TrustRecord>>;
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>;
28
+ export type LoadedBucket = {
29
+ account: string;
30
+ bucketName: string;
31
+ routing: RemoteConfig;
32
+ routingJSON: string;
33
+ selfEntries: HostedConfig[];
34
+ self: HostedConfig | undefined;
35
+ store: IBucketStore;
36
+ };
37
+ export declare function getLoadedBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined>;
38
+ export declare function assertMutable(bucket: LoadedBucket, filePath: string, writeTime: number): Promise<void>;
39
+ export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
40
+ lastModified?: number;
41
+ }): Promise<void>;
42
+ export declare function deleteBucketFile(account: string, bucketName: string, filePath: string): Promise<void>;
43
+ export declare function getLocalArchives(account: string, bucketName: string): IArchives;