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
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import { RemoteConfig } from "../IArchives";
|
|
4
|
-
|
|
5
|
-
export declare function
|
|
4
|
+
/** A store's folder, from the only three things that identify it. The name is the config entry's name and nothing else: the same name is the same storage, whatever its window or route say, and a different name is different storage even for the same URL. Nothing about a folder changes when the routing does. */
|
|
5
|
+
export declare function getBucketFolder(name: string, account: string, bucketName: string): string;
|
|
6
|
+
export type StoreFolder = {
|
|
7
|
+
account: string;
|
|
8
|
+
name: string;
|
|
9
|
+
bucketName: string;
|
|
10
|
+
folder: string;
|
|
11
|
+
};
|
|
12
|
+
/** Every store this server holds for an account, found by walking the disk. */
|
|
13
|
+
export declare function listAccountStoreFolders(account: string): Promise<StoreFolder[]>;
|
|
14
|
+
/** Every store this server holds for ONE bucket - one per name it has ever been given. */
|
|
15
|
+
export declare function listBucketStoreFolders(account: string, bucketName: string): Promise<StoreFolder[]>;
|
|
16
|
+
export declare function readRoutingFile(folder: string): Promise<{
|
|
17
|
+
data: Buffer;
|
|
18
|
+
writeTime: number;
|
|
19
|
+
} | undefined>;
|
|
20
|
+
/** The bucket's routing config as this server holds it: the newest copy among its stores. Each store keeps its own, and they converge - so when they disagree, the one written most recently is the one that has heard the most. */
|
|
21
|
+
export declare function readNewestRoutingFile(account: string, bucketName: string): Promise<{
|
|
22
|
+
data: Buffer;
|
|
23
|
+
writeTime: number;
|
|
24
|
+
size: number;
|
|
25
|
+
name: string;
|
|
26
|
+
} | undefined>;
|
|
6
27
|
export declare function readRoutingFromDisk(account: string, bucketName: string): Promise<RemoteConfig | undefined>;
|
|
7
|
-
/**
|
|
28
|
+
/** What an anonymous URL read of the routing file gets: the same newest copy. */
|
|
8
29
|
export declare function getRoutingFileResult(account: string, bucketName: string): Promise<{
|
|
9
30
|
data: Buffer;
|
|
10
31
|
writeTime: number;
|
|
@@ -4,47 +4,81 @@ import { RemoteConfig } from "../IArchives";
|
|
|
4
4
|
import { ROUTING_FILE, parseRoutingData } from "./remoteConfig";
|
|
5
5
|
import { getStorageServerConfig } from "./serverConfig";
|
|
6
6
|
|
|
7
|
-
// The on-disk layout
|
|
7
|
+
// The on-disk layout, and what can be learned from it without opening a store. The layout IS the state: <account>/<name>/<bucket> is a store, and the set of folders is the set of stores this server has - there is no separate record of them to fall out of step.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
// Everything this server stores for buckets. "buckets3" because the layout under it changed shape - the previous generations named folders after the route, which meant re-routing renamed the storage.
|
|
10
|
+
const BUCKETS_FOLDER = "buckets4";
|
|
11
|
+
|
|
12
|
+
/** A store's folder, from the only three things that identify it. The name is the config entry's name and nothing else: the same name is the same storage, whatever its window or route say, and a different name is different storage even for the same URL. Nothing about a folder changes when the routing does. */
|
|
13
|
+
export function getBucketFolder(name: string, account: string, bucketName: string): string {
|
|
14
|
+
return path.join(getStorageServerConfig().folder, BUCKETS_FOLDER, account, name, bucketName);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readDirectory(folder: string): Promise<string[]> {
|
|
18
|
+
try {
|
|
19
|
+
return await fs.promises.readdir(folder);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
if ((e as { code?: string }).code === "ENOENT") return [];
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type StoreFolder = { account: string; name: string; bucketName: string; folder: string };
|
|
27
|
+
|
|
28
|
+
/** Every store this server holds for an account, found by walking the disk. */
|
|
29
|
+
export async function listAccountStoreFolders(account: string): Promise<StoreFolder[]> {
|
|
30
|
+
let accountFolder = path.join(getStorageServerConfig().folder, BUCKETS_FOLDER, account);
|
|
31
|
+
let found: StoreFolder[] = [];
|
|
32
|
+
for (let name of await readDirectory(accountFolder)) {
|
|
33
|
+
for (let bucketName of await readDirectory(path.join(accountFolder, name))) {
|
|
34
|
+
found.push({ account, name, bucketName, folder: getBucketFolder(name, account, bucketName) });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return found;
|
|
13
38
|
}
|
|
14
|
-
|
|
15
|
-
|
|
39
|
+
|
|
40
|
+
/** Every store this server holds for ONE bucket - one per name it has ever been given. */
|
|
41
|
+
export async function listBucketStoreFolders(account: string, bucketName: string): Promise<StoreFolder[]> {
|
|
42
|
+
return (await listAccountStoreFolders(account)).filter(x => x.bucketName === bucketName);
|
|
16
43
|
}
|
|
17
44
|
|
|
18
45
|
/** The routing file is ours, on our own disk, at a path we know - so it is read directly. Going through an ArchivesDisk would construct a whole store (handle cache sweep loop, uploads-folder cleanup) just to read one file. */
|
|
19
46
|
function getRoutingFilePath(folder: string): string {
|
|
20
47
|
return path.join(folder, "files", ROUTING_FILE);
|
|
21
48
|
}
|
|
22
|
-
export async function readRoutingFile(folder: string): Promise<Buffer | undefined> {
|
|
49
|
+
export async function readRoutingFile(folder: string): Promise<{ data: Buffer; writeTime: number } | undefined> {
|
|
50
|
+
let filePath = getRoutingFilePath(folder);
|
|
23
51
|
try {
|
|
24
|
-
|
|
52
|
+
let data = await fs.promises.readFile(filePath);
|
|
53
|
+
let stats = await fs.promises.stat(filePath);
|
|
54
|
+
return { data, writeTime: stats.mtimeMs };
|
|
25
55
|
} catch (e) {
|
|
26
56
|
if ((e as { code?: string }).code === "ENOENT") return undefined;
|
|
27
57
|
throw e;
|
|
28
58
|
}
|
|
29
59
|
}
|
|
30
60
|
|
|
61
|
+
/** The bucket's routing config as this server holds it: the newest copy among its stores. Each store keeps its own, and they converge - so when they disagree, the one written most recently is the one that has heard the most. */
|
|
62
|
+
export async function readNewestRoutingFile(account: string, bucketName: string): Promise<{ data: Buffer; writeTime: number; size: number; name: string } | undefined> {
|
|
63
|
+
let newest: { data: Buffer; writeTime: number; size: number; name: string } | undefined;
|
|
64
|
+
for (let store of await listBucketStoreFolders(account, bucketName)) {
|
|
65
|
+
let file = await readRoutingFile(store.folder);
|
|
66
|
+
if (!file) continue;
|
|
67
|
+
if (newest && file.writeTime <= newest.writeTime) continue;
|
|
68
|
+
newest = { data: file.data, writeTime: file.writeTime, size: file.data.length, name: store.name };
|
|
69
|
+
}
|
|
70
|
+
return newest;
|
|
71
|
+
}
|
|
72
|
+
|
|
31
73
|
export async function readRoutingFromDisk(account: string, bucketName: string): Promise<RemoteConfig | undefined> {
|
|
32
|
-
let
|
|
33
|
-
if (!
|
|
34
|
-
return parseRoutingData(data);
|
|
74
|
+
let newest = await readNewestRoutingFile(account, bucketName);
|
|
75
|
+
if (!newest) return undefined;
|
|
76
|
+
return parseRoutingData(newest.data);
|
|
35
77
|
}
|
|
36
78
|
|
|
37
|
-
/**
|
|
79
|
+
/** What an anonymous URL read of the routing file gets: the same newest copy. */
|
|
38
80
|
export async function getRoutingFileResult(account: string, bucketName: string): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
let data = await fs.promises.readFile(filePath);
|
|
42
|
-
let stats = await fs.promises.stat(filePath);
|
|
43
|
-
return { data, writeTime: stats.mtimeMs, size: data.length };
|
|
44
|
-
} catch (e) {
|
|
45
|
-
if ((e as { code?: string }).code === "ENOENT") return undefined;
|
|
46
|
-
throw e;
|
|
47
|
-
}
|
|
81
|
+
return await readNewestRoutingFile(account, bucketName);
|
|
48
82
|
}
|
|
49
83
|
|
|
50
84
|
export type BucketDiskInfo = {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function showCertTrustModal(serverUrl: string): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { isNode } from "socket-function/src/misc";
|
|
3
|
+
import { css } from "typesafecss";
|
|
4
|
+
|
|
5
|
+
// Shown in the browser when we can't establish a connection to a storage server - most often because it serves a self-signed certificate (see --selfSigned) that this browser has not accepted yet. Gives the user a link to the server, where the browser lets them accept the certificate (the ?trustCert=1 page, see accessPage.tsx); once accepted, the app's own background connection to that server starts working. A no-op in Node.
|
|
6
|
+
|
|
7
|
+
// One modal per server origin, so repeated connection failures (there are many, on a retry loop) never stack modals
|
|
8
|
+
const shownForOrigin = new Set<string>();
|
|
9
|
+
|
|
10
|
+
export function showCertTrustModal(serverUrl: string): void {
|
|
11
|
+
if (isNode()) return;
|
|
12
|
+
let origin: string;
|
|
13
|
+
try {
|
|
14
|
+
origin = new URL(serverUrl).origin;
|
|
15
|
+
} catch {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (shownForOrigin.has(origin)) return;
|
|
19
|
+
shownForOrigin.add(origin);
|
|
20
|
+
|
|
21
|
+
let container = document.createElement("div");
|
|
22
|
+
document.body.appendChild(container);
|
|
23
|
+
let close = () => {
|
|
24
|
+
preact.render(null, container);
|
|
25
|
+
container.remove();
|
|
26
|
+
// Leave it in shownForOrigin: the user dismissed it, so don't re-pop it on the next failure of the same server
|
|
27
|
+
};
|
|
28
|
+
preact.render(<CertTrustModal origin={origin} onClose={close} />, container);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CertTrustModal(props: { origin: string; onClose: () => void }) {
|
|
32
|
+
// Loading this page at all means the browser accepted the certificate; the page then tells the user they can come back
|
|
33
|
+
let trustUrl = props.origin + "/?trustCert=1";
|
|
34
|
+
return <div className={css.fixed.pos(0, 0).fillBoth.zIndex(2147483647)
|
|
35
|
+
.hbox(0).justifyContent("center").alignItems("center").hsla(0, 0, 0, 0.5)
|
|
36
|
+
}>
|
|
37
|
+
<div className={css.vbox(12).pad2(24).maxWidth(480).hsl(0, 0, 100).borderRadius(8)}>
|
|
38
|
+
<div>Can't reach the storage server at <code>{props.origin}</code>.</div>
|
|
39
|
+
<div>
|
|
40
|
+
If this server uses a self-signed certificate, the browser blocks the connection until you accept it. Open the server below, accept the certificate warning, then return to this page - it will connect automatically.
|
|
41
|
+
</div>
|
|
42
|
+
<a href={trustUrl} target="_blank" rel="noreferrer">Open {props.origin} and trust its certificate</a>
|
|
43
|
+
<div className={css.hbox(8).justifyContent("flex-end")}>
|
|
44
|
+
<button onClick={props.onClose}>Close</button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>;
|
|
48
|
+
}
|
|
@@ -7,3 +7,8 @@ export function getArg(name: string): string | undefined {
|
|
|
7
7
|
}
|
|
8
8
|
return value;
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
/** A valueless boolean flag: true when --name is present (with nothing, or a following flag). */
|
|
12
|
+
export function getFlag(name: string): boolean {
|
|
13
|
+
return process.argv.includes(`--${name}`);
|
|
14
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
-
import { IArchives, RemoteConfig, RemoteConfigBase,
|
|
3
|
+
import { IArchives, RemoteConfig, RemoteConfigBase, ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, MoveFileConfig, SetConfig, SetLargeFileConfig } from "../IArchives";
|
|
4
4
|
import { ServerBucketInfo, ActiveBucketInfo } from "./storageServerState";
|
|
5
5
|
/** The address, port, account, and bucket name a bucket routing URL addresses. Throws when the URL isn't a hosted bucket routing URL (https://host:port/file/<account>/<bucketName>/storage/storagerouting.json). */
|
|
6
6
|
export { parseHostedUrl, parseBackblazeUrl, getBucketBaseUrl } from "./remoteConfig";
|
|
7
|
-
|
|
7
|
+
/** A client for ONE source - see storeSources.ts. Re-exported here because a chain is built out of them. */
|
|
8
|
+
export { createApiArchives } from "./storeSources";
|
|
8
9
|
export type ArchivesChainOptions = {
|
|
9
10
|
/** Outside of node we default to read-only downloads over the public URLs (no API connection) when the config has public sources. Set this to connect to the API anyway - needed for writing, listing, and any other operation the plain URL form cannot serve. */
|
|
10
11
|
directConnect?: boolean;
|
|
@@ -91,15 +92,15 @@ export declare class ArchivesChain implements IArchives {
|
|
|
91
92
|
set(fileName: string, data: Buffer, config?: SetConfig): Promise<string>;
|
|
92
93
|
private setRoutingConfig;
|
|
93
94
|
del(fileName: string, config?: DelConfig): Promise<void>;
|
|
95
|
+
/** See IArchives.move. When one node is the write target for BOTH paths, that node moves the file itself - the bytes never come through us - with the same wrong-window/route re-resolution as any write. When the paths route to different shards no single node holds both, so the move degrades to a copy through us plus a delete, CONFIRMED at the destination before the source is touched. No smart timeout on the node-side move: it can be a big file's worth of node-side work, which the upload-sized deadlines would misjudge. */
|
|
96
|
+
move(config: MoveFileConfig): Promise<void>;
|
|
94
97
|
private getVariableShardTargets;
|
|
95
98
|
/** The key setVariableShard would materialize for this VARIABLE_SHARD key (a value in the preferred shard's route range), without writing anything. */
|
|
96
99
|
getShardKey(key: string): Promise<string>;
|
|
97
100
|
private setVariableShard;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
getNextData(): Promise<Buffer | undefined>;
|
|
102
|
-
}): Promise<void>;
|
|
101
|
+
/** A large file is written exactly like a small one - same write node, same wrong-window/route re-resolution, same fallbacks - so a value's SIZE never decides its write semantics (set streams through here past LARGE_SET_THRESHOLD, and a file that grew past it must not suddenly lose the availability its caller asked for). The one difference: every attempt after the first has to rewind the stream, so a config without restartStream gets a single attempt. */
|
|
102
|
+
setLargeFile(config: SetLargeFileConfig): Promise<void>;
|
|
103
|
+
private setLargeFileOnce;
|
|
103
104
|
getURL(path: string): Promise<string>;
|
|
104
105
|
/** Every URL that could serve this path: public sources matching both the path's route and the current valid window. The first is the write node's (first matching source in config order, see runPrimary - the one guaranteed current); the rest are ranked fastest-first by measured latency. Empty when none qualify. */
|
|
105
106
|
getURLs(path: string): Promise<string[]>;
|
|
@@ -2,11 +2,12 @@ import { isNode, sort, timeoutToUndefinedSilent, watchSlowPromise } from "socket
|
|
|
2
2
|
import { delay } from "socket-function/src/batching";
|
|
3
3
|
import {
|
|
4
4
|
IArchives, RemoteConfig, RemoteConfigBase, SourceConfig,
|
|
5
|
-
ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, SetConfig, STORAGE_WRONG_VALID_WINDOW,
|
|
5
|
+
ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, MoveFileConfig, SetConfig, SetLargeFileConfig, STORAGE_WRONG_VALID_WINDOW,
|
|
6
6
|
STORAGE_WRONG_ROUTE, FULL_ROUTE, VARIABLE_SHARD, LARGE_SET_THRESHOLD, bufferChunkStream,
|
|
7
7
|
} from "../IArchives";
|
|
8
|
+
import { copyArchiveFile } from "../archiveHelpers";
|
|
8
9
|
import {
|
|
9
|
-
ROUTING_FILE, getConfigVersion, parseHostedUrl, parseBackblazeUrl,
|
|
10
|
+
ROUTING_FILE, getConfigVersion, parseHostedUrl, parseBackblazeUrl, parseRoutingData, assertValidRemoteConfig,
|
|
10
11
|
normalizeRemoteConfig, normalizeSource, serializeRemoteConfig,
|
|
11
12
|
getRoute, routeContains, parseVariableRoute, getBucketBaseUrl, buildFileUrl,
|
|
12
13
|
} from "./remoteConfig";
|
|
@@ -16,8 +17,8 @@ import { SocketFunction } from "socket-function/SocketFunction";
|
|
|
16
17
|
import { ArchivesRemote, parseStorageUrl, authenticateStorage } from "./ArchivesRemote";
|
|
17
18
|
import { onServerRoutingChanged } from "./storageClientController";
|
|
18
19
|
import { ArchivesBackblaze } from "../backblaze";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
20
|
+
import { ArchivesDisk } from "../ArchivesDisk";
|
|
21
|
+
import { ServerBucketInfo, ActiveBucketInfo } from "./storageServerState";
|
|
21
22
|
import { RemoteStorageController, STORAGE_NOT_AUTHENTICATED } from "./storageController";
|
|
22
23
|
import { SourceWrapper, RETRY_START_DELAY, RETRY_MAX_DELAY, RETRY_GROWTH } from "./sourceWrapper";
|
|
23
24
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
@@ -50,16 +51,8 @@ type SmartTimeout = {
|
|
|
50
51
|
/** The address, port, account, and bucket name a bucket routing URL addresses. Throws when the URL isn't a hosted bucket routing URL (https://host:port/file/<account>/<bucketName>/storage/storagerouting.json). */
|
|
51
52
|
export { parseHostedUrl, parseBackblazeUrl, getBucketBaseUrl } from "./remoteConfig";
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return new ArchivesBackblaze({ bucketName: parseBackblazeUrl(source.url).bucketName, public: source.public, immutable: source.immutable, allowedOrigins: source.allowedOrigins });
|
|
56
|
-
}
|
|
57
|
-
let parsed = parseHostedUrl(source.url);
|
|
58
|
-
if (isNode() && isOwnAddress(parsed.address, parsed.port)) {
|
|
59
|
-
return getLocalArchives(parsed.account, parsed.bucketName, source);
|
|
60
|
-
}
|
|
61
|
-
return new ArchivesRemote({ url: source.url, waitForAccess: false, sourceConfig: source });
|
|
62
|
-
}
|
|
54
|
+
/** A client for ONE source - see storeSources.ts. Re-exported here because a chain is built out of them. */
|
|
55
|
+
export { createApiArchives } from "./storeSources";
|
|
63
56
|
|
|
64
57
|
type ChainState = {
|
|
65
58
|
config: RemoteConfig;
|
|
@@ -77,10 +70,6 @@ function configWindowCurrent(config: SourceConfig): boolean {
|
|
|
77
70
|
return start <= now && now < end;
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
function configAcceptsWrites(config: SourceConfig): boolean {
|
|
81
|
-
return configWindowCurrent(config);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
73
|
function materializeShardKey(key: string, target: SourceWrapper): string {
|
|
85
74
|
let [start, end] = target.config.route || FULL_ROUTE;
|
|
86
75
|
return key.replace(VARIABLE_SHARD, VARIABLE_SHARD + "_" + (start + Math.random() * (end - start)));
|
|
@@ -498,7 +487,7 @@ export class ArchivesChain implements IArchives {
|
|
|
498
487
|
attempt++;
|
|
499
488
|
let attemptStart = Date.now();
|
|
500
489
|
let state = await this.getState();
|
|
501
|
-
let target = state.sources.find(x =>
|
|
490
|
+
let target = state.sources.find(x => configWindowCurrent(x.config) && (config.route === undefined || routeContains(x.config.route, config.route)));
|
|
502
491
|
try {
|
|
503
492
|
if (!target) {
|
|
504
493
|
throw new Error(`No source is the ${config.write && "write target" || "primary read source"} for ${this.getDebugName()}${config.route !== undefined && ` (route ${config.route})` || ""} (every source is outside its valid window or outside the key's route)`);
|
|
@@ -782,7 +771,7 @@ export class ArchivesChain implements IArchives {
|
|
|
782
771
|
public async hasWriteAccess(): Promise<boolean> {
|
|
783
772
|
let state = await this.getState();
|
|
784
773
|
for (let source of state.sources) {
|
|
785
|
-
if (!
|
|
774
|
+
if (!configWindowCurrent(source.config)) continue;
|
|
786
775
|
if (!await source.hasWriteAccess()) return false;
|
|
787
776
|
}
|
|
788
777
|
return true;
|
|
@@ -799,8 +788,8 @@ export class ArchivesChain implements IArchives {
|
|
|
799
788
|
return await this.setVariableShard(fileName, data, config);
|
|
800
789
|
}
|
|
801
790
|
if (data.length > LARGE_SET_THRESHOLD) {
|
|
802
|
-
// Streamed automatically so callers never have to call setLargeFile themselves when they already hold the buffer - one giant message would exceed the wire limit and lag every other client on the connection
|
|
803
|
-
await this.setLargeFile({ path: fileName,
|
|
791
|
+
// Streamed automatically so callers never have to call setLargeFile themselves when they already hold the buffer - one giant message would exceed the wire limit and lag every other client on the connection. The whole config goes with it (including fallbacks, which the buffer's restartStream makes possible): a file must not get different write semantics just for being big.
|
|
792
|
+
await this.setLargeFile({ path: fileName, ...config, ...bufferChunkStream(data) });
|
|
804
793
|
return fileName;
|
|
805
794
|
}
|
|
806
795
|
await this.request({ write: true, fallbacks: config?.fallbacks, route: getRoute(fileName), timeout: { uploadBytes: data.length, label: `Upload of ${JSON.stringify(fileName)} (${data.length} bytes)` } }, archives => archives.set(fileName, data, config));
|
|
@@ -808,6 +797,8 @@ export class ArchivesChain implements IArchives {
|
|
|
808
797
|
}
|
|
809
798
|
|
|
810
799
|
private async setRoutingConfig(data: Buffer, config?: { lastModified?: number }): Promise<string> {
|
|
800
|
+
// Checked before the first node is written to, not just by each node as it arrives: every server would reject it anyway, and finding that out one node at a time is how a config write ends up half-applied
|
|
801
|
+
assertValidRemoteConfig(parseRoutingData(data));
|
|
811
802
|
let state = await this.getState();
|
|
812
803
|
let writeTime = config?.lastModified || Date.now();
|
|
813
804
|
let written: string[] = [];
|
|
@@ -835,11 +826,42 @@ export class ArchivesChain implements IArchives {
|
|
|
835
826
|
await this.request({ write: true, fallbacks: config?.fallbacks, route: getRoute(fileName), timeout: { uploadBytes: 0, label: `Deletion of ${JSON.stringify(fileName)}` } }, archives => archives.del(fileName, config));
|
|
836
827
|
}
|
|
837
828
|
|
|
829
|
+
/** See IArchives.move. When one node is the write target for BOTH paths, that node moves the file itself - the bytes never come through us - with the same wrong-window/route re-resolution as any write. When the paths route to different shards no single node holds both, so the move degrades to a copy through us plus a delete, CONFIRMED at the destination before the source is touched. No smart timeout on the node-side move: it can be a big file's worth of node-side work, which the upload-sized deadlines would misjudge. */
|
|
830
|
+
public async move(config: MoveFileConfig): Promise<void> {
|
|
831
|
+
if (config.fromPath === config.toPath) return;
|
|
832
|
+
if (config.fromPath === ROUTING_FILE || config.toPath === ROUTING_FILE) {
|
|
833
|
+
throw new Error(`The routing config ${JSON.stringify(ROUTING_FILE)} cannot be moved`);
|
|
834
|
+
}
|
|
835
|
+
let fromRoute = getRoute(config.fromPath);
|
|
836
|
+
let toRoute = getRoute(config.toPath);
|
|
837
|
+
let state = await this.getState();
|
|
838
|
+
let target = state.sources.find(x => configWindowCurrent(x.config) && routeContains(x.config.route, fromRoute));
|
|
839
|
+
if (target && routeContains(target.config.route, toRoute)) {
|
|
840
|
+
await this.request({ write: true, route: fromRoute }, async archives => {
|
|
841
|
+
if (!archives.move) {
|
|
842
|
+
throw new Error(`${archives.getDebugName()} does not support move (moving ${JSON.stringify(config.fromPath)} to ${JSON.stringify(config.toPath)})`);
|
|
843
|
+
}
|
|
844
|
+
await archives.move(config);
|
|
845
|
+
});
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
// Inline rather than moveArchiveFile, which - given one archives that implements move - would just call back into this method
|
|
849
|
+
let copied = await copyArchiveFile({ from: this, to: this, path: config.fromPath, toPath: config.toPath });
|
|
850
|
+
if (!copied) {
|
|
851
|
+
throw new Error(`Cannot move ${JSON.stringify(config.fromPath)} to ${JSON.stringify(config.toPath)}: the source file does not exist on ${this.getDebugName()}`);
|
|
852
|
+
}
|
|
853
|
+
let confirmed = await this.getInfo(config.toPath);
|
|
854
|
+
if (!confirmed) {
|
|
855
|
+
throw new Error(`Not deleting ${JSON.stringify(config.fromPath)} after copying it to ${JSON.stringify(config.toPath)}: the destination does not report it (${this.getDebugName()}) - the file is left at the source`);
|
|
856
|
+
}
|
|
857
|
+
await this.del(config.fromPath);
|
|
858
|
+
}
|
|
859
|
+
|
|
838
860
|
// One write target per route range, lowest latency first. Within a shard the target is ALWAYS the first source in config order (see runPrimary - writes must stay on the same node): connectivity only decides which SHARD we pick, never which node within it, so a shard whose node is disconnected is dropped entirely when connectedOnly is set.
|
|
839
861
|
private getVariableShardTargets(state: ChainState, config: { connectedOnly: boolean }): SourceWrapper[] {
|
|
840
862
|
let targetsByRoute = new Map<string, SourceWrapper>();
|
|
841
863
|
for (let source of state.sources) {
|
|
842
|
-
if (!
|
|
864
|
+
if (!configWindowCurrent(source.config)) continue;
|
|
843
865
|
let routeKey = JSON.stringify(source.config.route || FULL_ROUTE);
|
|
844
866
|
if (!targetsByRoute.has(routeKey)) {
|
|
845
867
|
targetsByRoute.set(routeKey, source);
|
|
@@ -881,7 +903,7 @@ export class ArchivesChain implements IArchives {
|
|
|
881
903
|
// The shard picking already retries across shards, so a stuck shard just costs its timeout and we move on. Large data streams via setLargeFile - the key is already materialized here, so the no-VARIABLE_SHARD restriction on setLargeFile doesn't apply.
|
|
882
904
|
await this.applySmartTimeout({ uploadBytes: data.length, label: `Variable-shard upload of ${JSON.stringify(fullKey)} (${data.length} bytes)` }, target, () => {
|
|
883
905
|
if (data.length > LARGE_SET_THRESHOLD) {
|
|
884
|
-
return target.write(archives => archives.setLargeFile({ path: fullKey,
|
|
906
|
+
return target.write(archives => archives.setLargeFile({ path: fullKey, ...config, ...bufferChunkStream(data) }));
|
|
885
907
|
}
|
|
886
908
|
return target.write(archives => archives.set(fullKey, data, config)) as any;
|
|
887
909
|
});
|
|
@@ -909,15 +931,32 @@ export class ArchivesChain implements IArchives {
|
|
|
909
931
|
}
|
|
910
932
|
}
|
|
911
933
|
|
|
912
|
-
|
|
934
|
+
/** A large file is written exactly like a small one - same write node, same wrong-window/route re-resolution, same fallbacks - so a value's SIZE never decides its write semantics (set streams through here past LARGE_SET_THRESHOLD, and a file that grew past it must not suddenly lose the availability its caller asked for). The one difference: every attempt after the first has to rewind the stream, so a config without restartStream gets a single attempt. */
|
|
935
|
+
public async setLargeFile(config: SetLargeFileConfig): Promise<void> {
|
|
913
936
|
if (config.path.includes(VARIABLE_SHARD) && parseVariableRoute(config.path) === undefined) {
|
|
914
937
|
throw new Error(`setLargeFile does not support VARIABLE_SHARD keys (there is no way to return the materialized key); write the file with set, or materialize the key yourself. Key: ${JSON.stringify(config.path)}`);
|
|
915
938
|
}
|
|
916
939
|
let route = getRoute(config.path);
|
|
940
|
+
const restartStream = config.restartStream;
|
|
941
|
+
if (!restartStream) {
|
|
942
|
+
await this.setLargeFileOnce(config, route);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
let attempt = 0;
|
|
946
|
+
await this.request({ write: true, fallbacks: config.fallbacks, route }, async archives => {
|
|
947
|
+
attempt++;
|
|
948
|
+
// The previous attempt consumed some (or all) of the stream, and this source needs the file from its first byte
|
|
949
|
+
if (attempt > 1) await restartStream();
|
|
950
|
+
await watchSlowPromise(`setLargeFile|${config.path}`, archives.setLargeFile(config));
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// A stream that cannot rewind gets exactly ONE attempt at the write node: retrying anywhere (another source, or this one again) would send whatever is left of an already-consumed stream as if it were the whole file.
|
|
955
|
+
private async setLargeFileOnce(config: SetLargeFileConfig, route: number): Promise<void> {
|
|
917
956
|
let recheckedAvailability = false;
|
|
918
957
|
while (true) {
|
|
919
958
|
let state = await this.getState();
|
|
920
|
-
let target = state.sources.find(x =>
|
|
959
|
+
let target = state.sources.find(x => configWindowCurrent(x.config) && routeContains(x.config.route, route));
|
|
921
960
|
if (!target) {
|
|
922
961
|
if (!recheckedAvailability) {
|
|
923
962
|
recheckedAvailability = true;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { startIntermediateMaintenance } from "./
|
|
3
|
+
import { startIntermediateMaintenance } from "./intermediateManagement";
|
|
4
4
|
|
|
5
5
|
const PARAMETERS_TIMELINE_FILE_REGEX = /^(\d+)-parameters\.json$/;
|
|
6
6
|
const DEPLOY_DETECT_RETRY_DELAY = 5 * 1000;
|