use std::{error::Error, fmt};
use crate::{
cdk::types::Principal,
model::blob_storage::BlobRootHash,
storage::stable::blob_storage::{
BlobDeletionPendingRecord, BlobStorageStore, StorageGatewayPrincipalRecord,
StoredBlobRecord,
},
};
pub struct BlobStorageLifecycleOps;
impl BlobStorageLifecycleOps {
pub fn register_live(
hash: &BlobRootHash,
now_ns: u64,
) -> Result<BlobRegisterOutcome, BlobStorageLifecycleError> {
if BlobStorageStore::get_pending_deletion(hash).is_some() {
return Err(BlobStorageLifecycleError::BlobPendingDeletion);
}
if BlobStorageStore::get_stored_blob(hash).is_some() {
return Ok(BlobRegisterOutcome::AlreadyLive);
}
BlobStorageStore::upsert_stored_blob(hash, StoredBlobRecord::new(hash, now_ns));
Ok(BlobRegisterOutcome::Registered)
}
#[must_use]
pub fn is_live(hash: &BlobRootHash) -> bool {
BlobStorageStore::get_stored_blob(hash).is_some()
&& BlobStorageStore::get_pending_deletion(hash).is_none()
}
pub fn require_live(
hash: &BlobRootHash,
) -> Result<StoredBlobRecord, BlobStorageLifecycleError> {
let Some(record) = BlobStorageStore::get_stored_blob(hash) else {
return Err(BlobStorageLifecycleError::BlobNotLive);
};
if BlobStorageStore::get_pending_deletion(hash).is_some() {
return Err(BlobStorageLifecycleError::BlobPendingDeletion);
}
Ok(record)
}
pub fn mark_pending_delete(
hash: &BlobRootHash,
now_ns: u64,
) -> Result<BlobPendingDeletionOutcome, BlobStorageLifecycleError> {
if BlobStorageStore::get_stored_blob(hash).is_none() {
return Err(BlobStorageLifecycleError::BlobNotLive);
}
if BlobStorageStore::get_pending_deletion(hash).is_some() {
return Ok(BlobPendingDeletionOutcome::AlreadyPendingDeletion);
}
BlobStorageStore::upsert_pending_deletion(
hash,
BlobDeletionPendingRecord::new(hash, now_ns),
);
Ok(BlobPendingDeletionOutcome::MarkedPendingDeletion)
}
pub fn confirm_deleted_by_gateway(hash: &BlobRootHash) {
BlobStorageStore::remove_pending_deletion(hash);
BlobStorageStore::remove_stored_blob(hash);
}
#[must_use]
pub fn stored_blob_count() -> u64 {
BlobStorageStore::stored_blob_count()
}
#[must_use]
pub fn pending_deletion_count() -> u64 {
BlobStorageStore::pending_deletion_count()
}
#[must_use]
pub fn pending_deletion_hashes() -> Vec<String> {
BlobStorageStore::pending_deletions()
.into_iter()
.map(|(key, _)| key.as_str().to_string())
.collect()
}
pub fn upsert_gateway_principal(principal: Principal, now_ns: u64) {
BlobStorageStore::upsert_gateway_principal(
principal,
StorageGatewayPrincipalRecord::new(principal, now_ns),
);
}
pub fn remove_gateway_principal(principal: Principal) -> bool {
BlobStorageStore::remove_gateway_principal(principal).is_some()
}
#[must_use]
pub fn gateway_principal_count() -> u64 {
BlobStorageStore::gateway_principal_count()
}
#[must_use]
pub fn is_gateway_principal(principal: Principal) -> bool {
BlobStorageStore::get_gateway_principal(principal).is_some()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BlobRegisterOutcome {
Registered,
AlreadyLive,
}
impl BlobRegisterOutcome {
#[must_use]
pub const fn inserted(self) -> bool {
matches!(self, Self::Registered)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BlobPendingDeletionOutcome {
MarkedPendingDeletion,
AlreadyPendingDeletion,
}
impl BlobPendingDeletionOutcome {
#[must_use]
pub const fn inserted(self) -> bool {
matches!(self, Self::MarkedPendingDeletion)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BlobStorageLifecycleError {
BlobNotLive,
BlobPendingDeletion,
}
impl fmt::Display for BlobStorageLifecycleError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BlobNotLive => formatter.write_str("blob is not registered live"),
Self::BlobPendingDeletion => formatter.write_str("blob is pending deletion"),
}
}
}
impl Error for BlobStorageLifecycleError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
model::blob_storage::BlobRootHash, storage::stable::blob_storage::BlobStorageStore,
};
fn hash(value: &str) -> BlobRootHash {
BlobRootHash::try_from(value).expect("valid blob root hash")
}
fn h1() -> BlobRootHash {
hash("sha256:1111111111111111111111111111111111111111111111111111111111111111")
}
fn h2() -> BlobRootHash {
hash("sha256:2222222222222222222222222222222222222222222222222222222222222222")
}
fn p(id: u8) -> Principal {
Principal::from_slice(&[id; 29])
}
#[test]
fn register_live_is_idempotent_until_pending_deletion() {
BlobStorageStore::clear();
let hash = h1();
assert_eq!(
BlobStorageLifecycleOps::register_live(&hash, 10).expect("register"),
BlobRegisterOutcome::Registered
);
assert_eq!(
BlobStorageLifecycleOps::register_live(&hash, 20).expect("register again"),
BlobRegisterOutcome::AlreadyLive
);
assert!(BlobStorageLifecycleOps::is_live(&hash));
BlobStorageLifecycleOps::mark_pending_delete(&hash, 30).expect("mark pending");
assert_eq!(
BlobStorageLifecycleOps::register_live(&hash, 40),
Err(BlobStorageLifecycleError::BlobPendingDeletion)
);
assert!(!BlobStorageLifecycleOps::is_live(&hash));
}
#[test]
fn mark_pending_delete_requires_live_blob() {
BlobStorageStore::clear();
let hash = h1();
assert_eq!(
BlobStorageLifecycleOps::mark_pending_delete(&hash, 10),
Err(BlobStorageLifecycleError::BlobNotLive)
);
}
#[test]
fn gateway_confirmation_removes_pending_and_live_state() {
BlobStorageStore::clear();
let hash = h1();
BlobStorageLifecycleOps::register_live(&hash, 10).expect("register");
assert_eq!(BlobStorageLifecycleOps::stored_blob_count(), 1);
assert_eq!(BlobStorageLifecycleOps::pending_deletion_count(), 0);
BlobStorageLifecycleOps::mark_pending_delete(&hash, 20).expect("mark pending");
assert_eq!(BlobStorageLifecycleOps::stored_blob_count(), 1);
assert_eq!(BlobStorageLifecycleOps::pending_deletion_count(), 1);
assert_eq!(
BlobStorageLifecycleOps::pending_deletion_hashes(),
vec![hash.as_str().to_string()]
);
BlobStorageLifecycleOps::confirm_deleted_by_gateway(&hash);
assert!(!BlobStorageLifecycleOps::is_live(&hash));
assert_eq!(BlobStorageLifecycleOps::stored_blob_count(), 0);
assert_eq!(BlobStorageLifecycleOps::pending_deletion_count(), 0);
assert!(BlobStorageLifecycleOps::pending_deletion_hashes().is_empty());
BlobStorageLifecycleOps::confirm_deleted_by_gateway(&hash);
assert_eq!(BlobStorageLifecycleOps::stored_blob_count(), 0);
assert_eq!(BlobStorageLifecycleOps::pending_deletion_count(), 0);
}
#[test]
fn gateway_confirmation_matches_inventory_edge_cases() {
BlobStorageStore::clear();
let unknown = h1();
let live_only = h2();
BlobStorageLifecycleOps::confirm_deleted_by_gateway(&unknown);
assert_eq!(BlobStorageLifecycleOps::stored_blob_count(), 0);
assert_eq!(BlobStorageLifecycleOps::pending_deletion_count(), 0);
BlobStorageLifecycleOps::register_live(&live_only, 10).expect("register");
assert!(BlobStorageLifecycleOps::is_live(&live_only));
BlobStorageLifecycleOps::confirm_deleted_by_gateway(&live_only);
assert!(!BlobStorageLifecycleOps::is_live(&live_only));
assert_eq!(BlobStorageLifecycleOps::stored_blob_count(), 0);
assert_eq!(BlobStorageLifecycleOps::pending_deletion_count(), 0);
}
#[test]
fn re_registration_after_confirmation_requires_explicit_register() {
BlobStorageStore::clear();
let hash = h1();
BlobStorageLifecycleOps::register_live(&hash, 10).expect("register");
BlobStorageLifecycleOps::mark_pending_delete(&hash, 20).expect("mark pending");
BlobStorageLifecycleOps::confirm_deleted_by_gateway(&hash);
assert!(!BlobStorageLifecycleOps::is_live(&hash));
assert_eq!(BlobStorageLifecycleOps::stored_blob_count(), 0);
assert_eq!(BlobStorageLifecycleOps::pending_deletion_count(), 0);
assert_eq!(
BlobStorageLifecycleOps::register_live(&hash, 30).expect("explicit re-register"),
BlobRegisterOutcome::Registered
);
assert!(BlobStorageLifecycleOps::is_live(&hash));
assert_eq!(BlobStorageLifecycleOps::stored_blob_count(), 1);
assert_eq!(BlobStorageLifecycleOps::pending_deletion_count(), 0);
}
#[test]
fn gateway_principal_registry_is_idempotent() {
BlobStorageStore::clear();
let gateway = p(42);
assert!(!BlobStorageLifecycleOps::is_gateway_principal(gateway));
assert_eq!(BlobStorageLifecycleOps::gateway_principal_count(), 0);
BlobStorageLifecycleOps::upsert_gateway_principal(gateway, 10);
BlobStorageLifecycleOps::upsert_gateway_principal(gateway, 20);
assert!(BlobStorageLifecycleOps::is_gateway_principal(gateway));
assert_eq!(BlobStorageLifecycleOps::gateway_principal_count(), 1);
assert!(BlobStorageLifecycleOps::remove_gateway_principal(gateway));
assert!(!BlobStorageLifecycleOps::remove_gateway_principal(gateway));
assert!(!BlobStorageLifecycleOps::is_gateway_principal(gateway));
assert_eq!(BlobStorageLifecycleOps::gateway_principal_count(), 0);
}
}