| … | … | @@ -0,0 +1,96 @@ |
| 1 | /** |
| 2 | * Data Export Module |
| 3 | * |
| 4 | * Exports notebooks, notes, and media metadata as portable JSON structures. |
| 5 | * Used by the /api/export endpoints for backup and migration. |
| 6 | */ |
| 7 | |
| 8 | import fs from 'bare-fs' |
| 9 | import path from 'bare-path' |
| 10 | import { extractMediaHashes } from './multiwriter/notebook-apply.js' |
| 11 | |
| 12 | // Read app version from package.json at module load time |
| 13 | let _version = '0.0.0' |
| 14 | try { |
| 15 | const _dir = path.dirname(new URL(import.meta.url).pathname) |
| 16 | const _pkg = JSON.parse(fs.readFileSync(path.join(_dir, '..', 'package.json'), 'utf-8')) |
| 17 | _version = _pkg.version || _version |
| 18 | } catch {} |
| 19 | export const APP_VERSION = _version |
| 20 | |
| 21 | /** |
| 22 | * Build export manifest describing the export contents. |
| 23 | */ |
| 24 | export function buildExportManifest (notebooks, version) { |
| 25 | return { |
| 26 | version: version || '0.0.0', |
| 27 | format: 'hypernote-export-v1', |
| 28 | exportedAt: new Date().toISOString(), |
| 29 | notebooks: notebooks.map(nb => ({ |
| 30 | slug: nb.slug, |
| 31 | name: nb.name, |
| 32 | noteCount: nb.noteCount || 0 |
| 33 | })) |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | /** |
| 38 | * Export a single notebook as a portable data structure. |
| 39 | * Returns { metadata, notes: [{ id, slug, title, content, tags, ... }], mediaMap: { hash: { size, mimeType } } } |
| 40 | */ |
| 41 | export async function exportNotebook (noteManager, mediaStore, slug) { |
| 42 | const metadata = await noteManager.getNotebook(slug) |
| 43 | if (!metadata) return null |
| 44 | |
| 45 | const notes = await noteManager.listNotes(slug) |
| 46 | const mediaMap = {} |
| 47 | |
| 48 | // Collect all media hashes referenced in note content |
| 49 | for (const note of notes) { |
| 50 | const hashes = extractMediaHashes(note.content || '') |
| 51 | for (const hash of hashes) { |
| 52 | if (mediaMap[hash]) continue |
| 53 | const info = await mediaStore.getInfo(hash) |
| 54 | if (info) { |
| 55 | mediaMap[hash] = { size: info.size, mimeType: info.mimeType } |
| 56 | } |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | return { |
| 61 | metadata: { |
| 62 | slug: metadata.slug, |
| 63 | name: metadata.name, |
| 64 | description: metadata.description || '', |
| 65 | visibility: metadata.visibility || 'public', |
| 66 | createdAt: metadata.createdAt, |
| 67 | updatedAt: metadata.updatedAt |
| 68 | }, |
| 69 | notes: notes.map(n => ({ |
| 70 | id: n.id, |
| 71 | slug: n.slug, |
| 72 | title: n.title, |
| 73 | content: n.content || '', |
| 74 | tags: n.tags || [], |
| 75 | type: n.type || 'markdown', |
| 76 | createdAt: n.createdAt, |
| 77 | updatedAt: n.updatedAt |
| 78 | })), |
| 79 | mediaMap |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | /** |
| 84 | * Export all notebooks. |
| 85 | */ |
| 86 | export async function exportAll (noteManager, mediaStore, version) { |
| 87 | const notebooks = await noteManager.listNotebooks() |
| 88 | const manifest = buildExportManifest(notebooks, version) |
| 89 | |
| 90 | const data = {} |
| 91 | for (const nb of notebooks) { |
| 92 | data[nb.slug] = await exportNotebook(noteManager, mediaStore, nb.slug) |
| 93 | } |
| 94 | |
| 95 | return { manifest, notebooks: data } |
| 96 | } |