OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / test / unit / media-migration.test.js

254 lines · javascript ref:
import test from 'brittle'
import Corestore from 'corestore'
import Hyperdrive from 'hyperdrive'
import path from 'bare-path'
import fs from 'bare-fs'
import os from 'bare-os'
import { MediaStore } from '../../lib/media-store.js'

function tmpdir () {
  const dir = path.join(os.tmpdir(), 'hn-mig-test-' + Date.now() + '-' + Math.random().toString(36).slice(2))
  fs.mkdirSync(dir, { recursive: true })
  return dir
}

function rmrf (dir) {
  try {
    for (const f of fs.readdirSync(dir)) {
      const fp = path.join(dir, f)
      const stat = fs.statSync(fp)
      if (stat.isDirectory()) rmrf(fp)
      else fs.unlinkSync(fp)
    }
    fs.rmdirSync(dir)
  } catch {}
}

test('migration - deduplicates identical files across notebooks', async (t) => {
  const dir = tmpdir()
  try {
    const store = new Corestore(dir)
    const media = new MediaStore(store)
    await media.ready()

    // Simulate two drives with overlapping media
    const driveA = new Hyperdrive(store.namespace('notebook-alpha'))
    await driveA.ready()
    const driveB = new Hyperdrive(store.namespace('notebook-beta'))
    await driveB.ready()

    const imgBuf = Buffer.from('fake image data for dedup test')
    const vidBuf = Buffer.from('fake video data unique')

    await driveA.put('/media/photo1.jpg', imgBuf)
    await driveA.put('/media/clip.mp4', vidBuf)
    await driveB.put('/media/same-photo.jpg', imgBuf) // same content

    // Simulate migration logic
    const drives = [
      { slug: 'alpha', drive: driveA },
      { slug: 'beta', drive: driveB }
    ]

    let migrated = 0
    let deduped = 0
    const mimeTypes = {
      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', mov: 'video/quicktime',
      mp3: 'audio/mpeg', ogg: 'audio/ogg'
    }

    for (const { slug, drive } of drives) {
      const files = []
      for await (const f of drive.readdir('/media/')) files.push(f)
      for (const filename of files) {
        const buf = await drive.get('/media/' + filename)
        if (!buf) continue
        const ext = filename.split('.').pop().toLowerCase()
        const mimeType = mimeTypes[ext] || 'application/octet-stream'
        const hash = media._hash(buf)
        const existed = await media.has(hash)
        await media.put(buf, mimeType)
        if (existed) deduped++
        else migrated++

        // Store legacy mapping
        await media._index.put('legacy:' + filename, {
          hash,
          slug,
          migratedAt: Date.now()
        })
      }
    }

    t.is(migrated, 2, 'should migrate 2 unique files')
    t.is(deduped, 1, 'should detect 1 duplicate')

    const all = await media.listAll()
    t.is(all.length, 2, 'MediaStore should have 2 unique blobs')

    await store.close()
  } finally {
    rmrf(dir)
  }
})

test('migration - legacy filename mapping resolves to correct hash', async (t) => {
  const dir = tmpdir()
  try {
    const store = new Corestore(dir)
    const media = new MediaStore(store)
    await media.ready()

    const buf = Buffer.from('legacy image content')
    const hash = await media.put(buf, 'image/png')

    // Store legacy mapping (as migration would)
    await media._index.put('legacy:old-photo.png', {
      hash,
      slug: 'general',
      migratedAt: Date.now()
    })

    // Look up by legacy filename
    const legacyEntry = await media._index.get('legacy:old-photo.png')
    t.ok(legacyEntry, 'legacy entry should exist')
    t.is(legacyEntry.value.hash, hash, 'hash should match')

    // Verify we can retrieve the blob via the mapped hash
    const info = await media.getInfo(legacyEntry.value.hash)
    t.ok(info, 'should find info via mapped hash')
    t.is(info.mimeType, 'image/png')
    t.is(info.size, buf.length)

    const retrieved = await media.get(legacyEntry.value.hash)
    t.alike(retrieved, buf, 'should retrieve original content via legacy mapping')

    await store.close()
  } finally {
    rmrf(dir)
  }
})

