A Gallery Should Not Download Your Archive
Why photo previews became a separate asynchronous workflow, and why a correct cloud object is only one part of a working mobile gallery.
A gallery tile needs a small image. Downloading a multi-megabyte original for each visible tile would make browsing compete with backup for bandwidth and battery.
That requirement creates a second representation of the media: a preview that is useful for browsing and replaceable because the original remains authoritative.
It also creates another workflow whose failures need their own meaning.
Separate storage success from preview readiness
The cloud catalogue distinguishes PENDING_UPLOAD, PROCESSING, READY and FAILED.
READY means preview processing has succeeded. A failure in image decoding is different from a network failure while uploading the original. Combining both under one generic “upload failed” message would give the user the wrong recovery action.
The pipeline is:
S3 original created
↓
SQS message
↓
Lambda preview worker
↓
S3 preview + catalogue status update
The queue lets processing occur independently of the client’s connection. The phone does not need to stay online until decoding finishes.
The queue creates delivery obligations
A worker must tolerate repeated delivery. Lambda’s event source mappings can process records more than once, so handlers need idempotent behavior. AWS describes this requirement.
The worker resolves the media identity from the original key and reads the corresponding catalogue record. It skips already-ready media and avoids recreating missing or deleting records.
It returns individual failed message IDs rather than failing the whole batch. The event source mapping enables partial batch responses, allowing successful messages to leave the queue while failures are retried. AWS documents that configuration.
The queue sends repeatedly failing messages to a dead-letter queue after five receives. The worker’s terminal failure status uses that same threshold.
This gives a failed item an operational destination. It does not automatically repair the item; investigation and an appropriate redrive still matter.
Decode the file you actually received
The worker checks original metadata and validates decoded image format rather than trusting a filename extension.
For supported ordinary image formats, Sharp generates a JPEG preview. HEIC and HEIF use a WebAssembly decoder before image transformation. Dimensions are checked before allocating the decoded image, and the pixel limit is sixty million.
Default preview settings are a maximum dimension of 720 pixels and JPEG quality 70. The transformation respects orientation, avoids enlargement and produces a compact browsing representation. The original stays unchanged.
These limits bound a family application’s processing workload. They also mean some otherwise legitimate files can be unsupported; the UI needs to preserve that distinction.
Video processing follows a different boundary
The backend does not download and decode the entire video to extract frames.
During preparation, the mobile application extracts preview images and stores them alongside its working file. It uploads them to signed destinations before completing the multipart workflow. The cloud worker reads an available supplied image and normalizes it into the gallery preview.
This moves frame extraction to the device that already has the video. It keeps the backend smaller but adds client work and a dependency: an uploaded video without its required image can fail preview processing.
The architecture has made a tradeoff, not removed the problem.
A correct preview object can still produce a broken gallery
One cross-platform preview failure came from local asynchronous coordination.
The repository shared an in-flight download among callers. Its cleanup callback returned the future being removed from the in-flight map:
download.whenComplete(() => inFlight.remove(id));
That return value was the same tracked future. Returning it from the completion callback created a circular wait. The file could arrive on disk while consumers kept waiting for completion.
Using a block callback made cleanup a side effect without returning that future:
download.whenComplete(() {
inFlight.remove(id);
});
The lesson was concrete: proving that the worker produced a JPEG did not prove the mobile request completed. The path from cloud status to visible pixels included a signed download, a local file, an in-flight map, Flutter’s image cache and widget state.
Repair the cache as a workflow
A corrupt cached image requires more than another widget rebuild.
The retry path waits for any previous download, removes the local cached file and path, and requests the preview again. Replacing a file also evicts its decoded FileImage entry so the UI does not reuse obsolete image data.
The widget resolves a new preview when its media identity or relevant preview information changes. That matters when the same widget instance is reused for another gallery item.
Regression checks cover concurrent requests, download failure and retry, corrupt-cache replacement, and widget reuse. They test the boundaries where a healthy backend can coexist with a broken user experience.
Small derived images save bandwidth. Making them recoverable requires treating processing, downloading and rendering as related but distinct stages.