#!/usr/bin/env node
/**
* Direct restore — import notes from portable backup directly into Hyperdrives.
* Bypasses HTTP API (no auth needed). Server must be STOPPED.
*
* Usage:
* node scripts/restore-direct.js <backup-dir> [storage-path]
*/
import Corestore from 'corestore'
import Hyperbee from 'hyperbee'
import Hyperdrive from 'hyperdrive'
import fs from 'fs'
import path from 'path'
import crypto from 'crypto'
const args = process.argv.slice(2)
const backupDir = args[0]
const storagePath = args[1] || process.env.HOME + '/.config/hypernote/server-data'
if (!backupDir || !fs.existsSync(backupDir)) {
console.log('Usage: node scripts/restore-direct.js <backup-dir> [storage-path]')
process.exit(1)
}
console.log('=== Direct Restore ===')
console.log('Backup:', backupDir)
console.log('Storage:', storagePath)
console.log('')
const store = new Corestore(storagePath)
await store.ready()
const registryCore = store.get({ name: 'registry' })
const registry = new Hyperbee(registryCore, { keyEncoding: 'utf-8', valueEncoding: 'json' })
await registry.ready()
// Build drive index from registry
const driveIndex = new Map()
for await (const entry of registry.createReadStream({ gte: 'drive:', lt: 'drive:\xff' })) {
const slug = entry.key.replace('drive:', '')
driveIndex.set(slug, entry.value)
}
let totalRestored = 0
let totalSkipped = 0
let totalCreated = 0
const notebooksDir = path.join(backupDir, 'notebooks')
if (!fs.existsSync(notebooksDir)) {
console.log('No notebooks directory found')
await store.close()
process.exit(1)
}
for (const slug of fs.readdirSync(notebooksDir)) {
const nbDir = path.join(notebooksDir, slug)
if (!fs.statSync(nbDir).isDirectory()) continue
const notesDir = path.join(nbDir, 'notes')
if (!fs.existsSync(notesDir)) continue
console.log(`\n=== ${slug} ===`)
// Get or create drive for this notebook
const driveInfo = driveIndex.get(slug)
let drive
if (driveInfo && driveInfo.key) {
// Skip remote drives — we can only write to local ones
if (driveInfo.isLocal === false) {
console.log(' SKIP: remote notebook (read-only)')
continue
}
drive = new Hyperdrive(store.namespace('notebook-' + slug), Buffer.from(driveInfo.key, 'hex'))
await Promise.race([
drive.ready(),
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 10000))
]).catch(() => { drive = null })
} else {
// Create new drive for this notebook
drive = new Hyperdrive(store.namespace('notebook-' + slug))
await drive.ready()
const keyHex = drive.key.toString('hex')
const dkeyHex = drive.discoveryKey.toString('hex')
await registry.put('drive:' + slug, { key: keyHex, discoveryKey: dkeyHex, slug, isLocal: true })
driveIndex.set(slug, { key: keyHex })
console.log(' Created drive for: ' + slug)
totalCreated++
}
// Check if drive opened and is writable
if (!drive) {
console.log(' SKIP: drive failed to open')
continue
}
if (!drive.db.feed.writable) {
console.log(' SKIP: drive is read-only (remote notebook)')
continue
}
// Read existing note IDs from drive
const existingIds = new Set()
try {
for await (const f of drive.readdir('/notes')) {
if (f.endsWith('.json')) existingIds.add(f.replace('.json', ''))
}
} catch {}
// Find all meta files in backup
const metaFiles = fs.readdirSync(notesDir).filter(f => f.endsWith('.meta.json'))
for (const metaFile of metaFiles) {
const meta = JSON.parse(fs.readFileSync(path.join(notesDir, metaFile), 'utf-8'))
const noteId = meta.id
const noteSlug = meta.slug
// Skip if already exists
if (existingIds.has(noteId)) {
totalSkipped++
continue
}
// Read content
const baseName = metaFile.replace('.meta.json', '')
let content = ''
const mdFile = path.join(notesDir, baseName + '.md')
const jsonFile = path.join(notesDir, baseName + '.json')
if (fs.existsSync(mdFile)) {
content = fs.readFileSync(mdFile, 'utf-8')
} else if (fs.existsSync(jsonFile)) {
content = fs.readFileSync(jsonFile, 'utf-8')
}
// Write note to drive
const noteObj = {
id: noteId,
slug: noteSlug || baseName,
title: meta.title || 'Untitled',
content,
tags: meta.tags || [],
type: meta.type || 'markdown',
createdAt: meta.createdAt || Date.now(),
updatedAt: meta.updatedAt || Date.now()
}
await drive.put('/notes/' + noteId + '.json', Buffer.from(JSON.stringify(noteObj, null, 2)))
console.log(' + ' + (meta.title || noteId))
totalRestored++
}
// Update notebook metadata noteCount
const nbMeta = await registry.get('notebook:' + slug)
if (nbMeta && nbMeta.value) {
const updated = { ...nbMeta.value, noteCount: existingIds.size + totalRestored }
await registry.put('notebook:' + slug, updated)
}
}
// Restore media from media-all
const mediaAllDir = path.join(backupDir, 'media-all')
if (fs.existsSync(mediaAllDir)) {
console.log('\n=== Restoring media ===')
// Open MediaStore
const mediaStore = store.namespace('media-store')
const blobCore = mediaStore.get({ name: 'blobs' })
const indexCore = mediaStore.get({ name: 'index' })
await blobCore.ready()
await indexCore.ready()
// Dynamic import for Hyperblobs
const { default: Hyperblobs } = await import('hyperblobs')
const blobs = new Hyperblobs(blobCore)
const index = new Hyperbee(indexCore, { keyEncoding: 'utf-8', valueEncoding: 'json' })
await index.ready()
let mediaRestored = 0
let mediaSkipped = 0
for (const file of fs.readdirSync(mediaAllDir)) {
const ext = path.extname(file)
const hash = path.basename(file, ext)
// Skip if already in index
const existing = await index.get('blob:' + hash)
if (existing) { mediaSkipped++; continue }
const MIME = {
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
'.mp4': 'video/mp4', '.webm': 'video/webm', '.mp3': 'audio/mpeg'
}
const mimeType = MIME[ext] || 'application/octet-stream'
const data = fs.readFileSync(path.join(mediaAllDir, file))
const blobId = await blobs.put(data)
await index.put('blob:' + hash, { blobId, size: data.length, mimeType, createdAt: Date.now() })
console.log(' + ' + file + ' (' + (data.length / 1024).toFixed(0) + 'KB)')
mediaRestored++
}
console.log(`\nMedia: ${mediaRestored} restored, ${mediaSkipped} skipped (already exist)`)
}
console.log(`\n=== Restore Complete ===`)
console.log(`Notes restored: ${totalRestored}`)
console.log(`Notes skipped: ${totalSkipped} (already exist)`)
console.log(`Drives created: ${totalCreated}`)
await store.close()