test('migration - preserves correct mime types from file extensions', async (t) => {
  const dir = tmpdir()
  try {
    const store = new Corestore(dir)
    const media = new MediaStore(store)
    await media.ready()

    const drive = new Hyperdrive(store.namespace('notebook-test'))
    await drive.ready()

    await drive.put('/media/photo.jpg', Buffer.from('jpeg data'))
    await drive.put('/media/animation.gif', Buffer.from('gif data'))
    await drive.put('/media/video.mp4', Buffer.from('mp4 data'))
    await drive.put('/media/unknown.xyz', Buffer.from('unknown data'))

    const mimeTypes = {
      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', mov: 'video/quicktime',
      mp3: 'audio/mpeg', ogg: 'audio/ogg'
    }

    const results = {}
    const files = []
    for await (const f of drive.readdir('/media/')) files.push(f)

    for (const filename of files) {
      const buf = await drive.get('/media/' + filename)
      const ext = filename.split('.').pop().toLowerCase()
      const mimeType = mimeTypes[ext] || 'application/octet-stream'
      const hash = await media.put(buf, mimeType)
      results[filename] = { hash, mimeType }
    }

    // Verify mime types
    const jpgInfo = await media.getInfo(results['photo.jpg'].hash)
    t.is(jpgInfo.mimeType, 'image/jpeg', 'jpg should map to image/jpeg')

    const gifInfo = await media.getInfo(results['animation.gif'].hash)
    t.is(gifInfo.mimeType, 'image/gif', 'gif should map to image/gif')

    const mp4Info = await media.getInfo(results['video.mp4'].hash)
    t.is(mp4Info.mimeType, 'video/mp4', 'mp4 should map to video/mp4')

    const unknownInfo = await media.getInfo(results['unknown.xyz'].hash)
    t.is(unknownInfo.mimeType, 'application/octet-stream', 'unknown ext should fallback to octet-stream')

    await store.close()
  } finally {
    rmrf(dir)
  }
})

test('migration - skips empty files gracefully', async (t) => {
  const dir = tmpdir()
  try {
    const store = new Corestore(dir)
    const media = new MediaStore(store)
    await media.ready()

    const drive = new Hyperdrive(store.namespace('notebook-empty'))
    await drive.ready()

    // Write a real file and an empty file
    await drive.put('/media/real.png', Buffer.from('real image data'))
    await drive.put('/media/empty.png', Buffer.alloc(0))

    let migrated = 0
    const files = []
    for await (const f of drive.readdir('/media/')) files.push(f)

    for (const filename of files) {
      const buf = await drive.get('/media/' + filename)
      if (!buf || buf.length === 0) continue
      await media.put(buf, 'image/png')
      migrated++
    }

    t.is(migrated, 1, 'should only migrate non-empty files')

    const all = await media.listAll()
    t.is(all.length, 1, 'MediaStore should have 1 blob')

    await store.close()
  } finally {
    rmrf(dir)
  }
})

test('migration - handles drives with no /media/ directory', async (t) => {
  const dir = tmpdir()
  try {
    const store = new Corestore(dir)
    const media = new MediaStore(store)
    await media.ready()

    const drive = new Hyperdrive(store.namespace('notebook-nomedia'))
    await drive.ready()

    // Only write notes, no media
    await drive.put('/notes/test.json', Buffer.from('{}'))

    let files = []
    try {
      const iter = drive.readdir('/media/')
      for await (const f of iter) files.push(f)
    } catch {
      // no /media/ dir — expected
    }

    t.is(files.length, 0, 'should find no media files')

    const all = await media.listAll()
    t.is(all.length, 0, 'MediaStore should be empty')

    await store.close()
  } finally {
    rmrf(dir)
  }
})