OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note commits / 2094f92

Add extractMediaHashes for push-based media garbage collection
Extracts media identifiers from markdown note content by matching both /api/notebooks/.../media/IDENTIFIER URLs and bare 64-char hex hash references. Needed for tracking which media blobs are still referenced by notes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2094f92e693850941371d7bef37306d1fa5b2f8c
by plankatron <fleeky@gmail.com> · 2026-03-19T16:19:12.000Z

Changed files (2) +63 -0

lib/multiwriter/notebook-apply.js +24 -0
……@@ -119,3 +119,27 @@ export async function notebookApply (batch, view, base) {
119119
120120 await b.flush()
121121}
122
123/**
124 * Extract media hashes/identifiers from markdown note content.
125 * Matches both old-style filenames and new SHA-256 hashes in media URLs.
126 */
127export function extractMediaHashes (content) {
128 const hashes = new Set()
129 if (!content) return hashes
130
131 // Match ![...](/api/notebooks/.../media/IDENTIFIER)
132 const urlPattern = /!\[[^\]]*\]\([^)]*\/media\/([a-zA-Z0-9._-]+)\)/g
133 let match
134 while ((match = urlPattern.exec(content)) !== null) {
135 hashes.add(match[1])
136 }
137
138 // Match bare hash references ![...](HASH) where HASH is 64 hex chars
139 const hashPattern = /!\[[^\]]*\]\(([a-f0-9]{64})\)/g
140 while ((match = hashPattern.exec(content)) !== null) {
141 hashes.add(match[1])
142 }
143
144 return hashes
145}
test/unit/notebook-gc.test.js +39 -0
……@@ -0,0 +1,39 @@
1import test from 'brittle'
2import { extractMediaHashes } from '../../lib/multiwriter/notebook-apply.js'
3
4test('extractMediaHashes - finds image hashes in markdown', async (t) => {
5 const content = 'Some text ![photo](/api/notebooks/general/media/abc123def456) and more ![vid](/api/notebooks/general/media/789012fed345)'
6 const hashes = extractMediaHashes(content)
7 t.is(hashes.size, 2)
8 t.ok(hashes.has('abc123def456'))
9 t.ok(hashes.has('789012fed345'))
10})
11
12test('extractMediaHashes - returns empty set for no media', async (t) => {
13 const content = 'Just text, no images'
14 const hashes = extractMediaHashes(content)
15 t.is(hashes.size, 0)
16})
17
18test('extractMediaHashes - returns empty set for null/undefined', async (t) => {
19 t.is(extractMediaHashes(null).size, 0)
20 t.is(extractMediaHashes(undefined).size, 0)
21 t.is(extractMediaHashes('').size, 0)
22})
23
24test('extractMediaHashes - handles 64-char hex hash references', async (t) => {
25 const hash = 'a'.repeat(64)
26 const content = '![image](' + hash + ')'
27 const hashes = extractMediaHashes(content)
28 t.is(hashes.size, 1)
29 t.ok(hashes.has(hash))
30})
31
32test('extractMediaHashes - handles mixed old and new style refs', async (t) => {
33 const hash = 'b'.repeat(64)
34 const content = '![old](/api/notebooks/dev/media/lq3x2abc.jpg)\n![new](' + hash + ')'
35 const hashes = extractMediaHashes(content)
36 t.is(hashes.size, 2)
37 t.ok(hashes.has('lq3x2abc.jpg'))
38 t.ok(hashes.has(hash))
39})