feat(api): presigned uploads and item file attachments #12

Closed
opened 2026-02-07 02:19:23 +00:00 by forbes · 0 comments
Owner

Context

Items currently support a single file per revision via POST /api/items/{partNumber}/file (multipart upload). The CreateItemPane redesign (#11+) needs:

  1. Presigned upload URLs so the browser can upload directly to MinIO (with progress tracking)
  2. Multiple file attachments per item (not tied to revisions)
  3. Thumbnail support on items

Database Migration

CREATE TABLE item_files (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE,
  filename TEXT NOT NULL,
  content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
  size BIGINT NOT NULL DEFAULT 0,
  object_key TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_item_files_item ON item_files(item_id);

ALTER TABLE items ADD COLUMN thumbnail_key TEXT;

Note: items already has sourcing_type, sourcing_link, long_description, standard_cost from migration 010. The revision table already has thumbnail_key. This adds thumbnail_key to items directly for item-level thumbnails.

New API Endpoints

Presigned Upload URL

POST /api/uploads/presign
Auth: Editor+
Request:  { "filename": "bracket.FCStd", "content_type": "application/octet-stream", "size": 2400000 }
Response: { "object_key": "uploads/tmp/{uuid}/{filename}", "upload_url": "https://minio.../...", "expires_at": "..." }

Generates a presigned PUT URL via MinIO SDK. Objects go to a temp prefix initially.

List Item Files

GET /api/items/{partNumber}/files
Auth: Viewer+
Response: [{ "id": "uuid", "filename": "...", "content_type": "...", "size": 123, "object_key": "...", "created_at": "..." }]

Associate File with Item

POST /api/items/{partNumber}/files
Auth: Editor+
Request:  { "object_key": "uploads/tmp/{uuid}/bracket.FCStd", "filename": "bracket.FCStd", "content_type": "...", "size": 2400000 }
Response: 201 Created with file object

Moves object from temp prefix to items/{item_id}/files/{file_id} and creates item_files row.

Delete Item File

DELETE /api/items/{partNumber}/files/{fileId}
Auth: Editor+
Response: 204

Set Item Thumbnail

PUT /api/items/{partNumber}/thumbnail
Auth: Editor+
Request:  { "object_key": "uploads/tmp/{uuid}/thumb.png" }
Response: 204

Copies to items/{item_id}/thumbnail.png, updates items.thumbnail_key.

Implementation Notes

  • Presigned URLs should expire in 15 minutes
  • Temp uploads should have a cleanup job or TTL (not in this issue, but note for future)
  • The existing POST /api/items/{partNumber}/file (revision file) remains unchanged
  • CORS on MinIO must allow PUT from the frontend origin

Acceptance Criteria

  • Migration creates item_files table and items.thumbnail_key column
  • POST /api/uploads/presign returns valid MinIO presigned URL
  • Browser can PUT a file to the presigned URL
  • POST /api/items/{pn}/files associates uploaded file with item
  • GET /api/items/{pn}/files lists item files
  • DELETE /api/items/{pn}/files/{id} removes file
  • PUT /api/items/{pn}/thumbnail sets thumbnail
  • Go builds clean
## Context Items currently support a single file per revision via `POST /api/items/{partNumber}/file` (multipart upload). The CreateItemPane redesign (#11+) needs: 1. **Presigned upload URLs** so the browser can upload directly to MinIO (with progress tracking) 2. **Multiple file attachments per item** (not tied to revisions) 3. **Thumbnail support** on items ## Database Migration ```sql CREATE TABLE item_files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE, filename TEXT NOT NULL, content_type TEXT NOT NULL DEFAULT 'application/octet-stream', size BIGINT NOT NULL DEFAULT 0, object_key TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_item_files_item ON item_files(item_id); ALTER TABLE items ADD COLUMN thumbnail_key TEXT; ``` Note: `items` already has `sourcing_type`, `sourcing_link`, `long_description`, `standard_cost` from migration 010. The revision table already has `thumbnail_key`. This adds `thumbnail_key` to items directly for item-level thumbnails. ## New API Endpoints ### Presigned Upload URL ``` POST /api/uploads/presign Auth: Editor+ Request: { "filename": "bracket.FCStd", "content_type": "application/octet-stream", "size": 2400000 } Response: { "object_key": "uploads/tmp/{uuid}/{filename}", "upload_url": "https://minio.../...", "expires_at": "..." } ``` Generates a presigned PUT URL via MinIO SDK. Objects go to a temp prefix initially. ### List Item Files ``` GET /api/items/{partNumber}/files Auth: Viewer+ Response: [{ "id": "uuid", "filename": "...", "content_type": "...", "size": 123, "object_key": "...", "created_at": "..." }] ``` ### Associate File with Item ``` POST /api/items/{partNumber}/files Auth: Editor+ Request: { "object_key": "uploads/tmp/{uuid}/bracket.FCStd", "filename": "bracket.FCStd", "content_type": "...", "size": 2400000 } Response: 201 Created with file object ``` Moves object from temp prefix to `items/{item_id}/files/{file_id}` and creates `item_files` row. ### Delete Item File ``` DELETE /api/items/{partNumber}/files/{fileId} Auth: Editor+ Response: 204 ``` ### Set Item Thumbnail ``` PUT /api/items/{partNumber}/thumbnail Auth: Editor+ Request: { "object_key": "uploads/tmp/{uuid}/thumb.png" } Response: 204 ``` Copies to `items/{item_id}/thumbnail.png`, updates `items.thumbnail_key`. ## Implementation Notes - Presigned URLs should expire in 15 minutes - Temp uploads should have a cleanup job or TTL (not in this issue, but note for future) - The existing `POST /api/items/{partNumber}/file` (revision file) remains unchanged - CORS on MinIO must allow PUT from the frontend origin ## Acceptance Criteria - [ ] Migration creates `item_files` table and `items.thumbnail_key` column - [ ] `POST /api/uploads/presign` returns valid MinIO presigned URL - [ ] Browser can PUT a file to the presigned URL - [ ] `POST /api/items/{pn}/files` associates uploaded file with item - [ ] `GET /api/items/{pn}/files` lists item files - [ ] `DELETE /api/items/{pn}/files/{id}` removes file - [ ] `PUT /api/items/{pn}/thumbnail` sets thumbnail - [ ] Go builds clean
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/silo#12