An object whose :storage-target id is not in the S3 routes configuration
can no longer be located. Reads and serving now fail with
:invalid-storage-target instead of silently using the default bucket,
and garbage collection refuses to delete the row: it logs an error and
parks the row (deleted_at +1 day, no deletion attempts, no give-up)
until the target is configured again. Legacy rows (no target) and the
default target keep working and deleting normally.
Also addresses review findings: per-target dedup isolation and
stale-repair coverage, legacy-row GC coverage through the real task
paths, a single declared-target schema, closing already-built S3
clients when target init fails, and loader edge pins. Docs and memory
updated.
Closes#11630
AI-assisted-by: deepseek-flash
Introduce optional named S3 targets and route internal semantic buckets
(for example tempfile) to them, keeping the fs/s3 backend choice and the
storage_object.backend value unchanged.
Targets and routes are read from an EDN file referenced by
PENPOT_OBJECTS_STORAGE_S3_ROUTES_FILE. The chosen target id is stored in
the object metadata as :storage-target and resolved on reads, URL
signing, deduplication, individual deletes and both GC tasks. The
implicit :default target is built from PENPOT_OBJECTS_STORAGE_S3_*, so
behavior is unchanged when the file is absent. Bulk deletion now carries
the target, and one S3 client/presigner pair is shared per distinct
region/endpoint.
Closes#11630
AI-assisted-by: deepseek-flash