Skip to content
Documentation GitHub
Content

Attachment System

Status: Implemented Depends On: Page System, Permission System


The Attachment System manages file uploads (images, PDFs, documents) stored alongside workspace content. Files are stored on the local filesystem with metadata tracked in SQLite. The system provides content-addressable deduplication via SHA-256, extension-based allowlist security, quota enforcement, and reference tracking to prevent orphaned files.

Attachments are the storage backbone for both the dedicated Image Block content type and inline file references.


Framework (Tauri)
└── Commands apps/desktop/src-tauri/src/commands/
upload_attachment, upload_attachment_bytes,
get_attachment, get_attachment_file,
list_attachments, delete_attachment
Application
├── UploadAttachmentUseCase crates/application/src/attachment/upload.rs
│ SHA-256 hash, dedup, quota check, magic-byte validation, file write
├── DeleteAttachmentUseCase crates/application/src/attachment/delete.rs
│ Reference guard, metadata-first deletion, variant cache cleanup
├── BulkDeleteAttachmentsUseCase crates/application/src/attachment/bulk_delete.rs
├── GetAttachmentUseCase crates/application/src/attachment/get.rs
├── GetAttachmentFileUseCase crates/application/src/attachment/get_file.rs
├── GetImageVariantUseCase crates/application/src/attachment/get_variant.rs
├── ListAttachmentsUseCase crates/application/src/attachment/list.rs
├── ListAttachmentsWithRefsUseCase crates/application/src/attachment/list_with_refs.rs
├── GetStorageUsageUseCase crates/application/src/attachment/get_storage_usage.rs
├── CheckStorageQuotaUseCase crates/application/src/attachment/check_quota.rs
├── UpdateAttachmentReferencesUseCase crates/application/src/attachment/update_references.rs
├── EnqueueAttachmentSyncUseCase crates/application/src/attachment/enqueue_sync.rs
├── DownloadRemoteAttachmentUseCase crates/application/src/attachment/download_attachment.rs
├── AttachmentSyncCoordinator crates/application/src/attachment/sync_coordinator.rs
├── AttachmentRepository (trait) crates/application/src/attachment/services.rs
├── ImageProcessingProvider (trait) crates/application/src/attachment/services.rs
└── AttachmentSyncProvider (trait) crates/application/src/attachment/services.rs
Domain
├── Attachment crates/domain/src/attachment.rs
├── AttachmentReference crates/domain/src/attachment.rs
├── ContentType crates/domain/src/attachment.rs
├── AttachmentSyncStatus crates/domain/src/attachment.rs
├── ImageVariant crates/domain/src/attachment.rs
├── StorageQuota crates/domain/src/attachment.rs
├── StorageUsage crates/domain/src/attachment.rs
└── AttachmentSummary crates/domain/src/attachment.rs
Infrastructure
└── SqliteAttachmentRepository crates/infrastructure/sqlite/src/workspace/attachment_repository.rs

~/Inklings/Workspaces/MyVault/
├── .inklings/inklings.db # Attachment metadata (attachments + attachment_references tables)
└── attachments/
├── {uuid}.png # Original file: {attachment_id}.{ext}
├── {uuid}.pdf
├── {uuid} # Files with no extension (e.g., dotfiles)
└── .cache/
├── {uuid}_thumb.webp # 200px thumbnail variant
└── {uuid}_display.webp # 2000px display-optimized variant

Files are named {attachment_id}.{extension} for direct filesystem access. The .cache/ subdirectory holds generated image variants (WebP format).


