OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-inspector / lib / drive-inspector.js

183 lines · javascript ref:
import Corestore from 'corestore'
import Hyperdrive from 'hyperdrive'
import b4a from 'b4a'
import { enumerateCores } from './core-enumerator.js'
import { join } from 'path'

export async function openDrive (storagePath, opts = {}) {
  const { key = null } = opts
  const store = new Corestore(storagePath)
  await store.ready()

  const drive = key
    ? new Hyperdrive(store, b4a.isBuffer(key) ? key : b4a.from(key, 'hex'))
    : new Hyperdrive(store)

  await drive.ready()
  return { store, drive }
}

// Discover all Hyperdrives in a corestore by finding cores with Hyperbee headers
export async function discoverDrives (storagePath, opts = {}) {
  const dbPath = join(storagePath, 'db')
  const cores = await enumerateCores(dbPath, opts)

  const store = new Corestore(storagePath)
  await store.ready()

  const drives = []

  for (const c of cores) {
    if (!c.publicKey) continue

    const keyBuf = b4a.from(c.publicKey, 'hex')
    const core = store.get({ key: keyBuf })
    await core.ready()

    if (core.length > 0) {
      try {
        const block0 = await core.get(0, { wait: false })
        if (block0) {
          // Check for Hyperbee header signature: starts with protobuf field for "hyperbee"
          const hexStart = b4a.toString(block0.subarray(0, 12), 'hex')
          if (hexStart.startsWith('0a0868797065726265')) {
            // Extract content key from header
            let contentKey = null
            if (block0.length > 12) {
              try {
                // The content key is at offset 12 (tag 0x12, length 0x22=34, then 0x0a 0x20 + 32 bytes key)
                const contentKeyStart = block0.indexOf(Buffer.from([0x0a, 0x20]), 10)
                if (contentKeyStart >= 0 && contentKeyStart + 34 <= block0.length) {
                  contentKey = b4a.toString(block0.subarray(contentKeyStart + 2, contentKeyStart + 34), 'hex')
                }
              } catch {}
            }

            // Scan some blocks for embedded file paths
            const filePaths = []
            const scanLimit = Math.min(core.length, 200)
            for (let i = 0; i < scanLimit; i++) {
              try {
                const block = await core.get(i, { wait: false })
                if (!block) continue
                const str = b4a.toString(block, 'utf8')
                const paths = str.match(/\/[a-zA-Z0-9._\-/]+\.[a-zA-Z0-9]+/g)
                if (paths) {
                  for (const p of paths) {
                    if (!filePaths.includes(p) && filePaths.length < 100) filePaths.push(p)
                  }
                }
              } catch { break }
            }

            drives.push({
              key: c.publicKey,
              discoveryKey: c.discoveryKey,
              corePointer: c.corePointer,
              alias: c.alias,
              version: core.length,
              byteLength: core.byteLength,
              writable: core.writable,
              contentKey,
              filePaths
            })
          }
        }
      } catch {}
    }

    await core.close()
  }

  await store.close()
  return drives
}

export async function listDriveFiles (drive, folder = '/', opts = {}) {
  const { recursive = true } = opts
  const files = []

  const stream = drive.list(folder, { recursive })

  return new Promise((resolve, reject) => {
    stream.on('data', (entry) => {
      const blob = entry.value?.blob
      files.push({
        path: entry.key,
        executable: entry.value?.executable || false,
        linkname: entry.value?.linkname || null,
        metadata: entry.value?.metadata || null,
        blobOffset: blob?.blockOffset,
        blobLength: blob?.blockLength,
        byteOffset: blob?.byteOffset,
        byteLength: blob?.byteLength
      })
    })
    stream.on('end', () => resolve(files))
    stream.on('error', reject)
  })
}

export async function getDriveStats (drive) {
  const files = await listDriveFiles(drive)

  let totalBytes = 0
  let blobBlocks = 0
  const extensions = new Map()
  const topDirs = new Map()

  for (const f of files) {
    const bytes = f.byteLength || 0
    totalBytes += bytes
    blobBlocks += f.blobLength || 0

    // Count by extension
    const ext = f.path.includes('.') ? f.path.split('.').pop() : '(none)'
    const extInfo = extensions.get(ext) || { count: 0, bytes: 0 }
    extInfo.count++
    extInfo.bytes += bytes
    extensions.set(ext, extInfo)

    // Count by top-level directory
    const parts = f.path.split('/')
    const topDir = parts.length > 2 ? '/' + parts[1] : '/'
    const dirInfo = topDirs.get(topDir) || { count: 0, bytes: 0 }
    dirInfo.count++
    dirInfo.bytes += bytes
    topDirs.set(topDir, dirInfo)
  }

  return {
    fileCount: files.length,
    totalBytes,
    blobBlocks,
    driveKey: b4a.toString(drive.key, 'hex'),
    driveVersion: drive.version,
    writable: drive.writable,
    extensions: [...extensions.entries()]
      .map(([ext, info]) => ({ ext, ...info }))
      .sort((a, b) => b.bytes - a.bytes),
    topDirs: [...topDirs.entries()]
      .map(([dir, info]) => ({ dir, ...info }))
      .sort((a, b) => b.bytes - a.bytes),
    files
  }
}

export async function readDriveFile (drive, path) {
  return drive.get(path)
}

export async function driveSummary (drive) {
  const stats = await getDriveStats(drive)
  return {
    key: stats.driveKey,
    version: stats.driveVersion,
    writable: stats.writable,
    files: stats.fileCount,
    totalBytes: stats.totalBytes,
    topExtensions: stats.extensions.slice(0, 10),
    topDirs: stats.topDirs.slice(0, 10)
  }
}