// 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')
}