pinokiod 8.0.74 → 8.0.76

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.
@@ -894,7 +894,7 @@ class AutomaticScans {
894
894
  })
895
895
  }
896
896
 
897
- async verifiedAutomaticHash(active, entry, memory, verifiedHashes, counts) {
897
+ async currentAutomaticEntry(active, entry, counts) {
898
898
  this.checkpoint(active)
899
899
  let current
900
900
  try {
@@ -915,7 +915,20 @@ class AutomaticScans {
915
915
  active.app, entry, "snapshot-changed", counts)
916
916
  return null
917
917
  }
918
- const currentEntry = Object.assign({}, entry, fileSnapshot(current))
918
+ return {
919
+ stat: current,
920
+ entry: Object.assign({}, entry, fileSnapshot(current))
921
+ }
922
+ }
923
+
924
+ async verifiedAutomaticHash(
925
+ active, entry, memory, verifiedHashes, counts, prepared = null
926
+ ) {
927
+ const checked = prepared || await this.currentAutomaticEntry(
928
+ active, entry, counts)
929
+ if (!checked) return null
930
+ const current = checked.stat
931
+ const currentEntry = checked.entry
919
932
  const key = this.automaticHashKey(currentEntry)
920
933
  const cachedSnapshot = {
921
934
  size: entry.cached_size,
@@ -976,16 +989,26 @@ class AutomaticScans {
976
989
  async runPrecheck(active) {
977
990
  const app = active.app
978
991
  const root = path.resolve(this.vault.kernel.homedir, "api", app)
992
+ const startedAt = Date.now()
993
+ active.startedAt = startedAt
994
+ const markStage = (stage, entry = null) => {
995
+ active.stage = stage
996
+ active.stagePath = entry && entry.path ? entry.path : null
997
+ active.stageKind = entry && entry.kind ? entry.kind : null
998
+ }
999
+ markStage("app-root-check")
979
1000
  if (!await this.appRootIsAvailable(app)) {
980
1001
  const error = new Error("That app is no longer available.")
981
1002
  error.code = "ENOENT"
982
1003
  throw error
983
1004
  }
984
- const startedAt = Date.now()
985
1005
  const counts = {
986
1006
  files: 0,
987
1007
  bytes: 0,
988
1008
  candidates: 0,
1009
+ registry_pages: 0,
1010
+ candidate_checks: 0,
1011
+ peer_checks: 0,
989
1012
  path_errors: 0,
990
1013
  hashed: 0,
991
1014
  hash_bytes: 0,
@@ -993,6 +1016,7 @@ class AutomaticScans {
993
1016
  hash_failures: 0,
994
1017
  unstable_hashes: 0
995
1018
  }
1019
+ markStage("threshold-read")
996
1020
  const threshold = await this.candidateThreshold()
997
1021
  const paths = [...new Set(active.paths || [])].filter((filePath) =>
998
1022
  typeof filePath === "string" && inside(root, filePath))
@@ -1004,6 +1028,7 @@ class AutomaticScans {
1004
1028
  changed_paths: paths.length,
1005
1029
  policy: "metadata-prefilter-sha256"
1006
1030
  })
1031
+ markStage("staging-reset")
1007
1032
  await this.vault.registry.beginAutomaticPrecheck(app)
1008
1033
  try {
1009
1034
  this.checkpoint(active)
@@ -1023,9 +1048,11 @@ class AutomaticScans {
1023
1048
  return true
1024
1049
  }
1025
1050
  }
1051
+ markStage("changed-parent-metadata")
1026
1052
  const safePaths = await this.changedPathsWithSafeParents(
1027
1053
  paths, root, metadataOptions, () => this.checkpoint(active))
1028
1054
  this.checkpoint(active)
1055
+ markStage("changed-path-metadata")
1029
1056
  const safeStats = await this.statChangedPaths(
1030
1057
  safePaths, metadataOptions, () => this.checkpoint(active))
1031
1058
  const statsByPath = new Map(safePaths.map((filePath, index) =>
@@ -1052,95 +1079,164 @@ class AutomaticScans {
1052
1079
  gid: stat.gid
1053
1080
  })
1054
1081
  }
1055
- counts.candidates = candidates.length
1056
1082
  if (candidates.length) {
1083
+ markStage("staging-write")
1057
1084
  await this.vault.registry.stageAutomaticPrecheckFiles(app, candidates)
1058
1085
  }
1059
1086
  this.checkpoint(active)
1087
+ markStage("app-root-recheck")
1060
1088
  if (!await this.appRootIsAvailable(app)) {
1061
1089
  const error = new Error("That app is no longer available.")
1062
1090
  error.code = "ENOENT"
1063
1091
  throw error
1064
1092
  }
1065
- const verifiedMatches = []
1093
+ this.log("verification-started", {
1094
+ app,
1095
+ files: counts.files,
1096
+ bytes: counts.bytes,
1097
+ threshold_candidates: candidates.length,
1098
+ duration_ms: Date.now() - startedAt
1099
+ })
1100
+ let proof = null
1066
1101
  let group = null
1102
+ const acceptProof = (candidate, peer, hash) => {
1103
+ proof = { path: candidate.path, hash }
1104
+ this.log("first-proof-exit", {
1105
+ app,
1106
+ candidate_path: candidate.path,
1107
+ peer_path: peer.path,
1108
+ peer_kind: peer.kind,
1109
+ size: candidate.size,
1110
+ duration_ms: Date.now() - startedAt,
1111
+ registry_pages: counts.registry_pages,
1112
+ candidate_checks: counts.candidate_checks,
1113
+ peer_checks: counts.peer_checks,
1114
+ hashed: counts.hashed,
1115
+ hash_bytes: counts.hash_bytes,
1116
+ hash_reuses: counts.hash_reuses
1117
+ })
1118
+ }
1067
1119
  const preparePeers = () => {
1068
1120
  if (!group || group.unmatched) return
1069
1121
  group.unmatched = new Map()
1070
1122
  for (const [hash, value] of group.candidatesByHash) {
1071
- if (value.identities.size > 1) {
1072
- for (const candidate of value.candidates) {
1073
- group.matched.add(pathKey(candidate.path))
1074
- }
1075
- } else {
1076
- const identity = value.identities.values().next().value
1077
- group.unmatched.set(hash, {
1078
- identity,
1079
- candidates: value.candidates
1080
- })
1081
- }
1123
+ group.unmatched.set(hash, {
1124
+ identity: value.identities.values().next().value,
1125
+ candidate: value.candidate
1126
+ })
1082
1127
  }
1083
1128
  }
1084
- const finishGroup = () => {
1085
- if (!group) return
1086
- preparePeers()
1087
- for (const [hash, value] of group.candidatesByHash) {
1088
- for (const candidate of value.candidates) {
1089
- if (!group.matched.has(pathKey(candidate.path))) continue
1090
- verifiedMatches.push({ path: candidate.path, hash })
1129
+ const hashCandidates = async (peerIdentity = null) => {
1130
+ if (!group || group.candidatesHashed) return !!group
1131
+ const identities = new Set(group.candidates.map((candidate) =>
1132
+ this.automaticIdentityKey(candidate.entry)))
1133
+ if (identities.size < 2 &&
1134
+ (!peerIdentity || identities.has(peerIdentity))) {
1135
+ return false
1136
+ }
1137
+ group.candidatesHashed = true
1138
+ counts.candidates += group.candidates.length
1139
+ for (const candidate of group.candidates) {
1140
+ this.checkpoint(active)
1141
+ markStage("candidate-hash", candidate.entry)
1142
+ const verified = await this.verifiedAutomaticHash(
1143
+ active,
1144
+ candidate.entry,
1145
+ group.memory,
1146
+ verifiedHashes,
1147
+ counts,
1148
+ candidate
1149
+ )
1150
+ if (!verified) continue
1151
+ const hash = verified.hash
1152
+ let value = group.candidatesByHash.get(hash)
1153
+ if (!value) {
1154
+ value = { candidate: verified, identities: new Set() }
1155
+ group.candidatesByHash.set(hash, value)
1156
+ }
1157
+ value.identities.add(this.automaticIdentityKey(verified))
1158
+ if (value.identities.size > 1) {
1159
+ acceptProof(value.candidate, verified, hash)
1160
+ return true
1091
1161
  }
1092
1162
  }
1163
+ preparePeers()
1164
+ return true
1165
+ }
1166
+ const finishGroup = async () => {
1167
+ if (!group) return false
1168
+ await hashCandidates()
1169
+ if (proof) return true
1093
1170
  group = null
1171
+ return false
1172
+ }
1173
+ const complete = async () => {
1174
+ this.checkpoint(active)
1175
+ markStage("result-signature")
1176
+ const result = await this.vault.registry.automaticPrecheckResult(
1177
+ app, proof ? [proof] : [])
1178
+ this.checkpoint(active)
1179
+ return Object.assign({}, counts, {
1180
+ signature: result.signature,
1181
+ verified_files: Math.max(0, Number(result.files) || 0),
1182
+ duration_ms: Date.now() - startedAt
1183
+ })
1094
1184
  }
1095
1185
  let cursor = null
1096
1186
  do {
1187
+ markStage("candidate-query")
1188
+ counts.registry_pages += 1
1097
1189
  const page = await this.vault.registry.automaticPrecheckEntries(
1098
1190
  app, cursor, ENTRY_BATCH_SIZE)
1099
1191
  this.checkpoint(active)
1100
1192
  for (const entry of page.entries) {
1101
1193
  const key = `${entry.dev}\0${entry.size}`
1102
1194
  if (!group || group.key !== key) {
1103
- finishGroup()
1195
+ if (await finishGroup()) return await complete()
1104
1196
  group = {
1105
1197
  key,
1198
+ candidates: [],
1199
+ candidatesHashed: false,
1106
1200
  candidatesByHash: new Map(),
1107
- matched: new Set(),
1108
1201
  unmatched: null,
1109
1202
  memory: new Map()
1110
1203
  }
1111
1204
  }
1112
1205
  if (entry.kind === "changed") {
1113
- const candidate = entry
1114
1206
  this.checkpoint(active)
1115
- const verified = await this.verifiedAutomaticHash(
1116
- active, candidate, group.memory, verifiedHashes, counts)
1117
- if (!verified) continue
1118
- const hash = verified.hash
1119
- let value = group.candidatesByHash.get(hash)
1120
- if (!value) {
1121
- value = { candidates: [], identities: new Set() }
1122
- group.candidatesByHash.set(hash, value)
1123
- }
1124
- value.candidates.push(verified)
1125
- value.identities.add(this.automaticIdentityKey(verified))
1207
+ markStage("candidate-metadata", entry)
1208
+ counts.candidate_checks += 1
1209
+ const candidate = await this.currentAutomaticEntry(
1210
+ active, entry, counts)
1211
+ if (candidate) group.candidates.push(candidate)
1126
1212
  continue
1127
1213
  }
1128
- preparePeers()
1214
+ if (!group.candidatesHashed) await hashCandidates()
1215
+ if (proof) return await complete()
1216
+ if (group.candidatesHashed && !group.unmatched.size) continue
1217
+ markStage("peer-metadata", entry)
1218
+ counts.peer_checks += 1
1219
+ const peer = await this.currentAutomaticEntry(active, entry, counts)
1220
+ if (!peer) continue
1221
+ const identity = this.automaticIdentityKey(peer.entry)
1222
+ if (!await hashCandidates(identity)) continue
1223
+ if (proof) return await complete()
1129
1224
  if (!group.unmatched.size) continue
1225
+ if (![...group.unmatched.values()].some((wanted) =>
1226
+ wanted.identity !== identity)) continue
1130
1227
  this.checkpoint(active)
1228
+ markStage("peer-hash", entry)
1131
1229
  const verified = await this.verifiedAutomaticHash(
1132
- active, entry, group.memory, verifiedHashes, counts)
1230
+ active, entry, group.memory, verifiedHashes, counts, peer)
1133
1231
  const wanted = verified && group.unmatched.get(verified.hash)
1134
- const identity = verified && this.automaticIdentityKey(verified)
1135
1232
  if (!wanted || wanted.identity === identity) {
1136
1233
  continue
1137
1234
  }
1138
- for (const candidate of wanted.candidates) {
1139
- group.matched.add(pathKey(candidate.path))
1140
- }
1141
- group.unmatched.delete(verified.hash)
1235
+ acceptProof(wanted.candidate, verified, verified.hash)
1236
+ return await complete()
1142
1237
  }
1143
1238
  if (verifiedHashes.size >= ENTRY_BATCH_SIZE) {
1239
+ markStage("hash-cache-write")
1144
1240
  await this.vault.registry.rememberHashCache(
1145
1241
  [...verifiedHashes.values()])
1146
1242
  verifiedHashes.clear()
@@ -1148,24 +1244,26 @@ class AutomaticScans {
1148
1244
  }
1149
1245
  cursor = page.next_cursor
1150
1246
  } while (cursor)
1151
- finishGroup()
1152
- this.checkpoint(active)
1153
- const result = await this.vault.registry.automaticPrecheckResult(
1154
- app, verifiedMatches)
1155
- this.checkpoint(active)
1156
- return Object.assign({}, counts, {
1157
- signature: result.signature,
1158
- verified_files: Math.max(0, Number(result.files) || 0),
1159
- duration_ms: Date.now() - startedAt
1160
- })
1247
+ await finishGroup()
1248
+ return await complete()
1161
1249
  } finally {
1162
1250
  try {
1163
1251
  if (verifiedHashes.size) {
1164
- await this.vault.registry.rememberHashCache(
1165
- [...verifiedHashes.values()])
1252
+ try {
1253
+ await this.vault.registry.rememberHashCache(
1254
+ [...verifiedHashes.values()])
1255
+ } catch (error) {
1256
+ markStage("hash-cache-write")
1257
+ throw error
1258
+ }
1166
1259
  }
1167
1260
  } finally {
1168
- await this.vault.registry.abortAutomaticPrecheck(app)
1261
+ try {
1262
+ await this.vault.registry.abortAutomaticPrecheck(app)
1263
+ } catch (error) {
1264
+ markStage("staging-cleanup")
1265
+ throw error
1266
+ }
1169
1267
  }
1170
1268
  }
1171
1269
  }
@@ -1269,10 +1367,22 @@ class AutomaticScans {
1269
1367
  ? (error.code === "EVAULTCANCELLED" ? "cancelled" : "failed")
1270
1368
  : "complete",
1271
1369
  cancel_reason: reason,
1272
- duration_ms: result && result.duration_ms,
1370
+ duration_ms: result
1371
+ ? result.duration_ms
1372
+ : error && active.startedAt
1373
+ ? Date.now() - active.startedAt
1374
+ : undefined,
1375
+ failure_stage: error && active.stage,
1376
+ failure_path: error && (error.path || active.stagePath),
1377
+ failure_kind: error && active.stageKind,
1378
+ error_code: error && error.code,
1379
+ error_syscall: error && error.syscall,
1273
1380
  files: result && result.files,
1274
1381
  bytes: result && result.bytes,
1275
1382
  candidates: result && result.candidates,
1383
+ registry_pages: result && result.registry_pages,
1384
+ candidate_checks: result && result.candidate_checks,
1385
+ peer_checks: result && result.peer_checks,
1276
1386
  verified_files: result && result.verified_files,
1277
1387
  hashed: result && result.hashed,
1278
1388
  hash_bytes: result && result.hash_bytes,
@@ -2801,15 +2801,44 @@ class RegistryCore {
2801
2801
  let dev = Number(after.dev) || 0
2802
2802
  let size = Number(after.size) || 0
2803
2803
  let afterPath = typeof after.path === "string" ? after.path : ""
2804
+ const possiblePeer = `(
2805
+ EXISTS (
2806
+ SELECT 1 FROM automatic_precheck_files peer
2807
+ WHERE peer.app = candidate.app
2808
+ AND peer.dev = candidate.dev
2809
+ AND peer.size = candidate.size
2810
+ AND pinokio_path_key(peer.path) !=
2811
+ pinokio_path_key(candidate.path)
2812
+ )
2813
+ OR EXISTS (
2814
+ SELECT 1 FROM files peer
2815
+ WHERE peer.dev = candidate.dev
2816
+ AND peer.size = candidate.size
2817
+ AND peer.unavailable_reason IS NOT 'stale'
2818
+ AND pinokio_path_key(peer.path) !=
2819
+ pinokio_path_key(candidate.path)
2820
+ )
2821
+ OR EXISTS (
2822
+ SELECT 1 FROM anchors peer
2823
+ WHERE peer.verified_at IS NOT NULL
2824
+ AND peer.dev = candidate.dev
2825
+ AND peer.size = candidate.size
2826
+ AND pinokio_path_key(peer.path) !=
2827
+ pinokio_path_key(candidate.path)
2828
+ )
2829
+ )`
2804
2830
  if (!phase) {
2805
2831
  const group = this.database.prepare(`
2806
- SELECT dev, size
2807
- FROM automatic_precheck_files
2808
- WHERE app = @app AND (
2809
- @has_cursor = 0 OR dev > @dev OR (dev = @dev AND size > @size)
2810
- )
2811
- GROUP BY dev, size
2812
- ORDER BY dev, size
2832
+ SELECT candidate.dev, candidate.size
2833
+ FROM automatic_precheck_files candidate
2834
+ WHERE candidate.app = @app
2835
+ AND ${possiblePeer}
2836
+ AND (
2837
+ @has_cursor = 0 OR candidate.dev > @dev OR
2838
+ (candidate.dev = @dev AND candidate.size > @size)
2839
+ )
2840
+ GROUP BY candidate.dev, candidate.size
2841
+ ORDER BY candidate.dev, candidate.size
2813
2842
  LIMIT 1
2814
2843
  `).get({
2815
2844
  app,
@@ -3001,29 +3030,42 @@ class RegistryCore {
3001
3030
  hashes.set(canonicalPathKey(entry.path), entry.hash)
3002
3031
  }
3003
3032
  const rows = this.database.prepare(`
3004
- SELECT path, dev, size
3033
+ SELECT path, dev, ino, size, mtime, ctime
3005
3034
  FROM automatic_precheck_files
3006
3035
  WHERE app = ?
3007
- ORDER BY pinokio_path_key(path), dev, size
3036
+ ORDER BY pinokio_path_key(path), path, dev, ino, size, mtime, ctime
3008
3037
  `).iterate(app)
3009
3038
  const digest = crypto.createHash("sha256")
3010
- let files = 0
3039
+ let proof = null
3040
+ digest.update("automatic-precheck-snapshots\0")
3011
3041
  for (const row of rows) {
3012
- const hash = hashes.get(canonicalPathKey(row.path))
3013
- if (!hash) continue
3014
- digest.update(canonicalPathKey(row.path))
3015
- digest.update("\0")
3016
- digest.update(String(row.dev))
3017
- digest.update("\0")
3018
- digest.update(String(row.size))
3019
- digest.update("\0")
3020
- digest.update(hash)
3021
- digest.update("\n")
3022
- files += 1
3042
+ const canonicalPath = canonicalPathKey(row.path)
3043
+ const hash = hashes.get(canonicalPath)
3044
+ if (!proof && hash) proof = { canonicalPath, row, hash }
3045
+ for (const value of [
3046
+ canonicalPath,
3047
+ row.dev,
3048
+ row.ino,
3049
+ row.size,
3050
+ row.mtime,
3051
+ row.ctime
3052
+ ]) {
3053
+ digest.update(String(value))
3054
+ digest.update("\0")
3055
+ }
3023
3056
  }
3057
+ if (!proof) return { signature: null, files: 0 }
3058
+ digest.update("automatic-precheck-proof\0")
3059
+ digest.update(proof.canonicalPath)
3060
+ digest.update("\0")
3061
+ digest.update(String(proof.row.dev))
3062
+ digest.update("\0")
3063
+ digest.update(String(proof.row.size))
3064
+ digest.update("\0")
3065
+ digest.update(proof.hash)
3024
3066
  return {
3025
- signature: files ? digest.digest("hex") : null,
3026
- files
3067
+ signature: digest.digest("hex"),
3068
+ files: 1
3027
3069
  }
3028
3070
  }
3029
3071
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.74",
3
+ "version": "8.0.76",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1678,6 +1678,40 @@ body.vault-page .vault-view-tabs {
1678
1678
  .vault-view-tabs .vault-nav-count {
1679
1679
  font-size: 10.5px;
1680
1680
  }
1681
+ .vault-view-tabs .vault-nav-row.attention {
1682
+ background: color-mix(in srgb, var(--vault-warning) 9%, var(--task-panel));
1683
+ color: var(--vault-warning);
1684
+ }
1685
+ .vault-view-tabs .vault-nav-row.attention:hover {
1686
+ background: color-mix(in srgb, var(--vault-warning) 14%, var(--task-panel));
1687
+ }
1688
+ .vault-view-tabs .vault-nav-row.attention.selected {
1689
+ border-bottom-color: var(--task-accent);
1690
+ }
1691
+ .vault-view-tabs .vault-nav-row.attention .vault-nav-name {
1692
+ color: var(--vault-warning);
1693
+ font-weight: 680;
1694
+ }
1695
+ .vault-view-tabs .vault-nav-row.attention .vault-nav-name::before {
1696
+ width: 6px;
1697
+ height: 6px;
1698
+ display: inline-block;
1699
+ margin-right: 6px;
1700
+ border-radius: 50%;
1701
+ background: var(--vault-warning);
1702
+ content: "";
1703
+ vertical-align: 1px;
1704
+ }
1705
+ .vault-view-tabs .vault-nav-row.attention .vault-nav-count {
1706
+ min-width: 20px;
1707
+ padding: 1px 5px;
1708
+ border-radius: 5px;
1709
+ background: color-mix(in srgb, var(--vault-warning) 72%, var(--task-panel));
1710
+ color: #241300;
1711
+ font-weight: 720;
1712
+ line-height: 1.35;
1713
+ text-align: center;
1714
+ }
1681
1715
  .vault-pane-action-note { color: var(--task-muted); font-size: 11.5px; white-space: nowrap; }
1682
1716
  .vault-toolbar {
1683
1717
  display: flex;
@@ -707,16 +707,20 @@ const eventLabels = {
707
707
  }
708
708
  const renderViews = () => {
709
709
  const counts = state.data.inventory.counts
710
+ const attentionViews = new Set(["duplicates", "reclaimable"])
710
711
  el("views-label").textContent = COPY.views
711
712
  const views = IS_APP_MODE
712
713
  ? ["all", "duplicates", "unavailable", "shared", "tracked", "activity"]
713
714
  : ["all", "duplicates", "unavailable", "shared", "tracked", "reclaimable", "activity"]
714
- el("vault-views").innerHTML = views.map((view) => `
715
- <button class="vault-nav-row ${state.view === view ? "selected" : ""}" type="button" data-view="${view}" ${state.view === view ? 'aria-current="page"' : ""}>
715
+ el("vault-views").innerHTML = views.map((view) => {
716
+ const needsAttention = attentionViews.has(view) && Number(counts[view]) > 0
717
+ return `
718
+ <button class="vault-nav-row ${state.view === view ? "selected" : ""} ${needsAttention ? "attention" : ""}" type="button" data-view="${view}" ${state.view === view ? 'aria-current="page"' : ""}>
716
719
  <i class="${viewIcon[view]}"></i>
717
720
  <span class="vault-nav-copy"><span class="vault-nav-name">${esc(viewLabel[view])}</span></span>
718
- <span class="vault-nav-count ${view === "duplicates" && counts[view] ? "attention" : ""}">${counts[view]}</span>
719
- </button>`).join("")
721
+ <span class="vault-nav-count ${needsAttention ? "attention" : ""}">${counts[view]}</span>
722
+ </button>`
723
+ }).join("")
720
724
  }
721
725
 
722
726
  const renderSourceNode = (source, depth, counts) => {