pinokiod 8.0.41 → 8.0.44
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/kernel/vault/constants.js +1 -5
- package/kernel/vault/hash_worker.js +1 -7
- package/kernel/vault/index.js +515 -873
- package/kernel/vault/registry.js +35 -157
- package/kernel/vault/sweeper.js +106 -181
- package/kernel/vault/walker.js +1 -8
- package/package.json +1 -1
- package/server/index.js +3 -20
- package/server/public/storage-size.js +3 -1
- package/server/public/style.css +0 -13
- package/server/public/vault.css +60 -173
- package/server/public/vault.js +397 -626
- package/server/views/app.ejs +6 -85
- package/server/views/partials/main_sidebar.ejs +1 -4
- package/server/views/partials/vault_workspace.ejs +2 -5
- package/server/views/vault.ejs +1 -1
- package/server/views/vault_app.ejs +1 -1
- package/test/vault-engine.test.js +123 -212
- package/test/vault-sweep.test.js +163 -292
- package/test/vault-ui.test.js +718 -979
- package/server/public/vault-nav.js +0 -96
package/kernel/vault/registry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const crypto = require('crypto')
|
|
4
|
-
const { SHA256_RE
|
|
4
|
+
const { SHA256_RE } = require('./constants')
|
|
5
5
|
|
|
6
6
|
const isMissingError = (error) => !!(error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
7
7
|
const isRecord = (value) => !!(value && typeof value === "object" && !Array.isArray(value))
|
|
@@ -42,15 +42,13 @@ class Registry {
|
|
|
42
42
|
}
|
|
43
43
|
reset() {
|
|
44
44
|
this.blobs = new Map() // hash -> { size, first_seen, source_urls, verified_at, orphan }
|
|
45
|
-
this.links = new Map() // path -> { hash, app, source_id, dev, ino, created, batch_id? }
|
|
45
|
+
this.links = new Map() // path -> { hash, app, source_id, dev, ino, created, mode, batch_id? }
|
|
46
46
|
this.scanIndex = new Map() // path -> { size, mtime, dev, ino, hash, source_id }
|
|
47
47
|
this.duplicates = new Map() // path -> { hash, size, app, source_id, discovered } pending user conversion
|
|
48
48
|
this.excluded = new Map() // path -> { ts, source_id, size } user chose "keep independent"
|
|
49
|
-
this.lastScan = null
|
|
50
|
-
this.lastScanAttempt = null // last global scan attempt, complete or partial
|
|
49
|
+
this.lastScan = null // scan totals, duration, and hash work
|
|
51
50
|
this.sourceScans = new Map() // source id -> last complete scoped scan
|
|
52
|
-
this.
|
|
53
|
-
this.settings = { external_sources: [] } // durable user preferences, not derived scan state
|
|
51
|
+
this.totals = { lifetime_bytes_saved: 0 }
|
|
54
52
|
this.byIno = new Map() // "dev:ino" -> hash
|
|
55
53
|
this.pathsByIno = new Map() // "dev:ino" -> Set(paths), for bounded group updates
|
|
56
54
|
this.eventsCache = null
|
|
@@ -170,24 +168,9 @@ class Registry {
|
|
|
170
168
|
scanFor(sourceId = null) {
|
|
171
169
|
return sourceId ? (this.sourceScans.get(sourceId) || null) : this.lastScan
|
|
172
170
|
}
|
|
173
|
-
scanAttemptFor(sourceId = null) {
|
|
174
|
-
return sourceId
|
|
175
|
-
? (this.sourceScanAttempts.get(sourceId) || null)
|
|
176
|
-
: this.lastScanAttempt
|
|
177
|
-
}
|
|
178
171
|
setLastScan(meta, sourceId = null) {
|
|
179
|
-
if (sourceId)
|
|
180
|
-
|
|
181
|
-
this.sourceScanAttempts.set(sourceId, meta)
|
|
182
|
-
} else {
|
|
183
|
-
this.lastScan = meta
|
|
184
|
-
this.lastScanAttempt = meta
|
|
185
|
-
}
|
|
186
|
-
this.schedulePersist()
|
|
187
|
-
}
|
|
188
|
-
setScanAttempt(meta, sourceId = null) {
|
|
189
|
-
if (sourceId) this.sourceScanAttempts.set(sourceId, meta)
|
|
190
|
-
else this.lastScanAttempt = meta
|
|
172
|
+
if (sourceId) this.sourceScans.set(sourceId, meta)
|
|
173
|
+
else this.lastScan = meta
|
|
191
174
|
this.schedulePersist()
|
|
192
175
|
}
|
|
193
176
|
inoKey(dev, ino) {
|
|
@@ -217,10 +200,9 @@ class Registry {
|
|
|
217
200
|
// A path has exactly one derived classification. Registering a physical
|
|
218
201
|
// name always clears an older pending-duplicate classification first.
|
|
219
202
|
this.duplicates.delete(filePath)
|
|
220
|
-
const next = Object.assign({ created: Date.now() }, entry)
|
|
221
|
-
delete next.mode
|
|
203
|
+
const next = Object.assign({ created: Date.now(), mode: "link" }, entry)
|
|
222
204
|
const sameTrackedFile = previous && previous.hash === next.hash &&
|
|
223
|
-
previous.dev === next.dev && previous.ino === next.ino
|
|
205
|
+
previous.mode === next.mode && previous.dev === next.dev && previous.ino === next.ino
|
|
224
206
|
if (next.batch_id === undefined && sameTrackedFile && previous.batch_id) {
|
|
225
207
|
next.batch_id = previous.batch_id
|
|
226
208
|
}
|
|
@@ -303,46 +285,22 @@ class Registry {
|
|
|
303
285
|
}
|
|
304
286
|
this.schedulePersist()
|
|
305
287
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
? { source_urls: [...value.source_urls] }
|
|
312
|
-
: {})
|
|
313
|
-
: value
|
|
314
|
-
]))
|
|
315
|
-
return {
|
|
316
|
-
blobs: cloneMap(this.blobs),
|
|
317
|
-
links: cloneMap(this.links),
|
|
318
|
-
scanIndex: cloneMap(this.scanIndex),
|
|
319
|
-
duplicates: cloneMap(this.duplicates),
|
|
320
|
-
excluded: cloneMap(this.excluded),
|
|
321
|
-
lastScan: this.lastScan ? Object.assign({}, this.lastScan) : null,
|
|
322
|
-
lastScanAttempt: this.lastScanAttempt ? Object.assign({}, this.lastScanAttempt) : null,
|
|
323
|
-
sourceScans: cloneMap(this.sourceScans),
|
|
324
|
-
sourceScanAttempts: cloneMap(this.sourceScanAttempts),
|
|
325
|
-
settings: Object.assign({}, this.settings, {
|
|
326
|
-
external_sources: [...(this.settings.external_sources || [])]
|
|
327
|
-
}),
|
|
328
|
-
byIno: new Map(this.byIno),
|
|
329
|
-
pathsByIno: new Map([...this.pathsByIno].map(([key, paths]) => [key, new Set(paths)]))
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
replaceState(state, options = {}) {
|
|
288
|
+
addSaved(bytes) {
|
|
289
|
+
this.totals.lifetime_bytes_saved += bytes
|
|
290
|
+
this.schedulePersist()
|
|
291
|
+
}
|
|
292
|
+
replaceState(state) {
|
|
333
293
|
this.blobs = state.blobs
|
|
334
294
|
this.links = state.links
|
|
335
295
|
this.scanIndex = state.scanIndex
|
|
336
296
|
this.duplicates = state.duplicates
|
|
337
297
|
this.excluded = state.excluded
|
|
338
298
|
this.lastScan = state.lastScan
|
|
339
|
-
this.lastScanAttempt = state.lastScanAttempt || null
|
|
340
299
|
this.sourceScans = state.sourceScans
|
|
341
|
-
this.
|
|
342
|
-
this.settings = Object.assign({ external_sources: [] }, state.settings)
|
|
300
|
+
this.totals = state.totals
|
|
343
301
|
this.byIno = state.byIno
|
|
344
302
|
this.pathsByIno = state.pathsByIno
|
|
345
|
-
|
|
303
|
+
this.schedulePersist()
|
|
346
304
|
}
|
|
347
305
|
// Returns { corrupt: true } when the snapshot exists but cannot be parsed,
|
|
348
306
|
// so the caller can rebuild from disk instead of blocking startup.
|
|
@@ -375,16 +333,10 @@ class Registry {
|
|
|
375
333
|
}))
|
|
376
334
|
}
|
|
377
335
|
}
|
|
378
|
-
const legacyCopies = []
|
|
379
336
|
for (const [k, v] of Object.entries(json.links || {})) {
|
|
380
337
|
if (!isRecord(v) || !SHA256_RE.test(v.hash) ||
|
|
381
338
|
!Number.isFinite(v.dev) || !Number.isFinite(v.ino)) continue
|
|
382
|
-
|
|
383
|
-
legacyCopies.push([k, v])
|
|
384
|
-
continue
|
|
385
|
-
}
|
|
386
|
-
const link = Object.assign({}, v)
|
|
387
|
-
delete link.mode
|
|
339
|
+
const link = Object.assign({}, v, { mode: v.mode === "copy" ? "copy" : "link" })
|
|
388
340
|
this.links.set(k, link)
|
|
389
341
|
if (link.dev !== undefined && link.ino !== undefined) {
|
|
390
342
|
const key = this.inoKey(link.dev, link.ino)
|
|
@@ -406,26 +358,6 @@ class Registry {
|
|
|
406
358
|
}))
|
|
407
359
|
}
|
|
408
360
|
}
|
|
409
|
-
// Older builds recorded independent files as mode:"copy" links. They were
|
|
410
|
-
// never shared. Migrate only their review metadata and never recreate that
|
|
411
|
-
// unsupported state.
|
|
412
|
-
for (const [filePath, link] of legacyCopies) {
|
|
413
|
-
if (this.duplicates.has(filePath) || !this.blobs.has(link.hash)) continue
|
|
414
|
-
const indexed = this.scanIndex.get(filePath)
|
|
415
|
-
if (!indexed || indexed.hash !== link.hash) continue
|
|
416
|
-
this.duplicates.set(filePath, {
|
|
417
|
-
hash: link.hash,
|
|
418
|
-
size: indexed.size,
|
|
419
|
-
app: link.app || null,
|
|
420
|
-
source_id: link.source_id || indexed.source_id || null,
|
|
421
|
-
discovered: link.created || Date.now(),
|
|
422
|
-
dev: indexed.dev,
|
|
423
|
-
ino: indexed.ino,
|
|
424
|
-
mtime: indexed.mtime,
|
|
425
|
-
ctime: indexed.ctime,
|
|
426
|
-
unavailable_reason: "unsupported_disk"
|
|
427
|
-
})
|
|
428
|
-
}
|
|
429
361
|
for (const [k, v] of Object.entries(json.excluded || {})) {
|
|
430
362
|
if (!isRecord(v)) continue
|
|
431
363
|
const linked = this.links.get(k)
|
|
@@ -439,45 +371,14 @@ class Registry {
|
|
|
439
371
|
size: Number.isFinite(v.size) && v.size >= 0 ? v.size : 0
|
|
440
372
|
}))
|
|
441
373
|
}
|
|
442
|
-
if (isRecord(json.
|
|
443
|
-
|
|
444
|
-
if (!isRecord(value)) continue
|
|
445
|
-
const complete = isRecord(value.last_complete) ? value.last_complete : null
|
|
446
|
-
const attempt = isRecord(value.last_attempt) ? value.last_attempt : null
|
|
447
|
-
if (scopeId === "global") {
|
|
448
|
-
this.lastScan = complete
|
|
449
|
-
this.lastScanAttempt = attempt
|
|
450
|
-
} else {
|
|
451
|
-
if (complete) this.sourceScans.set(scopeId, complete)
|
|
452
|
-
if (attempt) this.sourceScanAttempts.set(scopeId, attempt)
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
} else {
|
|
456
|
-
if (isRecord(json.last_scan)) {
|
|
457
|
-
this.lastScan = json.last_scan
|
|
458
|
-
this.lastScanAttempt = json.last_scan
|
|
459
|
-
}
|
|
460
|
-
for (const [sourceId, scan] of Object.entries(json.source_scans || {})) {
|
|
461
|
-
if (!isRecord(scan)) continue
|
|
462
|
-
this.sourceScans.set(sourceId, scan)
|
|
463
|
-
this.sourceScanAttempts.set(sourceId, scan)
|
|
464
|
-
}
|
|
374
|
+
if (isRecord(json.last_scan)) {
|
|
375
|
+
this.lastScan = json.last_scan
|
|
465
376
|
}
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const seen = new Set()
|
|
472
|
-
for (const value of json.settings.external_sources) {
|
|
473
|
-
if (typeof value !== "string" || !path.isAbsolute(value)) continue
|
|
474
|
-
const resolved = path.resolve(value)
|
|
475
|
-
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved
|
|
476
|
-
if (seen.has(key)) continue
|
|
477
|
-
seen.add(key)
|
|
478
|
-
this.settings.external_sources.push(resolved)
|
|
479
|
-
}
|
|
480
|
-
}
|
|
377
|
+
for (const [sourceId, scan] of Object.entries(json.source_scans || {})) {
|
|
378
|
+
if (isRecord(scan)) this.sourceScans.set(sourceId, scan)
|
|
379
|
+
}
|
|
380
|
+
if (json.totals && Number.isFinite(json.totals.lifetime_bytes_saved)) {
|
|
381
|
+
this.totals = { lifetime_bytes_saved: Math.max(0, json.totals.lifetime_bytes_saved) }
|
|
481
382
|
}
|
|
482
383
|
return { corrupt: false, existed: true }
|
|
483
384
|
}
|
|
@@ -491,12 +392,7 @@ class Registry {
|
|
|
491
392
|
}
|
|
492
393
|
async endBatch(options = {}) {
|
|
493
394
|
if (this.persistDepth > 0) this.persistDepth -= 1
|
|
494
|
-
if (this.persistDepth) return
|
|
495
|
-
if (options.discard) {
|
|
496
|
-
this.persistDirty = false
|
|
497
|
-
return
|
|
498
|
-
}
|
|
499
|
-
if (!this.persistDirty) return
|
|
395
|
+
if (this.persistDepth || !this.persistDirty) return
|
|
500
396
|
if (options.flush === false) this.schedulePersist()
|
|
501
397
|
else await this.flush()
|
|
502
398
|
}
|
|
@@ -518,21 +414,6 @@ class Registry {
|
|
|
518
414
|
this.persistTimer = null
|
|
519
415
|
}
|
|
520
416
|
this.persistDirty = false
|
|
521
|
-
const scans = {
|
|
522
|
-
global: {
|
|
523
|
-
last_complete: this.lastScan,
|
|
524
|
-
last_attempt: this.lastScanAttempt
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
for (const sourceId of new Set([
|
|
528
|
-
...this.sourceScans.keys(),
|
|
529
|
-
...this.sourceScanAttempts.keys()
|
|
530
|
-
])) {
|
|
531
|
-
scans[sourceId] = {
|
|
532
|
-
last_complete: this.sourceScans.get(sourceId) || null,
|
|
533
|
-
last_attempt: this.sourceScanAttempts.get(sourceId) || null
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
417
|
const json = {
|
|
537
418
|
version: 1,
|
|
538
419
|
blobs: Object.fromEntries(this.blobs),
|
|
@@ -540,8 +421,9 @@ class Registry {
|
|
|
540
421
|
scan_index: Object.fromEntries(this.scanIndex),
|
|
541
422
|
duplicates: Object.fromEntries(this.duplicates),
|
|
542
423
|
excluded: Object.fromEntries(this.excluded),
|
|
543
|
-
|
|
544
|
-
|
|
424
|
+
last_scan: this.lastScan,
|
|
425
|
+
source_scans: Object.fromEntries(this.sourceScans),
|
|
426
|
+
totals: this.totals
|
|
545
427
|
}
|
|
546
428
|
const write = async () => {
|
|
547
429
|
await this.atomicWrite(this.snapshotPath, JSON.stringify(json))
|
|
@@ -561,25 +443,21 @@ class Registry {
|
|
|
561
443
|
}
|
|
562
444
|
}
|
|
563
445
|
async appendEvent(event) {
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
async appendEvents(events) {
|
|
567
|
-
if (!events.length) return
|
|
568
|
-
const entries = events.map((event) => Object.assign({ ts: Date.now() }, event))
|
|
569
|
-
const contents = entries.map((event) => JSON.stringify(event)).join("\n") + "\n"
|
|
446
|
+
const line = JSON.stringify(Object.assign({ ts: Date.now() }, event)) + "\n"
|
|
570
447
|
const append = async () => {
|
|
571
|
-
await this.appendFile(this.eventsPath,
|
|
572
|
-
if (this.eventsCache) this.eventsCache.push(
|
|
573
|
-
this.eventsSinceCompact +=
|
|
448
|
+
await this.appendFile(this.eventsPath, line)
|
|
449
|
+
if (this.eventsCache) this.eventsCache.push(JSON.parse(line))
|
|
450
|
+
this.eventsSinceCompact += 1
|
|
574
451
|
if ((this.eventsCache && this.eventsCache.length > this.maxEvents + this.compactEvery) ||
|
|
575
452
|
this.eventsSinceCompact >= this.compactEvery) {
|
|
576
453
|
await this.compactEventsNow()
|
|
577
454
|
}
|
|
578
455
|
}
|
|
579
456
|
const pending = this.eventPromise.then(append, append)
|
|
580
|
-
// One ordered writer
|
|
581
|
-
//
|
|
582
|
-
//
|
|
457
|
+
// One ordered writer prevents a scan with many matches from opening an
|
|
458
|
+
// unbounded number of concurrent append operations. Keep the writer
|
|
459
|
+
// usable after a failure while returning the real append result so the
|
|
460
|
+
// dashboard can expose missing activity without misreporting the action.
|
|
583
461
|
this.eventPromise = pending.then(
|
|
584
462
|
() => { this.eventError = null },
|
|
585
463
|
(error) => {
|