OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-vdrive / lib / locations.cjs

68 lines · javascript ref:
// 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 }