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)
}
})