sliftutils 1.7.48 → 1.7.49
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/index.d.ts +76 -33
- package/package.json +1 -1
- package/storage/ArchivesDisk.d.ts +3 -4
- package/storage/ArchivesDisk.ts +9 -2
- package/storage/IArchives.d.ts +21 -6
- package/storage/IArchives.ts +25 -6
- package/storage/backblaze.d.ts +3 -4
- package/storage/backblaze.ts +87 -35
- package/storage/dist/ArchivesDisk.ts.cache +3 -2
- package/storage/dist/IArchives.ts.cache +2 -2
- package/storage/dist/backblaze.ts.cache +8 -2
- package/storage/remoteStorage/ArchivesRemote.d.ts +3 -5
- package/storage/remoteStorage/ArchivesRemote.ts +5 -5
- package/storage/remoteStorage/ArchivesUrl.d.ts +2 -1
- package/storage/remoteStorage/ArchivesUrl.ts +13 -3
- package/storage/remoteStorage/blobStore.d.ts +13 -4
- package/storage/remoteStorage/blobStore.ts +191 -105
- package/storage/remoteStorage/createArchives.d.ts +3 -5
- package/storage/remoteStorage/createArchives.ts +5 -10
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +3 -3
- package/storage/remoteStorage/dist/blobStore.ts.cache +178 -91
- package/storage/remoteStorage/dist/createArchives.ts.cache +2 -2
- package/storage/remoteStorage/dist/storageController.ts.cache +4 -4
- package/storage/remoteStorage/dist/storageServerState.ts.cache +46 -12
- package/storage/remoteStorage/remoteConfig.d.ts +3 -1
- package/storage/remoteStorage/remoteConfig.ts +11 -1
- package/storage/remoteStorage/sourcesList.d.ts +15 -0
- package/storage/remoteStorage/sourcesList.ts +69 -0
- package/storage/remoteStorage/storageController.d.ts +3 -3
- package/storage/remoteStorage/storageController.ts +6 -9
- package/storage/remoteStorage/storageServerState.d.ts +3 -0
- package/storage/remoteStorage/storageServerState.ts +37 -12
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
-
import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig } from "../IArchives";
|
|
3
|
+
import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig, ArchiveFileInfo, ChangesAfterConfig } from "../IArchives";
|
|
4
4
|
export declare const ROUTING_FILE = "storage/storagerouting.json";
|
|
5
5
|
/** The variable-shard route override embedded in the key ("<sentinel>_<value>", see VARIABLE_SHARD), or undefined when the key has no sentinel or the sentinel has no value yet. */
|
|
6
6
|
export declare function parseVariableRoute(key: string): number | undefined;
|
|
7
7
|
/** Where a key routes in [0, 1). A materialized variable-shard suffix completely overrides the hash. */
|
|
8
8
|
export declare function getRoute(key: string): number;
|
|
9
|
+
/** The in-memory getChangesAfter2 emulation, for backends without a native change feed: a full listing filtered down to files written after config.time whose keys route into config.routes. */
|
|
10
|
+
export declare function filterChanges(files: ArchiveFileInfo[], config: ChangesAfterConfig): ArchiveFileInfo[];
|
|
9
11
|
export declare function routeContains(route: [number, number] | undefined, value: number): boolean;
|
|
10
12
|
export declare function routesOverlap(a: [number, number] | undefined, b: [number, number] | undefined): boolean;
|
|
11
13
|
/** The overlap of two route ranges, or undefined when they don't overlap. */
|
|
@@ -2,7 +2,7 @@ import { sort } from "socket-function/src/misc";
|
|
|
2
2
|
import { getBufferInt } from "socket-function/src/bits";
|
|
3
3
|
import { setFlag } from "socket-function/require/compileFlags";
|
|
4
4
|
import jsSha256 from "js-sha256";
|
|
5
|
-
import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig, FULL_VALID_WINDOW, FULL_ROUTE, VARIABLE_SHARD } from "../IArchives";
|
|
5
|
+
import { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig, ArchiveFileInfo, ChangesAfterConfig, FULL_VALID_WINDOW, FULL_ROUTE, VARIABLE_SHARD } from "../IArchives";
|
|
6
6
|
|
|
7
7
|
setFlag(require, "js-sha256", "allowclient", true);
|
|
8
8
|
|
|
@@ -31,6 +31,16 @@ export function getRoute(key: string): number {
|
|
|
31
31
|
return hash % ROUTE_PRECISION / ROUTE_PRECISION;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** The in-memory getChangesAfter2 emulation, for backends without a native change feed: a full listing filtered down to files written after config.time whose keys route into config.routes. */
|
|
35
|
+
export function filterChanges(files: ArchiveFileInfo[], config: ChangesAfterConfig): ArchiveFileInfo[] {
|
|
36
|
+
return files.filter(file => {
|
|
37
|
+
if (file.createTime <= config.time) return false;
|
|
38
|
+
let routes = config.routes;
|
|
39
|
+
if (routes && !routes.some(route => routeContains(route, getRoute(file.path)))) return false;
|
|
40
|
+
return true;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
// Route ranges are [start, end) - inclusive start, exclusive end
|
|
35
45
|
export function routeContains(route: [number, number] | undefined, value: number): boolean {
|
|
36
46
|
if (!route) return true;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class SourcesList {
|
|
2
|
+
private filePath;
|
|
3
|
+
constructor(filePath: string);
|
|
4
|
+
private urls;
|
|
5
|
+
private indexes;
|
|
6
|
+
private endsClean;
|
|
7
|
+
private lastReload;
|
|
8
|
+
private appendQueue;
|
|
9
|
+
private load;
|
|
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
|
+
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
|
+
ensure(url: string): Promise<number>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
|
|
3
|
+
const RELOAD_THROTTLE = 5 * 1000;
|
|
4
|
+
|
|
5
|
+
// 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.
|
|
6
|
+
export class SourcesList {
|
|
7
|
+
constructor(private filePath: string) { }
|
|
8
|
+
private urls: string[] = [];
|
|
9
|
+
private indexes = new Map<string, number>();
|
|
10
|
+
private endsClean = true;
|
|
11
|
+
private lastReload = 0;
|
|
12
|
+
private appendQueue = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
private async load(): Promise<void> {
|
|
15
|
+
let content = "";
|
|
16
|
+
try {
|
|
17
|
+
content = await fs.promises.readFile(this.filePath, "utf8");
|
|
18
|
+
} catch (e) {
|
|
19
|
+
if ((e as { code?: string }).code !== "ENOENT") throw e;
|
|
20
|
+
}
|
|
21
|
+
this.endsClean = !content || content.endsWith("\n");
|
|
22
|
+
let lines = content.split("\n");
|
|
23
|
+
if (lines[lines.length - 1] === "") {
|
|
24
|
+
lines.pop();
|
|
25
|
+
}
|
|
26
|
+
this.urls = lines;
|
|
27
|
+
this.indexes.clear();
|
|
28
|
+
for (let i = 0; i < lines.length; i++) {
|
|
29
|
+
if (!this.indexes.has(lines[i])) {
|
|
30
|
+
this.indexes.set(lines[i], i);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public getUrl(sourcesListIndex: number): string | undefined {
|
|
36
|
+
return this.urls[sourcesListIndex];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 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. */
|
|
40
|
+
public async getUrlReloading(sourcesListIndex: number): Promise<string | undefined> {
|
|
41
|
+
if (sourcesListIndex < this.urls.length) return this.urls[sourcesListIndex];
|
|
42
|
+
if (Date.now() - this.lastReload < RELOAD_THROTTLE) return undefined;
|
|
43
|
+
this.lastReload = Date.now();
|
|
44
|
+
await this.load();
|
|
45
|
+
return this.urls[sourcesListIndex];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 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. */
|
|
49
|
+
public ensure(url: string): Promise<number> {
|
|
50
|
+
if (url.includes("\n")) {
|
|
51
|
+
throw new Error(`Source URLs cannot contain newlines (they are stored one per line): ${JSON.stringify(url)}`);
|
|
52
|
+
}
|
|
53
|
+
let result = this.appendQueue.then(async () => {
|
|
54
|
+
await this.load();
|
|
55
|
+
let existing = this.indexes.get(url);
|
|
56
|
+
if (existing !== undefined) return existing;
|
|
57
|
+
let prefix = this.endsClean && "" || "\n";
|
|
58
|
+
await fs.promises.appendFile(this.filePath, prefix + url + "\n");
|
|
59
|
+
this.endsClean = true;
|
|
60
|
+
let index = this.urls.length;
|
|
61
|
+
this.urls.push(url);
|
|
62
|
+
this.indexes.set(url, index);
|
|
63
|
+
console.log(`Registered new source ${JSON.stringify(url)} as sourcesListIndex ${index} in ${this.filePath}`);
|
|
64
|
+
return index;
|
|
65
|
+
});
|
|
66
|
+
this.appendQueue = result.then(() => { }, () => { });
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
-
import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
|
|
3
|
+
import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig } from "../IArchives";
|
|
4
4
|
import { ServerBucketInfo, ActiveBucketInfo } from "./storageServerState";
|
|
5
5
|
export declare const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
|
|
6
6
|
export declare const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
|
|
@@ -70,7 +70,7 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
|
|
|
70
70
|
writeTime: number;
|
|
71
71
|
size: number;
|
|
72
72
|
} | undefined>;
|
|
73
|
-
set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number) => Promise<void>;
|
|
73
|
+
set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean) => Promise<void>;
|
|
74
74
|
del: (account: string, bucketName: string, path: string) => Promise<void>;
|
|
75
75
|
getInfo: (account: string, bucketName: string, path: string) => Promise<{
|
|
76
76
|
writeTime: number;
|
|
@@ -80,7 +80,7 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
|
|
|
80
80
|
shallow?: boolean;
|
|
81
81
|
type?: "files" | "folders";
|
|
82
82
|
}) => Promise<ArchiveFileInfo[]>;
|
|
83
|
-
|
|
83
|
+
getChangesAfter2: (account: string, bucketName: string, config: ChangesAfterConfig) => Promise<ArchiveFileInfo[]>;
|
|
84
84
|
getArchivesConfig: (account: string, bucketName: string) => Promise<ArchivesConfig>;
|
|
85
85
|
listBuckets: (account: string) => Promise<ServerBucketInfo[]>;
|
|
86
86
|
getActiveBucket: (account: string, bucketName: string) => Promise<ActiveBucketInfo | string>;
|
|
@@ -6,7 +6,7 @@ import { performLocalCall } from "socket-function/src/callManager";
|
|
|
6
6
|
import { RequireController } from "socket-function/require/RequireController";
|
|
7
7
|
import { timeInMinute } from "socket-function/src/misc";
|
|
8
8
|
import { getCommonName, getPublicIdentifier, getOwnMachineId, verify, verifyMachineIdForPublicKey } from "../../misc/https/certs";
|
|
9
|
-
import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, IMMUTABLE_CACHE_TIME } from "../IArchives";
|
|
9
|
+
import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, IMMUTABLE_CACHE_TIME } from "../IArchives";
|
|
10
10
|
import { ROUTING_FILE } from "./remoteConfig";
|
|
11
11
|
import {
|
|
12
12
|
getStorageServerConfig, getTrust, getRequests, getLoadedBucket, writeBucketFile,
|
|
@@ -294,10 +294,10 @@ class RemoteStorageControllerBase {
|
|
|
294
294
|
if (!bucket) return undefined;
|
|
295
295
|
return await bucket.store.get2(path, { range });
|
|
296
296
|
}
|
|
297
|
-
async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number): Promise<void> {
|
|
297
|
+
async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean): Promise<void> {
|
|
298
298
|
assertValidName(bucketName, "bucket name");
|
|
299
299
|
assertValidPath(path);
|
|
300
|
-
await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified });
|
|
300
|
+
await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified, forceSetImmutable });
|
|
301
301
|
}
|
|
302
302
|
async del(account: string, bucketName: string, path: string): Promise<void> {
|
|
303
303
|
assertValidName(bucketName, "bucket name");
|
|
@@ -315,13 +315,10 @@ class RemoteStorageControllerBase {
|
|
|
315
315
|
if (!bucket) return [];
|
|
316
316
|
return await bucket.store.findInfo(prefix, config);
|
|
317
317
|
}
|
|
318
|
-
async
|
|
318
|
+
async getChangesAfter2(account: string, bucketName: string, config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
|
|
319
319
|
let bucket = await getBucket(account, bucketName);
|
|
320
320
|
if (!bucket) return [];
|
|
321
|
-
|
|
322
|
-
throw new Error(`Bucket ${account}/${bucketName} does not support getChangesAfter (rawDisk buckets have no index)`);
|
|
323
|
-
}
|
|
324
|
-
return await bucket.store.getChangesAfter(time);
|
|
321
|
+
return await bucket.store.getChangesAfter2(config);
|
|
325
322
|
}
|
|
326
323
|
async getArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
|
|
327
324
|
let bucket = await getBucket(account, bucketName);
|
|
@@ -523,7 +520,7 @@ export const RemoteStorageController = SocketFunction.register(
|
|
|
523
520
|
del: { hooks: [accountAccess] },
|
|
524
521
|
getInfo: { hooks: [accountAccess] },
|
|
525
522
|
findInfo: { hooks: [accountAccess] },
|
|
526
|
-
|
|
523
|
+
getChangesAfter2: { hooks: [accountAccess] },
|
|
527
524
|
getArchivesConfig: { hooks: [accountAccess] },
|
|
528
525
|
getIndexInfo: { hooks: [accountAccess] },
|
|
529
526
|
listBuckets: { hooks: [accountAccess] },
|
|
@@ -46,10 +46,13 @@ export type LoadedBucket = {
|
|
|
46
46
|
};
|
|
47
47
|
export declare function addExtraListenPort(port: number): void;
|
|
48
48
|
export declare function removeExtraListenPort(port: number): void;
|
|
49
|
+
/** 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
|
+
export declare function resolveSourceArchives(url: string): IArchives;
|
|
49
51
|
export declare function getLoadedBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined>;
|
|
50
52
|
export declare function assertMutable(bucket: LoadedBucket, filePath: string, writeTime: number): Promise<void>;
|
|
51
53
|
export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
|
|
52
54
|
lastModified?: number;
|
|
55
|
+
forceSetImmutable?: boolean;
|
|
53
56
|
}): Promise<void>;
|
|
54
57
|
export declare function getBucketConfig(bucket: LoadedBucket): ArchivesConfig;
|
|
55
58
|
/** 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. */
|
|
@@ -9,10 +9,10 @@ import { ArchivesDisk } from "../ArchivesDisk";
|
|
|
9
9
|
import { BlobStore, IBucketStore, DEFAULT_FAST_WRITE_DELAY, WINDOW_END_FLUSH_MARGIN } from "./blobStore";
|
|
10
10
|
import {
|
|
11
11
|
RemoteConfig, HostedConfig, BackblazeConfig, IArchives, ArchivesSource, ArchiveFileInfo, ArchivesConfig,
|
|
12
|
-
ArchivesSyncStatus,
|
|
12
|
+
ArchivesSyncStatus, ChangesAfterConfig, SetConfig,
|
|
13
13
|
STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, FULL_ROUTE,
|
|
14
14
|
} from "../IArchives";
|
|
15
|
-
import { ROUTING_FILE, parseRoutingData, serializeRemoteConfig, parseHostedUrl, replaceHostedUrlPort, buildFileUrl, getConfigVersion, getRoute, routeContains, routeIntersection } from "./remoteConfig";
|
|
15
|
+
import { ROUTING_FILE, parseRoutingData, serializeRemoteConfig, parseHostedUrl, replaceHostedUrlPort, buildFileUrl, getConfigVersion, getRoute, routeContains, routeIntersection, normalizeSource } from "./remoteConfig";
|
|
16
16
|
import { injectIntermediateSource, expireIntermediateSources, getIntermediateSources, findSplitUrl, nextIntermediateVersion, INTERMEDIATE_EXPIRE_GRACE } from "./intermediateSources";
|
|
17
17
|
import { getTakeoverIntermediate } from "./deployTakeover";
|
|
18
18
|
import { createApiArchives, listServerActiveBucketKeys } from "./createArchives";
|
|
@@ -424,6 +424,21 @@ type StorePlan = {
|
|
|
424
424
|
structureKey: string;
|
|
425
425
|
};
|
|
426
426
|
|
|
427
|
+
const resolvedSourceArchives = new Map<string, IArchives>();
|
|
428
|
+
/** 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. */
|
|
429
|
+
export function resolveSourceArchives(url: string): IArchives {
|
|
430
|
+
let existing = resolvedSourceArchives.get(url);
|
|
431
|
+
if (existing) return existing;
|
|
432
|
+
let archives: IArchives;
|
|
433
|
+
if (url.startsWith("https://")) {
|
|
434
|
+
archives = createApiArchives(normalizeSource(url));
|
|
435
|
+
} else {
|
|
436
|
+
archives = new ArchivesDisk(url);
|
|
437
|
+
}
|
|
438
|
+
resolvedSourceArchives.set(url, archives);
|
|
439
|
+
return archives;
|
|
440
|
+
}
|
|
441
|
+
|
|
427
442
|
function sourceIdentity(sourceConfig: HostedConfig | BackblazeConfig | undefined): string {
|
|
428
443
|
if (!sourceConfig) return "disk";
|
|
429
444
|
return JSON.stringify({ ...sourceConfig, validWindow: undefined, route: undefined });
|
|
@@ -491,6 +506,7 @@ function buildBucket(account: string, bucketName: string, routing: RemoteConfig,
|
|
|
491
506
|
}
|
|
492
507
|
let sources: ArchivesSource[] = plan.sourceSpecs.map(spec => ({
|
|
493
508
|
source: spec.sourceConfig && createApiArchives(spec.sourceConfig) || new ArchivesDisk(folder),
|
|
509
|
+
url: spec.sourceConfig?.url || folder,
|
|
494
510
|
validWindow: spec.validWindow,
|
|
495
511
|
route: spec.route,
|
|
496
512
|
noFullSync: spec.noFullSync,
|
|
@@ -503,6 +519,7 @@ function buildBucket(account: string, bucketName: string, routing: RemoteConfig,
|
|
|
503
519
|
},
|
|
504
520
|
readerDiskLimit: plan.readerDiskLimit,
|
|
505
521
|
onWriteCounted: (kind, bytes) => countBucketWrite(`${account}/${bucketName}`, kind, bytes),
|
|
522
|
+
resolveSourceUrl: resolveSourceArchives,
|
|
506
523
|
});
|
|
507
524
|
}
|
|
508
525
|
let loaded: LoadedBucket = { account, bucketName, routing, routingJSON: JSON.stringify(routing), selfEntries, self, store, structureKey: plan.structureKey };
|
|
@@ -584,10 +601,11 @@ async function checkRoutingChanged(account: string, bucketName: string, config?:
|
|
|
584
601
|
loaded.store.updateSources(plan.sourceSpecs.map(spec => {
|
|
585
602
|
const sourceConfig = spec.sourceConfig;
|
|
586
603
|
if (!sourceConfig) {
|
|
587
|
-
return { identity: sourceIdentity(undefined), validWindow: spec.validWindow, create: (): IArchives => new ArchivesDisk(getBucketFolder(account, bucketName)) };
|
|
604
|
+
return { identity: sourceIdentity(undefined), url: getBucketFolder(account, bucketName), validWindow: spec.validWindow, create: (): IArchives => new ArchivesDisk(getBucketFolder(account, bucketName)) };
|
|
588
605
|
}
|
|
589
606
|
return {
|
|
590
607
|
identity: sourceIdentity(sourceConfig),
|
|
608
|
+
url: sourceConfig.url,
|
|
591
609
|
validWindow: spec.validWindow,
|
|
592
610
|
route: spec.route,
|
|
593
611
|
noFullSync: spec.noFullSync,
|
|
@@ -666,7 +684,7 @@ async function writeRoutingConfig(account: string, bucketName: string, data: Buf
|
|
|
666
684
|
await scheduleRoutingReload(account, bucketName);
|
|
667
685
|
}
|
|
668
686
|
|
|
669
|
-
export async function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: { lastModified?: number }): Promise<void> {
|
|
687
|
+
export async function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: { lastModified?: number; forceSetImmutable?: boolean }): Promise<void> {
|
|
670
688
|
assertWritesAllowed();
|
|
671
689
|
if (filePath === ROUTING_FILE) {
|
|
672
690
|
let key = `${account}/${bucketName}`;
|
|
@@ -691,14 +709,24 @@ export async function writeBucketFile(account: string, bucketName: string, fileP
|
|
|
691
709
|
throw new Error(`${STORAGE_WRONG_ROUTE} This server does not handle route ${route} (key ${JSON.stringify(filePath)}) for bucket ${account}/${bucketName} (our routes at this time: ${JSON.stringify(timeValid.map(x => x.route || FULL_ROUTE))}). Re-resolve the source for this key and retry.`);
|
|
692
710
|
}
|
|
693
711
|
}
|
|
694
|
-
|
|
712
|
+
if (config?.forceSetImmutable) {
|
|
713
|
+
if (!config.lastModified) {
|
|
714
|
+
throw new Error(`forceSetImmutable requires lastModified (synchronization writes are ordered by their write time), writing ${JSON.stringify(filePath)} to bucket ${account}/${bucketName}`);
|
|
715
|
+
}
|
|
716
|
+
// Immutability wins: an existing path is kept instead of the push throwing (see SetConfig.forceSetImmutable)
|
|
717
|
+
let self = selectEntryAt(loaded.selfEntries, writeTime, route);
|
|
718
|
+
if (self?.immutable && await loaded.store.getInfo(filePath)) return;
|
|
719
|
+
} else {
|
|
720
|
+
await assertMutable(loaded, filePath, writeTime);
|
|
721
|
+
}
|
|
695
722
|
await loaded.store.set(filePath, data, { ...getWriteConfig(loaded, writeTime, route), lastModified: writeTime });
|
|
696
723
|
}
|
|
697
724
|
|
|
698
725
|
export function getBucketConfig(bucket: LoadedBucket): ArchivesConfig {
|
|
699
726
|
let progress = bucket.store.getSyncProgress?.();
|
|
700
727
|
return {
|
|
701
|
-
|
|
728
|
+
// Native change-feed support = the store keeps an index (rawDisk buckets emulate getChangesAfter2 with a full listing) - clients use this to pick their scan cadence
|
|
729
|
+
supportsChangesAfter: bucket.store instanceof BlobStore,
|
|
702
730
|
remoteConfig: bucket.routing,
|
|
703
731
|
index: progress?.index,
|
|
704
732
|
indexSources: progress?.sources,
|
|
@@ -1033,7 +1061,7 @@ class ArchivesLocalBucket implements IArchives {
|
|
|
1033
1061
|
if (!bucket) return undefined;
|
|
1034
1062
|
return await bucket.store.get2(fileName, config);
|
|
1035
1063
|
}
|
|
1036
|
-
public async set(fileName: string, data: Buffer, config?:
|
|
1064
|
+
public async set(fileName: string, data: Buffer, config?: SetConfig): Promise<string> {
|
|
1037
1065
|
await writeBucketFile(this.account, this.bucketName, fileName, data, config);
|
|
1038
1066
|
return fileName;
|
|
1039
1067
|
}
|
|
@@ -1087,13 +1115,10 @@ class ArchivesLocalBucket implements IArchives {
|
|
|
1087
1115
|
public async hasWriteAccess(): Promise<boolean> {
|
|
1088
1116
|
return true;
|
|
1089
1117
|
}
|
|
1090
|
-
public async
|
|
1118
|
+
public async getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
|
|
1091
1119
|
let bucket = await this.getBucket();
|
|
1092
1120
|
if (!bucket) return [];
|
|
1093
|
-
|
|
1094
|
-
throw new Error(`Bucket ${this.account}/${this.bucketName} does not support getChangesAfter (rawDisk buckets have no index)`);
|
|
1095
|
-
}
|
|
1096
|
-
return await bucket.store.getChangesAfter(time);
|
|
1121
|
+
return await bucket.store.getChangesAfter2(config);
|
|
1097
1122
|
}
|
|
1098
1123
|
public async getSyncStatus(): Promise<ArchivesSyncStatus> {
|
|
1099
1124
|
let bucket = await this.getBucket();
|