feat(services): add S3CompatibleImageFileStorage (first cloud impl of ImageFileStorageBase)#9182
feat(services): add S3CompatibleImageFileStorage (first cloud impl of ImageFileStorageBase)#9182goanpeca wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an opt-in S3-compatible image file storage backend that implements ImageFileStorageBase, parallel to the existing on-disk implementation. The backend is selected via a new storage_backend config field defaulting to "disk", so behavior is unchanged for existing users. boto3 is reused (already a transitive dep), with conveniences for AWS S3, Backblaze B2, and any S3-compatible store.
Changes:
- New
S3CompatibleImageFileStorageplus user-metadata-based workflow/graph lookups and a synthetics3://get_pathstub. - New
storage_backend,s3_bucket,s3_endpoint_urlconfig fields and dispatch independencies.initialize(). - Documentation pages (mkdocs and starlight) plus unit tests against a hand-rolled fake S3 client.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| invokeai/app/services/image_files/image_files_s3.py | New S3-compatible image storage implementation. |
| invokeai/app/services/config/config_default.py | Adds storage_backend, s3_bucket, s3_endpoint_url settings. |
| invokeai/app/api/dependencies.py | Selects disk vs. S3 image storage at startup. |
| tests/app/services/image_files/test_image_files_s3.py | Unit tests using a fake S3 client. |
| tests/test_config.py | Tests for the new storage_backend config field. |
| docs/src/content/docs/configuration/object-storage.mdx | Starlight docs page for the new backend. |
| docs-old/configuration/object-storage.md | mkdocs docs page mirror. |
| mkdocs.yml | Adds the new docs page to navigation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
741cf75 to
3c5a782
Compare
3c5a782 to
f9cd46c
Compare
f9cd46c to
c52003e
Compare
9fe96e7 to
978871e
Compare
acfdafe to
fec66d6
Compare
fec66d6 to
02b5820
Compare
02b5820 to
99de05d
Compare
|
Thanks for an awesome project! One honest note: this PR grew a fair bit beyond the original "add an S3 backend" idea. To actually serve images from a backend that has no local filesystem path, I ended up adding a streaming read path ( If that's more than you'd like to take in one go, I'm very happy to split it into smaller, focused PRs. One natural cut would be landing the streaming-serving refactor first (it stands on its own and also benefits the existing disk backend), then adding Either way I'm glad to keep iterating, rebase, or adjust anything you'd like. And the offer still stands to provide a Backblaze B2 test bucket plus a scoped key if you'd like the real-bucket integration test wired into CI. Thanks again for the time and attention here! |
FindingsLow- HEAD Evidence chain:
To expose this issue, add a test that issues a HEAD request to That is only needed for install with a Proxy that could cache. The Frontend and Backend does not use a HEAD request. Medium (test gap) - the most common 404 path, a missing image record, is never tested. In production a bogus To expose this issue, add a test that makes Low - S3-backed responses are emitted without To expose this issue, add a test asserting a Open Questions
|
|
@goanpeca There are comments on this PR that need to be addressed. Are you still actively working on it? |
|
Hi @lstein yes! will review them now :) Sorry for the delay! |
… ImageFileStorageBase) Add S3CompatibleImageFileStorage parallel to the disk backend, selectable via an opt-in storage_backend (disk | s3) config field (defaults to disk). Works against AWS S3 and any S3-compatible store (Backblaze B2, MinIO, ...) via s3_endpoint_url. No new runtime dependency (boto3 already ships with the download service). Stream the image read path so backends without a local filesystem path are served without buffering whole images in memory: - ImageFileStorageBase gains open_stream() and get_local_path(); disk and S3 both implement them. - Image-serving routes use FileResponse when a local path exists, else a chunked StreamingResponse over open_stream(), with blocking lookups in a threadpool and an RFC 5987-safe inline Content-Disposition. - Bulk download streams object bytes into the zip instead of loading images. Store non-ASCII workflow/graph metadata as hex-encoded S3 user-metadata within the metadata size budget. Closes invoke-ai#9121, closes invoke-ai#1425.
|
@Pfannkuchensack thanks for the review. HEAD /full on S3
Missing image record 404 test gap
S3 responses missing Content-Length
Open Question: rollback can delete pre-existing object
Open Question: switching storage_backend does not migrate existing images
Residual: duplicate image_records.get() lookup on S3 GET
|
…torage # Conflicts: # invokeai/app/services/config/config_default.py # tests/test_config.py # uv.lock
…backend
The image-subfolder-move feature merged from main added three abstract
methods to ImageFileStorageBase (image_root, thumbnail_root,
evict_cache_paths). The S3 backend predated them, so it could not be
instantiated ("Can't instantiate abstract class ... with abstract
methods"), crashing startup when storage_backend="s3".
Provide synthetic s3:// roots (identification only, mirroring get_path)
and a no-op evict_cache_paths (this backend keeps no local image cache).
Object storage has no directory structure and the image-move service is filesystem-only (os.replace/fsync/empty-dir cleanup), so reorganizing by subfolder strategy can't work on S3 — a strategy switch would strand existing objects. For storage_backend="s3": - do not create ImageMoveService (image_moves API returns 503) - force flat storage at write time regardless of image_subfolder_strategy - hide the strategy selector and storage-maintenance UI in settings
Clarify that storage_backend is a deploy-time choice with no migration path between disk and s3 (switching orphans the gallery), and that s3 is intended for hosted/multi-replica deployments decoupling output from the local filesystem. Correct the now-inaccurate object-layout section (S3 stores flat; the image_subfolder_strategy has no effect and its move-based maintenance is unavailable), and note the disk-only restriction in the yaml reference, the maintenance feature doc, and the generated settings reference.
|
Picking this up to get it merge-ready. Summary of what I changed and why: Merged Fixed a startup crash introduced by the merge. Disabled the subfolder feature for S3. Object storage has no directory
Docs. Documented that All backend gates, frontend I have tested all the PR offers. Works fine, is a bit slow on the save but that was to be expected. |
Summary
First concrete cloud impl of
ImageFileStorageBase. The ABC was introduced in #1650 with cloud storage explicitly named as the design rationale ("if someone wants to use cloud storage for their images, they should be able to replace the image storage service easily."), but no implementation has ever landed.S3CompatibleImageFileStoragelives atinvokeai/app/services/image_files/image_files_s3.py, parallel toimage_files_disk.py. It works against AWS S3 by default and any S3-compatible store (Backblaze B2, MinIO, etc.) whens3_endpoint_urlis set. It is selected via a new opt-instorage_backend: Literal["disk", "s3"]field onInvokeAIAppConfig, defaulting to"disk", so behavior is unchanged for existing users. No new runtime dep; boto3 is already pulled in by thedownloadservice.To serve images from a backend with no local filesystem path, the read path now streams instead of buffering:
ImageFileStorageBasegainsopen_stream()andget_local_path(); both disk and S3 implement them.FileResponse(sendfile) when a real local path exists (disk) and fall back to a chunkedStreamingResponseoveropen_stream()otherwise (S3). Blocking lookups run in a threadpool, and theinlineContent-Disposition is built RFC 5987-safe (header-injection and path-separator hardened).Non-ASCII workflow/graph metadata is stored as hex-encoded S3 user-metadata within the S3 metadata size budget.
Related Issues / Discussions
Open Questions
Opening as draft so these can be discussed against concrete code. Carried over from #9121:
DiskImageFileStoragekeeps an in-process LRU; the S3 impl is stateless. Ship the cache now, or v1 without and follow up after profiling?pil_compress_level— disk reads it from config at save time; S3 currently uses PIL's default. Thread it through?get_pathcontract — typedPath, but S3 has no real path. We return a synthetics3://...; long-term answer is presigned URLs. OK as a stub?S3CompatibleObjectSerializer(latents) andS3PresignedUrlService(frontend direct-fetch). Keep them as separate PRs?Testing
Unit / router (no network, runs in CI):
_FakeS3Clientinjected viaclient=(no new test dep, no moto).FileResponsevsStreamingResponse, 404 mapping, Content-Disposition injection/path-separator safety.storage_backendconfig selection.Real-bucket integration (opt-in, gated):
tests/integration/test_image_files_s3_real.pyruns the full save → read → metadata → delete pipeline against an actual bucket. Markedslowand skipped unless theCI_TEST_S3_*env vars are set, so normal/CI runs never touch a real bucket. Parametrized over AWS-style and B2-style credential env names.CI_TEST_S3_BUCKET=... CI_TEST_S3_REGION=... \ CI_TEST_S3_ACCESS_KEY_ID=... CI_TEST_S3_SECRET_ACCESS_KEY=... \ # B2: also set CI_TEST_S3_ENDPOINT_URL=https://s3.<region>.backblazeb2.com uv run pytest -m s3_integration tests/integration/test_image_files_s3_real.py -vA manual
workflow_dispatchCI job (.github/workflows/s3-integration.yml) runs this against repo secrets. We can offer a Backblaze B2 test bucket + scoped key for maintainers to wire up.QA Instructions
Generate an image, confirm gallery preview + workflow round-trip, confirm
images/<name>.pngandthumbnails/<name>.webpland in the bucket, delete from gallery and confirm both keys disappear. Smoke-tested against AWSus-east-1and Backblaze B2us-west-004.Checklist