Content-Addressed Fetch — Implementation Plan
Context
Demarkus already has the building blocks for content addressing: SHA-256 hash chains in the versioned store, content hashes computed for etags, and a client cache at ~/.mark/cache/. The hub pattern just shipped for discovery. Content addressing adds the "fetch from anywhere" primitive — any server that has the content can serve it, verified by hash.
Approach
No new verbs. FETCH handles it via path detection. Five incremental steps, each independently testable.
Step 1: Add content-hash to FETCH responses
File: server/internal/handler/handler.go
- Add
computeContentHash(body string) string— SHA-256 of the stripped body (notdoc.Contentwhich includes store frontmatter) - Call it in
serveDocument()afterstripFrontmatter(), setmeta["content-hash"] = "sha256-<hex>" - Also add to
handleFetchVersion()for historical version responses - Tests: verify
content-hashappears in FETCH responses, matches expected hash of body
Step 2: Add hash index to the Store
File: server/internal/store/store.go
- Add fields to
Storestruct:hashIndex map[string]string,hashMu sync.RWMutex BuildHashIndex()— walks content root following symlinks (current versions only), reads each, strips frontmatter, hashes body, populates index. Skipsversions/dirs and archived docs. Returns error only on walk failure, logs individual file errors.LookupHash(hash string) (string, bool)— read-locked lookupUpdateHashIndex(reqPath string, body []byte)— write-locked upsertRemoveHashEntry(reqPath string)— write-locked removal by path (iterates to find matching path)- Tests: table-driven for build, lookup, update, remove
Step 3: Update hash index on writes
File: server/internal/store/store.go
- In
Write()after successful symlink rename (line ~576), calls.UpdateHashIndex(reqPath, content)—contenthere is the raw body parameter, not stored bytes - In
SetArchived()— callRemoveHashEntrywhen archiving,UpdateHashIndexwhen unarchiving Append()flows throughWrite()so it's handled automatically- Tests: write a doc, verify hash index updated; archive, verify removed
Step 4: Handle hash-based FETCH
File: server/internal/handler/handler.go
isHashPath(path string) (string, bool)— validates path matches/sha256-<64 hex chars>(73 chars total:/+sha256-+ 64 hex)handleFetchByHash(w, req, hash)— callsStore.LookupHash(hash), if not found → not-found, otherwiseStore.Get(path, 0)→serveDocument()- Insert hash detection at the top of
handleFetch(), beforeparseVersionPath - Tests: fetch by valid hash, fetch unknown hash, malformed hash path
Step 5: Wire up on startup
File: server/cmd/demarkus-server/main.go
- After
s := store.New(cfg.ContentDir)(line 102), calls.BuildHashIndex() - Log entry count:
logger.Info("content hash index built", "entries", s.HashIndexSize()) - Non-fatal on error (log warning, continue)
Key Design Decisions
content-hashis separate frometag: etag hashesdoc.Content(includes store frontmatter), content-hash hashes the stripped body (what the client receives). Different values.- Current versions only: historical versions get
content-hashin their response but aren't indexed for lookup. Keeps the index small. - Archived docs excluded: archiving removes from index, unarchiving re-adds.
- In-memory index, no persistence: the content directory is the source of truth. Rebuilt on startup.
- Path syntax
FETCH /sha256-<hex>: detected by pattern match, no protocol changes needed.
Files Modified
| File | Change |
|---|---|
server/internal/handler/handler.go |
content-hash in responses, hash path detection, handleFetchByHash |
server/internal/store/store.go |
Hash index struct fields, Build/Lookup/Update/Remove methods, index updates in Write/SetArchived |
server/cmd/demarkus-server/main.go |
One-line startup call to BuildHashIndex |
server/internal/handler/handler_test.go |
Tests for content-hash, hash fetch, malformed paths |
server/internal/store/store_test.go |
Tests for hash index operations |
Verification
cd server && go test ./...after each stepbash pre-commit.shat the end- Manual: start server, FETCH a doc, note
content-hashin response, FETCH by that hash
Related documents
- Journal 2026-03-08: session in which this plan was written
- Federation: fetch-from-anywhere primitive this hash addressing enables