// Persist contentHash -> (file, offset, length) beside the drive.
//
// A drive stores index -> contentHash. To SERVE it, something must turn a
// contentHash back into bytes. Rebuilding that by re-chunking every published
// file costs O(all bytes) on every boot — untenable for a seeder holding a
// repo's whole release history.
//
// So write it once, at publish time, when we already know both halves. Paths
// are stored RELATIVE to a root so the index survives the tree being moved,
// and so it never leaks an absolute layout into a file peers could see.
//
// This is the same reverse index hyper-overlay is missing (see its issue #140);
// scoped here to one drive, it is cheap: ~90 bytes per unique chunk as JSON.
const { isBare } = require('which-runtime')
const fs = isBare ? require('bare-fs') : require('fs')
const path = isBare ? require('bare-path') : require('path')
const FILE = 'locations.json'
const VERSION = 1
function locationsPath (storeDir) {
return path.join(storeDir, FILE)
}
/**
* @param {string} storeDir the drive's store directory
* @param {Map<string,{file,offset,length}>} locations from ContentStore
* @param {string} root paths are recorded relative to this
*/
function saveLocations (storeDir, locations, root) {
const entries = []
for (const [hash, loc] of locations) {
const rel = path.relative(root, loc.file)
// Never record something outside the root — it would not resolve on another
// machine, and it would bake a local layout into a shared artifact.
if (rel.startsWith('..') || path.isAbsolute(rel)) continue
entries.push([hash, rel, loc.offset, loc.length])
}
fs.mkdirSync(storeDir, { recursive: true })
const tmp = locationsPath(storeDir) + '.tmp'
fs.writeFileSync(tmp, JSON.stringify({ version: VERSION, entries }))
fs.renameSync(tmp, locationsPath(storeDir))
return entries.length
}
/**
* Merge a saved index into a ContentStore, resolving paths against `root`.
* Returns how many entries were loaded, or 0 when there is no index.
*/
function loadLocations (storeDir, content, root) {
let raw
try {
raw = JSON.parse(fs.readFileSync(locationsPath(storeDir), 'utf8'))
} catch {
return 0
}
if (!raw || raw.version !== VERSION || !Array.isArray(raw.entries)) return 0
let n = 0
for (const [hash, rel, offset, length] of raw.entries) {
content.add(hash, path.join(root, rel), offset, length)
n++
}
return n
}
module.exports = { saveLocations, loadLocations, locationsPath }