Notechondria
Version: 0.1.40 Build Date: 2026-04-19T10:00
What's Changed
Attachment CDN rework, commit 1 of 3: shared LocalAttachmentStore
Ships the platform-split local attachment storage that the editor /
planner / portal wiring will consume in later rounds. This commit
stays self-contained: no app-shell changes, no UI. The new store
sits alongside LocalArchive in
frontend/notechondria_shared/lib/src/utils/ and exposes a single
LocalAttachmentStore facade with a platform-specific backend
selected via conditional import.
API
LocalAttachmentStore.open()\u2014 async singleton.put(noteUuid, filename, contentType, bytes)\u2014 writes the blob and metadata, returns aLocalAttachmentrecord with a canonicallocalUrlof shapelocal://<note_uuid>/<filename>. The store owns filename sanitization (same rules asLocalArchive: slashes \u2192_, control bytes stripped) and enforces a 20 MB per-file cap viamaxBytesPerAttachment(matches the existing editor upload cap).getBytes({localUrl | noteUuid+filename})\u2014 throws a \u00a71.7-shapedLocalAttachmentStoreExceptionif the entry doesn't exist.get(\u2026)\u2014 non-throwing metadata lookup that returns null for missing entries.listForNote(noteUuid)\u2014 sorted list of attachments, empty list when the note has none.delete(\u2026)/deleteAllForNote(noteUuid)\u2014 silent no-op on missing; native path cleans up empty per-note directories so a long session doesn't leave thousands of stubs on disk.totalBytes()\u2014 sum of every stored attachment's bytes, used for future storage-budget warnings on web.parseLocalUrl(url)\u2014 returns({noteUuid, filename})or null; rejects nested subpaths and non-local://schemes so the markdown preview's custom image builder can reliably resolve or skip a URL.
Migration shim
LocalAttachmentStore.migrateBase64Drafts(List<Map>) walks a
drafts bucket, finds any metadata_json['queued_attachments']
entries with inline bytes_base64 payloads (the 0.1.37 format),
writes each blob into the store, and:
- Rewrites the draft's markdown body by substituting every
data:<content-type>;base64,<base64>URI with the newlocal://<note_uuid>/<filename>URL. - Replaces the queued entry's
bytes_base64field withlocal_url,content_type, andsize_bytespointers.
Drafts with an empty queue pass through untouched. A lookup
callback lets the caller map draft \u2192 note_uuid; the default
uses the server-issued uuid when available and falls back to
local-<client_draft_id> so store keys never collide with server
uuids. Covered by 2 unit tests.
Backends
- Native (
local_attachment_store_io.dart): usespath_provider.getApplicationSupportDirectory()to anchor the store under<app_support>/notechondria/attachments/. Underflutter test(wherepath_provideris unregistered) it falls back to a tempdir viaDirectory.systemTemp.createTemp, which keeps unit tests self-contained without any mock-file-system dependency. - Web (
local_attachment_store_web.dart): ships an in-memory map keyed by(noteUuid, filename). The blob survives the tab's lifetime but not across reloads. A follow-up round swaps this for an IndexedDB implementation (idb_shim-backed). The public contract stays identical, so editor / planner / portal wiring can land on top of the placeholder.
Tests
New frontend/notechondria_shared/test/local_attachment_store_test.dart
with 9 passing cases:
- put + get round-trips bytes + metadata.
parseLocalUrlrejects malformed inputs (wrong scheme, nested paths, missing uuid, missing filename).listForNotereturns sorted entries;deleteAllForNoteclears.- Per-file cap rejects oversized payloads with a \u00a71.7-shaped error.
getBytesthrows a \u00a71.7-shaped error for missing entries.- Filename sanitization strips
/,\u0000, and control bytes. migrateBase64Draftsmoves inline base64 into the store and rewrites markdown + queue entries.migrateBase64Draftspasses through drafts with no queue.totalBytesreports the sum of stored attachment blobs.
Combined with the existing local_archive_test.dart, the shared
test suite is now at 15 passing cases.
Files Changed
New
docs/versions/0.1.40.md(this file).frontend/notechondria_shared/lib/src/utils/local_attachment_store.dart\u2014 facade + migration shim + abstractLocalAttachmentBackend.frontend/notechondria_shared/lib/src/utils/local_attachment_store_io.dart\u2014 native filesystem backend.frontend/notechondria_shared/lib/src/utils/local_attachment_store_web.dart\u2014 in-memory web backend (IndexedDB-backed in a later round).frontend/notechondria_shared/test/local_attachment_store_test.dart\u2014 9 unit tests.
Modified
VERSION: 0.1.39 \u2192 0.1.40.frontend/notechondria_shared/pubspec.yaml: addspath_provider: ^2.1.0.frontend/notechondria_shared/lib/notechondria_shared.dart: exports the newLocalAttachment*symbols.
Verification
notechondria_shared:flutter analyze\u2014 3 pre-existing info-level hints (2surfaceVariantdeprecation, 1constsuggestion on a test local). No errors.flutter test -r compact\u2014 15 tests pass.editor_app/planner_app/portal_app: issue counts unchanged vs 0.1.39 (52 / 70 / 68). Smoke tests pass on all three \u2014 the new shared dependency doesn't regress anything.
Notes / follow-ups
Next in the three-commit plan for the attachment CDN rework:
- Commit 2 \u2014 editor wiring. Replace the
_pickAndUploadAttachmentbase64-queue path ineditor_app/lib/modules/note_editor.dartwithLocalAttachmentStore.put(\u2026)+local://URL embedding. Rewrite_promoteQueuedAttachmentsinapp_shell.dartto stream bytes fromLocalAttachmentStore.getBytes(\u2026)instead of base64-decoding, delete the local blob on successful upload, and rewrite the bodylocal://...URL to the CDN URL. Add a flutter_markdown custom image builder solocal://URLs render in-place. Add an "Attachments" section under the editor toolbar. - Commit 3 \u2014 planner + portal wiring. Same pattern, their
inlined editors in
modules/learner.dart. - IndexedDB backend on web. Swap the in-memory map in
local_attachment_store_web.dartforidb_shim-backed storage once the editor wiring round proves the flow end-to-end. - Storage-budget warning. Wire
totalBytes()into the settings / debug log card so users see local consumption before hitting browser eviction thresholds.