OverviewFilesCommitsIssuesDocsReleasesPeers

pearbrowser-desktop / scripts / extract-drive.js

117 lines · javascript ref:
/**
 * Extract every file from a published Hyperdrive to a local directory.
 *
 * Useful when you've lost the local source for a site that's still
 * being seeded somewhere on the swarm (or pinned on HiveRelay).
 *
 * Usage:
 *   node scripts/extract-drive.js <64-hex-drive-key> <out-dir>
 *
 * Example — pull the PearBrowser homepage drive's contents:
 *   node scripts/extract-drive.js \
 *     f1e72b4bec5abd134aa3fde1ed58aa4a9ab97ff1a90ca29a258de0dacdce65fb \
 *     ./recovered-homepage
 *
 * Mechanics:
 *   1. Boot a tiny Corestore + Hyperswarm in /tmp
 *   2. Open the Hyperdrive at the given key
 *   3. Join the discovery topic and wait for at least one peer
 *   4. Walk drive.list('/') and write each file to <out-dir>
 */

import Hyperswarm from 'hyperswarm'
import Corestore from 'corestore'
import Hyperdrive from 'hyperdrive'
import b4a from 'b4a'
import { mkdtempSync, mkdirSync, writeFileSync, existsSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { tmpdir } from 'node:os'

const [, , keyArg, outArg] = process.argv
if (!keyArg || !outArg) {
  console.error('usage: node scripts/extract-drive.js <64-hex-key> <out-dir>')
  process.exit(2)
}
if (!/^[0-9a-f]{64}$/i.test(keyArg)) {
  console.error('error: key must be 64-char hex')
  process.exit(2)
}

const key = b4a.from(keyArg.toLowerCase(), 'hex')
const outDir = resolve(outArg)

console.log('🍐 Extracting drive')
console.log('   key:', keyArg)
console.log('   out:', outDir)
console.log()

// Throwaway storage so we don't pollute anything
const storage = mkdtempSync(join(tmpdir(), 'extract-drive-'))
const store = new Corestore(storage)
const swarm = new Hyperswarm()
const drive = new Hyperdrive(store, key)

swarm.on('connection', (conn) => store.replicate(conn))

await drive.ready()
console.log('   discoveryKey:', b4a.toString(drive.discoveryKey, 'hex').slice(0, 12) + '…')
console.log()

const discovery = swarm.join(drive.discoveryKey, { server: false, client: true })
process.stdout.write('   Looking for peers...')

await new Promise((resolve, reject) => {
  let connected = false
  const onConn = () => {
    if (connected) return
    connected = true
    process.stdout.write(' ✓ found peer\n')
    // Give a moment to start receiving blocks
    setTimeout(resolve, 1500)
  }
  swarm.on('connection', onConn)
  setTimeout(() => {
    if (!connected) {
      process.stdout.write(' ✗\n')
      reject(new Error('no peers within 30s — drive may be unreachable'))
    }
  }, 30000)
})
await discovery.flushed().catch(() => {})

// Walk the drive
console.log()
console.log('   Walking drive contents...')
mkdirSync(outDir, { recursive: true })

let count = 0
let bytes = 0
const errors = []

for await (const entry of drive.list('/', { recursive: true })) {
  if (!entry.value || !entry.value.blob) continue
  try {
    const buf = await drive.get(entry.key)
    if (!buf) continue
    const filePath = join(outDir, entry.key)
    mkdirSync(dirname(filePath), { recursive: true })
    writeFileSync(filePath, buf)
    count += 1
    bytes += buf.length
    console.log(`     + ${entry.key} (${buf.length} bytes)`)
  } catch (err) {
    errors.push({ key: entry.key, error: err.message })
    console.error(`     ✗ ${entry.key}: ${err.message}`)
  }
}

console.log()
console.log(`✅ Extracted ${count} file(s), ${(bytes / 1024).toFixed(1)} KB total`)
if (errors.length) console.log(`   ${errors.length} error(s) — see above`)
console.log()
console.log(`   Output: ${outDir}`)

await swarm.destroy()
await store.close()
process.exit(0)