diff --git a/core/src/repositories/cas_repository.rs b/core/src/repositories/cas_repository.rs index 1b696c5..3470871 100644 --- a/core/src/repositories/cas_repository.rs +++ b/core/src/repositories/cas_repository.rs @@ -153,11 +153,14 @@ pub trait CasRepository: Send + Sync { /// # 示例 /// /// ```rust,ignore - /// if repository.exists(&object_id).await { + /// if repository.exists(&object_id).await? { /// println!("Object exists"); /// } /// ``` - async fn exists(&self, id: &ObjectId) -> bool; + /// + /// 返回 `Result` 以便调用方区分“对象不存在”(`Ok(false)`)与底层存储/IO 故障 + /// (`Err`),而不是把故障静默当成不存在。 + async fn exists(&self, id: &ObjectId) -> crate::Result; /// 增加引用计数 /// diff --git a/crates/bat-cas-engine/src/repository.rs b/crates/bat-cas-engine/src/repository.rs index 1c0f9fc..ea7c199 100644 --- a/crates/bat-cas-engine/src/repository.rs +++ b/crates/bat-cas-engine/src/repository.rs @@ -37,7 +37,7 @@ impl FileSystemCasRepository { /// 存储对象并增加引用计数。 pub async fn store(&self, data: &[u8]) -> Result { let hash = compute_hash(data); - let existed = self.storage.exists(&hash).await; + let existed = self.storage.exists(&hash).await?; let stored_hash = self.storage.put(data).await?; // 单条 UPSERT 原子建行并 +1:对象行不会在 store 期间以 ref_count=0 @@ -75,13 +75,13 @@ impl FileSystemCasRepository { } /// 检查对象是否存在。 - pub async fn exists(&self, hash: &Hash) -> bool { + pub async fn exists(&self, hash: &Hash) -> Result { self.storage.exists(hash).await } /// 增加引用计数。 pub async fn add_reference(&self, hash: &Hash) -> Result { - if !self.storage.exists(hash).await { + if !self.storage.exists(hash).await? { return Err(CasError::ObjectNotFound(hash.to_string())); } @@ -169,8 +169,8 @@ mod tests { repo.remove_reference(&delete).await.unwrap(); assert_eq!(repo.gc().await.unwrap(), 1); - assert!(repo.exists(&keep).await); - assert!(!repo.exists(&delete).await); + assert!(repo.exists(&keep).await.unwrap()); + assert!(!repo.exists(&delete).await.unwrap()); assert!(matches!( repo.get_reference_count(&delete).await, Err(CasError::ObjectNotFound(_)) @@ -209,7 +209,7 @@ mod tests { assert_eq!(repo.get_reference_count(&hash).await.unwrap(), 1); assert!(repo.gc_candidates().await.unwrap().is_empty()); assert_eq!(repo.gc().await.unwrap(), 0); - assert!(repo.exists(&hash).await); + assert!(repo.exists(&hash).await.unwrap()); } #[tokio::test] @@ -224,7 +224,7 @@ mod tests { assert_eq!(repo.add_reference(&hash).await.unwrap(), 1); // gc 的原子闸门应跳过它,对象文件保留。 assert_eq!(repo.gc().await.unwrap(), 0); - assert!(repo.exists(&hash).await); + assert!(repo.exists(&hash).await.unwrap()); assert_eq!(repo.get_reference_count(&hash).await.unwrap(), 1); } diff --git a/crates/bat-cas-engine/src/storage.rs b/crates/bat-cas-engine/src/storage.rs index 80de5d0..1b268a7 100644 --- a/crates/bat-cas-engine/src/storage.rs +++ b/crates/bat-cas-engine/src/storage.rs @@ -20,7 +20,10 @@ pub trait Storage: Send + Sync { async fn get(&self, hash: &Hash) -> Result>; /// 检查对象文件是否存在。 - async fn exists(&self, hash: &Hash) -> bool; + /// + /// 返回 `Result` 以便区分“确实不存在”(`Ok(false)`)与底层 IO 故障(`Err`), + /// 避免把权限或其它 IO 错误静默当成不存在。 + async fn exists(&self, hash: &Hash) -> Result; /// 删除对象文件。 async fn delete(&self, hash: &Hash) -> Result<()>; @@ -115,7 +118,7 @@ impl FileSystemStorage { } async fn validate_existing_object(&self, hash: &Hash) -> Result { - if !self.exists(hash).await { + if !self.exists(hash).await? { return Ok(false); } @@ -249,10 +252,8 @@ impl Storage for FileSystemStorage { Ok(data) } - async fn exists(&self, hash: &Hash) -> bool { - tokio::fs::try_exists(self.object_path(hash)) - .await - .unwrap_or(false) + async fn exists(&self, hash: &Hash) -> Result { + Ok(tokio::fs::try_exists(self.object_path(hash)).await?) } async fn delete(&self, hash: &Hash) -> Result<()> { @@ -347,10 +348,10 @@ mod tests { let hash = storage.put(b"Test data").await.unwrap(); - assert!(storage.exists(&hash).await); + assert!(storage.exists(&hash).await.unwrap()); let deleted = storage.delete(&hash).await; deleted.unwrap(); - assert!(!storage.exists(&hash).await); + assert!(!storage.exists(&hash).await.unwrap()); } #[tokio::test] diff --git a/infrastructure/src/cas/filesystem.rs b/infrastructure/src/cas/filesystem.rs index c994ac0..4f34757 100644 --- a/infrastructure/src/cas/filesystem.rs +++ b/infrastructure/src/cas/filesystem.rs @@ -92,14 +92,13 @@ impl CasRepository for FileSystemCasRepository { Ok(data) } - async fn exists(&self, id: &ObjectId) -> bool { - let Ok(hash) = Self::parse_object_id(id) else { - return false; - }; - let Ok(engine) = self.engine().await else { - return false; - }; - engine.exists(&hash).await + async fn exists(&self, id: &ObjectId) -> bat_core::Result { + let hash = Self::parse_object_id(id)?; + self.engine() + .await? + .exists(&hash) + .await + .map_err(Self::map_error) } async fn add_reference(&self, id: &ObjectId) -> bat_core::Result { @@ -176,7 +175,7 @@ mod tests { assert_eq!(repo.remove_reference(&id).await.unwrap(), 0); assert_eq!(repo.gc().await.unwrap(), 1); - assert!(!repo.exists(&id).await); + assert!(!repo.exists(&id).await.unwrap()); let missing = repo.get(&id).await; assert!(matches!(missing, Err(bat_core::Error::NotFound(_)))); } @@ -191,6 +190,23 @@ mod tests { assert!(matches!(error, bat_core::Error::InvalidArgument(_))); } + #[tokio::test] + async fn exists_distinguishes_missing_from_invalid_id() { + let temp_dir = TempDir::new().unwrap(); + let repo = FileSystemCasRepository::new(temp_dir.path()); + repo.init().await.unwrap(); + + // 合法但不存在的对象:Ok(false),而非静默错误。 + let absent = repo.store(b"absent-seed").await.unwrap(); + repo.remove_reference(&absent).await.unwrap(); + repo.gc().await.unwrap(); + assert!(!repo.exists(&absent).await.unwrap()); + + // 非法 ObjectId:返回 Err 而非 Ok(false),让调用方能区分故障。 + let error = repo.exists(&"not-a-hash".to_string()).await.unwrap_err(); + assert!(matches!(error, bat_core::Error::InvalidArgument(_))); + } + #[tokio::test] async fn store_returns_blake3_hex_object_id() { let temp_dir = TempDir::new().unwrap(); diff --git a/infrastructure/src/import.rs b/infrastructure/src/import.rs index 88c6334..9f5f210 100644 --- a/infrastructure/src/import.rs +++ b/infrastructure/src/import.rs @@ -576,7 +576,7 @@ mod tests { assert_eq!(report.imported.len(), 4); assert_eq!(report.skipped, vec!["synthetic/catalog.json".to_string()]); - assert!(cas.exists(&report.imported[0].object_id).await); + assert!(cas.exists(&report.imported[0].object_id).await.unwrap()); assert_eq!(report.imported[0].resource_type, ResourceType::AssetBundle); assert_eq!( report.imported[0].category, diff --git a/infrastructure/tests/synthetic_phase2_import.rs b/infrastructure/tests/synthetic_phase2_import.rs index 6d886bd..02e216b 100644 --- a/infrastructure/tests/synthetic_phase2_import.rs +++ b/infrastructure/tests/synthetic_phase2_import.rs @@ -92,7 +92,7 @@ async fn imports_synthetic_addressables_catalog_bundle_against_golden() { .unwrap(); assert_eq!(report.imported.len(), 1); - assert!(cas.exists(&report.imported[0].object_id).await); + assert!(cas.exists(&report.imported[0].object_id).await.unwrap()); let indexed = resources.list(ResourceQuery::all()).await.unwrap(); let actual = json!({