querysub 0.641.0 → 0.643.0
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.643.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
80
80
|
"pako": "^2.1.0",
|
|
81
81
|
"peggy": "^5.0.6",
|
|
82
|
-
"sliftutils": "^1.7.
|
|
83
|
-
"socket-function": "^1.2.
|
|
82
|
+
"sliftutils": "^1.7.114",
|
|
83
|
+
"socket-function": "^1.2.32",
|
|
84
84
|
"terser": "^5.31.0",
|
|
85
85
|
"typenode": "^6.6.1",
|
|
86
86
|
"typesafecss": "^0.32.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sha256 } from "js-sha256";
|
|
2
2
|
import { getAllNodeIds, getOwnThreadId } from "../../-f-node-discovery/NodeDiscovery";
|
|
3
|
-
import { archives, pathValueArchives } from "../pathValueArchives";
|
|
3
|
+
import { archives, archivesRecycleBin, pathValueArchives } from "../pathValueArchives";
|
|
4
4
|
import { ignoreErrors, logErrors, timeoutToUndefinedSilent } from "../../errors";
|
|
5
5
|
import { green, magenta } from "socket-function/src/formatting/logColors";
|
|
6
6
|
import { devDebugbreak, isPublic } from "../../config";
|
|
@@ -111,6 +111,83 @@ export async function getLiveSnapshotList(): Promise<ArchiveSnapshotList> {
|
|
|
111
111
|
return { loadTime: Date.now() - startTime, snapshots: [liveOverview] };
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// Reading every stored snapshot at once is expensive, so we cap the concurrency rather than firing
|
|
115
|
+
// thousands of reads in parallel.
|
|
116
|
+
const SNAPSHOT_READ_PARALLEL = 32;
|
|
117
|
+
|
|
118
|
+
export type SnapshotFileCounts = {
|
|
119
|
+
live: number;
|
|
120
|
+
recycled: number;
|
|
121
|
+
missing: number;
|
|
122
|
+
};
|
|
123
|
+
export type RecycleBinCheck = {
|
|
124
|
+
// Snapshot file name (or "live") => how its files are distributed across live/recycled/missing.
|
|
125
|
+
counts: { [snapshotFile: string]: SnapshotFileCounts };
|
|
126
|
+
liveFileCount: number;
|
|
127
|
+
recycledFileCount: number;
|
|
128
|
+
snapshotCount: number;
|
|
129
|
+
loadTime: number;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Classifies every file of every snapshot as live / recycled / missing, entirely server-side. This
|
|
133
|
+
// lists the whole live store, the whole recycle bin, and reads every snapshot's file list (all in
|
|
134
|
+
// parallel), then intersects - so the client gets only the per-snapshot counts, never the (large)
|
|
135
|
+
// underlying file listings. Expensive (can take minutes), so it's an explicit on-demand action.
|
|
136
|
+
export async function checkRecycleBin(): Promise<RecycleBinCheck> {
|
|
137
|
+
let startTime = Date.now();
|
|
138
|
+
|
|
139
|
+
async function getLiveFilePaths(): Promise<string[]> {
|
|
140
|
+
// Zero-byte files are deletes on new storage servers, matching how the archive locker filters them.
|
|
141
|
+
return (await archives().findInfo("")).filter(x => x.size > 0).map(x => x.path);
|
|
142
|
+
}
|
|
143
|
+
async function getRecycleBinFilePaths(): Promise<string[]> {
|
|
144
|
+
return (await archivesRecycleBin().findInfo("")).filter(x => x.size > 0).map(x => x.path);
|
|
145
|
+
}
|
|
146
|
+
async function getSnapshotFileLists(): Promise<{ file: string; files: string[] }[]> {
|
|
147
|
+
let snapshotFiles = await snapshots().find("");
|
|
148
|
+
let lists: { file: string; files: string[] }[] = [];
|
|
149
|
+
async function readOne(file: string): Promise<void> {
|
|
150
|
+
let data = await snapshots().get(file);
|
|
151
|
+
if (!data) return;
|
|
152
|
+
lists.push({ file, files: data.toString().split("\n") });
|
|
153
|
+
}
|
|
154
|
+
let readParallel = runInParallel({ parallelCount: SNAPSHOT_READ_PARALLEL }, readOne);
|
|
155
|
+
await Promise.all(snapshotFiles.map(readParallel));
|
|
156
|
+
return lists;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let [liveFiles, recycledFiles, snapshotLists] = await Promise.all([
|
|
160
|
+
getLiveFilePaths(),
|
|
161
|
+
getRecycleBinFilePaths(),
|
|
162
|
+
getSnapshotFileLists(),
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
let liveSet = new Set(liveFiles);
|
|
166
|
+
let recycledSet = new Set(recycledFiles);
|
|
167
|
+
let counts: { [snapshotFile: string]: SnapshotFileCounts } = {};
|
|
168
|
+
for (let snapshot of snapshotLists) {
|
|
169
|
+
let count: SnapshotFileCounts = { live: 0, recycled: 0, missing: 0 };
|
|
170
|
+
for (let file of snapshot.files) {
|
|
171
|
+
if (liveSet.has(file)) {
|
|
172
|
+
count.live++;
|
|
173
|
+
} else if (recycledSet.has(file)) {
|
|
174
|
+
count.recycled++;
|
|
175
|
+
} else {
|
|
176
|
+
count.missing++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
counts[snapshot.file] = count;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
counts,
|
|
184
|
+
liveFileCount: liveFiles.length,
|
|
185
|
+
recycledFileCount: recycledFiles.length,
|
|
186
|
+
snapshotCount: snapshotLists.length,
|
|
187
|
+
loadTime: Date.now() - startTime,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
114
191
|
export async function getSnapshot(snapshotFile: string | "live"): Promise<ArchiveSnapshotRead> {
|
|
115
192
|
let snapshot = await getSnapshotBase(snapshotFile);
|
|
116
193
|
let locker = await pathValueArchives.getArchiveLocker();
|
|
@@ -173,6 +173,7 @@ export function formatValue(value: unknown, formatter: JSXFormatter = "guess", c
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
export function toSpaceCase(text: string) {
|
|
176
|
+
if (typeof text !== "string") return String(text);
|
|
176
177
|
if (text.startsWith("_")) return text;
|
|
177
178
|
return text
|
|
178
179
|
// "camelCase" => "camel Case"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module.allowclient = true;
|
|
2
2
|
|
|
3
|
-
import { ArchiveSnapshotList, ArchiveSnapshotOverview, ArchiveSnapshotRead, downloadSnapshot, getLiveSnapshotList, getSavedSnapshotList, getSnapshot, loadSnapshot, saveSnapshot, uploadSnapshot } from "../../0-path-value-core/archiveLocks/archiveSnapshots";
|
|
3
|
+
import { ArchiveSnapshotList, ArchiveSnapshotOverview, ArchiveSnapshotRead, checkRecycleBin as checkRecycleBinServer, downloadSnapshot, getLiveSnapshotList, getSavedSnapshotList, getSnapshot, loadSnapshot, RecycleBinCheck, saveSnapshot, uploadSnapshot } from "../../0-path-value-core/archiveLocks/archiveSnapshots";
|
|
4
4
|
import { qreact } from "../../4-dom/qreact";
|
|
5
|
+
import preact from "preact";
|
|
5
6
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
6
7
|
import { getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
|
|
7
8
|
import { Table } from "../../5-diagnostics/Table";
|
|
@@ -34,6 +35,16 @@ export class SnapshotViewer extends qreact.Component {
|
|
|
34
35
|
saving: t.boolean(false),
|
|
35
36
|
uploading: t.boolean(false),
|
|
36
37
|
downloading: t.lookup(t.boolean),
|
|
38
|
+
// Bulk recycle-bin check. The server classifies every snapshot's files as live/recycled/missing
|
|
39
|
+
// and returns just the counts; `active` stays true after it finishes so the columns keep showing.
|
|
40
|
+
recycleCheck: {
|
|
41
|
+
active: t.boolean(false),
|
|
42
|
+
running: t.boolean(false),
|
|
43
|
+
error: t.optional<string>(),
|
|
44
|
+
summary: t.optional<string>(),
|
|
45
|
+
// snapshot file name => classified counts (atomic, so we write the whole triple at once)
|
|
46
|
+
results: t.lookup(t.atomic<{ live: number; recycled: number; missing: number }>()),
|
|
47
|
+
},
|
|
37
48
|
});
|
|
38
49
|
render() {
|
|
39
50
|
let controller = SnapshotViewerSynced(getBrowserUrlNode());
|
|
@@ -51,6 +62,20 @@ export class SnapshotViewer extends qreact.Component {
|
|
|
51
62
|
);
|
|
52
63
|
let file = selectedSnapshot.value;
|
|
53
64
|
if (!file || typeof file !== "string") {
|
|
65
|
+
let recycleCheck = this.state.recycleCheck;
|
|
66
|
+
let rows: SnapshotListRow[] = snapshotList.map(snapshot => {
|
|
67
|
+
let counts = recycleCheck.results[snapshot.file];
|
|
68
|
+
// The live "snapshot" isn't a stored file, but by definition every file in it is live.
|
|
69
|
+
if (!counts && snapshot.file === "live" && recycleCheck.active) {
|
|
70
|
+
counts = { live: snapshot.fileCount, recycled: 0, missing: 0 };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...snapshot,
|
|
74
|
+
liveCount: counts?.live,
|
|
75
|
+
recycledCount: counts?.recycled,
|
|
76
|
+
missingCount: counts?.missing,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
54
79
|
return (
|
|
55
80
|
<div class={css.pad2(10).vbox(10)}>
|
|
56
81
|
{loadStatus}
|
|
@@ -59,14 +84,23 @@ export class SnapshotViewer extends qreact.Component {
|
|
|
59
84
|
<Button onClick={() => triggerSnapshotUpload(this)}>
|
|
60
85
|
{this.state.uploading && "Uploading..." || "Upload Snapshot"}
|
|
61
86
|
</Button>
|
|
87
|
+
<Button onClick={() => checkRecycleBin(this)}>
|
|
88
|
+
{recycleCheck.running && "Checking Recycle Bin..." || "Check Recycle Bin"}
|
|
89
|
+
</Button>
|
|
62
90
|
</div>
|
|
91
|
+
{recycleCheck.running && <div>Checking recycle bin (this can take a few minutes)...</div>}
|
|
92
|
+
{recycleCheck.summary && <div>{recycleCheck.summary}</div>}
|
|
93
|
+
{recycleCheck.error && <div class={css.color("red").whiteSpace("pre-wrap")}>{recycleCheck.error}</div>}
|
|
63
94
|
<Table
|
|
64
|
-
rows={
|
|
95
|
+
rows={rows}
|
|
65
96
|
columns={{
|
|
66
97
|
time: {},
|
|
67
98
|
fileCount: {},
|
|
68
99
|
valueCount: {},
|
|
69
100
|
byteCount: {},
|
|
101
|
+
liveCount: { title: "Live", formatter: formatCountCell },
|
|
102
|
+
recycledCount: { title: "Recycled", formatter: formatCountCell },
|
|
103
|
+
missingCount: { title: "Missing", formatter: formatMissingCell },
|
|
70
104
|
file: {
|
|
71
105
|
title: "Actions",
|
|
72
106
|
formatter: (file, context) => {
|
|
@@ -80,7 +114,7 @@ export class SnapshotViewer extends qreact.Component {
|
|
|
80
114
|
try {
|
|
81
115
|
await SnapshotViewerSynced(getBrowserUrlNode()).saveLiveSnapshot.promise();
|
|
82
116
|
} finally {
|
|
83
|
-
Querysub.
|
|
117
|
+
Querysub.commit(() => {
|
|
84
118
|
this.state.saving = false;
|
|
85
119
|
});
|
|
86
120
|
}
|
|
@@ -202,9 +236,59 @@ function summarizeSnapshotByStatus(files: ArchiveSnapshotRead["files"]): Snapsho
|
|
|
202
236
|
return rows;
|
|
203
237
|
}
|
|
204
238
|
|
|
239
|
+
type SnapshotListRow = ArchiveSnapshotOverview & {
|
|
240
|
+
liveCount?: number;
|
|
241
|
+
recycledCount?: number;
|
|
242
|
+
missingCount?: number;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
function formatCountCell(value: unknown): string {
|
|
246
|
+
if (typeof value !== "number") return "";
|
|
247
|
+
return formatNumber(value);
|
|
248
|
+
}
|
|
249
|
+
function formatMissingCell(value: unknown): preact.ComponentChild {
|
|
250
|
+
if (typeof value !== "number") return "";
|
|
251
|
+
if (value === 0) return "0";
|
|
252
|
+
// Missing files in a snapshot mean it can no longer be fully restored - call that out.
|
|
253
|
+
return <span class={css.color("red").fontWeight("bold")}>{formatNumber(value)}</span>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function checkRecycleBin(component: SnapshotViewer) {
|
|
257
|
+
if (component.state.recycleCheck.running) return;
|
|
258
|
+
const controller = SnapshotViewerController.nodes[getBrowserUrlNode()];
|
|
259
|
+
Querysub.commit(() => {
|
|
260
|
+
let rc = component.state.recycleCheck;
|
|
261
|
+
rc.active = true;
|
|
262
|
+
rc.running = true;
|
|
263
|
+
rc.error = undefined;
|
|
264
|
+
rc.summary = undefined;
|
|
265
|
+
for (let key of Object.keys(rc.results)) {
|
|
266
|
+
delete rc.results[key];
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
// The server does all the listing/reading/classifying and returns only the per-snapshot counts.
|
|
271
|
+
let check = await controller.checkRecycleBin();
|
|
272
|
+
Querysub.commit(() => {
|
|
273
|
+
let rc = component.state.recycleCheck;
|
|
274
|
+
for (let [snapshotFile, counts] of Object.entries(check.counts)) {
|
|
275
|
+
rc.results[snapshotFile] = counts;
|
|
276
|
+
}
|
|
277
|
+
rc.summary = `Checked ${formatNumber(check.snapshotCount)} snapshots against ${formatNumber(check.liveFileCount)} live and ${formatNumber(check.recycledFileCount)} recycled files in ${formatTime(check.loadTime)}.`;
|
|
278
|
+
rc.running = false;
|
|
279
|
+
});
|
|
280
|
+
} catch (e: any) {
|
|
281
|
+
console.error(red(`Failed to check recycle bin: ${e.stack}`));
|
|
282
|
+
Querysub.commit(() => {
|
|
283
|
+
component.state.recycleCheck.error = e.stack || String(e);
|
|
284
|
+
component.state.recycleCheck.running = false;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
205
289
|
async function downloadSnapshotFile(component: SnapshotViewer, file: string) {
|
|
206
290
|
if (component.state.downloading[file]) return;
|
|
207
|
-
Querysub.
|
|
291
|
+
Querysub.commit(() => {
|
|
208
292
|
component.state.downloading[file] = true;
|
|
209
293
|
});
|
|
210
294
|
try {
|
|
@@ -224,7 +308,7 @@ async function downloadSnapshotFile(component: SnapshotViewer, file: string) {
|
|
|
224
308
|
} catch (e: any) {
|
|
225
309
|
console.error(red(`Failed to download snapshot ${file}: ${e.stack}`));
|
|
226
310
|
} finally {
|
|
227
|
-
Querysub.
|
|
311
|
+
Querysub.commit(() => {
|
|
228
312
|
delete component.state.downloading[file];
|
|
229
313
|
});
|
|
230
314
|
}
|
|
@@ -238,7 +322,7 @@ async function triggerSnapshotUpload(component: SnapshotViewer) {
|
|
|
238
322
|
input.onchange = async () => {
|
|
239
323
|
let selected = input.files?.[0];
|
|
240
324
|
if (!selected) return;
|
|
241
|
-
Querysub.
|
|
325
|
+
Querysub.commit(() => {
|
|
242
326
|
component.state.uploading = true;
|
|
243
327
|
});
|
|
244
328
|
try {
|
|
@@ -249,7 +333,7 @@ async function triggerSnapshotUpload(component: SnapshotViewer) {
|
|
|
249
333
|
} catch (e: any) {
|
|
250
334
|
console.error(red(`Failed to upload snapshot: ${e.stack}`));
|
|
251
335
|
} finally {
|
|
252
|
-
Querysub.
|
|
336
|
+
Querysub.commit(() => {
|
|
253
337
|
component.state.uploading = false;
|
|
254
338
|
});
|
|
255
339
|
}
|
|
@@ -316,6 +400,10 @@ class SnapshotViewerControllerBase {
|
|
|
316
400
|
return await downloadSnapshot(snapshotFile);
|
|
317
401
|
}
|
|
318
402
|
|
|
403
|
+
public async checkRecycleBin(): Promise<RecycleBinCheck> {
|
|
404
|
+
return await checkRecycleBinServer();
|
|
405
|
+
}
|
|
406
|
+
|
|
319
407
|
public async uploadSnapshot(buffer: Buffer): Promise<ArchiveSnapshotOverview> {
|
|
320
408
|
return await uploadSnapshot(buffer);
|
|
321
409
|
}
|
|
@@ -332,6 +420,8 @@ export const SnapshotViewerController = SocketFunction.register(
|
|
|
332
420
|
saveLiveSnapshot: {},
|
|
333
421
|
downloadSnapshot: {},
|
|
334
422
|
uploadSnapshot: {},
|
|
423
|
+
// Returns a per-snapshot count map (not the underlying listings), but there can be many snapshots.
|
|
424
|
+
checkRecycleBin: { responseLimit: 1024 * 1024 * 1024 },
|
|
335
425
|
example: {}
|
|
336
426
|
}),
|
|
337
427
|
() => ({
|