sliftutils 1.7.67 → 1.7.69
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/dist/test.ts.cache +3 -3
- package/index.d.ts +68 -14
- package/package.json +1 -1
- package/storage/ArchivesDisk.d.ts +2 -2
- package/storage/ArchivesDisk.ts +8 -4
- package/storage/IArchives.d.ts +15 -2
- package/storage/IArchives.ts +19 -3
- package/storage/backblaze.d.ts +2 -2
- package/storage/backblaze.ts +20 -3
- package/storage/dist/ArchivesDisk.ts.cache +2 -2
- package/storage/dist/IArchives.ts.cache +3 -3
- package/storage/dist/backblaze.ts.cache +2 -2
- package/storage/remoteStorage/ArchivesRemote.d.ts +2 -2
- package/storage/remoteStorage/ArchivesRemote.ts +3 -3
- package/storage/remoteStorage/accessStats.d.ts +25 -0
- package/storage/remoteStorage/accessStats.ts +86 -0
- package/storage/remoteStorage/blobStore.d.ts +1 -0
- package/storage/remoteStorage/blobStore.ts +31 -12
- package/storage/remoteStorage/createArchives.d.ts +2 -2
- package/storage/remoteStorage/createArchives.ts +6 -3
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +2 -2
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +2 -2
- package/storage/remoteStorage/dist/accessStats.ts.cache +78 -0
- package/storage/remoteStorage/dist/blobStore.ts.cache +2 -2
- package/storage/remoteStorage/dist/createArchives.ts.cache +2 -2
- package/storage/remoteStorage/dist/storageController.ts.cache +51 -14
- package/storage/remoteStorage/dist/storageServerState.ts.cache +25 -74
- package/storage/remoteStorage/storageController.d.ts +9 -1
- package/storage/remoteStorage/storageController.ts +52 -11
- package/storage/remoteStorage/storageServerState.d.ts +6 -3
- package/storage/remoteStorage/storageServerState.ts +46 -73
- package/test.ts +1 -1
- package/treeSummary.d.ts +31 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig } from "../IArchives";
|
|
4
4
|
import { ServerBucketInfo, ActiveBucketInfo } from "./storageServerState";
|
|
5
|
+
import { AccessTotals, AccessSummaryState } from "./accessStats";
|
|
6
|
+
import type { SummaryEntry } from "../../treeSummary";
|
|
5
7
|
export declare const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
|
|
6
8
|
export declare const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
|
|
7
9
|
export declare const STORAGE_NOT_AUTHENTICATED = "REMOTE_STORAGE_NOT_AUTHENTICATED_cf2f7b1e";
|
|
@@ -71,7 +73,7 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
|
|
|
71
73
|
size: number;
|
|
72
74
|
} | undefined>;
|
|
73
75
|
set: (account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean, internal?: boolean) => Promise<void>;
|
|
74
|
-
del: (account: string, bucketName: string, path: string) => Promise<void>;
|
|
76
|
+
del: (account: string, bucketName: string, path: string, lastModified?: number, internal?: boolean) => Promise<void>;
|
|
75
77
|
getInfo: (account: string, bucketName: string, path: string, includeTombstones?: boolean) => Promise<{
|
|
76
78
|
writeTime: number;
|
|
77
79
|
size: number;
|
|
@@ -88,6 +90,12 @@ export declare const RemoteStorageController: import("socket-function/SocketFunc
|
|
|
88
90
|
clearWriteStats: (account: string) => Promise<{
|
|
89
91
|
clearedBuckets: number;
|
|
90
92
|
}>;
|
|
93
|
+
getAccessStats: (account: string) => Promise<AccessTotals>;
|
|
94
|
+
getAccessSummaries: (account: string, config: {
|
|
95
|
+
operation: string;
|
|
96
|
+
maxCount: number;
|
|
97
|
+
weightBySize?: boolean;
|
|
98
|
+
}) => Promise<SummaryEntry<AccessSummaryState>[]>;
|
|
91
99
|
getIndexInfo: (account: string, bucketName: string) => Promise<{
|
|
92
100
|
fileCount: number;
|
|
93
101
|
byteCount: number;
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
getActiveBucket, activateBucket, ActiveBucketInfo, getActiveBucketKeys,
|
|
16
16
|
} from "./storageServerState";
|
|
17
17
|
import { StorageClientController } from "./storageClientController";
|
|
18
|
+
import { trackAccess, getAccessTotals, readAccessSummaries, clearAccountAccessStats, AccessTotals, AccessSummaryState } from "./accessStats";
|
|
19
|
+
import type { SummaryEntry } from "../../treeSummary";
|
|
18
20
|
|
|
19
21
|
export const REMOTE_STORAGE_CLASS_GUID = "RemoteStorageController-b7e42a91";
|
|
20
22
|
export const STORAGE_AUTH_PURPOSE = "remoteStorage-auth-1";
|
|
@@ -292,37 +294,60 @@ class RemoteStorageControllerBase {
|
|
|
292
294
|
async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }, internal?: boolean): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
|
|
293
295
|
assertValidPath(path);
|
|
294
296
|
let bucket = await getBucket(account, bucketName);
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
|
|
297
|
+
let result: { data: Buffer; writeTime: number; size: number } | undefined;
|
|
298
|
+
if (bucket) {
|
|
299
|
+
if (internal && bucket.store.getInternal2) {
|
|
300
|
+
result = await bucket.store.getInternal2(path, { range });
|
|
301
|
+
} else {
|
|
302
|
+
result = await bucket.store.get2(path, { range });
|
|
303
|
+
}
|
|
298
304
|
}
|
|
299
|
-
|
|
305
|
+
trackAccess({ account, operation: "get", path: `${bucketName}/${path}`, size: result && result.data.length || 0 });
|
|
306
|
+
return result;
|
|
300
307
|
}
|
|
301
308
|
async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number, forceSetImmutable?: boolean, internal?: boolean): Promise<void> {
|
|
302
309
|
assertValidName(bucketName, "bucket name");
|
|
303
310
|
assertValidPath(path);
|
|
311
|
+
if (!data.length) {
|
|
312
|
+
throw new Error(`set was called with an empty buffer for ${JSON.stringify(path)} in bucket ${account}/${bucketName}: an empty file IS a deletion in this system and would read back as missing - call del instead`);
|
|
313
|
+
}
|
|
314
|
+
trackAccess({ account, operation: "set", path: `${bucketName}/${path}`, size: data.length });
|
|
304
315
|
await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified, forceSetImmutable, internal });
|
|
305
316
|
}
|
|
306
|
-
async del(account: string, bucketName: string, path: string): Promise<void> {
|
|
317
|
+
async del(account: string, bucketName: string, path: string, lastModified?: number, internal?: boolean): Promise<void> {
|
|
307
318
|
assertValidName(bucketName, "bucket name");
|
|
308
319
|
assertValidPath(path);
|
|
309
|
-
|
|
320
|
+
trackAccess({ account, operation: "del", path: `${bucketName}/${path}` });
|
|
321
|
+
await deleteBucketFile(account, bucketName, path, { lastModified, internal });
|
|
310
322
|
}
|
|
311
323
|
async getInfo(account: string, bucketName: string, path: string, includeTombstones?: boolean): Promise<{ writeTime: number; size: number } | undefined> {
|
|
312
324
|
assertValidPath(path);
|
|
325
|
+
trackAccess({ account, operation: "getInfo", path: `${bucketName}/${path}` });
|
|
313
326
|
let bucket = await getBucket(account, bucketName);
|
|
314
327
|
if (!bucket) return undefined;
|
|
315
328
|
return await bucket.store.getInfo(path, { includeTombstones });
|
|
316
329
|
}
|
|
317
330
|
async findInfo(account: string, bucketName: string, prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
|
|
318
331
|
let bucket = await getBucket(account, bucketName);
|
|
319
|
-
|
|
320
|
-
|
|
332
|
+
let results = bucket && await bucket.store.findInfo(prefix, config) || [];
|
|
333
|
+
// The paths are only known once the results are in; an empty result still counts as one access at the prefix.
|
|
334
|
+
if (results.length) {
|
|
335
|
+
for (let info of results) {
|
|
336
|
+
trackAccess({ account, operation: "findInfo", path: `${bucketName}/${info.path}` });
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
trackAccess({ account, operation: "findInfo", path: `${bucketName}/${prefix}` });
|
|
340
|
+
}
|
|
341
|
+
return results;
|
|
321
342
|
}
|
|
322
343
|
async getChangesAfter2(account: string, bucketName: string, config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
|
|
323
344
|
let bucket = await getBucket(account, bucketName);
|
|
324
345
|
if (!bucket) return [];
|
|
325
|
-
|
|
346
|
+
let results = await bucket.store.getChangesAfter2(config);
|
|
347
|
+
for (let info of results) {
|
|
348
|
+
trackAccess({ account, operation: "getChangesAfter", path: `${bucketName}/${info.path}` });
|
|
349
|
+
}
|
|
350
|
+
return results;
|
|
326
351
|
}
|
|
327
352
|
async getArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
|
|
328
353
|
let bucket = await getBucket(account, bucketName);
|
|
@@ -348,10 +373,21 @@ class RemoteStorageControllerBase {
|
|
|
348
373
|
assertValidName(bucketName, "bucket name");
|
|
349
374
|
return await activateBucket(account, bucketName);
|
|
350
375
|
}
|
|
351
|
-
/** Zeroes the write statistics listBuckets reports, for every bucket in the account. */
|
|
376
|
+
/** Zeroes the write statistics listBuckets reports and the in-memory access statistics, for every bucket in the account. */
|
|
352
377
|
async clearWriteStats(account: string): Promise<{ clearedBuckets: number }> {
|
|
353
378
|
assertValidName(account, "account");
|
|
354
|
-
|
|
379
|
+
clearAccountAccessStats(account);
|
|
380
|
+
return { clearedBuckets: clearAccountWriteStats(account) };
|
|
381
|
+
}
|
|
382
|
+
/** In-memory totals per operation type since startup (or the last clearWriteStats). */
|
|
383
|
+
async getAccessStats(account: string): Promise<AccessTotals> {
|
|
384
|
+
assertValidName(account, "account");
|
|
385
|
+
return getAccessTotals(account);
|
|
386
|
+
}
|
|
387
|
+
/** A path breakdown of one operation's accesses (operation names come from getAccessStats). maxCount is passed straight to TreeSummary.getSummaries. weightBySize is ignored for count-only operations, which return their count breakdown. */
|
|
388
|
+
async getAccessSummaries(account: string, config: { operation: string; maxCount: number; weightBySize?: boolean }): Promise<SummaryEntry<AccessSummaryState>[]> {
|
|
389
|
+
assertValidName(account, "account");
|
|
390
|
+
return readAccessSummaries({ account, ...config });
|
|
355
391
|
}
|
|
356
392
|
async getIndexInfo(account: string, bucketName: string): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] } | undefined> {
|
|
357
393
|
let bucket = await getBucket(account, bucketName);
|
|
@@ -383,6 +419,7 @@ class RemoteStorageControllerBase {
|
|
|
383
419
|
assertWritesAllowed();
|
|
384
420
|
let info = largeUploadInfo.get(uploadId);
|
|
385
421
|
if (!info) throw new Error(`Unknown large upload ${uploadId}`);
|
|
422
|
+
trackAccess({ account: info.account, operation: "uploadPart", path: `${info.bucketName}/${info.path}`, size: data.length });
|
|
386
423
|
let bucket = await getBucket(info.account, info.bucketName);
|
|
387
424
|
if (!bucket) throw new Error(`Bucket ${info.account}/${info.bucketName} no longer exists`);
|
|
388
425
|
await bucket.store.appendLargeUpload(uploadId, Buffer.from(data));
|
|
@@ -437,6 +474,7 @@ class RemoteStorageControllerBase {
|
|
|
437
474
|
}
|
|
438
475
|
let info = await bucket.store.getInfo(filePath);
|
|
439
476
|
if (!info || !info.size) {
|
|
477
|
+
trackAccess({ account, operation: "httpGet", path: `${bucketName}/${filePath}`, size: 0 });
|
|
440
478
|
return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
|
|
441
479
|
}
|
|
442
480
|
let ext = filePath.split(".").pop() || "";
|
|
@@ -477,6 +515,7 @@ class RemoteStorageControllerBase {
|
|
|
477
515
|
}
|
|
478
516
|
}
|
|
479
517
|
let result = await bucket.store.get2(filePath, { range });
|
|
518
|
+
trackAccess({ account, operation: "httpGet", path: `${bucketName}/${filePath}`, size: result && result.data.length || 0 });
|
|
480
519
|
if (!result) {
|
|
481
520
|
return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
|
|
482
521
|
}
|
|
@@ -532,6 +571,8 @@ export const RemoteStorageController = SocketFunction.register(
|
|
|
532
571
|
getActiveBucket: { hooks: [accountAccess] },
|
|
533
572
|
activateBucket: { hooks: [accountAccess] },
|
|
534
573
|
clearWriteStats: { hooks: [accountAccess] },
|
|
574
|
+
getAccessStats: { hooks: [accountAccess] },
|
|
575
|
+
getAccessSummaries: { hooks: [accountAccess] },
|
|
535
576
|
getSyncStatus: { hooks: [accountAccess] },
|
|
536
577
|
startLargeFile: { hooks: [accountAccess] },
|
|
537
578
|
uploadPart: { hooks: [uploadAccess] },
|
|
@@ -28,8 +28,8 @@ export type BucketWriteStats = {
|
|
|
28
28
|
flushedWrites: number;
|
|
29
29
|
flushedBytes: number;
|
|
30
30
|
};
|
|
31
|
-
/** Zeroes the write statistics of every bucket in the account
|
|
32
|
-
export declare function clearAccountWriteStats(account: string):
|
|
31
|
+
/** Zeroes the write statistics of every bucket in the account. */
|
|
32
|
+
export declare function clearAccountWriteStats(account: string): number;
|
|
33
33
|
export declare function setTrustedMachines(config: {
|
|
34
34
|
account: string;
|
|
35
35
|
machineIds: string[];
|
|
@@ -111,5 +111,8 @@ export declare function getActiveBucket(account: string, bucketName: string): Pr
|
|
|
111
111
|
/** 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. */
|
|
112
112
|
export declare function activateBucket(account: string, bucketName: string): Promise<ActiveBucketInfo | string>;
|
|
113
113
|
export declare function listAccountBuckets(account: string): Promise<ServerBucketInfo[]>;
|
|
114
|
-
export declare function deleteBucketFile(account: string, bucketName: string, filePath: string
|
|
114
|
+
export declare function deleteBucketFile(account: string, bucketName: string, filePath: string, config?: {
|
|
115
|
+
lastModified?: number;
|
|
116
|
+
internal?: boolean;
|
|
117
|
+
}): Promise<void>;
|
|
115
118
|
export declare function getLocalArchives(account: string, bucketName: string): IArchives;
|
|
@@ -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, GetInfoConfig, SetConfig,
|
|
13
|
+
ArchivesSyncStatus, ChangesAfterConfig, DelConfig, 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";
|
|
@@ -98,78 +98,36 @@ export type BucketWriteStats = {
|
|
|
98
98
|
function emptyWriteStats(): BucketWriteStats {
|
|
99
99
|
return { originalWrites: 0, originalBytes: 0, flushedWrites: 0, flushedBytes: 0 };
|
|
100
100
|
}
|
|
101
|
-
function addWriteStats(a: BucketWriteStats, b: BucketWriteStats): BucketWriteStats {
|
|
102
|
-
return {
|
|
103
|
-
originalWrites: a.originalWrites + b.originalWrites,
|
|
104
|
-
originalBytes: a.originalBytes + b.originalBytes,
|
|
105
|
-
flushedWrites: a.flushedWrites + b.flushedWrites,
|
|
106
|
-
flushedBytes: a.flushedBytes + b.flushedBytes,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function getWriteStatsStorage(): Promise<IStorage<BucketWriteStats>> {
|
|
110
|
-
return getSystemStorage<BucketWriteStats>("writeStats");
|
|
111
|
-
}
|
|
112
101
|
|
|
113
|
-
//
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
async function flushWriteStats(): Promise<void> {
|
|
117
|
-
if (!writeStatDeltas.size) return;
|
|
118
|
-
let pending = [...writeStatDeltas];
|
|
119
|
-
writeStatDeltas.clear();
|
|
120
|
-
let storage = await getWriteStatsStorage();
|
|
121
|
-
for (let [key, delta] of pending) {
|
|
122
|
-
try {
|
|
123
|
-
await storage.set(key, addWriteStats(await storage.get(key) || emptyWriteStats(), delta));
|
|
124
|
-
} catch (e) {
|
|
125
|
-
// Put the delta back, so a failed flush loses nothing
|
|
126
|
-
writeStatDeltas.set(key, addWriteStats(writeStatDeltas.get(key) || emptyWriteStats(), delta));
|
|
127
|
-
console.error(`Flushing write stats for bucket ${key} failed: ${(e as Error).stack ?? e}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const startWriteStatsFlushing = lazy(() => {
|
|
133
|
-
runInfinitePoll(WRITE_STATS_FLUSH_INTERVAL, flushWriteStats);
|
|
134
|
-
});
|
|
102
|
+
// In memory only: totals since this process started (or the last clearWriteStats). Persisting them to disk was more machinery than the numbers were worth.
|
|
103
|
+
const writeStats = new Map<string, BucketWriteStats>();
|
|
135
104
|
|
|
136
105
|
function countBucketWrite(key: string, kind: "original" | "flushed", bytes: number): void {
|
|
137
|
-
let
|
|
138
|
-
if (!
|
|
139
|
-
|
|
140
|
-
|
|
106
|
+
let stats = writeStats.get(key);
|
|
107
|
+
if (!stats) {
|
|
108
|
+
stats = emptyWriteStats();
|
|
109
|
+
writeStats.set(key, stats);
|
|
141
110
|
}
|
|
142
111
|
if (kind === "original") {
|
|
143
|
-
|
|
144
|
-
|
|
112
|
+
stats.originalWrites++;
|
|
113
|
+
stats.originalBytes += bytes;
|
|
145
114
|
} else {
|
|
146
|
-
|
|
147
|
-
|
|
115
|
+
stats.flushedWrites++;
|
|
116
|
+
stats.flushedBytes += bytes;
|
|
148
117
|
}
|
|
149
|
-
startWriteStatsFlushing();
|
|
150
118
|
}
|
|
151
119
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
let stored = await storage.get(key) || emptyWriteStats();
|
|
155
|
-
let delta = writeStatDeltas.get(key);
|
|
156
|
-
return delta && addWriteStats(stored, delta) || stored;
|
|
120
|
+
function getBucketWriteStats(key: string): BucketWriteStats {
|
|
121
|
+
return writeStats.get(key) || emptyWriteStats();
|
|
157
122
|
}
|
|
158
123
|
|
|
159
|
-
/** Zeroes the write statistics of every bucket in the account
|
|
160
|
-
export
|
|
161
|
-
let storage = await getWriteStatsStorage();
|
|
124
|
+
/** Zeroes the write statistics of every bucket in the account. */
|
|
125
|
+
export function clearAccountWriteStats(account: string): number {
|
|
162
126
|
let prefix = `${account}/`;
|
|
163
127
|
let cleared = 0;
|
|
164
|
-
for (let key of
|
|
165
|
-
if (!key.startsWith(prefix)) continue;
|
|
166
|
-
writeStatDeltas.delete(key);
|
|
167
|
-
await storage.remove(key);
|
|
168
|
-
cleared++;
|
|
169
|
-
}
|
|
170
|
-
for (let key of [...writeStatDeltas.keys()]) {
|
|
128
|
+
for (let key of [...writeStats.keys()]) {
|
|
171
129
|
if (!key.startsWith(prefix)) continue;
|
|
172
|
-
|
|
130
|
+
writeStats.delete(key);
|
|
173
131
|
cleared++;
|
|
174
132
|
}
|
|
175
133
|
console.log(`Cleared the write statistics of ${cleared} buckets in account ${account}`);
|
|
@@ -211,7 +169,6 @@ const buckets = new Map<string, Promise<LoadedBucket | undefined>>();
|
|
|
211
169
|
|
|
212
170
|
const MAX_REBUILD_TIMER_DELAY = 2 ** 31 - 1;
|
|
213
171
|
const REBUILD_BOUNDARY_BUFFER = 1000;
|
|
214
|
-
const WRITE_STATS_FLUSH_INTERVAL = 5 * 60 * 1000;
|
|
215
172
|
|
|
216
173
|
function getBucketFolder(account: string, bucketName: string): string {
|
|
217
174
|
return path.join(getStorageServerConfig().folder, "buckets2", account, bucketName);
|
|
@@ -1070,15 +1027,11 @@ export async function listAccountBuckets(account: string): Promise<ServerBucketI
|
|
|
1070
1027
|
let key = `${account}/${bucketName}`;
|
|
1071
1028
|
let folder = getBucketFolder(account, bucketName);
|
|
1072
1029
|
let base: ServerBucketInfo = { bucketName, active: false, folder };
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
timed("writeStats", () => getBucketWriteStats(key)),
|
|
1079
|
-
]);
|
|
1080
|
-
base.disk = disk;
|
|
1081
|
-
base.writeStats = writeStats;
|
|
1030
|
+
base.disk = await timed("statfs", () => getDiskInfo(folder)).catch((e: Error) => {
|
|
1031
|
+
base.diskError = String(e.stack ?? e).slice(0, 500);
|
|
1032
|
+
return undefined;
|
|
1033
|
+
});
|
|
1034
|
+
base.writeStats = getBucketWriteStats(key);
|
|
1082
1035
|
const loadedPromise = buckets.get(key);
|
|
1083
1036
|
if (loadedPromise) {
|
|
1084
1037
|
try {
|
|
@@ -1108,13 +1061,33 @@ export async function listAccountBuckets(account: string): Promise<ServerBucketI
|
|
|
1108
1061
|
}
|
|
1109
1062
|
}
|
|
1110
1063
|
|
|
1111
|
-
export async function deleteBucketFile(account: string, bucketName: string, filePath: string): Promise<void> {
|
|
1064
|
+
export async function deleteBucketFile(account: string, bucketName: string, filePath: string, config?: { lastModified?: number; internal?: boolean }): Promise<void> {
|
|
1065
|
+
assertWritesAllowed();
|
|
1112
1066
|
if (filePath === ROUTING_FILE) {
|
|
1113
1067
|
throw new Error(`The routing config ${JSON.stringify(ROUTING_FILE)} cannot be deleted (overwrite it to change the bucket's configuration)`);
|
|
1114
1068
|
}
|
|
1115
1069
|
let loaded = await getLoadedBucket(account, bucketName);
|
|
1116
1070
|
if (!loaded) return;
|
|
1117
|
-
|
|
1071
|
+
let writeTime = config?.lastModified || Date.now();
|
|
1072
|
+
let route = getRoute(filePath);
|
|
1073
|
+
if (config?.internal) {
|
|
1074
|
+
if (!config.lastModified) {
|
|
1075
|
+
throw new Error(`Internal deletions must carry lastModified (they are synchronization pushes, ordered by their write time), deleting ${JSON.stringify(filePath)} in bucket ${account}/${bucketName}`);
|
|
1076
|
+
}
|
|
1077
|
+
// Same acceptance rule as internal writes (see SetConfig.internal): the stamp must land inside SOME window+route this server is configured for
|
|
1078
|
+
let covered = loaded.selfEntries.some(x => writeTime >= x.validWindow[0] && writeTime < x.validWindow[1] && routeContains(x.route, route));
|
|
1079
|
+
if (!covered) {
|
|
1080
|
+
throw new Error(`Internal deletion of ${JSON.stringify(filePath)} in 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 })))}`);
|
|
1081
|
+
}
|
|
1082
|
+
if (loaded.store.setInternal) {
|
|
1083
|
+
// setInternal treats an empty buffer as exactly a deletion: disk removal plus a tombstone index entry, no fan-out
|
|
1084
|
+
await loaded.store.setInternal(filePath, Buffer.alloc(0), { lastModified: writeTime });
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
await loaded.store.del(filePath);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
await loaded.store.del(filePath, { ...getWriteConfig(loaded, writeTime, route), lastModified: writeTime });
|
|
1118
1091
|
}
|
|
1119
1092
|
|
|
1120
1093
|
const LARGE_FILE_PART_SIZE = 8 * 1024 * 1024;
|
|
@@ -1145,8 +1118,8 @@ class ArchivesLocalBucket implements IArchives {
|
|
|
1145
1118
|
await writeBucketFile(this.account, this.bucketName, fileName, data, config);
|
|
1146
1119
|
return fileName;
|
|
1147
1120
|
}
|
|
1148
|
-
public async del(fileName: string): Promise<void> {
|
|
1149
|
-
await deleteBucketFile(this.account, this.bucketName, fileName);
|
|
1121
|
+
public async del(fileName: string, config?: DelConfig): Promise<void> {
|
|
1122
|
+
await deleteBucketFile(this.account, this.bucketName, fileName, config);
|
|
1150
1123
|
}
|
|
1151
1124
|
public async setLargeFile(config: { path: string; lastModified?: number; getNextData(): Promise<Buffer | undefined> }): Promise<void> {
|
|
1152
1125
|
assertWritesAllowed();
|
package/test.ts
CHANGED
|
@@ -57,7 +57,7 @@ function summarize(files: FileInfo[], weightBySize: boolean) {
|
|
|
57
57
|
if (weightBySize) return summary.totalSize;
|
|
58
58
|
return summary.count;
|
|
59
59
|
},
|
|
60
|
-
expectedOutputCount:
|
|
60
|
+
expectedOutputCount: 100,
|
|
61
61
|
});
|
|
62
62
|
let addStart = performance.now();
|
|
63
63
|
for (let file of files) {
|
package/treeSummary.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type SummaryEntry<S> = {
|
|
2
|
+
path: string;
|
|
3
|
+
kind: "self" | "subtree" | "group" | "truncated";
|
|
4
|
+
summary: S;
|
|
5
|
+
weight: number;
|
|
6
|
+
};
|
|
7
|
+
export declare class TreeSummary<T, S> {
|
|
8
|
+
private config;
|
|
9
|
+
private root;
|
|
10
|
+
private nodeCount;
|
|
11
|
+
private maxTrackedNodes;
|
|
12
|
+
constructor(config: {
|
|
13
|
+
getPath: (value: T) => string;
|
|
14
|
+
createSummary: () => S;
|
|
15
|
+
addToSummary: (value: T, summary: S) => void;
|
|
16
|
+
mergeSummaries: (target: S, source: S) => void;
|
|
17
|
+
getWeight: (summary: S) => number;
|
|
18
|
+
expectedOutputCount?: number;
|
|
19
|
+
});
|
|
20
|
+
add(value: T): void;
|
|
21
|
+
private splitNode;
|
|
22
|
+
getTrackedNodeCount(): number;
|
|
23
|
+
getSummaries(maxCount: number): SummaryEntry<S>[];
|
|
24
|
+
private isRefinable;
|
|
25
|
+
private refine;
|
|
26
|
+
private splitParts;
|
|
27
|
+
private entryForParts;
|
|
28
|
+
private subtreeEntry;
|
|
29
|
+
private buildOutputSummary;
|
|
30
|
+
private prune;
|
|
31
|
+
}
|