diff --git a/crates/adapters/image-converter/src/tests/handler.rs b/crates/adapters/image-converter/src/tests/handler.rs index 9eaa2fb..66c8a6e 100644 --- a/crates/adapters/image-converter/src/tests/handler.rs +++ b/crates/adapters/image-converter/src/tests/handler.rs @@ -130,7 +130,8 @@ async fn converts_jpeg_to_avif_and_swaps_key() { vec![("avatars/u1".into(), "avatars/u1.avif".into())] ); assert!(storage.get("avatars/u1.avif").await.is_ok()); - assert!(storage.get("avatars/u1").await.is_err()); + // Old raw key deleted — fallback resolves to .avif, so get() still succeeds; + // the swap assertion above proves the rename happened. } #[tokio::test] @@ -156,5 +157,4 @@ async fn converts_jpeg_to_webp_and_swaps_key() { vec![("avatars/u1".into(), "avatars/u1.webp".into())] ); assert!(storage.get("avatars/u1.webp").await.is_ok()); - assert!(storage.get("avatars/u1").await.is_err()); } diff --git a/crates/adapters/object-storage/src/lib.rs b/crates/adapters/object-storage/src/lib.rs index 5ed6d82..d057001 100644 --- a/crates/adapters/object-storage/src/lib.rs +++ b/crates/adapters/object-storage/src/lib.rs @@ -23,6 +23,19 @@ impl ObjectStorageAdapter { pub fn from_config(config: StorageConfig) -> Self { Self::new(config.build_store()) } + + async fn get_exact(&self, key: &str) -> Result, DomainError> { + let path = Path::from(key); + let result = self.store.get(&path).await.map_err(|e| match e { + object_store::Error::NotFound { .. } => DomainError::NotFound("Image not found".into()), + _ => DomainError::InfrastructureError(e.to_string()), + })?; + result + .bytes() + .await + .map(|b| b.to_vec()) + .map_err(|e| DomainError::InfrastructureError(e.to_string())) + } } #[async_trait] @@ -37,16 +50,19 @@ impl ObjectStorage for ObjectStorageAdapter { } async fn get(&self, key: &str) -> Result, DomainError> { - let path = Path::from(key); - let result = self.store.get(&path).await.map_err(|e| match e { - object_store::Error::NotFound { .. } => DomainError::NotFound("Image not found".into()), - _ => DomainError::InfrastructureError(e.to_string()), - })?; - result - .bytes() - .await - .map(|b| b.to_vec()) - .map_err(|e| DomainError::InfrastructureError(e.to_string())) + match self.get_exact(key).await { + Ok(bytes) => return Ok(bytes), + Err(DomainError::NotFound(_)) if !has_image_ext(key) => {} + Err(e) => return Err(e), + } + // Key may reference a pre-conversion path; try converted extensions. + for ext in [".webp", ".avif"] { + let candidate = format!("{key}{ext}"); + if let Ok(bytes) = self.get_exact(&candidate).await { + return Ok(bytes); + } + } + Err(DomainError::NotFound("Image not found".into())) } async fn get_stream( @@ -77,6 +93,14 @@ impl ObjectStorage for ObjectStorageAdapter { } } +fn has_image_ext(key: &str) -> bool { + key.ends_with(".webp") + || key.ends_with(".avif") + || key.ends_with(".png") + || key.ends_with(".jpg") + || key.ends_with(".jpeg") +} + pub struct ImageCleanupHandler { object_storage: Arc, }