sliftutils 1.7.65 → 1.7.67
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.md +1 -0
- package/dist/test.ts.cache +187 -36
- package/dist/treeSummary.ts.cache +359 -0
- package/index.d.ts +56 -18
- package/package.json +1 -1
- package/storage/ArchivesDisk.d.ts +2 -2
- package/storage/ArchivesDisk.ts +3 -2
- package/storage/IArchives.d.ts +19 -2
- package/storage/IArchives.ts +23 -5
- package/storage/backblaze.d.ts +2 -2
- package/storage/backblaze.ts +5 -4
- package/storage/dist/ArchivesDisk.ts.cache +6 -4
- package/storage/dist/IArchives.ts.cache +6 -6
- package/storage/dist/backblaze.ts.cache +7 -6
- package/storage/remoteStorage/ArchivesRemote.d.ts +2 -2
- package/storage/remoteStorage/ArchivesRemote.ts +5 -5
- package/storage/remoteStorage/ArchivesUrl.d.ts +2 -2
- package/storage/remoteStorage/ArchivesUrl.ts +8 -6
- package/storage/remoteStorage/blobStore.d.ts +21 -3
- package/storage/remoteStorage/blobStore.ts +43 -12
- package/storage/remoteStorage/createArchives.d.ts +2 -2
- package/storage/remoteStorage/createArchives.ts +5 -6
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +7 -7
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +12 -8
- package/storage/remoteStorage/dist/blobStore.ts.cache +43 -13
- package/storage/remoteStorage/dist/createArchives.ts.cache +6 -7
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +3 -4
- package/storage/remoteStorage/dist/storageController.ts.cache +11 -8
- package/storage/remoteStorage/dist/storageServerState.ts.cache +38 -9
- package/storage/remoteStorage/sourceWrapper.ts +2 -3
- package/storage/remoteStorage/storageController.d.ts +3 -3
- package/storage/remoteStorage/storageController.ts +8 -5
- package/storage/remoteStorage/storageServerState.d.ts +3 -0
- package/storage/remoteStorage/storageServerState.ts +34 -7
- package/test-files-cache.json +1 -0
- package/test.ts +127 -10
- package/treeSummary.ts +331 -54
|
@@ -6,7 +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 {
|
|
9
|
+
import { getLocalArchives, isOwnAddress } from "./storageServerState";
|
|
10
10
|
|
|
11
11
|
export const RETRY_START_DELAY = 2 * 1000;
|
|
12
12
|
export const RETRY_MAX_DELAY = 5 * 60 * 1000;
|
|
@@ -75,8 +75,7 @@ export class SourceWrapper {
|
|
|
75
75
|
return wrapper;
|
|
76
76
|
}
|
|
77
77
|
let parsed = parseHostedUrl(config.url);
|
|
78
|
-
|
|
79
|
-
if (server && parsed.address === server.domain && parsed.port === server.port) {
|
|
78
|
+
if (isNode() && isOwnAddress(parsed.address, parsed.port)) {
|
|
80
79
|
// A bucket hosted by our own process - use it directly instead of calling ourselves
|
|
81
80
|
wrapper.api = getLocalArchives(parsed.account, parsed.bucketName);
|
|
82
81
|
return wrapper;
|
|
@@ -65,14 +65,14 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
|
|
|
65
65
|
get2: (account: string, bucketName: string, path: string, range?: {
|
|
66
66
|
start: number;
|
|
67
67
|
end: number;
|
|
68
|
-
}) => Promise<{
|
|
68
|
+
}, internal?: boolean) => Promise<{
|
|
69
69
|
data: Buffer;
|
|
70
70
|
writeTime: number;
|
|
71
71
|
size: number;
|
|
72
72
|
} | undefined>;
|
|
73
|
-
set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean) => Promise<void>;
|
|
73
|
+
set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean, internal?: boolean) => Promise<void>;
|
|
74
74
|
del: (account: string, bucketName: string, path: string) => Promise<void>;
|
|
75
|
-
getInfo: (account: string, bucketName: string, path: string) => Promise<{
|
|
75
|
+
getInfo: (account: string, bucketName: string, path: string, includeTombstones?: boolean) => Promise<{
|
|
76
76
|
writeTime: number;
|
|
77
77
|
size: number;
|
|
78
78
|
} | undefined>;
|
|
@@ -289,27 +289,30 @@ class RemoteStorageControllerBase {
|
|
|
289
289
|
let result = await this.get2(account, bucketName, path, range);
|
|
290
290
|
return result && result.data || undefined;
|
|
291
291
|
}
|
|
292
|
-
async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
|
|
292
|
+
async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }, internal?: boolean): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
|
|
293
293
|
assertValidPath(path);
|
|
294
294
|
let bucket = await getBucket(account, bucketName);
|
|
295
295
|
if (!bucket) return undefined;
|
|
296
|
+
if (internal && bucket.store.getInternal2) {
|
|
297
|
+
return await bucket.store.getInternal2(path, { range });
|
|
298
|
+
}
|
|
296
299
|
return await bucket.store.get2(path, { range });
|
|
297
300
|
}
|
|
298
|
-
async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean): Promise<void> {
|
|
301
|
+
async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean, internal?: boolean): Promise<void> {
|
|
299
302
|
assertValidName(bucketName, "bucket name");
|
|
300
303
|
assertValidPath(path);
|
|
301
|
-
await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified, forceSetImmutable });
|
|
304
|
+
await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified, forceSetImmutable, internal });
|
|
302
305
|
}
|
|
303
306
|
async del(account: string, bucketName: string, path: string): Promise<void> {
|
|
304
307
|
assertValidName(bucketName, "bucket name");
|
|
305
308
|
assertValidPath(path);
|
|
306
309
|
await deleteBucketFile(account, bucketName, path);
|
|
307
310
|
}
|
|
308
|
-
async getInfo(account: string, bucketName: string, path: string): Promise<{ writeTime: number; size: number } | undefined> {
|
|
311
|
+
async getInfo(account: string, bucketName: string, path: string, includeTombstones?: boolean): Promise<{ writeTime: number; size: number } | undefined> {
|
|
309
312
|
assertValidPath(path);
|
|
310
313
|
let bucket = await getBucket(account, bucketName);
|
|
311
314
|
if (!bucket) return undefined;
|
|
312
|
-
return await bucket.store.getInfo(path);
|
|
315
|
+
return await bucket.store.getInfo(path, { includeTombstones });
|
|
313
316
|
}
|
|
314
317
|
async findInfo(account: string, bucketName: string, prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
|
|
315
318
|
let bucket = await getBucket(account, bucketName);
|
|
@@ -46,6 +46,8 @@ export type LoadedBucket = {
|
|
|
46
46
|
};
|
|
47
47
|
export declare function addExtraListenPort(port: number): void;
|
|
48
48
|
export declare function removeExtraListenPort(port: number): void;
|
|
49
|
+
/** Whether address:port is this server process. The ONE self test - findSelfIndexes, createApiArchives, and SourceWrapper all consult it, so "is this me" cannot disagree between the routing plan and connection building: a URL that is us on an extra listen port must never become a network client to ourselves, which is how infinite self-request loops form. */
|
|
50
|
+
export declare function isOwnAddress(address: string, port: number): boolean;
|
|
49
51
|
/** 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. */
|
|
50
52
|
export declare function resolveSourceArchives(url: string): IArchives;
|
|
51
53
|
/** Our role in a bucket's routing config, summarized across ALL currently-valid self entries. Stored instead of a single representative HostedConfig, so nothing can accidentally use one entry's route or flags where the union is required - the standard config has the same URL twice: a routed write-shard entry plus an unrouted read-everything entry. */
|
|
@@ -63,6 +65,7 @@ export declare function assertMutable(bucket: LoadedBucket, filePath: string, wr
|
|
|
63
65
|
export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
|
|
64
66
|
lastModified?: number;
|
|
65
67
|
forceSetImmutable?: boolean;
|
|
68
|
+
internal?: boolean;
|
|
66
69
|
}): Promise<void>;
|
|
67
70
|
export declare function getBucketConfig(bucket: LoadedBucket): ArchivesConfig;
|
|
68
71
|
/** 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. */
|
|
@@ -10,7 +10,7 @@ import { ArchivesDisk } from "../ArchivesDisk";
|
|
|
10
10
|
import { BlobStore, IBucketStore, DEFAULT_FAST_WRITE_DELAY, WINDOW_END_FLUSH_MARGIN } from "./blobStore";
|
|
11
11
|
import {
|
|
12
12
|
RemoteConfig, HostedConfig, BackblazeConfig, IArchives, ArchivesSource, ArchiveFileInfo, ArchivesConfig,
|
|
13
|
-
ArchivesSyncStatus, ChangesAfterConfig, GetConfig, SetConfig,
|
|
13
|
+
ArchivesSyncStatus, ChangesAfterConfig, GetConfig, GetInfoConfig, SetConfig,
|
|
14
14
|
STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, FULL_ROUTE,
|
|
15
15
|
} from "../IArchives";
|
|
16
16
|
import { ROUTING_FILE, parseRoutingData, serializeRemoteConfig, parseHostedUrl, replaceHostedUrlPort, buildFileUrl, getConfigVersion, getRoute, routeContains, routeIntersection, normalizeSource } from "./remoteConfig";
|
|
@@ -224,15 +224,22 @@ export function addExtraListenPort(port: number): void {
|
|
|
224
224
|
export function removeExtraListenPort(port: number): void {
|
|
225
225
|
extraListenPorts.delete(port);
|
|
226
226
|
}
|
|
227
|
+
/** Whether address:port is this server process. The ONE self test - findSelfIndexes, createApiArchives, and SourceWrapper all consult it, so "is this me" cannot disagree between the routing plan and connection building: a URL that is us on an extra listen port must never become a network client to ourselves, which is how infinite self-request loops form. */
|
|
228
|
+
export function isOwnAddress(address: string, port: number): boolean {
|
|
229
|
+
let config = getStorageServerConfigOptional();
|
|
230
|
+
if (!config) return false;
|
|
231
|
+
if (address !== config.domain) return false;
|
|
232
|
+
return port === config.port || extraListenPorts.has(port);
|
|
233
|
+
}
|
|
234
|
+
|
|
227
235
|
function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: string): number[] {
|
|
228
|
-
let { domain, port } = getStorageServerConfig();
|
|
229
236
|
let indexes: number[] = [];
|
|
230
237
|
for (let i = 0; i < routing.sources.length; i++) {
|
|
231
238
|
let source = routing.sources[i];
|
|
232
239
|
if (typeof source === "string" || source.type !== "remote") continue;
|
|
233
240
|
let parsed = parseHostedUrl(source.url);
|
|
234
|
-
if (parsed.
|
|
235
|
-
if (parsed.
|
|
241
|
+
if (parsed.account !== account || parsed.bucketName !== bucketName) continue;
|
|
242
|
+
if (isOwnAddress(parsed.address, parsed.port)) {
|
|
236
243
|
indexes.push(i);
|
|
237
244
|
}
|
|
238
245
|
}
|
|
@@ -737,7 +744,7 @@ async function writeRoutingConfig(account: string, bucketName: string, data: Buf
|
|
|
737
744
|
await scheduleRoutingReload(account, bucketName);
|
|
738
745
|
}
|
|
739
746
|
|
|
740
|
-
export async function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: { lastModified?: number; forceSetImmutable?: boolean }): Promise<void> {
|
|
747
|
+
export async function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: { lastModified?: number; forceSetImmutable?: boolean; internal?: boolean }): Promise<void> {
|
|
741
748
|
assertWritesAllowed();
|
|
742
749
|
if (filePath === ROUTING_FILE) {
|
|
743
750
|
let key = `${account}/${bucketName}`;
|
|
@@ -772,6 +779,23 @@ export async function writeBucketFile(account: string, bucketName: string, fileP
|
|
|
772
779
|
} else {
|
|
773
780
|
await assertMutable(loaded, filePath, writeTime);
|
|
774
781
|
}
|
|
782
|
+
if (config?.internal) {
|
|
783
|
+
if (!config.lastModified) {
|
|
784
|
+
throw new Error(`Internal writes must carry lastModified (they are synchronization pushes, ordered by their write time), writing ${JSON.stringify(filePath)} to bucket ${account}/${bucketName}`);
|
|
785
|
+
}
|
|
786
|
+
// See SetConfig.internal: the stamp must land inside SOME window+route this server is configured for (any window, including past ones - synchronization moves old data), so a confused peer can't stuff data onto a node that was never meant to hold it
|
|
787
|
+
let covered = loaded.selfEntries.some(x => writeTime >= x.validWindow[0] && writeTime < x.validWindow[1] && routeContains(x.route, route));
|
|
788
|
+
if (!covered) {
|
|
789
|
+
throw new Error(`Internal write of ${JSON.stringify(filePath)} to bucket ${account}/${bucketName} rejected: writeTime ${writeTime} (${new Date(writeTime).toISOString()}) at route ${route} is outside every window/route this server is configured for: ${JSON.stringify(loaded.selfEntries.map(x => ({ validWindow: x.validWindow, route: x.route || FULL_ROUTE })))}`);
|
|
790
|
+
}
|
|
791
|
+
if (loaded.store.setInternal) {
|
|
792
|
+
await loaded.store.setInternal(filePath, data, { lastModified: writeTime });
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
// rawDisk stores: the normal set already IS the disk
|
|
796
|
+
await loaded.store.set(filePath, data, { lastModified: writeTime });
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
775
799
|
await loaded.store.set(filePath, data, { ...getWriteConfig(loaded, writeTime, route), lastModified: writeTime });
|
|
776
800
|
}
|
|
777
801
|
|
|
@@ -1112,6 +1136,9 @@ class ArchivesLocalBucket implements IArchives {
|
|
|
1112
1136
|
public async get2(fileName: string, config?: GetConfig): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
|
|
1113
1137
|
let bucket = await this.getBucket();
|
|
1114
1138
|
if (!bucket) return undefined;
|
|
1139
|
+
if (config?.internal && bucket.store.getInternal2) {
|
|
1140
|
+
return await bucket.store.getInternal2(fileName, config);
|
|
1141
|
+
}
|
|
1115
1142
|
return await bucket.store.get2(fileName, config);
|
|
1116
1143
|
}
|
|
1117
1144
|
public async set(fileName: string, data: Buffer, config?: SetConfig): Promise<string> {
|
|
@@ -1143,10 +1170,10 @@ class ArchivesLocalBucket implements IArchives {
|
|
|
1143
1170
|
throw e;
|
|
1144
1171
|
}
|
|
1145
1172
|
}
|
|
1146
|
-
public async getInfo(fileName: string): Promise<{ writeTime: number; size: number } | undefined> {
|
|
1173
|
+
public async getInfo(fileName: string, config?: GetInfoConfig): Promise<{ writeTime: number; size: number } | undefined> {
|
|
1147
1174
|
let bucket = await this.getBucket();
|
|
1148
1175
|
if (!bucket) return undefined;
|
|
1149
|
-
return await bucket.store.getInfo(fileName);
|
|
1176
|
+
return await bucket.store.getInfo(fileName, config);
|
|
1150
1177
|
}
|
|
1151
1178
|
public async findInfo(prefix: string, config?: { shallow?: boolean; type: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
|
|
1152
1179
|
let bucket = await this.getBucket();
|