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
|
@@ -100,7 +100,7 @@ export function addExtraListenPort(port: number): void {
|
|
|
100
100
|
export function removeExtraListenPort(port: number): void {
|
|
101
101
|
extraListenPorts.delete(port);
|
|
102
102
|
}
|
|
103
|
-
/** Whether address:port is this server process
|
|
103
|
+
/** Whether address:port is this server process, including its extra listen ports (a deploy switchover's alternate port is still us). Used to tell which config entries are OUR copy of a bucket - the stores we run - as opposed to peers we synchronize with. Talking to ourselves is not one of the things it prevents: a source that happens to be us is reached over the API like any other. */
|
|
104
104
|
export function isOwnAddress(address: string, port: number): boolean {
|
|
105
105
|
let config = getStorageServerConfigOptional();
|
|
106
106
|
if (!config) return false;
|
|
@@ -6,8 +6,7 @@ import { ROUTING_FILE, getBucketBaseUrl, parseHostedUrl, parseBackblazeUrl, pars
|
|
|
6
6
|
import { ArchivesRemote } from "./ArchivesRemote";
|
|
7
7
|
import { ArchivesUrl } from "./ArchivesUrl";
|
|
8
8
|
import { ArchivesBackblaze } from "../backblaze";
|
|
9
|
-
import {
|
|
10
|
-
import { getLocalArchives } from "./storageServerState";
|
|
9
|
+
import { showCertTrustModal } from "./certTrustModal";
|
|
11
10
|
|
|
12
11
|
export const RETRY_START_DELAY = 2 * 1000;
|
|
13
12
|
export const RETRY_MAX_DELAY = 5 * 60 * 1000;
|
|
@@ -75,12 +74,7 @@ export class SourceWrapper {
|
|
|
75
74
|
}
|
|
76
75
|
return wrapper;
|
|
77
76
|
}
|
|
78
|
-
|
|
79
|
-
if (isNode() && isOwnAddress(parsed.address, parsed.port)) {
|
|
80
|
-
// A bucket hosted by our own process - use it directly instead of calling ourselves
|
|
81
|
-
wrapper.api = getLocalArchives(parsed.account, parsed.bucketName, config);
|
|
82
|
-
return wrapper;
|
|
83
|
-
}
|
|
77
|
+
// A bucket hosted by our own process is reached the same way as any other: over the API, to our own port
|
|
84
78
|
wrapper.remote = new ArchivesRemote({ url: config.url, waitForAccess: false, sourceConfig: config });
|
|
85
79
|
wrapper.api = wrapper.remote;
|
|
86
80
|
if (config.public !== false) {
|
|
@@ -128,6 +122,8 @@ export class SourceWrapper {
|
|
|
128
122
|
let now = Date.now();
|
|
129
123
|
if (now >= this.cooldownUntil && this.isConnectionProblemWorthReporting()) {
|
|
130
124
|
console.error(`Cannot connect to storage ${this.getDebugName()}; skipping it until ${new Date(now + SOURCE_FAILURE_COOLDOWN).toISOString()} (now ${new Date(now).toISOString()}) unless no other source can serve the request`);
|
|
125
|
+
// In a browser this is often an untrusted (self-signed) certificate the user hasn't accepted yet - offer them the trust flow (a no-op in Node, and shown at most once per server)
|
|
126
|
+
showCertTrustModal(this.config.url);
|
|
131
127
|
}
|
|
132
128
|
this.cooldownUntil = now + SOURCE_FAILURE_COOLDOWN;
|
|
133
129
|
if (!this.background || this.reconnectRunning) return;
|
|
@@ -8,8 +8,6 @@ export declare class SourcesList {
|
|
|
8
8
|
private appendQueue;
|
|
9
9
|
private load;
|
|
10
10
|
getUrl(sourcesListIndex: number): string | undefined;
|
|
11
|
-
/** For a sourcesListIndex beyond our in-memory list (another process appended since we read): re-reads the file, at most once per RELOAD_THROTTLE. Returning undefined can therefore mean "throttled", not "definitely absent" - never treat it as proof the index is bogus. */
|
|
12
11
|
getUrlReloading(sourcesListIndex: number): Promise<string | undefined>;
|
|
13
|
-
/** The sourcesListIndex of the url, appending it if it is new. Appends are serialized within this process; before each append the file is re-read, so appends by other processes are picked up instead of duplicated. */
|
|
14
12
|
ensure(url: string): Promise<number>;
|
|
15
13
|
}
|
|
@@ -3,7 +3,6 @@ import path from "path";
|
|
|
3
3
|
|
|
4
4
|
const RELOAD_THROTTLE = 5 * 1000;
|
|
5
5
|
|
|
6
|
-
// The persistent identity behind IndexEntry.sourcesListIndex: an append-only file of source URLs / disk folder paths, one per line, where a URL's 0-based line number IS the sourcesListIndex the index persists. Because the file is only ever appended to, a line number means the same URL forever - across restarts and across processes sharing the folder. A torn append (crash mid-write) leaves an unterminated partial line; the next writer terminates it before appending, so the garbage line permanently occupies its index (harmless - it never matches a real URL) and every reader agrees on the numbering.
|
|
7
6
|
export class SourcesList {
|
|
8
7
|
constructor(private filePath: string) { }
|
|
9
8
|
private urls: string[] = [];
|
|
@@ -37,7 +36,6 @@ export class SourcesList {
|
|
|
37
36
|
return this.urls[sourcesListIndex];
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
/** For a sourcesListIndex beyond our in-memory list (another process appended since we read): re-reads the file, at most once per RELOAD_THROTTLE. Returning undefined can therefore mean "throttled", not "definitely absent" - never treat it as proof the index is bogus. */
|
|
41
39
|
public async getUrlReloading(sourcesListIndex: number): Promise<string | undefined> {
|
|
42
40
|
if (sourcesListIndex < this.urls.length) return this.urls[sourcesListIndex];
|
|
43
41
|
if (Date.now() - this.lastReload < RELOAD_THROTTLE) return undefined;
|
|
@@ -46,7 +44,6 @@ export class SourcesList {
|
|
|
46
44
|
return this.urls[sourcesListIndex];
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
/** The sourcesListIndex of the url, appending it if it is new. Appends are serialized within this process; before each append the file is re-read, so appends by other processes are picked up instead of duplicated. */
|
|
50
47
|
public ensure(url: string): Promise<number> {
|
|
51
48
|
if (url.includes("\n")) {
|
|
52
49
|
throw new Error(`Source URLs cannot contain newlines (they are stored one per line): ${JSON.stringify(url)}`);
|
|
@@ -14,6 +14,12 @@ If the client tries to do a write where the valid state is far enough away or th
|
|
|
14
14
|
|
|
15
15
|
Storage routing JSON is only written to if we have write access and it's only written to on startup, it doesn't propagate. that way things don't revert without the developer intentionally rerunning it.
|
|
16
16
|
|
|
17
|
+
## A source's name is the storage; everything else about it is policy
|
|
18
|
+
|
|
19
|
+
Every source entry carries a name, and that name is the storage it refers to: one folder on the server that holds it, one store, one index. Entries sharing a name (for the same account and bucket) ARE the same storage, however many there are and whatever their windows and routes say — and a request resolves to a store by that name alone, comparing nothing else. Windows and routes are policy layered on top: they decide WHEN a source is written to and WHICH KEYS it takes, and changing them never moves a byte, because they were never what said where the bytes live.
|
|
20
|
+
|
|
21
|
+
This is why re-routing is safe and free. Previously the folder was derived from the route, so re-sharding renamed the storage under a running server and stranded whatever was in the old folder. Now the route can change as often as you like and the data does not move; if you want data in a different place, you say so by using a different name, which is a decision the developer makes deliberately. The flip side is that names must be treated as permanent identifiers: two unrelated entries sharing a name merge their data, and reusing a retired name hands the new entry the retired one's files. The server does exactly what the name says and does not try to guess.
|
|
22
|
+
|
|
17
23
|
## Redundancy, sharding, and deployment
|
|
18
24
|
|
|
19
25
|
For redundancy, we can just have multiple different configurations that will satisfy the same request. The first one is the one that we write to. If that one's down, we don't do writes.
|
|
@@ -22,14 +28,16 @@ However, there's also sharding, where we have different route ranges that values
|
|
|
22
28
|
|
|
23
29
|
The valid windows allow us to schedule deployments, so the nodes can switch over gracefully.
|
|
24
30
|
|
|
25
|
-
##
|
|
31
|
+
## The index is a Map + a transaction log, and our own disk is just another source
|
|
26
32
|
|
|
27
|
-
Each bucket's store keeps
|
|
33
|
+
Each bucket's store keeps an index of every file (path, write time, size, holding source) as a plain in-memory Map, with an append-only transaction log behind it (LogMap) — one file, one record per set, delete or purge. Deletions live in a second map beside it, holding the time each key was deleted, so a tombstone is not a file with a size of zero that every listing, scan and count has to remember to skip: it is simply not among the files. That also makes expiring them a walk of the tombstones alone. The index takes the time of every change and refuses anything older than what it has, so ordering is decided in one place rather than at each of the paths a write can arrive by — and a write older than the deletion that removed a key cannot bring it back. Existence checks and the file count are Map operations, listings and totals are one pass over memory, and nothing reads a source to answer them. The log is only ever there to rebuild the Map on startup: it is replayed once, and rewritten from the Map whenever it has grown past a few records per surviving value, so rewriting the same keys forever cannot make it grow forever. Appends are coalesced, so a crash can lose the last moment of index changes — which is acceptable precisely because of the section above: a scan reconstructs anything the index is missing. The trick that keeps the index accurate: our own disk is not special-cased, it is simply the first synchronization source. The same scan/reconcile code that synchronizes remote sources also synchronizes the disk with the index, so the index self-heals from the same machinery instead of needing separate consistency logic.
|
|
28
34
|
|
|
29
35
|
## Metadata first, data second
|
|
30
36
|
|
|
31
37
|
We have an index that says where our data is, which we load immediately. Therefore, we can start acting as the authority immediately. And then we do a fast sync based on all of our sources, one of them being a disk source, in order to update this. This means that almost all the time we are immediately ready, and if anything is out of sync, we'll find it as quickly as possible. The index and our syncs just synchronize which files exist, their write times, and their sizes, which is almost always sufficient to characterize a unique state, while being very fast, and supported for all sources (backblaze, etc).
|
|
32
38
|
|
|
39
|
+
Being the authority means the index IS the answer: a key that is not in it does not exist here, and a read that misses it does nothing special — it does not go poke the sources, and it does not wait. Scans are what fill the index, on their own schedule. Listings are the one exception, and they wait for our own disk's first scan before answering: a single key coming back missing is a miss the caller can retry, but a listing that quietly omits half a folder looks like an answer and gets acted on. The gap this leaves is a store that comes up with files in its folder that its persisted index doesn't know about (another process wrote them), which stay invisible to reads until the disk scan reaches them — and that is fine, because valid windows hand over with notice: a node becomes the write target at a scheduled time, not the moment it starts, so it has finished scanning long before anyone is asking it for those files.
|
|
40
|
+
|
|
33
41
|
## Client writes are consistent; client reads are redundant
|
|
34
42
|
|
|
35
43
|
Clients always write to the same node — the first source whose valid window is current — and if that node is down, the write FAILS rather than going to another node. A client having a network hiccup and wrongly deciding nodes are down must never scatter its writes across the chain; that would desynchronize the sources based on one client's flaky view of the network. Reads, by contrast, fail over freely across every redundant source: sources are synchronized copies, so reading from any of them is safe. Maximum read uptime, strictly consistent writes. (Write redundancy still exists — it just lives server-side: the receiving node fans writes out downstream and reconciliation heals anything missed, all ordered by the once-stamped write time.)
|
|
@@ -42,13 +50,19 @@ Machines authenticate with their certs.ts identity (proving ownership of their m
|
|
|
42
50
|
|
|
43
51
|
This is our special file that stores the routing information. We write it directly to each node. The client side tries to keep an updated version of these (that's mentioned earlier And is how the client can keep up to date even if the client's code isn't up to date, As long as at least one of the sources is still alive).
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
It is a file IN the store, and the store OWNS it: a store reads its own copy and configures itself from it - its routes, its windows, its flags, and which peers it synchronizes with. A store that has no copy is a complete, working store of just its own disk, valid always, for the whole key space, which is what lets a store exist before it has ever heard of a configuration.
|
|
54
|
+
|
|
55
|
+
It propagates by PULL, not push: beside the scan loops, every peer is asked for this one file every five minutes - far more often than a full rescan, because it is small and because it is the file that decides what everything else does. A copy with a greater version is written into our own store as an internal write, exactly like a scan storing anything else it pulled, and the store notices that file landing and re-configures itself. So an operator writing a config and a peer's copy arriving are the same event, and there is one mechanism instead of two. Older or equal versions are ignored, which is what stops a config from ever reverting.
|
|
56
|
+
|
|
57
|
+
It is exempt from every check a normal write passes - windows, routes, immutability, internal-write acceptance - because it is the file that DEFINES those things: judging it by the config it is about to replace is how a store gets stuck on a configuration it can never be told to leave. The write time still applies, latest wins.
|
|
46
58
|
|
|
47
59
|
## fast writes
|
|
48
60
|
|
|
49
61
|
We support a flag that does fast writes, which will cause us to batch all the writes in memory, Returning from the set call immediately. This allows you to do many writes to the same file with very little disk I/O. This uses a configurable delay amount. You could set it to zero and then we just won't delay it at all, and we'll flush everything to the disk immediately away.
|
|
50
62
|
|
|
51
|
-
The
|
|
63
|
+
The delay is a wrapper around ONE source, not a buffer inside the store: a delayed source takes the write into memory, returns, collapses repeated writes to the same path, and serves its own pending writes to anything reading through it. So each source gets its own delay, passed in when it is built — our own disk and our own storage servers take a short one (cross-node redundancy should not wait minutes), an expensive external source like backblaze takes the full one. The store itself has no idea any of this is happening; it writes to its sources and updates its index, and the index records the write when it is accepted, not when it reaches storage.
|
|
64
|
+
|
|
65
|
+
The deploy system will tell us if it's intending to switch over a source, which we use to create a virtual valid state window in the middle that uses a different port. That port is only ever a second way to reach a source we already have, so we never scan it (its listing would be the listing we already get from that source) and we never record it as the holder of anything — index entries name the source it was split out of, which outlives it.
|
|
52
66
|
|
|
53
67
|
We look at the valid state windows and we make sure we never delay past the valid state window. In fact, if we are within five minutes of being invalid due to the valid state window, we flush the writes immediately. That way, when the next valid window starts running, the writes will be already on disk.
|
|
54
68
|
|
|
@@ -107,6 +107,13 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
|
|
|
107
107
|
lastModified?: number;
|
|
108
108
|
internal?: boolean;
|
|
109
109
|
}) => Promise<void>;
|
|
110
|
+
move: (config: {
|
|
111
|
+
account: string;
|
|
112
|
+
bucketName: string;
|
|
113
|
+
fromPath: string;
|
|
114
|
+
toPath: string;
|
|
115
|
+
sourceConfig: SourceConfig;
|
|
116
|
+
}) => Promise<void>;
|
|
110
117
|
getInfo: (config: {
|
|
111
118
|
account: string;
|
|
112
119
|
bucketName: string;
|
|
@@ -181,6 +188,9 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
|
|
|
181
188
|
path: string;
|
|
182
189
|
sourceConfig: SourceConfig;
|
|
183
190
|
lastModified?: number;
|
|
191
|
+
forceSetImmutable?: boolean;
|
|
192
|
+
noChecks?: boolean;
|
|
193
|
+
internal?: boolean;
|
|
184
194
|
}) => Promise<string>;
|
|
185
195
|
uploadPart: (config: {
|
|
186
196
|
uploadId: string;
|
|
@@ -9,13 +9,13 @@ import { getCommonName, getPublicIdentifier, getOwnMachineId, verify, verifyMach
|
|
|
9
9
|
import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, FindConfig, SourceConfig, IMMUTABLE_CACHE_TIME } from "../IArchives";
|
|
10
10
|
import { ROUTING_FILE, getRoute, routeContains } from "./remoteConfig";
|
|
11
11
|
import {
|
|
12
|
-
getLoadedBucket, requireBucket, findBucketStore, readBucketInternal,
|
|
12
|
+
getLoadedBucket, requireBucket, findBucketStore, readBucketInternal, writeRoutingConfig,
|
|
13
13
|
getBucketConfig, bucketSyncStatus, bucketIndexTotals, LoadedStore, ActiveBucketInfo,
|
|
14
14
|
listAccountBuckets, ServerBucketInfo, clearAccountWriteStats,
|
|
15
15
|
getActiveBucket, activateBucket, getActiveBucketKeys,
|
|
16
16
|
} from "./storageServerState";
|
|
17
17
|
import { getStorageServerConfig, getTrust, getRequests, assertWritesAllowed } from "./serverConfig";
|
|
18
|
-
import {
|
|
18
|
+
import { BlobStore } from "./blobStore";
|
|
19
19
|
import { getRoutingFileResult } from "./bucketDisk";
|
|
20
20
|
import { StorageClientController } from "./storageClientController";
|
|
21
21
|
import { trackAccess, trackAccessCall, getAccessTotals, readAccessSummaries, clearAccountAccessStats, AccessTotals, AccessSummaryState } from "./accessStats";
|
|
@@ -291,7 +291,8 @@ class RemoteStorageControllerBase {
|
|
|
291
291
|
// Copied because the wire hands us a plain Uint8Array view, not a real Buffer
|
|
292
292
|
let data = Buffer.from(config.data);
|
|
293
293
|
if (config.path === ROUTING_FILE) {
|
|
294
|
-
|
|
294
|
+
// The store the caller selected owns this file: it applies it, enforces the version rule, and its peers pull it from there
|
|
295
|
+
return await writeRoutingConfig(config.account, config.bucketName, config.sourceConfig.name, data, config);
|
|
295
296
|
}
|
|
296
297
|
await withStore(config, store => store.set({ ...config, data }));
|
|
297
298
|
}
|
|
@@ -302,6 +303,15 @@ class RemoteStorageControllerBase {
|
|
|
302
303
|
await withStore(config, store => store.del(config));
|
|
303
304
|
}
|
|
304
305
|
@assertValidArgs
|
|
306
|
+
@trackAccessCall("move")
|
|
307
|
+
async move(config: { account: string; bucketName: string; fromPath: string; toPath: string; sourceConfig: SourceConfig }): Promise<void> {
|
|
308
|
+
assertWritesAllowed();
|
|
309
|
+
// assertValidArgs only knows the well-known `path` field, so the two paths of a move are validated here
|
|
310
|
+
assertValidPath(config.fromPath);
|
|
311
|
+
assertValidPath(config.toPath);
|
|
312
|
+
await withStore(config, store => store.move(config));
|
|
313
|
+
}
|
|
314
|
+
@assertValidArgs
|
|
305
315
|
@trackAccessCall("getInfo")
|
|
306
316
|
async getInfo(config: { account: string; bucketName: string; path: string; sourceConfig: SourceConfig; includeTombstones?: boolean }): Promise<{ writeTime: number; size: number } | undefined> {
|
|
307
317
|
if (config.path === ROUTING_FILE) {
|
|
@@ -370,12 +380,14 @@ class RemoteStorageControllerBase {
|
|
|
370
380
|
}
|
|
371
381
|
|
|
372
382
|
@assertValidArgs
|
|
373
|
-
async startLargeFile(config: { account: string; bucketName: string; path: string; sourceConfig: SourceConfig; lastModified?: number }): Promise<string> {
|
|
383
|
+
async startLargeFile(config: { account: string; bucketName: string; path: string; sourceConfig: SourceConfig; lastModified?: number; forceSetImmutable?: boolean; noChecks?: boolean; internal?: boolean }): Promise<string> {
|
|
374
384
|
assertWritesAllowed();
|
|
375
385
|
let target = await findBucketStore(config.account, config.bucketName, config.sourceConfig);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
386
|
+
// The store validates the write here, before any bytes move (see BlobStore.startLargeUpload) - a large file is a set, and its SetConfig decides whether it is even allowed
|
|
387
|
+
let write = { path: config.path, lastModified: config.lastModified, forceSetImmutable: config.forceSetImmutable, noChecks: config.noChecks, internal: config.internal };
|
|
388
|
+
let id = await target.store.startLargeUpload(write);
|
|
389
|
+
// The store name pins the upload's parts and finish to the same store the start picked (in-flight uploads survive a bucket rebuild: the part data lives in that store's folder, which a rebuilt store still opens)
|
|
390
|
+
largeUploadInfo.set(id, { ...write, account: config.account, bucketName: config.bucketName, storeName: target.name });
|
|
379
391
|
return id;
|
|
380
392
|
}
|
|
381
393
|
async uploadPart(config: { uploadId: string; data: Buffer }): Promise<void> {
|
|
@@ -392,7 +404,7 @@ class RemoteStorageControllerBase {
|
|
|
392
404
|
if (!info) throw new Error(`Unknown large upload ${config.uploadId}`);
|
|
393
405
|
largeUploadInfo.delete(config.uploadId);
|
|
394
406
|
let target = await findUploadStore(info);
|
|
395
|
-
await target.store.finishLargeUpload({ id: config.uploadId, path: info.path, lastModified: info.lastModified });
|
|
407
|
+
await target.store.finishLargeUpload({ id: config.uploadId, path: info.path, lastModified: info.lastModified, forceSetImmutable: info.forceSetImmutable, noChecks: info.noChecks, internal: info.internal });
|
|
396
408
|
}
|
|
397
409
|
/** Best-effort cleanup: an upload whose bucket or store has since vanished has nothing left to cancel. */
|
|
398
410
|
async cancelLargeFile(config: { uploadId: string }): Promise<void> {
|
|
@@ -401,8 +413,8 @@ class RemoteStorageControllerBase {
|
|
|
401
413
|
largeUploadInfo.delete(config.uploadId);
|
|
402
414
|
let bucket = await getLoadedBucket(info.account, info.bucketName);
|
|
403
415
|
if (!bucket) return;
|
|
404
|
-
const
|
|
405
|
-
let target = bucket.stores.find(x => x.
|
|
416
|
+
const storeName = info.storeName;
|
|
417
|
+
let target = bucket.stores.find(x => x.name === storeName);
|
|
406
418
|
if (!target) return;
|
|
407
419
|
await target.store.cancelLargeUpload({ id: config.uploadId });
|
|
408
420
|
}
|
|
@@ -442,7 +454,7 @@ class RemoteStorageControllerBase {
|
|
|
442
454
|
let info: { writeTime: number; size: number } | undefined;
|
|
443
455
|
if (filePath !== ROUTING_FILE) {
|
|
444
456
|
let route = getRoute(filePath);
|
|
445
|
-
httpStore = bucket.stores.find(s => routeContains(
|
|
457
|
+
httpStore = bucket.stores.find(s => s.entries.some(e => routeContains(e.route, route)));
|
|
446
458
|
info = httpStore && await httpStore.store.getInfo({ path: filePath }) || undefined;
|
|
447
459
|
} else {
|
|
448
460
|
info = await getRoutingFileResult(account, bucketName);
|
|
@@ -509,18 +521,18 @@ class RemoteStorageControllerBase {
|
|
|
509
521
|
}
|
|
510
522
|
|
|
511
523
|
/** The one resolution every data call does: the client's selected sourceConfig maps to exactly one store (loading the bucket - and so instantiating its stores - if needed), and fn runs on it. findBucketStore throws for missing buckets and unmatched configs. */
|
|
512
|
-
async function withStore<T>(config: { account: string; bucketName: string; sourceConfig: SourceConfig }, fn: (store:
|
|
524
|
+
async function withStore<T>(config: { account: string; bucketName: string; sourceConfig: SourceConfig }, fn: (store: BlobStore) => Promise<T>): Promise<T> {
|
|
513
525
|
let target = await findBucketStore(config.account, config.bucketName, config.sourceConfig);
|
|
514
526
|
return await fn(target.store);
|
|
515
527
|
}
|
|
516
528
|
|
|
517
|
-
const largeUploadInfo = new Map<string, { account: string; bucketName: string; path: string; lastModified?: number;
|
|
529
|
+
const largeUploadInfo = new Map<string, { account: string; bucketName: string; path: string; lastModified?: number; forceSetImmutable?: boolean; noChecks?: boolean; internal?: boolean; storeName: string }>();
|
|
518
530
|
|
|
519
|
-
async function findUploadStore(info: { account: string; bucketName: string;
|
|
531
|
+
async function findUploadStore(info: { account: string; bucketName: string; storeName: string }): Promise<LoadedStore> {
|
|
520
532
|
let bucket = await requireBucket(info.account, info.bucketName);
|
|
521
|
-
let target = bucket.stores.find(x => x.
|
|
533
|
+
let target = bucket.stores.find(x => x.name === info.storeName);
|
|
522
534
|
if (!target) {
|
|
523
|
-
throw new Error(`The store
|
|
535
|
+
throw new Error(`The store ${JSON.stringify(info.storeName)} this upload targets no longer exists on bucket ${info.account}/${info.bucketName} (available: ${JSON.stringify(bucket.stores.map(x => x.name))})`);
|
|
524
536
|
}
|
|
525
537
|
return target;
|
|
526
538
|
}
|
|
@@ -560,6 +572,7 @@ export const RemoteStorageController = SocketFunction.register(
|
|
|
560
572
|
get2: { hooks: [accountAccess] },
|
|
561
573
|
set: { hooks: [accountAccess] },
|
|
562
574
|
del: { hooks: [accountAccess] },
|
|
575
|
+
move: { hooks: [accountAccess] },
|
|
563
576
|
getInfo: { hooks: [accountAccess] },
|
|
564
577
|
findInfo: { hooks: [accountAccess] },
|
|
565
578
|
getChangesAfter2: { hooks: [accountAccess] },
|
|
@@ -19,6 +19,22 @@ const DISK_SPACE_CHECK_INTERVAL_MS = 15 * 60 * 1000;
|
|
|
19
19
|
// Below this fraction of the warn threshold, we start rejecting writes so the server itself doesn't tip the machine into instability. Reads/deletes still work so users can free space.
|
|
20
20
|
const HARD_REJECT_FRACTION = 0.1;
|
|
21
21
|
|
|
22
|
+
// This machine's LAN (internal) IPv4 - the address other machines on the same network reach us at. Used with --internal, where the ip-domain is based on the internal address instead of our public one.
|
|
23
|
+
function getInternalIP(): string {
|
|
24
|
+
let candidates: string[] = [];
|
|
25
|
+
for (let addrs of Object.values(os.networkInterfaces())) {
|
|
26
|
+
for (let addr of addrs || []) {
|
|
27
|
+
if (addr.family !== "IPv4" || addr.internal) continue;
|
|
28
|
+
candidates.push(addr.address);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!candidates.length) {
|
|
32
|
+
throw new Error(`--internal was set, but no non-loopback IPv4 network interface was found to use as the internal address`);
|
|
33
|
+
}
|
|
34
|
+
// Prefer a private-range address (the actual LAN ip) over any other non-loopback interface
|
|
35
|
+
return candidates.find(ip => /^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(ip)) || candidates[0];
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
// The remote storage server, as a library function: consumers call hostStorageServer() from their own process to start hosting (or use the storageserver bin, see storageServerCli.ts). The grantAccess.js bootstrap (next to this file) is what the access page's shown SSH command points at.
|
|
23
39
|
|
|
24
40
|
export type HostStorageServerConfig = {
|
|
@@ -28,6 +44,10 @@ export type HostStorageServerConfig = {
|
|
|
28
44
|
folder: string;
|
|
29
45
|
// When free space on the folder's drive drops below this many bytes, the server console.errors every 15 minutes. Below 10% of it, the server also rejects write operations (creating files, large uploads, new buckets) — reads, findInfo, and deletes still work so the user can free space. Default 25 GiB.
|
|
30
46
|
lowSpaceThresholdBytes?: number;
|
|
47
|
+
// LAN-only mode: do NOT forward ports (no UPnP/NAT mapping), and the ip-domain must be based on this machine's INTERNAL ipv4 (its LAN address) instead of its external ip. For a server other machines only reach over the local network.
|
|
48
|
+
internal?: boolean;
|
|
49
|
+
// Serve a self-signed TLS cert signed by this machine's CA, instead of obtaining a real (ACME/Cloudflare) certificate. For a domain we don't control DNS for. Clients trust it by trusting our CA, or by visiting the server once and accepting the certificate in the browser.
|
|
50
|
+
selfSigned?: boolean;
|
|
31
51
|
};
|
|
32
52
|
|
|
33
53
|
function formatBytes(bytes: number): string {
|
|
@@ -70,11 +90,12 @@ export async function hostStorageServer(config: HostStorageServerConfig): Promis
|
|
|
70
90
|
let { url, folder } = config;
|
|
71
91
|
let { address: domain, port } = parseStorageUrl(url);
|
|
72
92
|
let rootDomain = domain.split(".").slice(-2).join(".");
|
|
73
|
-
|
|
93
|
+
// With --internal the ip-domain is based on our LAN address; otherwise on our external ip
|
|
94
|
+
let ip = config.internal ? getInternalIP() : (await getExternalIP()).trim();
|
|
74
95
|
// The subdomain must be an ip domain: the domain's A record points at exactly one machine, so a dynamic name would let the same script run on two servers and silently fight over it. Encoding the IP makes that mistake fail loudly - the wrong machine's domain just doesn't match.
|
|
75
|
-
let allowedDomains = [`127-0-0-1.${rootDomain}`, `${
|
|
96
|
+
let allowedDomains = [`127-0-0-1.${rootDomain}`, `${ip.replaceAll(".", "-")}.${rootDomain}`];
|
|
76
97
|
if (!allowedDomains.includes(domain)) {
|
|
77
|
-
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}.
|
|
98
|
+
throw new Error(`The storage server domain is based on the machine's ${config.internal ? "INTERNAL (LAN)" : "external"} IP (the subdomain is the IP with dots replaced by dashes). Expected ${allowedDomains.join(" or ")}, was ${domain}. This machine's ${config.internal ? "internal" : "external"} IP is ${ip}.`);
|
|
78
99
|
}
|
|
79
100
|
await fsp.mkdir(folder, { recursive: true });
|
|
80
101
|
let lowSpaceThreshold = config.lowSpaceThresholdBytes ?? DEFAULT_LOW_SPACE_THRESHOLD_BYTES;
|
|
@@ -82,7 +103,7 @@ export async function hostStorageServer(config: HostStorageServerConfig): Promis
|
|
|
82
103
|
domain,
|
|
83
104
|
port,
|
|
84
105
|
rootDomain,
|
|
85
|
-
sshTarget: `${os.userInfo().username}@${
|
|
106
|
+
sshTarget: `${os.userInfo().username}@${ip}`,
|
|
86
107
|
serverCommand: `node ${getGrantAccessCliPath()} --url ${url}`,
|
|
87
108
|
folder: path.resolve(folder),
|
|
88
109
|
});
|
|
@@ -109,6 +130,9 @@ export async function hostStorageServer(config: HostStorageServerConfig): Promis
|
|
|
109
130
|
domain,
|
|
110
131
|
port,
|
|
111
132
|
setDNSRecord: true,
|
|
133
|
+
publicIp: ip,
|
|
134
|
+
internal: config.internal,
|
|
135
|
+
selfSigned: config.selfSigned,
|
|
112
136
|
portFallback: {
|
|
113
137
|
getAcquireDelay: getMainPortAcquireDelay,
|
|
114
138
|
onPortInUse: async () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hostStorageServer } from "./storageServer";
|
|
2
|
-
import { getArg } from "./cliArgs";
|
|
2
|
+
import { getArg, getFlag } from "./cliArgs";
|
|
3
3
|
|
|
4
4
|
// Hosts a storage server from the command line (via the storageserver bin, see package.json).
|
|
5
5
|
|
|
@@ -28,6 +28,10 @@ async function main() {
|
|
|
28
28
|
url,
|
|
29
29
|
folder,
|
|
30
30
|
lowSpaceThresholdBytes,
|
|
31
|
+
// Do not forward ports, and base the ip-domain on our internal LAN ip instead of our external ip (for LAN-only servers behind a NAT we can't/won't open)
|
|
32
|
+
internal: getFlag("internal"),
|
|
33
|
+
// Serve a self-signed TLS cert signed by this machine's CA instead of getting a real (ACME) one - for servers without a Cloudflare-managed domain. Clients trust it by trusting the CA, or by visiting the server once and accepting the certificate.
|
|
34
|
+
selfSigned: getFlag("selfSigned"),
|
|
31
35
|
});
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
-
import {
|
|
3
|
+
import { BlobStore } from "./blobStore";
|
|
4
4
|
import { RemoteConfig, HostedConfig, SourceConfig, IArchives, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
|
|
5
5
|
import { BucketDiskInfo } from "./bucketDisk";
|
|
6
6
|
import { SelfSummary } from "./storePlan";
|
|
7
|
+
/**
|
|
8
|
+
* What this server HAS, as opposed to what any of it does. Four things, and nothing else:
|
|
9
|
+
*
|
|
10
|
+
* - the stores, by folder (getStore) - made once, and self-configuring from there
|
|
11
|
+
* - which of them a request means (findBucketStore), which is only ever a name
|
|
12
|
+
* - what exists on disk, for listings and for the pages that report it
|
|
13
|
+
*
|
|
14
|
+
* It decides nothing about routes, windows or peers - a store reads its own config and works that
|
|
15
|
+
* out itself (BlobStore.applyRoutingConfig) - and it schedules nothing: switchovers and the handovers
|
|
16
|
+
* they cause live in intermediateManagement.
|
|
17
|
+
*/
|
|
7
18
|
export type LoadedStore = {
|
|
8
|
-
|
|
9
|
-
route?: [number, number];
|
|
19
|
+
name: string;
|
|
10
20
|
entries: HostedConfig[];
|
|
11
21
|
folder: string;
|
|
12
|
-
store:
|
|
22
|
+
store: BlobStore;
|
|
13
23
|
};
|
|
14
24
|
export type BucketState = {
|
|
15
25
|
account: string;
|
|
16
26
|
bucketName: string;
|
|
17
|
-
routing
|
|
18
|
-
routingJSON: string;
|
|
27
|
+
routing?: RemoteConfig;
|
|
19
28
|
selfEntries: HostedConfig[];
|
|
20
29
|
self: SelfSummary | undefined;
|
|
21
30
|
stores: LoadedStore[];
|
|
22
|
-
structureKey: string;
|
|
23
31
|
};
|
|
24
32
|
/** The loaded bucket, loading it (which instantiates its stores and starts their synchronization) if needed. A bucket that does not exist on this server throws - callers never see undefined buckets. */
|
|
25
33
|
export declare function requireBucket(account: string, bucketName: string): Promise<BucketState>;
|
|
26
|
-
/** The store serving a request: the
|
|
34
|
+
/** The store serving a request: the one the client's selected entry NAMES. Nothing else is compared - not the window, not the route, not the flags - because the name is what says which storage this is, and everything else is policy the store itself applies to the request. A client a config version behind on some flag therefore still reaches the right store, which is the whole point of naming it. */
|
|
27
35
|
export declare function findBucketStore(account: string, bucketName: string, sourceConfig: SourceConfig | undefined): Promise<LoadedStore>;
|
|
28
36
|
/** Internal (store-to-store) reads skip store selection entirely: the caller is another store whose index says this MACHINE holds the bytes - the persisted holder identity is just a URL, which cannot name a store. Whichever store's folder has the newest copy answers. */
|
|
29
37
|
export declare function readBucketInternal(account: string, bucketName: string, config: {
|
|
@@ -48,12 +56,25 @@ export declare function bucketIndexTotals(bucket: BucketState): Promise<{
|
|
|
48
56
|
fileCount: number;
|
|
49
57
|
byteCount: number;
|
|
50
58
|
}[];
|
|
51
|
-
}
|
|
59
|
+
}>;
|
|
60
|
+
/** The bucket states currently in memory - what the switchover machinery walks. Only what is loaded: loading a bucket starts its synchronization, and a bucket nothing has touched has no in-flight writes to hand over anyway. */
|
|
61
|
+
export declare function getBucketStates(): BucketState[];
|
|
52
62
|
/** A cached IArchives for a persisted source identity: a routing URL (hosted/backblaze) or a disk folder path - the form BlobStore's sources list stores. Configuration (valid windows, routes) decides WHEN a source should be used; for reading bytes the index says a source holds, the URL alone is enough - even for sources no longer in any config. */
|
|
53
63
|
export declare function resolveSourceArchives(url: string): IArchives;
|
|
64
|
+
export declare function getStore(account: string, bucketName: string, name: string): BlobStore;
|
|
54
65
|
export declare function getLoadedBucket(account: string, bucketName: string): Promise<BucketState | undefined>;
|
|
55
|
-
/**
|
|
56
|
-
export declare function
|
|
66
|
+
/** Re-reads the bucket off the disk, replacing the cached snapshot: what a routing-config write calls, because that write can CREATE a store (a new name gets a new folder) and the snapshot would keep answering with the stores it saw at load time. Existing stores are reused (getStore caches by folder), so this only picks up folder changes - it never restarts a store. */
|
|
67
|
+
export declare function reloadBucket(account: string, bucketName: string): Promise<BucketState | undefined>;
|
|
68
|
+
/**
|
|
69
|
+
* Writing the routing config is a write like any other: it goes into a store, and the store applies
|
|
70
|
+
* it to itself and lets its peers pull it. The only thing that happens here is picking WHICH store,
|
|
71
|
+
* because the writer names a source and a source names a store.
|
|
72
|
+
*
|
|
73
|
+
* In-flight switchover windows are re-injected first (see intermediateManagement): an operator's
|
|
74
|
+
* config knows nothing about a switchover that is happening right now, and writing it as-is would
|
|
75
|
+
* cancel it mid-flight.
|
|
76
|
+
*/
|
|
77
|
+
export declare function writeRoutingConfig(account: string, bucketName: string, name: string, data: Buffer, config?: {
|
|
57
78
|
lastModified?: number;
|
|
58
79
|
}): Promise<void>;
|
|
59
80
|
/** Which buckets this process currently has loaded - what a deploy successor asks its predecessor for, so it activates exactly the buckets that are actually in use. */
|
|
@@ -61,13 +82,6 @@ export declare function getActiveBucketKeys(): {
|
|
|
61
82
|
account: string;
|
|
62
83
|
bucketName: string;
|
|
63
84
|
}[];
|
|
64
|
-
export declare function rebuildAllLoadedBuckets(): Promise<void>;
|
|
65
|
-
/** Started by deployTakeover once we are actually a deploy successor listening on an alternate port. Until then there are no switchover windows to write or expire, so nothing polls. */
|
|
66
|
-
export declare const startIntermediateMaintenance: {
|
|
67
|
-
(): void;
|
|
68
|
-
reset(): void;
|
|
69
|
-
set(newValue: void): void;
|
|
70
|
-
};
|
|
71
85
|
export type ServerBucketInfo = {
|
|
72
86
|
bucketName: string;
|
|
73
87
|
active: boolean;
|
|
@@ -82,8 +96,8 @@ export type ServerBucketInfo = {
|
|
|
82
96
|
};
|
|
83
97
|
export type ActiveBucketInfo = {
|
|
84
98
|
folder: string;
|
|
85
|
-
/** The routing config the bucket is RUNNING on, straight from memory - including switchover windows written since it loaded */
|
|
86
|
-
routing
|
|
99
|
+
/** The routing config the bucket is RUNNING on, straight from memory - including switchover windows written since it loaded. Absent when none of its stores has one yet. */
|
|
100
|
+
routing?: RemoteConfig;
|
|
87
101
|
/** Our own entries in that config, and their summarized current role (routes union + flags) */
|
|
88
102
|
selfEntries: HostedConfig[];
|
|
89
103
|
self?: SelfSummary;
|
|
@@ -104,4 +118,3 @@ export type BucketWriteStats = {
|
|
|
104
118
|
};
|
|
105
119
|
/** Zeroes the write statistics of every bucket in the account. */
|
|
106
120
|
export declare function clearAccountWriteStats(account: string): number;
|
|
107
|
-
export declare function getLocalArchives(account: string, bucketName: string, sourceConfig: SourceConfig): IArchives;
|