OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note commits / 71a056b

feat(media): add MediaStore with content-addressed put/get/has
Content-addressed blob store using SHA-256 hashing backed by Hyperblobs + Hyperbee index. Supports put, get, has, getInfo, and automatic deduplication of identical content. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
71a056b2fed97065b78f5be9f79118a77ab096f2
by plankatron <fleeky@gmail.com> · 2026-03-19T16:14:30.000Z

Changed files (2) +167 -0

lib/media-store.js +60 -0
……@@ -0,0 +1,60 @@
1import Hyperblobs from 'hyperblobs'
2import Hyperbee from 'hyperbee'
3import crypto from 'bare-crypto'
4import b4a from 'b4a'
5
6export class MediaStore {
7 constructor (corestore, opts = {}) {
8 this._store = corestore.namespace('media-store')
9 this._blobCore = this._store.get({ name: 'blobs' })
10 this._blobs = new Hyperblobs(this._blobCore)
11 this._indexCore = this._store.get({ name: 'index' })
12 this._index = new Hyperbee(this._indexCore, {
13 keyEncoding: 'utf-8',
14 valueEncoding: 'json'
15 })
16 }
17
18 async ready () {
19 await this._blobCore.ready()
20 await this._index.ready()
21 }
22
23 _hash (buffer) {
24 const digest = crypto.createHash('sha256').update(buffer).digest()
25 return b4a.toString(digest, 'hex')
26 }
27
28 async put (buffer, mimeType) {
29 const hash = this._hash(buffer)
30 const existing = await this._index.get('blob:' + hash)
31 if (existing) return hash
32
33 const blobId = await this._blobs.put(buffer)
34 await this._index.put('blob:' + hash, {
35 blobId,
36 size: buffer.length,
37 mimeType: mimeType || 'application/octet-stream',
38 createdAt: Date.now()
39 })
40 return hash
41 }
42
43 async get (hash) {
44 const entry = await this._index.get('blob:' + hash)
45 if (!entry) return null
46 return this._blobs.get(entry.value.blobId)
47 }
48
49 async has (hash) {
50 const entry = await this._index.get('blob:' + hash)
51 return entry !== null
52 }
53
54 async getInfo (hash) {
55 const entry = await this._index.get('blob:' + hash)
56 return entry ? entry.value : null
57 }
58}
59
60export default MediaStore
test/unit/media-store.test.js +107 -0
……@@ -0,0 +1,107 @@
1import test from 'brittle'
2import fs from 'bare-fs'
3import path from 'bare-path'
4import os from 'bare-os'
5import Corestore from 'corestore'
6import { MediaStore } from '../../lib/media-store.js'
7
8function tmpSetup () {
9 const dir = path.join(os.tmpdir(), 'hn-ms-' + Date.now() + '-' + Math.random().toString(36).slice(2))
10 fs.mkdirSync(dir, { recursive: true })
11 const store = new Corestore(dir)
12 return { store, dir }
13}
14
15function rmrf (dir) {
16 try {
17 for (const f of fs.readdirSync(dir)) {
18 const fp = path.join(dir, f)
19 const stat = fs.statSync(fp)
20 if (stat.isDirectory()) rmrf(fp)
21 else fs.unlinkSync(fp)
22 }
23 fs.rmdirSync(dir)
24 } catch {}
25}
26
27test('MediaStore - put stores blob and returns sha256 hash', async (t) => {
28 const { store, dir } = tmpSetup()
29 const media = new MediaStore(store)
30 await media.ready()
31
32 const buf = Buffer.from('hello world')
33 const hash = await media.put(buf, 'text/plain')
34
35 t.is(typeof hash, 'string')
36 t.is(hash.length, 64)
37
38 await store.close()
39 rmrf(dir)
40})
41
42test('MediaStore - get retrieves stored blob by hash', async (t) => {
43 const { store, dir } = tmpSetup()
44 const media = new MediaStore(store)
45 await media.ready()
46
47 const buf = Buffer.from('test data for retrieval')
48 const hash = await media.put(buf, 'application/octet-stream')
49 const retrieved = await media.get(hash)
50
51 t.alike(retrieved, buf)
52
53 await store.close()
54 rmrf(dir)
55})
56
57test('MediaStore - has returns true for existing hash', async (t) => {
58 const { store, dir } = tmpSetup()
59 const media = new MediaStore(store)
60 await media.ready()
61
62 const buf = Buffer.from('existence check')
63 const hash = await media.put(buf, 'text/plain')
64
65 t.is(await media.has(hash), true)
66 t.is(await media.has('0'.repeat(64)), false)
67
68 await store.close()
69 rmrf(dir)
70})
71
72test('MediaStore - dedup: same content returns same hash, stored once', async (t) => {
73 const { store, dir } = tmpSetup()
74 const media = new MediaStore(store)
75 await media.ready()
76
77 const buf = Buffer.from('duplicate content')
78 const hash1 = await media.put(buf, 'text/plain')
79 const hash2 = await media.put(buf, 'text/plain')
80
81 t.is(hash1, hash2)
82
83 const info = await media.getInfo(hash1)
84 t.ok(info)
85 t.is(info.size, buf.length)
86
87 await store.close()
88 rmrf(dir)
89})
90
91test('MediaStore - getInfo returns metadata', async (t) => {
92 const { store, dir } = tmpSetup()
93 const media = new MediaStore(store)
94 await media.ready()
95
96 const buf = Buffer.from('metadata test')
97 const hash = await media.put(buf, 'image/jpeg')
98 const info = await media.getInfo(hash)
99
100 t.is(info.size, buf.length)
101 t.is(info.mimeType, 'image/jpeg')
102 t.ok(info.createdAt)
103 t.ok(info.blobId)
104
105 await store.close()
106 rmrf(dir)
107})