160 lines
5.4 KiB
Markdown
160 lines
5.4 KiB
Markdown
# Image Management
|
|
|
|
## Requirements
|
|
|
|
Multiple images are core functionality.
|
|
|
|
Use cases include:
|
|
- card front/back
|
|
- corners
|
|
- edges/whitening
|
|
- holographic details
|
|
- merchandise angles
|
|
- defects/details
|
|
|
|
## Workflow
|
|
|
|
`Upload → Validate → Store Original → Queue Processing → Generate Derivatives → Complete`
|
|
|
|
Support:
|
|
- multiple-file upload
|
|
- drag/drop
|
|
- preview
|
|
- reorder
|
|
- primary image
|
|
- delete
|
|
- add images later
|
|
|
|
Image count is configurable.
|
|
|
|
## Storage
|
|
|
|
Images are not stored in PostgreSQL.
|
|
|
|
Use an `ImageStorage` abstraction.
|
|
|
|
Initial:
|
|
- Docker-mounted local storage
|
|
|
|
Future:
|
|
- S3-compatible object storage
|
|
|
|
The API uses an `ImageStorage` contract with `put`, `get`, and `delete`
|
|
operations. The current local adapter rejects absolute and traversal paths and
|
|
does not overwrite an existing key. It stores bytes outside PostgreSQL; image
|
|
metadata remains in `inventory_images`.
|
|
|
|
## Processing
|
|
|
|
Validate:
|
|
- MIME type
|
|
- file signature where practical
|
|
- size
|
|
- image dimensions
|
|
- configured image count
|
|
|
|
The metadata service currently enforces JPEG, PNG, and WebP MIME types, a
|
|
10 MiB default file-size limit, and a 10,000-pixel maximum width and height.
|
|
Multipart upload validation now checks the declared MIME type against JPEG,
|
|
PNG, and WebP signatures before storage. The injected image inspector reads
|
|
dimensions and format from the bytes; the service validates those dimensions
|
|
and the actual buffer length, then verifies decoding before writing storage or
|
|
metadata. Multipart client dimensions are ignored.
|
|
|
|
Generate:
|
|
- thumbnail
|
|
- medium
|
|
- large/responsive derivatives
|
|
|
|
Modern formats such as WebP/AVIF may be used where appropriate.
|
|
|
|
Retain originals where practical.
|
|
|
|
Derivative orchestration currently defines thumbnail, medium, and large WebP
|
|
variants and writes them through `ImageStorage` using an injected renderer.
|
|
The concrete decoder/encoder and worker queue remain separate infrastructure
|
|
work so local development does not depend on a machine-specific binary.
|
|
|
|
The worker now owns the derivative job lifecycle and reports explicit
|
|
`PENDING`, `PROCESSING`, `COMPLETED`, or `FAILED` outcomes. Queue transport and
|
|
persistent job records remain deployment-specific work.
|
|
|
|
## Metadata
|
|
|
|
Store:
|
|
- storage key
|
|
- original filename
|
|
- MIME type
|
|
- file size
|
|
- dimensions
|
|
- alt text
|
|
- order
|
|
- primary flag
|
|
- processing status
|
|
- timestamps
|
|
|
|
## Alt text
|
|
|
|
Generate an initial suggestion automatically, but require the seller to be able to review/edit it.
|
|
|
|
Do not invent details not visible or supplied by the seller.
|
|
|
|
## Accessibility
|
|
|
|
Public galleries must support:
|
|
- keyboard operation
|
|
- accessible controls
|
|
- visible focus
|
|
- meaningful names
|
|
- focus management
|
|
- screen-reader-friendly state
|
|
|
|
Primary images power cards, search results, homepage previews, and social/share metadata.
|
|
|
|
## Admin error recovery
|
|
|
|
Keep the loaded gallery and alt-text drafts mounted during image actions and
|
|
refreshes. Show action failures separately from list-read failures. After every
|
|
action, including a failed multi-file upload or reorder, reload image metadata to
|
|
reconcile any partial success without replaying writes. If that refresh fails,
|
|
retain the last loaded gallery but disable mutations until an explicit reload
|
|
succeeds. Failed initial loads show a reload action instead of an empty manager.
|
|
Reload restores keyboard focus to the manager or the retry control. Pending
|
|
operations block conflicting writes and uploads, including drag-and-drop.
|
|
|
|
## Atomic adjacent moves
|
|
|
|
Move up/down uses one server operation. The repository locks the owning Inventory
|
|
Item and its images, selects current order by `sortOrder`, `createdAt`, then `id`,
|
|
and uses the domain move rule to choose the new order. An actual move assigns
|
|
contiguous zero-based positions, resolving tied legacy/upload positions, within
|
|
one PostgreSQL transaction. Any failure rolls back all changes. Concurrent moves
|
|
within one item serialize; primary selection and image metadata are unchanged.
|
|
Boundary moves are no-ops. No database migration is required.
|
|
|
|
## Image update validation
|
|
|
|
Alt-text edits use the same trimmed 1,000-character limit as image creation.
|
|
Explicit empty text or null may clear it; missing fields are invalid. The editor
|
|
shows the limit, prevents typing past it, and flags oversized pre-existing drafts
|
|
until corrected. Legacy absolute sort-order updates require a non-negative
|
|
PostgreSQL integer; ordinary Move controls continue using the atomic move API.
|
|
|
|
|
|
## Server-side upload inspection
|
|
|
|
The production `ImageInspector` uses the existing Sharp dependency behind
|
|
`readMetadata(data)` and `validateDecoding(data)` methods. The service checks
|
|
actual byte size before inspection, validates the 10,000-pixel dimension limit
|
|
against inspected metadata, and only then requests decoding. Sharp rejects
|
|
warnings/corruption and has a 100-million-input-pixel ceiling, matching the
|
|
maximum permitted dimension product. The tiny decoded output is discarded;
|
|
original storage and asynchronous derivatives retain their existing flow.
|
|
|
|
Malformed or undecodable images return `400 IMAGE_DECODE_INVALID`; format mismatch
|
|
uses `IMAGE_SIGNATURE_INVALID`, and inspected dimension violations use
|
|
`IMAGE_DIMENSIONS_INVALID`. Rejections happen before storage, metadata, or job
|
|
creation. No dependency or database migration is required. The separate JSON
|
|
metadata-registration endpoint has no uploaded bytes to inspect and retains its
|
|
existing required dimensions; multipart uploads no longer require them.
|