CREATE TABLE attachments (
id TEXT PRIMARY KEY,
original_filename TEXT NOT NULL,
file_extension TEXT NOT NULL,
content_type TEXT NOT NULL, -- MIME type string (human-readable)
size_bytes INTEGER NOT NULL,
content_hash TEXT NOT NULL, -- SHA-256 hex digest
sync_status TEXT NOT NULL DEFAULT 'local_only',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE UNIQUE INDEX idx_attachments_hash ON attachments(content_hash);
CREATE INDEX idx_attachments_sync_status ON attachments(sync_status);
CREATE INDEX idx_attachments_pending_sync ON attachments(sync_status)
WHERE sync_status IN ('local_only', 'failed');
CREATE TABLE attachment_references (
attachment_id TEXT NOT NULL,
page_id TEXT NOT NULL,
block_id TEXT,
FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE,
FOREIGN KEY (page_id) REFERENCES pages(id) ON DELETE CASCADE,
PRIMARY KEY (attachment_id, page_id)
);
CREATE INDEX idx_attachment_refs_page ON attachment_references(page_id);

The content_type column stores the MIME type string for human readability, but reconstruction uses ContentType::from_extension() on the file_extension column to avoid round-trip mismatches.


Every upload computes a SHA-256 hash of the file bytes. Before writing anything to disk, the system checks attachments.content_hash for an existing match. If found, the existing Attachment is returned immediately — no disk write, no new metadata row. This is transparent to the caller.

The domain layer enforces a security allowlist of permitted file extensions:

Images: png, jpg, jpeg, gif, webp, svg
Documents: pdf, txt, md
Office: doc, docx, xls, xlsx, ppt, pptx, odt, ods, csv, rtf

Unknown extensions are rejected at the domain Attachment::new() constructor. Empty extensions (e.g., Makefile, .gitignore) are allowed.

For image uploads, the infer crate validates that file content matches the declared extension. A mismatch (e.g., a .png extension on a JPEG file) is rejected with an error. For non-image types (office docs, PDFs), mismatches are logged as warnings but not blocked, since these formats have complex or optional magic byte signatures.

Two quotas are checked before writing to disk (fail-fast ordering):

QuotaDefaultCheck Order
Per-file size limit10 MBFirst (before hashing)
Workspace total limit100 MBSecond (after dedup check)

The workspace total check runs after the dedup lookup — if the content already exists, the quota is irrelevant.

Following the multi-DB transaction pattern, deletion order is: metadata first, file second. If metadata deletion succeeds but the file is already missing from disk, the operation still succeeds gracefully. Cached image variants (.cache/{id}_thumb.webp, .cache/{id}_display.webp) are cleaned up as well.

Single-attachment deletion is blocked if any page still references the attachment. The get_references_for_attachment() check prevents orphaning content. The BulkDeleteAttachmentsUseCase with force: true overrides this guard for cleanup operations.

All write operations require Capability::AttachmentsWrite. Read operations require Capability::AttachmentsRead. The permission guard is checked at the start of every use case.


The system defines two on-device optimization variants:

VariantMax DimensionFormatUse Case
Thumbnail200pxWebPGrid views, navigation
DisplayOptimized2000pxWebPIn-editor display

Variants are generated lazily via GetImageVariantUseCase using the ImageProcessingProvider trait. Generated files are cached in attachments/.cache/ and cleaned up on attachment deletion.


Attachments participate in cloud sync via three components:

  • AttachmentSyncStatus: Tracks per-attachment state (LocalOnly -> Synced, with RemoteOnly, Downloading, Failed states).
  • AttachmentSyncProvider (trait): Cloud storage operations (upload/download bytes, metadata push/pull). Infrastructure implements this with Supabase Storage.
  • AttachmentSyncCoordinator: Orchestrates sync — enqueues uploads after local saves, handles downloads for remote-only attachments.
  • attachment_sync_queue table: Durable queue for pending uploads with retry tracking.

Stale downloads (status stuck in Downloading after a crash) are reset to RemoteOnly on workspace open via reset_stale_downloads().


ScenarioEntry Point
File upload (path-based)UploadAttachmentUseCase::execute()
File upload (raw bytes)Same use case, bytes passed directly
Dedup checkAttachmentRepository::get_by_hash()
Delete with reference guardDeleteAttachmentUseCase::execute()
Reference trackingUpdateAttachmentReferencesUseCase::execute()
Image variant generationGetImageVariantUseCase::execute()
Storage usage queryGetStorageUsageUseCase::execute()

Stores and manages file attachments. See also: Block Content System (Image blocks reference attachments), Page System.

Was this page helpful?