use super::record::read_checkpoint_record;
use crate::error::{CoreError, Result};
use crate::namespace::control::read_head_object;
use futures::StreamExt;
use loonfs_api::wire::control::{CheckpointOwner, CheckpointRecordLifecycle};
use loonfs_api::{
CheckpointId, CheckpointOwnerSummary, CheckpointSummary, ListCheckpointsResponse, NamespaceId,
};
use loonfs_objectstore::keys::{checkpoint_prefix, checkpoint_record};
use loonfs_objectstore::ObjectStore;
pub(crate) async fn list_checkpoints<S: ObjectStore + ?Sized>(
store: &S,
namespace_id: &NamespaceId,
) -> Result<ListCheckpointsResponse> {
read_head_object(store, namespace_id)
.await
.map_err(CoreError::load_head)?;
let prefix = checkpoint_prefix(namespace_id.as_str());
let mut keys = store.list_prefix_stream(&prefix);
let mut checkpoints = Vec::new();
while let Some(item) = keys.next().await {
let key = item.map_err(|error| CoreError::store(&prefix, &error))?;
let checkpoint_id = checkpoint_id_of(&key, namespace_id)?;
let Some(loaded) = read_checkpoint_record(store, namespace_id, &checkpoint_id).await?
else {
continue;
};
if loaded.state.state != (CheckpointRecordLifecycle::Active {}) {
continue;
}
let record = loaded.state;
checkpoints.push(CheckpointSummary {
checkpoint_id: record.checkpoint_id,
owner: match record.owner {
CheckpointOwner::User { name } => CheckpointOwnerSummary::User { name },
CheckpointOwner::Fork {
target_namespace_id,
} => CheckpointOwnerSummary::Fork {
target_namespace_id,
},
},
created_at_ms: record.created_at_ms,
expires_at_ms: record.expires_at_ms,
checkpoint_seq: record.manifest_head_seq,
manifest_id: record.manifest_id,
});
}
checkpoints.sort_by(|left, right| {
left.created_at_ms.cmp(&right.created_at_ms).then_with(|| {
left.checkpoint_id
.as_str()
.cmp(right.checkpoint_id.as_str())
})
});
Ok(ListCheckpointsResponse {
namespace_id: namespace_id.clone(),
checkpoints,
})
}
fn checkpoint_id_of(key: &str, namespace_id: &NamespaceId) -> Result<CheckpointId> {
let parsed = key
.rsplit('/')
.next()
.and_then(|name| name.strip_suffix(".json"))
.and_then(|id| CheckpointId::parse(id).ok())
.filter(|id| checkpoint_record(namespace_id.as_str(), id.as_str()) == key);
parsed.ok_or_else(|| {
CoreError::NamespaceCorrupt(format!("`{key}` is not a checkpoint record key"))
})
}