sliftutils 1.7.87 → 1.7.89
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/.claude/settings.local.json +3 -1
- package/dist/treeSummary.ts.cache +2 -2
- package/index.d.ts +675 -296
- package/misc/dist/environment.ts.cache +2 -2
- package/misc/dist/getSecret.ts.cache +2 -2
- package/misc/dist/strings.ts.cache +2 -2
- package/misc/dist/zip.ts.cache +2 -2
- package/misc/https/dist/certs.ts.cache +2 -2
- package/misc/https/dist/cloudflareHelpers.ts.cache +2 -2
- package/misc/https/dist/dns.ts.cache +2 -2
- package/misc/https/dist/hostServer.ts.cache +49 -21
- package/misc/https/dist/hostsFile.ts.cache +82 -0
- package/misc/https/dist/httpsCerts.ts.cache +2 -2
- package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
- package/misc/https/hostServer.d.ts +6 -2
- package/misc/https/hostServer.ts +52 -19
- package/misc/https/hostsFile.d.ts +7 -0
- package/misc/https/hostsFile.ts +66 -0
- package/package.json +1 -1
- package/render-utils/dist/observer.tsx.cache +2 -2
- package/storage/ArchivesDisk.d.ts +3 -6
- package/storage/ArchivesDisk.ts +26 -2
- package/storage/BulkDatabase2/BulkDatabaseReader.ts +1 -1
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +3 -3
- package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/blockCache.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/mergeLock.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/streamLog.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/syncClient.ts.cache +2 -2
- package/storage/IArchives.d.ts +37 -24
- package/storage/IArchives.ts +47 -62
- package/storage/LogMap.d.ts +56 -0
- package/storage/LogMap.ts +220 -0
- package/storage/archiveHelpers.d.ts +36 -0
- package/storage/archiveHelpers.ts +103 -0
- package/storage/backblaze.d.ts +7 -12
- package/storage/backblaze.ts +53 -48
- package/storage/dist/ArchivesDisk.ts.cache +28 -2
- package/storage/dist/FileFolderAPI.tsx.cache +2 -2
- package/storage/dist/IArchives.ts.cache +16 -52
- package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
- package/storage/dist/JSONStorage.ts.cache +2 -2
- package/storage/dist/LogMap.ts.cache +212 -0
- package/storage/dist/PendingManager.tsx.cache +2 -2
- package/storage/dist/TransactionStorage.ts.cache +2 -2
- package/storage/dist/backblaze.ts.cache +58 -44
- package/storage/dist/fileSystemPointer.ts.cache +2 -2
- package/storage/dist/remoteFileStorage.ts.cache +2 -2
- package/storage/remoteStorage/ArchivesDelayed.d.ts +66 -0
- package/storage/remoteStorage/ArchivesDelayed.ts +237 -0
- package/storage/remoteStorage/ArchivesRemote.d.ts +5 -6
- package/storage/remoteStorage/ArchivesRemote.ts +26 -5
- package/storage/remoteStorage/ArchivesUrl.d.ts +2 -5
- package/storage/remoteStorage/ArchivesUrl.ts +2 -2
- package/storage/remoteStorage/accessPage.tsx +12 -0
- package/storage/remoteStorage/accessStats.d.ts +1 -1
- package/storage/remoteStorage/accessStats.ts +3 -3
- package/storage/remoteStorage/blobStore.d.ts +162 -189
- package/storage/remoteStorage/blobStore.ts +495 -952
- package/storage/remoteStorage/bucketDisk.d.ts +24 -3
- package/storage/remoteStorage/bucketDisk.ts +56 -22
- package/storage/remoteStorage/certTrustModal.d.ts +1 -0
- package/storage/remoteStorage/certTrustModal.tsx +48 -0
- package/storage/remoteStorage/cliArgs.d.ts +2 -0
- package/storage/remoteStorage/cliArgs.ts +5 -0
- package/storage/remoteStorage/createArchives.d.ts +8 -7
- package/storage/remoteStorage/createArchives.ts +65 -26
- package/storage/remoteStorage/deployTakeover.ts +1 -1
- package/storage/remoteStorage/dist/ArchivesDelayed.ts.cache +220 -0
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +25 -5
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +3 -3
- package/storage/remoteStorage/dist/accessPage.tsx.cache +13 -3
- package/storage/remoteStorage/dist/accessStats.ts.cache +6 -6
- package/storage/remoteStorage/dist/blobStore.ts.cache +483 -965
- package/storage/remoteStorage/dist/bucketDisk.ts.cache +60 -29
- package/storage/remoteStorage/dist/certTrustModal.tsx.cache +55 -0
- package/storage/remoteStorage/dist/cliArgs.ts.cache +9 -4
- package/storage/remoteStorage/dist/createArchives.ts.cache +63 -26
- package/storage/remoteStorage/dist/deployTakeover.ts.cache +5 -5
- package/storage/remoteStorage/dist/intermediateManagement.ts.cache +286 -0
- package/storage/remoteStorage/dist/intermediateSources.ts.cache +2 -2
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +122 -56
- package/storage/remoteStorage/dist/serverConfig.ts.cache +4 -4
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +7 -11
- package/storage/remoteStorage/dist/sourcesList.ts.cache +3 -6
- package/storage/remoteStorage/dist/storageClientController.ts.cache +2 -2
- package/storage/remoteStorage/dist/storageController.ts.cache +27 -12
- package/storage/remoteStorage/dist/storageServer.ts.cache +27 -7
- package/storage/remoteStorage/dist/storageServerCli.ts.cache +7 -3
- package/storage/remoteStorage/dist/storageServerState.ts.cache +133 -696
- package/storage/remoteStorage/dist/storeConfig.ts.cache +52 -0
- package/storage/remoteStorage/dist/storePlan.ts.cache +110 -81
- package/storage/remoteStorage/dist/storeSources.ts.cache +51 -0
- package/storage/remoteStorage/dist/storeSync.ts.cache +650 -0
- package/storage/remoteStorage/dist/validation.ts.cache +14 -4
- package/storage/remoteStorage/intermediateManagement.d.ts +12 -0
- package/storage/remoteStorage/intermediateManagement.ts +264 -0
- package/storage/remoteStorage/remoteConfig.d.ts +32 -0
- package/storage/remoteStorage/remoteConfig.ts +115 -50
- package/storage/remoteStorage/serverConfig.d.ts +1 -1
- package/storage/remoteStorage/serverConfig.ts +1 -1
- package/storage/remoteStorage/sourceWrapper.ts +4 -8
- package/storage/remoteStorage/sourcesList.d.ts +0 -2
- package/storage/remoteStorage/sourcesList.ts +0 -3
- package/storage/remoteStorage/spec.md +18 -4
- package/storage/remoteStorage/storageController.d.ts +10 -0
- package/storage/remoteStorage/storageController.ts +29 -16
- package/storage/remoteStorage/storageServer.d.ts +2 -0
- package/storage/remoteStorage/storageServer.ts +28 -4
- package/storage/remoteStorage/storageServerCli.ts +5 -1
- package/storage/remoteStorage/storageServerState.d.ts +34 -21
- package/storage/remoteStorage/storageServerState.ts +147 -662
- package/storage/remoteStorage/storeConfig.d.ts +30 -0
- package/storage/remoteStorage/storeConfig.ts +62 -0
- package/storage/remoteStorage/storePlan.d.ts +29 -17
- package/storage/remoteStorage/storePlan.ts +112 -85
- package/storage/remoteStorage/storeSources.d.ts +11 -0
- package/storage/remoteStorage/storeSources.ts +47 -0
- package/storage/remoteStorage/storeSync.d.ts +53 -0
- package/storage/remoteStorage/storeSync.ts +632 -0
- package/storage/remoteStorage/validation.d.ts +2 -0
- package/storage/remoteStorage/validation.ts +10 -0
package/misc/https/hostServer.ts
CHANGED
|
@@ -8,9 +8,13 @@ import { delay } from "socket-function/src/batching";
|
|
|
8
8
|
import { getExternalIP } from "socket-function/src/networking";
|
|
9
9
|
import { forwardPort } from "socket-function/src/forwardPort";
|
|
10
10
|
import { magenta } from "socket-function/src/formatting/logColors";
|
|
11
|
-
import { getOwnMachineId, getThreadKeyCert, loadIdentityCA } from "./certs";
|
|
11
|
+
import { getOwnMachineId, getThreadKeyCert, loadIdentityCA, getIdentityCA, createX509, generateKeyPair } from "./certs";
|
|
12
12
|
import { generateCert, getAccountKey, parseCert } from "./httpsCerts";
|
|
13
13
|
import { setRecord } from "./dns";
|
|
14
|
+
import { setHostsEntry } from "./hostsFile";
|
|
15
|
+
|
|
16
|
+
// A self-signed cert is signed by our own CA and never renews via ACME, so it gets a long life
|
|
17
|
+
const SELF_SIGNED_LIFESPAN = 1000 * 60 * 60 * 24 * 365 * 10;
|
|
14
18
|
|
|
15
19
|
// Renew somewhere randomly between 40% and 60% of the way through the cert lifetime. The random threshold staggers parallel processes on the same machine, so usually one process renews early, and the others see the renewed cert on disk (we re-read the disk before every renewal check) and never renew themselves.
|
|
16
20
|
const RENEW_THRESHOLD_MIN = 0.4;
|
|
@@ -30,6 +34,10 @@ export type HostServerConfig = {
|
|
|
30
34
|
setDNSRecord?: boolean;
|
|
31
35
|
publicIp?: string;
|
|
32
36
|
allowHostnames?: string[];
|
|
37
|
+
/** LAN-only: do NOT forward the port (no UPnP/NAT mapping), for a server reachable only on the local network. */
|
|
38
|
+
internal?: boolean;
|
|
39
|
+
/** Serve a TLS cert signed by this machine's CA instead of obtaining a real (ACME) one. See getFreshHTTPSCert. */
|
|
40
|
+
selfSigned?: boolean;
|
|
33
41
|
/** When the port is busy (e.g. the previous deploy still holds it), mount on an alternate port instead (the socket server's built-in free-port scan), and keep trying to take the real port - once it frees, a raw TCP relay on the real port forwards to our listener (SocketFunction can only mount once per process). */
|
|
34
42
|
portFallback?: {
|
|
35
43
|
/** Delay until the next main-port acquisition attempt (tightened around the predecessor's scheduled death) */
|
|
@@ -50,12 +58,18 @@ export async function hostServer(config: HostServerConfig): Promise<string> {
|
|
|
50
58
|
|
|
51
59
|
if (config.setDNSRecord) {
|
|
52
60
|
let ip = config.publicIp || await getExternalIP();
|
|
53
|
-
|
|
61
|
+
if (config.selfSigned) {
|
|
62
|
+
// A self-signed server uses no Cloudflare at all (no ACME challenge and no public DNS record), so we resolve the ip-domain through THIS machine's hosts file instead. Other machines that need to reach it must resolve the domain themselves (a real DNS record, or the same hosts entry).
|
|
63
|
+
setHostsEntry({ ip, hostname: domain });
|
|
64
|
+
setHostsEntry({ ip: "127.0.0.1", hostname: "127-0-0-1." + rootDomain });
|
|
65
|
+
} else {
|
|
66
|
+
await setRecord("A", domain, ip);
|
|
67
|
+
}
|
|
54
68
|
}
|
|
55
69
|
|
|
56
|
-
let keyCert = await getFreshHTTPSCert(domain);
|
|
70
|
+
let keyCert = await getFreshHTTPSCert(domain, config.selfSigned);
|
|
57
71
|
let certListeners: ((value: { key: string; cert: string }) => void)[] = [];
|
|
58
|
-
void runCertRenewalLoop(domain, newKeyCert => {
|
|
72
|
+
void runCertRenewalLoop(domain, config.selfSigned, newKeyCert => {
|
|
59
73
|
keyCert = newKeyCert;
|
|
60
74
|
for (let listener of certListeners) {
|
|
61
75
|
listener(newKeyCert);
|
|
@@ -65,7 +79,8 @@ export async function hostServer(config: HostServerConfig): Promise<string> {
|
|
|
65
79
|
// With portFallback, a busy port is not an error: the underlying server scans for a free port instead (useAvailablePortIfPortInUse), and the nodeId tells us which port we actually got
|
|
66
80
|
let nodeId = await SocketFunction.mount({
|
|
67
81
|
public: true,
|
|
68
|
-
|
|
82
|
+
// --internal servers are LAN-only, so we never open a port on the router
|
|
83
|
+
autoForwardPort: !config.internal,
|
|
69
84
|
port,
|
|
70
85
|
useAvailablePortIfPortInUse: !!config.portFallback,
|
|
71
86
|
...getThreadKeyCert(rootDomain),
|
|
@@ -93,7 +108,7 @@ export async function hostServer(config: HostServerConfig): Promise<string> {
|
|
|
93
108
|
if (fallback && servingPort !== port) {
|
|
94
109
|
console.warn(`Port ${port} is in use (presumably by our predecessor); serving on alternate port ${servingPort} until it frees`);
|
|
95
110
|
await fallback.onPortInUse?.();
|
|
96
|
-
void runMainPortAcquireLoop(domain, port, servingPort, fallback);
|
|
111
|
+
void runMainPortAcquireLoop(domain, port, servingPort, fallback, !!config.internal);
|
|
97
112
|
}
|
|
98
113
|
fallback?.onListening(servingPort, servingPort === port);
|
|
99
114
|
console.log(magenta(`Hosting https://${domain}:${servingPort} (nodeId ${nodeId})`));
|
|
@@ -101,7 +116,7 @@ export async function hostServer(config: HostServerConfig): Promise<string> {
|
|
|
101
116
|
}
|
|
102
117
|
|
|
103
118
|
// SocketFunction can only mount once per process, so "taking over" the main port is a raw TCP relay forwarding to our real listener - TLS/SNI passes straight through, and clients address servers by ip domain (wildcard negotiation), so the extra hop is invisible.
|
|
104
|
-
async function runMainPortAcquireLoop(domain: string, mainPort: number, servingPort: number, fallback: NonNullable<HostServerConfig["portFallback"]
|
|
119
|
+
async function runMainPortAcquireLoop(domain: string, mainPort: number, servingPort: number, fallback: NonNullable<HostServerConfig["portFallback"]>, internal: boolean): Promise<void> {
|
|
105
120
|
while (true) {
|
|
106
121
|
await delay(fallback.getAcquireDelay());
|
|
107
122
|
let acquired = await new Promise<boolean>(resolve => {
|
|
@@ -124,24 +139,35 @@ async function runMainPortAcquireLoop(domain: string, mainPort: number, servingP
|
|
|
124
139
|
});
|
|
125
140
|
if (acquired) {
|
|
126
141
|
console.log(magenta(`Acquired main port ${mainPort} for https://${domain} (relaying to our listener on ${servingPort})`));
|
|
127
|
-
// The mount only forwarded OUR serving port; the main port needs its own mapping (the predecessor's mapping pointed at the same machine+port, but dies with it)
|
|
128
|
-
|
|
142
|
+
// The mount only forwarded OUR serving port; the main port needs its own mapping (the predecessor's mapping pointed at the same machine+port, but dies with it). LAN-only (--internal) servers never open router ports.
|
|
143
|
+
if (!internal) {
|
|
144
|
+
await forwardPort({ externalPort: mainPort, internalPort: mainPort });
|
|
145
|
+
}
|
|
129
146
|
fallback.onListening(mainPort, true);
|
|
130
147
|
return;
|
|
131
148
|
}
|
|
132
149
|
}
|
|
133
150
|
}
|
|
134
151
|
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
// Self-signed certs cache under a distinct name, so switching a domain between ACME and self-signed never reads back the wrong kind
|
|
153
|
+
function getCertDiskPath(domain: string, selfSigned?: boolean) {
|
|
154
|
+
return os.homedir() + `/httpscert_${selfSigned ? "selfsigned_" : ""}${domain}.json`;
|
|
137
155
|
}
|
|
138
|
-
function readCertFromDisk(domain: string): { key: string; cert: string } | undefined {
|
|
156
|
+
function readCertFromDisk(domain: string, selfSigned?: boolean): { key: string; cert: string } | undefined {
|
|
139
157
|
try {
|
|
140
|
-
return JSON.parse(fs.readFileSync(getCertDiskPath(domain), "utf8")) as { key: string; cert: string };
|
|
158
|
+
return JSON.parse(fs.readFileSync(getCertDiskPath(domain, selfSigned), "utf8")) as { key: string; cert: string };
|
|
141
159
|
} catch {
|
|
142
160
|
return undefined;
|
|
143
161
|
}
|
|
144
162
|
}
|
|
163
|
+
|
|
164
|
+
// A TLS cert for `domain` signed by this machine's identity CA, presented as a chain (leaf + CA) so a client that trusts the CA validates it. The key part of the ip-domain scheme is unchanged: browsers ignore nameConstraints and validate the SAN + chain; a browser that hasn't trusted the CA prompts the user, who accepts it once (the trust-cert flow), and node peers trust it via the CA.
|
|
165
|
+
function generateSelfSignedCert(domain: string): { key: string; cert: string } {
|
|
166
|
+
let rootDomain = domain.split(".").slice(-2).join(".");
|
|
167
|
+
let ca = getIdentityCA(rootDomain);
|
|
168
|
+
let leaf = createX509({ domain, issuer: ca, keyPair: generateKeyPair(), lifeSpan: SELF_SIGNED_LIFESPAN });
|
|
169
|
+
return { key: leaf.key.toString(), cert: leaf.cert.toString() + "\n" + ca.cert.toString() };
|
|
170
|
+
}
|
|
145
171
|
function getRenewTime(certPem: string, threshold: number) {
|
|
146
172
|
let certObj = parseCert(certPem);
|
|
147
173
|
let start = +new Date(certObj.validity.notBefore);
|
|
@@ -149,12 +175,19 @@ function getRenewTime(certPem: string, threshold: number) {
|
|
|
149
175
|
return start + (end - start) * threshold;
|
|
150
176
|
}
|
|
151
177
|
|
|
152
|
-
/** Returns the cached HTTPS cert for the domain, creating/renewing it first if it is past this process's renewal threshold. Reads the disk cache on every call, so a renewal done by a parallel process is picked up instead of renewing again. */
|
|
153
|
-
export async function getFreshHTTPSCert(domain: string): Promise<{ key: string; cert: string }> {
|
|
154
|
-
let keyCert = readCertFromDisk(domain);
|
|
178
|
+
/** Returns the cached HTTPS cert for the domain, creating/renewing it first if it is past this process's renewal threshold. Reads the disk cache on every call, so a renewal done by a parallel process is picked up instead of renewing again. selfSigned: sign it with this machine's CA instead of getting a real ACME cert. */
|
|
179
|
+
export async function getFreshHTTPSCert(domain: string, selfSigned?: boolean): Promise<{ key: string; cert: string }> {
|
|
180
|
+
let keyCert = readCertFromDisk(domain, selfSigned);
|
|
155
181
|
if (keyCert && getRenewTime(keyCert.cert, renewThreshold) > Date.now()) {
|
|
156
182
|
return keyCert;
|
|
157
183
|
}
|
|
184
|
+
if (selfSigned) {
|
|
185
|
+
// Cached so the served cert is stable across restarts (a browser's accepted-certificate exception is per exact cert)
|
|
186
|
+
console.log(magenta(keyCert ? `Self-signed cert for ${domain} is expiring, regenerating` : `No self-signed cert on disk for ${domain}, creating one (signed by this machine's CA)`));
|
|
187
|
+
let generated = generateSelfSignedCert(domain);
|
|
188
|
+
fs.writeFileSync(getCertDiskPath(domain, true), JSON.stringify(generated));
|
|
189
|
+
return generated;
|
|
190
|
+
}
|
|
158
191
|
if (keyCert) {
|
|
159
192
|
console.log(magenta(`HTTPS cert for ${domain} is past ${(renewThreshold * 100).toFixed(0)}% of its lifetime, renewing`));
|
|
160
193
|
} else {
|
|
@@ -176,12 +209,12 @@ export async function getFreshHTTPSCert(domain: string): Promise<{ key: string;
|
|
|
176
209
|
return { key: keyCert.key, cert: keyCert.cert };
|
|
177
210
|
}
|
|
178
211
|
|
|
179
|
-
async function runCertRenewalLoop(domain: string, onNewCert: (keyCert: { key: string; cert: string }) => void) {
|
|
180
|
-
let lastCert = readCertFromDisk(domain)?.cert;
|
|
212
|
+
async function runCertRenewalLoop(domain: string, selfSigned: boolean | undefined, onNewCert: (keyCert: { key: string; cert: string }) => void) {
|
|
213
|
+
let lastCert = readCertFromDisk(domain, selfSigned)?.cert;
|
|
181
214
|
while (true) {
|
|
182
215
|
await delay(CERT_CHECK_INTERVAL);
|
|
183
216
|
try {
|
|
184
|
-
let keyCert = await getFreshHTTPSCert(domain);
|
|
217
|
+
let keyCert = await getFreshHTTPSCert(domain, selfSigned);
|
|
185
218
|
if (keyCert.cert !== lastCert) {
|
|
186
219
|
lastCert = keyCert.cert;
|
|
187
220
|
console.log(magenta(`HTTPS cert for ${domain} updated, applying to running server`));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Ensures the hosts file maps `hostname` to `ip` (adding our tagged line, or updating it/an existing line for the same hostname). Idempotent. Returns false, with a warning telling the user the line to add by hand, if the file can't be written (writing the hosts file needs admin/root). */
|
|
2
|
+
export declare function setHostsEntry(config: {
|
|
3
|
+
ip: string;
|
|
4
|
+
hostname: string;
|
|
5
|
+
}): boolean;
|
|
6
|
+
/** Removes our managed entry for `hostname` (only lines we added, tagged with the marker). No-op if absent or the file can't be written. */
|
|
7
|
+
export declare function removeHostsEntry(hostname: string): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
|
|
4
|
+
// Cross-platform hosts-file editing, WITHOUT any third-party dependency. The approach (per-platform path, platform line endings, space-separated "<ip> <hostname>" entries, update-matching-hostname-else-append) is the same one the widely-used `hostile` npm package uses - but that package pulls in several dependencies (once, split, @ljharb/through), so this reimplements the ~40 lines we actually need instead of importing them. Used with --selfSigned, where we resolve the ip-domain locally via the hosts file instead of a public (Cloudflare) DNS record. NOTE: a hosts entry only affects THIS machine, so a self-signed server reachable from other machines still needs those machines to resolve its domain (a real DNS record, or the same hosts entry).
|
|
5
|
+
|
|
6
|
+
const WINDOWS = os.platform() === "win32";
|
|
7
|
+
// The canonical hosts-file locations (same as hostile)
|
|
8
|
+
const HOSTS_PATH = WINDOWS ? "C:/Windows/System32/drivers/etc/hosts" : "/etc/hosts";
|
|
9
|
+
const EOL = WINDOWS ? "\r\n" : "\n";
|
|
10
|
+
// Tags the lines we manage, so they are recognizable (and safe to rewrite/remove) without touching the user's own entries
|
|
11
|
+
const MARKER = "# managed by sliftutils hostsFile";
|
|
12
|
+
|
|
13
|
+
// The hostname of a line, or undefined for a blank/comment/malformed line. The marker is a comment, so it is stripped here too - matching works the same for our lines and the user's.
|
|
14
|
+
function lineHostname(line: string): string | undefined {
|
|
15
|
+
let noComment = line.replace(/#.*/, "").trim();
|
|
16
|
+
if (!noComment) return undefined;
|
|
17
|
+
let parts = noComment.split(/\s+/);
|
|
18
|
+
if (parts.length < 2) return undefined;
|
|
19
|
+
return parts[1];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Ensures the hosts file maps `hostname` to `ip` (adding our tagged line, or updating it/an existing line for the same hostname). Idempotent. Returns false, with a warning telling the user the line to add by hand, if the file can't be written (writing the hosts file needs admin/root). */
|
|
23
|
+
export function setHostsEntry(config: { ip: string; hostname: string }): boolean {
|
|
24
|
+
let { ip, hostname } = config;
|
|
25
|
+
let desired = `${ip} ${hostname} ${MARKER}`;
|
|
26
|
+
let lines: string[];
|
|
27
|
+
try {
|
|
28
|
+
lines = fs.readFileSync(HOSTS_PATH, "utf8").split(/\r?\n/);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error(`Could not read the hosts file ${HOSTS_PATH} to map ${hostname} -> ${ip}: ${(e as Error).stack ?? e}. Add this line yourself: ${ip} ${hostname}`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
// A trailing newline leaves a final "" - drop trailing blanks so we don't accumulate them, and re-add exactly one newline at the end when we write
|
|
34
|
+
while (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
35
|
+
|
|
36
|
+
let index = lines.findIndex(line => lineHostname(line) === hostname);
|
|
37
|
+
if (index >= 0) {
|
|
38
|
+
if (lines[index] === desired) return true;
|
|
39
|
+
lines[index] = desired;
|
|
40
|
+
} else {
|
|
41
|
+
lines.push(desired);
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
fs.writeFileSync(HOSTS_PATH, lines.join(EOL) + EOL);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error(`Could not write the hosts file ${HOSTS_PATH} to map ${hostname} -> ${ip} (writing it needs admin/root): ${(e as Error).stack ?? e}. Add this line yourself: ${ip} ${hostname}`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Removes our managed entry for `hostname` (only lines we added, tagged with the marker). No-op if absent or the file can't be written. */
|
|
53
|
+
export function removeHostsEntry(hostname: string): void {
|
|
54
|
+
let lines: string[];
|
|
55
|
+
try {
|
|
56
|
+
lines = fs.readFileSync(HOSTS_PATH, "utf8").split(/\r?\n/);
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let kept = lines.filter(line => !(line.includes(MARKER) && lineHostname(line) === hostname));
|
|
61
|
+
if (kept.length === lines.length) return;
|
|
62
|
+
while (kept.length && kept[kept.length - 1] === "") kept.pop();
|
|
63
|
+
try {
|
|
64
|
+
fs.writeFileSync(HOSTS_PATH, kept.join(EOL) + EOL);
|
|
65
|
+
} catch { }
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
3
3
|
//exports.observer = void 0;
|
|
4
4
|
process.env.NODE_ENV = "production";
|
|
@@ -36,4 +36,4 @@ function observer(Constructor) {
|
|
|
36
36
|
}
|
|
37
37
|
exports.observer = observer;
|
|
38
38
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2JzZXJ2ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJvYnNlcnZlci50c3giXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsT0FBTyxDQUFDLEdBQUcsQ0FBQyxRQUFRLEdBQUcsWUFBWSxDQUFDO0FBR3BDLCtCQUE0QztBQUM1Qyx1RUFBK0Q7QUFDL0QsSUFBQSxzQkFBTyxFQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsYUFBYSxFQUFFLElBQUksQ0FBQyxDQUFDO0FBQzlDLG1FQUFxRTtBQUVyRSxJQUFJLG9CQUFvQixHQUFHLENBQUMsQ0FBQztBQUM3QixTQUFnQixRQUFRLENBU3BCLFdBQWM7SUFFZCxJQUFJLElBQUksR0FBRyxXQUFXLENBQUMsSUFBSSxDQUFDO0lBQzVCLE9BQU8sS0FBTSxTQUFRLFdBQVc7UUFBekI7O1lBR0gsYUFBUSxHQUFHLElBQUksZUFBUSxDQUFDLFVBQVUsSUFBSSxJQUFJLG9CQUFvQixFQUFFLEVBQUUsRUFBRSxHQUFHLEVBQUU7Z0JBQ3JFLEtBQUssQ0FBQyxXQUFXLEVBQUUsQ0FBQztZQUN4QixDQUFDLENBQUMsQ0FBQztRQWNQLENBQUM7UUFsQkcsYUFBYTtRQUNiLE1BQU0sS0FBSyxJQUFJLEtBQUssT0FBTyxXQUFXLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztRQUk5QyxvQkFBb0I7O1lBQ2hCLElBQUksQ0FBQyxRQUFRLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDeEIsTUFBQSxLQUFLLENBQUMsb0JBQW9CLG9EQUFJLENBQUM7UUFDbkMsQ0FBQztRQUNELE1BQU0sQ0FBQyxHQUFHLElBQVc7WUFDakIsSUFBSSxNQUE2QixDQUFDO1lBQ2xDLElBQUksQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRTtnQkFDckIsSUFBQSxzQkFBWSxFQUFDLEdBQUcsRUFBRTtvQkFDZCxNQUFNLEdBQUksS0FBSyxDQUFDLE1BQWMsQ0FBQyxHQUFHLElBQUksQ0FBQyxDQUFDO2dCQUM1QyxDQUFDLEVBQUUsR0FBRyxJQUFJLFNBQVMsQ0FBQyxDQUFDO1lBQ3pCLENBQUMsQ0FBQyxDQUFDO1lBQ0gsT0FBTyxNQUFNLENBQUM7UUFDbEIsQ0FBQztLQUNKLENBQUM7QUFDTixDQUFDO0FBaENELDRCQWdDQyIsInNvdXJjZXNDb250ZW50IjpbInByb2Nlc3MuZW52Lk5PREVfRU5WID0gXCJwcm9kdWN0aW9uXCI7XG5cbmltcG9ydCAqIGFzIHByZWFjdCBmcm9tIFwicHJlYWN0XCI7XG5pbXBvcnQgeyBvYnNlcnZhYmxlLCBSZWFjdGlvbiB9IGZyb20gXCJtb2J4XCI7XG5pbXBvcnQgeyBzZXRGbGFnIH0gZnJvbSBcInNvY2tldC1mdW5jdGlvbi9yZXF1aXJlL2NvbXBpbGVGbGFnc1wiO1xuc2V0RmxhZyhyZXF1aXJlLCBcIm1vYnhcIiwgXCJhbGxvd2NsaWVudFwiLCB0cnVlKTtcbmltcG9ydCB7IG1lYXN1cmVCbG9jayB9IGZyb20gXCJzb2NrZXQtZnVuY3Rpb24vc3JjL3Byb2ZpbGluZy9tZWFzdXJlXCI7XG5cbmxldCBnbG9iYWxDb25zdHJ1Y3RPcmRlciA9IDE7XG5leHBvcnQgZnVuY3Rpb24gb2JzZXJ2ZXI8XG4gICAgVCBleHRlbmRzIHtcbiAgICAgICAgbmV3KC4uLmFyZ3M6IGFueVtdKToge1xuICAgICAgICAgICAgcmVuZGVyKCk6IHByZWFjdC5Db21wb25lbnRDaGlsZDtcbiAgICAgICAgICAgIGZvcmNlVXBkYXRlKGNhbGxiYWNrPzogKCkgPT4gdm9pZCk6IHZvaWQ7XG4gICAgICAgICAgICBjb21wb25lbnRXaWxsVW5tb3VudD8oKTogdm9pZDtcbiAgICAgICAgfVxuICAgIH1cbj4oXG4gICAgQ29uc3RydWN0b3I6IFRcbikge1xuICAgIGxldCBuYW1lID0gQ29uc3RydWN0b3IubmFtZTtcbiAgICByZXR1cm4gY2xhc3MgZXh0ZW5kcyBDb25zdHJ1Y3RvciB7XG4gICAgICAgIC8vIEB0cy1pZ25vcmVcbiAgICAgICAgc3RhdGljIGdldCBuYW1lKCkgeyByZXR1cm4gQ29uc3RydWN0b3IubmFtZTsgfVxuICAgICAgICByZWFjdGlvbiA9IG5ldyBSZWFjdGlvbihgcmVuZGVyLiR7bmFtZX0uJHtnbG9iYWxDb25zdHJ1Y3RPcmRlcisrfWAsICgpID0+IHtcbiAgICAgICAgICAgIHN1cGVyLmZvcmNlVXBkYXRlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBjb21wb25lbnRXaWxsVW5tb3VudCgpIHtcbiAgICAgICAgICAgIHRoaXMucmVhY3Rpb24uZGlzcG9zZSgpO1xuICAgICAgICAgICAgc3VwZXIuY29tcG9uZW50V2lsbFVubW91bnQ/LigpO1xuICAgICAgICB9XG4gICAgICAgIHJlbmRlciguLi5hcmdzOiBhbnlbXSkge1xuICAgICAgICAgICAgbGV0IG91dHB1dDogcHJlYWN0LkNvbXBvbmVudENoaWxkO1xuICAgICAgICAgICAgdGhpcy5yZWFjdGlvbi50cmFjaygoKSA9PiB7XG4gICAgICAgICAgICAgICAgbWVhc3VyZUJsb2NrKCgpID0+IHtcbiAgICAgICAgICAgICAgICAgICAgb3V0cHV0ID0gKHN1cGVyLnJlbmRlciBhcyBhbnkpKC4uLmFyZ3MpO1xuICAgICAgICAgICAgICAgIH0sIGAke25hbWV9fHJlbmRlcmApO1xuICAgICAgICAgICAgfSk7XG4gICAgICAgICAgICByZXR1cm4gb3V0cHV0O1xuICAgICAgICB9XG4gICAgfTtcbn0iXX0=
|
|
39
|
-
/* _JS_SOURCE_HASH = "
|
|
39
|
+
/* _JS_SOURCE_HASH = "66bf117905c840638829362c0f5010e30300b34621cecc47d4fbc18f0fe6fe54"; */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
-
import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, SetConfig } from "./IArchives";
|
|
3
|
+
import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, MoveFileConfig, SetConfig, SetLargeFileConfig } from "./IArchives";
|
|
4
4
|
export declare class ArchivesDisk implements IArchives {
|
|
5
5
|
private folder;
|
|
6
6
|
constructor(folder: string);
|
|
@@ -21,6 +21,7 @@ export declare class ArchivesDisk implements IArchives {
|
|
|
21
21
|
private filePath;
|
|
22
22
|
set(key: string, data: Buffer, config?: SetConfig): Promise<string>;
|
|
23
23
|
del(key: string, config?: DelConfig): Promise<void>;
|
|
24
|
+
move(config: MoveFileConfig): Promise<void>;
|
|
24
25
|
get(key: string, config?: GetConfig): Promise<Buffer | undefined>;
|
|
25
26
|
get2(key: string, config?: GetConfig): Promise<{
|
|
26
27
|
data: Buffer;
|
|
@@ -34,11 +35,7 @@ export declare class ArchivesDisk implements IArchives {
|
|
|
34
35
|
find(prefix: string, config?: FindConfig): Promise<string[]>;
|
|
35
36
|
findInfo(prefix: string, config?: FindConfig): Promise<ArchiveFileInfo[]>;
|
|
36
37
|
private collectFiles;
|
|
37
|
-
setLargeFile(config:
|
|
38
|
-
path: string;
|
|
39
|
-
lastModified?: number;
|
|
40
|
-
getNextData(): Promise<Buffer | undefined>;
|
|
41
|
-
}): Promise<void>;
|
|
38
|
+
setLargeFile(config: SetLargeFileConfig): Promise<void>;
|
|
42
39
|
startLargeUpload(): Promise<string>;
|
|
43
40
|
appendLargeUpload(id: string, data: Buffer): Promise<void>;
|
|
44
41
|
finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void>;
|
package/storage/ArchivesDisk.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { lazy } from "socket-function/src/caching";
|
|
4
4
|
import { runInfinitePoll } from "socket-function/src/batching";
|
|
5
5
|
import { sort, binarySearchBasic } from "socket-function/src/misc";
|
|
6
|
-
import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, SetConfig, assertValidLastModified } from "./IArchives";
|
|
6
|
+
import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, MoveFileConfig, SetConfig, SetLargeFileConfig, assertValidLastModified } from "./IArchives";
|
|
7
7
|
import { filterChanges } from "./remoteStorage/remoteConfig";
|
|
8
8
|
|
|
9
9
|
// The base file-system IArchives: storage is one-to-one with the file system, every key is exactly one real file under <folder>/files, so the file system itself is the index. File handles are cached and reused, and closed once idle (see FileHandleCache). All operations on a file run in serial, so they can't collide with each other or with handle closing. Used as the disk synchronization source of BlobStore (see remoteStorage/blobStore.ts).
|
|
@@ -191,6 +191,29 @@ export class ArchivesDisk implements IArchives {
|
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
public async move(config: MoveFileConfig): Promise<void> {
|
|
195
|
+
await this.init();
|
|
196
|
+
if (config.fromPath === config.toPath) return;
|
|
197
|
+
let fromPath = this.filePath(config.fromPath);
|
|
198
|
+
let toPath = this.filePath(config.toPath);
|
|
199
|
+
await this.handles.run(fromPath, async () => {
|
|
200
|
+
await this.handles.closeNow(fromPath);
|
|
201
|
+
await this.handles.run(toPath, async () => {
|
|
202
|
+
await this.handles.closeNow(toPath);
|
|
203
|
+
await fs.promises.mkdir(path.dirname(toPath), { recursive: true });
|
|
204
|
+
try {
|
|
205
|
+
await fs.promises.rename(fromPath, toPath);
|
|
206
|
+
} catch (e: any) {
|
|
207
|
+
if (e.code !== "ENOENT") throw e;
|
|
208
|
+
throw new Error(`Cannot move ${JSON.stringify(config.fromPath)} to ${JSON.stringify(config.toPath)} on ${this.getDebugName()}: the source file does not exist`);
|
|
209
|
+
}
|
|
210
|
+
// The rename preserves the mtime, but a move must stamp FRESH (see IArchives.move) - the old stamp is how a moved file loses to a stale tombstone at its new path
|
|
211
|
+
let now = new Date();
|
|
212
|
+
await fs.promises.utimes(toPath, now, now);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
194
217
|
public async get(key: string, config?: GetConfig): Promise<Buffer | undefined> {
|
|
195
218
|
let result = await this.get2(key, config);
|
|
196
219
|
return result && result.data || undefined;
|
|
@@ -276,7 +299,8 @@ export class ArchivesDisk implements IArchives {
|
|
|
276
299
|
}
|
|
277
300
|
}
|
|
278
301
|
|
|
279
|
-
|
|
302
|
+
// The raw disk has no windows, routes, or immutability, so the rest of the SetConfig has nothing to apply to here (the store wrapping this disk is what enforces them)
|
|
303
|
+
public async setLargeFile(config: SetLargeFileConfig): Promise<void> {
|
|
280
304
|
let id = await this.startLargeUpload();
|
|
281
305
|
try {
|
|
282
306
|
while (true) {
|
|
@@ -164,7 +164,7 @@ export class BulkDatabaseReader<T extends { key: string }> {
|
|
|
164
164
|
return { key, value: r.value as T[C], time: r.time };
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
// ──
|
|
167
|
+
// ── synchronized (reactive) reads ────────────────────────────────────────────────────────────────────────
|
|
168
168
|
getSingleFieldSync<C extends keyof T>(key: string, column: C): T[C] | undefined {
|
|
169
169
|
return this.getSingleFieldObjSync(key, column)?.value;
|
|
170
170
|
}
|