OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-inspector / lib / drive-ls.js

204 lines · javascript ref:
// List all files in a Hyperdrive stored in RocksDB
// Works read-only on locked databases — reads raw Hyperbee blocks
import RocksDB from 'rocksdb-native'
import b4a from 'b4a'
import { decodeNode, decodeDriveEntry, stripSubEncoder, decodeHeader } from './block-reader.js'

function encodeVarint (n) {
  const bytes = []
  while (n >= 0x80) { bytes.push((n & 0x7f) | 0x80); n >>>= 7 }
  bytes.push(n & 0x7f)
  return Buffer.from(bytes)
}

// Extract all Hyperdrive file entries from a specific dataPointer in a corestore RocksDB
export async function listDriveFromDB (dbPath, dataPointer, opts = {}) {
  const { readOnly = false, maxBlocks = Infinity } = opts

  const db = new RocksDB(dbPath, { readOnly, columnFamilies: ['corestore'] })
  await db.ready()

  const cs = db.columnFamily('corestore')

  // Key range for data blocks: varint(4) + varint(dataPointer) + varint(3=DATA_BLOCK)
  const prefix = Buffer.concat([encodeVarint(4), encodeVarint(dataPointer), encodeVarint(3)])
  const endPrefix = Buffer.concat([encodeVarint(4), encodeVarint(dataPointer), encodeVarint(4)])

  const files = []
  let header = null
  let blocksRead = 0

  const iter = cs.iterator({ gte: prefix, lt: endPrefix, limit: maxBlocks })

  await new Promise((resolve, reject) => {
    iter.on('data', ({ key, value }) => {
      blocksRead++
      if (!value) return

      // Decode block index from key
      let pos = prefix.length
      let blockIdx = 0
      let shift = 0
      while (pos < key.length) {
        const byte = key[pos]
        blockIdx |= (byte & 0x7f) << shift
        pos++
        if ((byte & 0x80) === 0) break
        shift += 7
      }

      if (blockIdx === 0) {
        try { header = decodeHeader(value) } catch {}
        return
      }

      try {
        const node = decodeNode(value)
        if (!node.key || node.key.length === 0) return

        const rawKey = stripSubEncoder(node.key)
        let filePath = ''
        try { filePath = b4a.toString(rawKey, 'utf8') } catch { return }
        if (!filePath.startsWith('/')) return

        let entry = null
        try { entry = decodeDriveEntry(node.value) } catch {}
        if (!entry) return

        files.push({
          path: filePath,
          blob: entry.blob,
          executable: entry.executable,
          linkname: entry.linkname,
          metadata: entry.metadata,
          bytes: entry.blob?.byteLength || 0,
          seq: blockIdx
        })
      } catch {}
    })
    iter.on('end', resolve)
    iter.on('error', reject)
  })

  await db.close()

  return { header, files, blocksRead }
}

// Build a directory tree from flat file list
export function buildTree (files) {
  const root = { name: '/', children: new Map(), files: [], totalBytes: 0, totalFiles: 0 }

  for (const f of files) {
    const parts = f.path.split('/').filter(Boolean)
    let node = root

    // Walk/create directory nodes
    for (let i = 0; i < parts.length - 1; i++) {
      if (!node.children.has(parts[i])) {
        node.children.set(parts[i], {
          name: parts[i],
          children: new Map(),
          files: [],
          totalBytes: 0,
          totalFiles: 0
        })
      }
      node = node.children.get(parts[i])
    }

    // Add file to its parent directory
    const fileName = parts[parts.length - 1]
    node.files.push({ name: fileName, ...f })
    node.totalFiles++
    node.totalBytes += f.bytes

    // Propagate totals up
    let parent = root
    for (let i = 0; i < parts.length - 1; i++) {
      parent.totalFiles++
      parent.totalBytes += f.bytes
      parent = parent.children.get(parts[i])
    }
  }

  return root
}

// Format tree as a string (like `ls -lR` or `tree`)
export function formatTree (root, opts = {}) {
  const { maxDepth = 3, minBytes = 0, showFiles = true, indent = '' } = opts
  const lines = []

  function fmt (bytes) {
    if (bytes >= 1024 * 1024 * 1024) return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'
    if (bytes >= 1024 * 1024) return (bytes / 1024 / 1024).toFixed(2) + ' MB'
    if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB'
    return bytes + ' B'
  }

  function walk (node, depth, prefix) {
    if (depth > maxDepth) return

    // Sort children by totalBytes descending
    const dirs = [...node.children.entries()].sort((a, b) => b[1].totalBytes - a[1].totalBytes)

    for (const [name, child] of dirs) {
      if (child.totalBytes < minBytes && minBytes > 0) continue
      const sizeStr = fmt(child.totalBytes).padStart(10)
      const countStr = `${child.totalFiles} files`.padStart(10)
      lines.push(`${prefix}${sizeStr}  ${countStr}  ${name}/`)
      walk(child, depth + 1, prefix + '  ')
    }

    // Show files at this level
    if (showFiles && depth <= maxDepth) {
      const sortedFiles = [...node.files].sort((a, b) => b.bytes - a.bytes)
      for (const f of sortedFiles) {
        if (f.bytes < minBytes && minBytes > 0) continue
        const sizeStr = fmt(f.bytes).padStart(10)
        const exec = f.executable ? ' *' : ''
        const link = f.linkname ? ` -> ${f.linkname}` : ''
        const blk = f.blob ? `  [blk ${f.blob.blockOffset}..${f.blob.blockOffset + f.blob.blockLength}]` : ''
        lines.push(`${prefix}${sizeStr}            ${f.name}${exec}${link}${blk}`)
      }
    }
  }

  const totalStr = fmt(root.totalBytes).padStart(10)
  lines.push(`${totalStr}  ${root.totalFiles} files  /`)
  walk(root, 1, '  ')

  return lines.join('\n')
}

// Format as flat ls output (like `ls -lhRS`)
export function formatFlat (files, opts = {}) {
  const { sortBy = 'size', limit = Infinity, filter = null } = opts
  const lines = []

  let sorted = [...files]
  if (sortBy === 'size') sorted.sort((a, b) => b.bytes - a.bytes)
  else if (sortBy === 'path') sorted.sort((a, b) => a.path.localeCompare(b.path))

  if (filter) {
    sorted = sorted.filter(f => f.path.includes(filter))
  }

  function fmt (bytes) {
    if (bytes >= 1024 * 1024 * 1024) return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'
    if (bytes >= 1024 * 1024) return (bytes / 1024 / 1024).toFixed(2) + ' MB'
    if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB'
    return bytes + ' B'
  }

  for (const f of sorted.slice(0, limit)) {
    const sizeStr = fmt(f.bytes).padStart(10)
    const exec = f.executable ? ' *' : ''
    const blk = f.blob ? `  [blk ${f.blob.blockOffset}..${f.blob.blockOffset + f.blob.blockLength}]` : ''
    lines.push(`${sizeStr}  ${f.path}${exec}${blk}`)
  }

  return lines.join('\n')
}