| … | … | @@ -43,7 +43,7 @@ |
| 43 | 43 | |
| 44 | 44 | import EventEmitter from '#node:events' |
| 45 | 45 | |
| 46 | | import { KIND_DIR } from 'hyper-overlay/tree' |
| 46 | import { computeTreeHash } from 'hyper-overlay/tree' |
| 47 | 47 | |
| 48 | 48 | const DEFAULT_INTERVAL = 30 * 60 * 1000 // 30 min |
| 49 | 49 | |
| … | … | @@ -191,41 +191,46 @@ export class PghRuntime extends EventEmitter { |
| 191 | 191 | */ |
| 192 | 192 | export async function installTree (store, rootHash, fetcher, opts = {}) { |
| 193 | 193 | store.init() |
| 194 | | let materialized = 0 |
| 195 | | let skipped = 0 |
| 196 | | const seen = new Set() |
| 197 | | |
| 198 | | async function visit (hash) { |
| 199 | | if (seen.has(hash)) return |
| 200 | | seen.add(hash) |
| 201 | 194 | |
| 202 | | if (store.hasTree(hash)) { |
| 203 | | skipped++ |
| 204 | | return |
| 205 | | } |
| 206 | | |
| 207 | | const tree = await fetcher.getTree(hash) |
| 208 | | if (!tree || !Array.isArray(tree.entries)) { |
| 209 | | throw new Error('fetcher returned invalid tree for ' + hash) |
| 210 | | } |
| 195 | if (store.hasTree(rootHash)) { |
| 196 | return { materialized: 0, skipped: 1 } |
| 197 | } |
| 211 | 198 | |
| 212 | | for (const e of tree.entries) { |
| 213 | | if (e.kind === KIND_DIR) await visit(e.childHash) |
| 214 | | } |
| 199 | const tree = await fetcher.getTree(rootHash) |
| 200 | if (!tree || !Array.isArray(tree.entries)) { |
| 201 | throw new Error('fetcher returned invalid tree for ' + rootHash) |
| 202 | } |
| 215 | 203 | |
| 216 | | await store.materialize(hash, tree.entries, { |
| 217 | | fetchContent: fetcher.getContent.bind(fetcher), |
| 218 | | fetchTree: async (h) => { |
| 219 | | const t = await fetcher.getTree(h) |
| 220 | | return t.entries |
| 221 | | }, |
| 222 | | source: opts.source |
| 223 | | }) |
| 224 | | materialized++ |
| 204 | // Content-address verification: entries MUST hash to rootHash. A P2P peer |
| 205 | // can answer with empty/wrong entries to signal "I don't have this tree"; |
| 206 | // without this check that silently materializes an empty/partial dir. Let |
| 207 | // the caller retry on another peer (EHASHMISMATCH). Matches pgh's installTree. |
| 208 | const actualRootHash = computeTreeHash(tree.entries) |
| 209 | if (actualRootHash !== rootHash) { |
| 210 | throw Object.assign( |
| 211 | new Error('root tree-hash mismatch: expected ' + rootHash.slice(0, 12) + '…, got ' + actualRootHash.slice(0, 12) + '… (peer may not have this tree)'), |
| 212 | { code: 'EHASHMISMATCH', expected: rootHash, actual: actualRootHash } |
| 213 | ) |
| 225 | 214 | } |
| 226 | 215 | |
| 227 | | await visit(rootHash) |
| 228 | | return { materialized, skipped } |
| 216 | // Single self-contained materialize — store.materialize recursively inlines |
| 217 | // subtree content via fetchTree/fetchContent. Subtree hashes do NOT get their |
| 218 | // own dependencies/<subhash>/ dir (that would duplicate every file N times |
| 219 | // across levels); dedup happens at the top-level tree layer. `skipPath` skips |
| 220 | // foreign subtrees (e.g. by-arch/<other-host>/). Mirrors pgh's tree-install.js |
| 221 | // — the manual subtree recursion this replaced created redundant store dirs |
| 222 | // and could not honor skipPath. |
| 223 | await store.materialize(rootHash, tree.entries, { |
| 224 | fetchContent: fetcher.getContent.bind(fetcher), |
| 225 | fetchTree: async (h) => { |
| 226 | const t = await fetcher.getTree(h) |
| 227 | return t.entries |
| 228 | }, |
| 229 | source: opts.source, |
| 230 | skipPath: opts.skipPath |
| 231 | }) |
| 232 | |
| 233 | return { materialized: 1, skipped: 0 } |
| 229 | 234 | } |
| 230 | 235 | |
| 231 | 236 | /** |
| … | … | @@ -279,7 +284,7 @@ export async function installRelease (store, release, fetcher, opts = {}) { |
| 279 | 284 | let materialized = 0 |
| 280 | 285 | let skipped = 0 |
| 281 | 286 | for (const hash of closureHashes) { |
| 282 | | const r = await installTree(store, hash, fetcher, { source }) |
| 287 | const r = await installTree(store, hash, fetcher, { source, skipPath: opts.skipPath }) |
| 283 | 288 | materialized += r.materialized |
| 284 | 289 | skipped += r.skipped |
| 285 | 290 | } |