sliftutils 1.7.33 → 1.7.34
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 +29 -2
- package/package.json +1 -1
- package/storage/remoteStorage/createArchives.d.ts +13 -1
- package/storage/remoteStorage/createArchives.ts +11 -1
- package/storage/remoteStorage/dist/createArchives.ts.cache +13 -3
- package/storage/remoteStorage/dist/storageController.ts.cache +28 -3
- package/storage/remoteStorage/dist/storageServerState.ts.cache +107 -37
- package/storage/remoteStorage/storageController.d.ts +3 -1
- package/storage/remoteStorage/storageController.ts +26 -1
- package/storage/remoteStorage/storageServerState.d.ts +13 -0
- package/storage/remoteStorage/storageServerState.ts +106 -31
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
|
|
4
|
-
import { ServerBucketInfo } from "./storageServerState";
|
|
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";
|
|
7
7
|
export declare const STORAGE_NOT_AUTHENTICATED = "REMOTE_STORAGE_NOT_AUTHENTICATED_cf2f7b1e";
|
|
@@ -79,6 +79,8 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
|
|
|
79
79
|
getChangesAfter: (account: string, bucketName: string, time: number) => Promise<ArchiveFileInfo[]>;
|
|
80
80
|
getArchivesConfig: (account: string, bucketName: string) => Promise<ArchivesConfig>;
|
|
81
81
|
listBuckets: (account: string) => Promise<ServerBucketInfo[]>;
|
|
82
|
+
getActiveBucket: (account: string, bucketName: string) => Promise<ActiveBucketInfo | string>;
|
|
83
|
+
activateBucket: (account: string, bucketName: string) => Promise<ActiveBucketInfo | string>;
|
|
82
84
|
clearWriteStats: (account: string) => Promise<{
|
|
83
85
|
clearedBuckets: number;
|
|
84
86
|
}>;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getStorageServerConfig, getTrust, getRequests, getLoadedBucket, writeBucketFile,
|
|
15
15
|
deleteBucketFile, assertWritesAllowed, assertMutable, LoadedBucket,
|
|
16
16
|
getBucketConfig, listAccountBuckets, ServerBucketInfo, clearAccountWriteStats,
|
|
17
|
+
getActiveBucket, activateBucket, ActiveBucketInfo,
|
|
17
18
|
} from "./storageServerState";
|
|
18
19
|
import { StorageClientController } from "./storageClientController";
|
|
19
20
|
|
|
@@ -23,6 +24,7 @@ export const STORAGE_NOT_AUTHENTICATED = "REMOTE_STORAGE_NOT_AUTHENTICATED_cf2f7
|
|
|
23
24
|
export const STORAGE_ACCESS_DENIED = "REMOTE_STORAGE_ACCESS_DENIED_9d81a4c0";
|
|
24
25
|
|
|
25
26
|
const AUTH_TIME_WINDOW = timeInMinute * 10;
|
|
27
|
+
const ACCESS_CHECK_SLOW_TIME = 50;
|
|
26
28
|
const MAX_SESSIONS = 100 * 1000;
|
|
27
29
|
const MAX_REQUESTS_PER_IP = 50;
|
|
28
30
|
|
|
@@ -325,7 +327,23 @@ class RemoteStorageControllerBase {
|
|
|
325
327
|
return getBucketConfig(bucket);
|
|
326
328
|
}
|
|
327
329
|
async listBuckets(account: string): Promise<ServerBucketInfo[]> {
|
|
328
|
-
|
|
330
|
+
let start = Date.now();
|
|
331
|
+
try {
|
|
332
|
+
return await listAccountBuckets(account);
|
|
333
|
+
} finally {
|
|
334
|
+
// The access hook (and the storage it initializes) runs before this, so a large gap between this and the caller's timing is the hook
|
|
335
|
+
console.log(`listBuckets(${account}) call body took ${Date.now() - start}ms`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** The live, in-memory state of one bucket, or a string saying why it is unavailable. Answered without touching the disk, so it is cheap - but only works while the bucket is loaded here. */
|
|
339
|
+
async getActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
340
|
+
assertValidName(bucketName, "bucket name");
|
|
341
|
+
return await getActiveBucket(account, bucketName);
|
|
342
|
+
}
|
|
343
|
+
/** Loads a bucket that exists on this server into memory (starting its synchronization) and returns its live state, or a string saying why it could not be loaded. */
|
|
344
|
+
async activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
345
|
+
assertValidName(bucketName, "bucket name");
|
|
346
|
+
return await activateBucket(account, bucketName);
|
|
329
347
|
}
|
|
330
348
|
/** Zeroes the write statistics listBuckets reports, for every bucket in the account. */
|
|
331
349
|
async clearWriteStats(account: string): Promise<{ clearedBuckets: number }> {
|
|
@@ -466,7 +484,12 @@ class RemoteStorageControllerBase {
|
|
|
466
484
|
const largeUploadInfo = new Map<string, { account: string; bucketName: string; path: string }>();
|
|
467
485
|
|
|
468
486
|
const accountAccess: SocketFunctionHook = async (context) => {
|
|
487
|
+
let start = Date.now();
|
|
469
488
|
await requireAccess(String(context.call.args[0]));
|
|
489
|
+
let duration = Date.now() - start;
|
|
490
|
+
if (duration > ACCESS_CHECK_SLOW_TIME) {
|
|
491
|
+
console.log(`Access check for ${context.call.functionName} took ${duration}ms`);
|
|
492
|
+
}
|
|
470
493
|
};
|
|
471
494
|
const uploadAccess: SocketFunctionHook = async (context) => {
|
|
472
495
|
let info = largeUploadInfo.get(String(context.call.args[0]));
|
|
@@ -499,6 +522,8 @@ export const RemoteStorageController = SocketFunction.register(
|
|
|
499
522
|
getArchivesConfig: { hooks: [accountAccess] },
|
|
500
523
|
getIndexInfo: { hooks: [accountAccess] },
|
|
501
524
|
listBuckets: { hooks: [accountAccess] },
|
|
525
|
+
getActiveBucket: { hooks: [accountAccess] },
|
|
526
|
+
activateBucket: { hooks: [accountAccess] },
|
|
502
527
|
clearWriteStats: { hooks: [accountAccess] },
|
|
503
528
|
getSyncStatus: { hooks: [accountAccess] },
|
|
504
529
|
startLargeFile: { hooks: [accountAccess] },
|
|
@@ -76,6 +76,19 @@ export type ServerBucketInfo = {
|
|
|
76
76
|
config?: ArchivesConfig;
|
|
77
77
|
error?: string;
|
|
78
78
|
};
|
|
79
|
+
export type ActiveBucketInfo = {
|
|
80
|
+
folder: string;
|
|
81
|
+
/** The routing config the bucket is RUNNING on, straight from memory - including switchover windows written since it loaded */
|
|
82
|
+
routing: RemoteConfig;
|
|
83
|
+
/** Our own entries in that config, and the one currently valid */
|
|
84
|
+
selfEntries: HostedConfig[];
|
|
85
|
+
self?: HostedConfig;
|
|
86
|
+
config: ArchivesConfig;
|
|
87
|
+
};
|
|
88
|
+
/** The live in-memory state of ONE bucket, answered without touching the disk (no routing file read, no statfs, no stored write stats). Returns an error string when the bucket is not loaded here, which is the normal state for a bucket nothing has accessed since startup. */
|
|
89
|
+
export declare function getActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
|
|
90
|
+
/** Loads a bucket that exists on this server's disk into memory, which starts its synchronization and window timers, and returns its live state. Nothing is written and no other server is contacted - unlike building an ArchivesChain for it, which would probe every source and could write the routing config. Already-loaded buckets just return their state. */
|
|
91
|
+
export declare function activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
|
|
79
92
|
export declare function listAccountBuckets(account: string): Promise<ServerBucketInfo[]>;
|
|
80
93
|
export declare function deleteBucketFile(account: string, bucketName: string, filePath: string): Promise<void>;
|
|
81
94
|
export declare function getLocalArchives(account: string, bucketName: string): IArchives;
|
|
@@ -818,48 +818,123 @@ async function getDiskInfo(folder: string): Promise<BucketDiskInfo> {
|
|
|
818
818
|
};
|
|
819
819
|
}
|
|
820
820
|
|
|
821
|
+
export type ActiveBucketInfo = {
|
|
822
|
+
folder: string;
|
|
823
|
+
/** The routing config the bucket is RUNNING on, straight from memory - including switchover windows written since it loaded */
|
|
824
|
+
routing: RemoteConfig;
|
|
825
|
+
/** Our own entries in that config, and the one currently valid */
|
|
826
|
+
selfEntries: HostedConfig[];
|
|
827
|
+
self?: HostedConfig;
|
|
828
|
+
config: ArchivesConfig;
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
/** The live in-memory state of ONE bucket, answered without touching the disk (no routing file read, no statfs, no stored write stats). Returns an error string when the bucket is not loaded here, which is the normal state for a bucket nothing has accessed since startup. */
|
|
832
|
+
export async function getActiveBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
833
|
+
let key = `${account}/${bucketName}`;
|
|
834
|
+
let loadedPromise = buckets.get(key);
|
|
835
|
+
if (!loadedPromise) {
|
|
836
|
+
return `Bucket ${key} is not loaded on this server, so it has no live state (it loads on first access)`;
|
|
837
|
+
}
|
|
838
|
+
let loaded: LoadedBucket | undefined;
|
|
839
|
+
try {
|
|
840
|
+
loaded = await loadedPromise;
|
|
841
|
+
} catch (e) {
|
|
842
|
+
return `Bucket ${key} failed to load on this server: ${String((e as Error).stack ?? e).slice(0, 500)}`;
|
|
843
|
+
}
|
|
844
|
+
if (!loaded) {
|
|
845
|
+
return `Bucket ${key} does not exist on this server`;
|
|
846
|
+
}
|
|
847
|
+
return toActiveBucketInfo(loaded);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function toActiveBucketInfo(loaded: LoadedBucket): ActiveBucketInfo {
|
|
851
|
+
return {
|
|
852
|
+
folder: getBucketFolder(loaded.account, loaded.bucketName),
|
|
853
|
+
routing: loaded.routing,
|
|
854
|
+
selfEntries: loaded.selfEntries,
|
|
855
|
+
self: loaded.self,
|
|
856
|
+
config: getBucketConfig(loaded),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** Loads a bucket that exists on this server's disk into memory, which starts its synchronization and window timers, and returns its live state. Nothing is written and no other server is contacted - unlike building an ArchivesChain for it, which would probe every source and could write the routing config. Already-loaded buckets just return their state. */
|
|
861
|
+
export async function activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
862
|
+
let key = `${account}/${bucketName}`;
|
|
863
|
+
let wasLoaded = buckets.has(key);
|
|
864
|
+
let loaded: LoadedBucket | undefined;
|
|
865
|
+
try {
|
|
866
|
+
loaded = await getLoadedBucket(account, bucketName);
|
|
867
|
+
} catch (e) {
|
|
868
|
+
return `Bucket ${key} failed to load on this server: ${String((e as Error).stack ?? e).slice(0, 500)}`;
|
|
869
|
+
}
|
|
870
|
+
if (!loaded) {
|
|
871
|
+
return `Bucket ${key} does not exist on this server (no routing file in ${getBucketFolder(account, bucketName)})`;
|
|
872
|
+
}
|
|
873
|
+
if (!wasLoaded) {
|
|
874
|
+
console.log(`Activated bucket ${key} on request: it is now loaded and synchronizing`);
|
|
875
|
+
}
|
|
876
|
+
return toActiveBucketInfo(loaded);
|
|
877
|
+
}
|
|
878
|
+
|
|
821
879
|
export async function listAccountBuckets(account: string): Promise<ServerBucketInfo[]> {
|
|
880
|
+
let start = Date.now();
|
|
881
|
+
let timings = new Map<string, number>();
|
|
882
|
+
async function timed<T>(name: string, run: () => Promise<T>): Promise<T> {
|
|
883
|
+
let began = Date.now();
|
|
884
|
+
try {
|
|
885
|
+
return await run();
|
|
886
|
+
} finally {
|
|
887
|
+
timings.set(name, (timings.get(name) || 0) + (Date.now() - began));
|
|
888
|
+
}
|
|
889
|
+
}
|
|
822
890
|
let accountFolder = path.join(getStorageServerConfig().folder, "buckets2", account);
|
|
823
891
|
let names: string[];
|
|
824
892
|
try {
|
|
825
|
-
names = await fs.promises.readdir(accountFolder);
|
|
893
|
+
names = await timed("readdir", () => fs.promises.readdir(accountFolder));
|
|
826
894
|
} catch {
|
|
827
895
|
return [];
|
|
828
896
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
897
|
+
try {
|
|
898
|
+
return await Promise.all(names.map(async bucketName => {
|
|
899
|
+
let key = `${account}/${bucketName}`;
|
|
900
|
+
let folder = getBucketFolder(account, bucketName);
|
|
901
|
+
let base: ServerBucketInfo = { bucketName, active: false, folder };
|
|
902
|
+
let [disk, writeStats] = await Promise.all([
|
|
903
|
+
timed("statfs", () => getDiskInfo(folder)).catch((e: Error) => {
|
|
904
|
+
base.diskError = String(e.stack ?? e).slice(0, 500);
|
|
905
|
+
return undefined;
|
|
906
|
+
}),
|
|
907
|
+
timed("writeStats", () => getBucketWriteStats(key)),
|
|
908
|
+
]);
|
|
909
|
+
base.disk = disk;
|
|
910
|
+
base.writeStats = writeStats;
|
|
911
|
+
const loadedPromise = buckets.get(key);
|
|
912
|
+
if (loadedPromise) {
|
|
913
|
+
try {
|
|
914
|
+
let loaded = await timed("awaitLoaded", () => loadedPromise);
|
|
915
|
+
if (loaded) {
|
|
916
|
+
return { ...base, active: true, config: getBucketConfig(loaded) };
|
|
917
|
+
}
|
|
918
|
+
} catch (e) {
|
|
919
|
+
return { ...base, active: true, error: String((e as Error).stack ?? e).slice(0, 500) };
|
|
920
|
+
}
|
|
921
|
+
}
|
|
844
922
|
try {
|
|
845
|
-
|
|
846
|
-
if (
|
|
847
|
-
return { ...base,
|
|
923
|
+
const data = await timed("readRoutingFile", () => readRoutingFile(folder));
|
|
924
|
+
if (!data) {
|
|
925
|
+
return { ...base, error: `No routing file (${ROUTING_FILE})` };
|
|
848
926
|
}
|
|
927
|
+
let parsed = await timed("parseRouting", async () => parseRoutingData(data));
|
|
928
|
+
return { ...base, config: { remoteConfig: parsed } };
|
|
849
929
|
} catch (e) {
|
|
850
|
-
return { ...base,
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
try {
|
|
854
|
-
let data = await readRoutingFile(folder);
|
|
855
|
-
if (!data) {
|
|
856
|
-
return { ...base, error: `No routing file (${ROUTING_FILE})` };
|
|
930
|
+
return { ...base, error: String((e as Error).stack ?? e).slice(0, 500) };
|
|
857
931
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
862
|
-
|
|
932
|
+
}));
|
|
933
|
+
} finally {
|
|
934
|
+
// The parts run concurrently, so these are summed wall times per part, not a breakdown of the total
|
|
935
|
+
let parts = [...timings].map(([name, ms]) => `${name} ${ms}ms`).join(", ");
|
|
936
|
+
console.log(`listAccountBuckets(${account}) took ${Date.now() - start}ms for ${names.length} buckets: ${parts}`);
|
|
937
|
+
}
|
|
863
938
|
}
|
|
864
939
|
|
|
865
940
|
export async function deleteBucketFile(account: string, bucketName: string, filePath: string): Promise<void> {
|