feat: record attachments through qbit-object-storage #3

Merged
KofTwentyTwo merged 1 commits from feature/attachments into develop 2026-10-13 21:15:25 +00:00
KofTwentyTwo commented 2026-10-13 21:13:32 +00:00 (Migrated from github.com)

Summary

Phase (c), qbit side, of QRun-IO/qqq#1136 (spec §4 row 5, §6.1): attachments of any type on any record.

Storage and table:

  • collaboration_attachment table, audited at FIELD level, listed in the nav as "Attachments".
  • Bytes go through qbit-object-storage (0.1.0-RC.2) into the configured bucket (default collaboration), at attachments/{table}/{recordId}/{uuid}/{fileName}.
  • Uploads stream through a bounded temporary file. They enforce the per-table size limit (default 100 MB) and blocked extensions (default none), and compute a sha256.

AttachmentsService:

  • upload, list (newest first, with canDelete), rename/describe, delete;
  • downloadUrl(id, inline) returns a 5-minute presigned GET that is never stored.

Previews, actions and cleanup:

  • Previews use the FILE_PREVIEW adornment with presigned URLs, the same pattern object storage uses. Object storage has no thumbnail generator, so there are no generated thumbnail files. I'll update the spec to match.
  • Delete attachments is a record action with streamed ETL Review/Result. Users delete their own attachments; collaboration.admin can delete anyone's.
  • AttachmentCleanup.onParentsDeleted is ready to wire to core's RecordReferenceCleanupInterface.
  • The home gets an Attachments card with the count and total size.

Security stand-in. security/ParentRecordAccess stands in for core's record-reference security (QRun-IO/qqq#1138), which is not released yet. It is marked for replacement:

  • The parent must be visible (GetAction as the user); an invisible parent gives "Not found".
  • Writing needs <table>.attach or table EDIT.
  • Admin browsing is restricted to rows whose parent table the user can READ. Counts are approximate where rows are post-filtered.

Not in this PR: the UI API endpoints. They wait for the extension point (#1128) in an RC.

Test plan

  • ./mvnw -o verify: 38 tests, 96.8% instruction and 100% class coverage, checkstyle clean. The tests use an in-memory storage adapter; a MinIO run is not included because Docker was unavailable in the sandbox.
  • CircleCI test_only.

🤖 Generated with Claude Code

## Summary Phase (c), qbit side, of QRun-IO/qqq#1136 (spec §4 row 5, §6.1): attachments of any type on any record. **Storage and table:** - `collaboration_attachment` table, audited at FIELD level, listed in the nav as "Attachments". - Bytes go through qbit-object-storage (0.1.0-RC.2) into the configured bucket (default `collaboration`), at `attachments/{table}/{recordId}/{uuid}/{fileName}`. - Uploads stream through a bounded temporary file. They enforce the per-table size limit (default 100 MB) and blocked extensions (default none), and compute a sha256. **`AttachmentsService`:** - upload, list (newest first, with `canDelete`), rename/describe, delete; - `downloadUrl(id, inline)` returns a 5-minute presigned GET that is never stored. **Previews, actions and cleanup:** - Previews use the FILE_PREVIEW adornment with presigned URLs, the same pattern object storage uses. Object storage has no thumbnail generator, so there are no generated thumbnail files. I'll update the spec to match. - **Delete attachments** is a record action with streamed ETL Review/Result. Users delete their own attachments; `collaboration.admin` can delete anyone's. - `AttachmentCleanup.onParentsDeleted` is ready to wire to core's `RecordReferenceCleanupInterface`. - The home gets an Attachments card with the count and total size. **Security stand-in.** `security/ParentRecordAccess` stands in for core's record-reference security (QRun-IO/qqq#1138), which is not released yet. It is marked for replacement: - The parent must be visible (GetAction as the user); an invisible parent gives "Not found". - Writing needs `<table>.attach` or table EDIT. - Admin browsing is restricted to rows whose parent table the user can READ. Counts are approximate where rows are post-filtered. **Not in this PR:** the UI API endpoints. They wait for the extension point (#1128) in an RC. ## Test plan - [x] `./mvnw -o verify`: 38 tests, 96.8% instruction and 100% class coverage, checkstyle clean. The tests use an in-memory storage adapter; a MinIO run is not included because Docker was unavailable in the sandbox. - [ ] CircleCI `test_only`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.