Sync 5: attachment blob sync — client id + content hash (M8)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 11s
CI & Build / Build & push image (push) Successful in 44s

Let native clients sync attachment blobs deterministically:
- note_attachments gains sha256 (migration 0018, nullable, no backfill).
  The delta feed's attachment metadata now carries size + sha256 so a
  client knows exactly which blobs it already has (dedupe) and can verify
  integrity after download.
- Upload accepts an optional client-supplied attachment id (multipart form
  field), so a file attached offline keeps its identity across sync;
  re-uploading an id the note already has is an idempotent no-op. The
  server hashes the stored bytes (sha256) on upload. Download by id already
  exists (owner/shared scoped).

Frontend Attachment type carries the new optional size/sha256.

(Still image-only mimes — broadening to any-file is task 1900. Blob sync
behavior is operator-verified on deploy.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-22 23:12:21 -04:00
co-authored by Claude Opus 4.8
parent 68abaa0f3f
commit 0ca244be3d
4 changed files with 63 additions and 2 deletions
@@ -23,4 +23,7 @@ class NoteAttachment(Base):
path: Mapped[str] = mapped_column(Text(), nullable=False) # relative to media_root
mime: Mapped[str] = mapped_column(Text(), nullable=False)
size: Mapped[int] = mapped_column(BigInteger(), nullable=False)
# Content hash (sha256 hex) for client-side dedupe + integrity over sync. Nullable
# for rows created before this column (no backfill).
sha256: Mapped[str | None] = mapped_column(Text(), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())