use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
use super::cid::Cid;
use super::config::Config;
use super::error::Error;
use super::tree::Tree;
const ROOT_MANIFEST_VERSION: u64 = 1;
#[derive(Serialize, Deserialize)]
struct RootManifestWire {
version: u64,
root: Option<Cid>,
config: Config,
#[serde(default, skip_serializing_if = "Option::is_none")]
created_at_millis: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
updated_at_millis: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RootManifest {
pub root: Option<Cid>,
pub config: Config,
pub created_at_millis: Option<u64>,
pub updated_at_millis: Option<u64>,
}
impl RootManifest {
pub fn new(root: Option<Cid>, config: Config) -> Self {
Self {
root,
config,
created_at_millis: None,
updated_at_millis: None,
}
}
pub fn with_timestamps_millis(
mut self,
created_at_millis: Option<u64>,
updated_at_millis: Option<u64>,
) -> Self {
self.created_at_millis = created_at_millis;
self.updated_at_millis = updated_at_millis;
self
}
pub fn with_created_at_millis(mut self, created_at_millis: u64) -> Self {
self.created_at_millis = Some(created_at_millis);
self
}
pub fn with_updated_at_millis(mut self, updated_at_millis: u64) -> Self {
self.updated_at_millis = Some(updated_at_millis);
self
}
pub fn from_tree_with_timestamps_millis(
tree: &Tree,
created_at_millis: Option<u64>,
updated_at_millis: Option<u64>,
) -> Self {
Self {
root: tree.root.clone(),
config: tree.config.clone(),
created_at_millis,
updated_at_millis,
}
}
pub fn from_tree(tree: &Tree) -> Self {
Self {
root: tree.root.clone(),
config: tree.config.clone(),
created_at_millis: None,
updated_at_millis: None,
}
}
pub fn into_tree(self) -> Tree {
Tree {
root: self.root,
config: self.config,
}
}
pub fn to_tree(&self) -> Tree {
Tree {
root: self.root.clone(),
config: self.config.clone(),
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
let wire = RootManifestWire {
version: ROOT_MANIFEST_VERSION,
root: self.root.clone(),
config: self.config.clone(),
created_at_millis: self.created_at_millis,
updated_at_millis: self.updated_at_millis,
};
serde_cbor::ser::to_vec_packed(&wire).map_err(|err| Error::Deserialize(err.to_string()))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
let wire: RootManifestWire =
serde_cbor::from_slice(bytes).map_err(|err| Error::Deserialize(err.to_string()))?;
if wire.version != ROOT_MANIFEST_VERSION {
return Err(Error::Deserialize(format!(
"unsupported root manifest version: {}",
wire.version
)));
}
Ok(Self {
root: wire.root,
config: wire.config,
created_at_millis: wire.created_at_millis,
updated_at_millis: wire.updated_at_millis,
})
}
}
impl From<Tree> for RootManifest {
fn from(tree: Tree) -> Self {
Self {
root: tree.root,
config: tree.config,
created_at_millis: None,
updated_at_millis: None,
}
}
}
impl From<&Tree> for RootManifest {
fn from(tree: &Tree) -> Self {
Self::from_tree(tree)
}
}
impl From<RootManifest> for Tree {
fn from(manifest: RootManifest) -> Self {
manifest.into_tree()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct NamedRootManifest {
pub name: Vec<u8>,
pub manifest: RootManifest,
}
impl NamedRootManifest {
pub fn new(name: Vec<u8>, manifest: RootManifest) -> Self {
Self { name, manifest }
}
pub fn into_named_root(self) -> NamedRoot {
NamedRoot {
name: self.name,
tree: self.manifest.into_tree(),
}
}
pub fn to_named_root(&self) -> NamedRoot {
NamedRoot {
name: self.name.clone(),
tree: self.manifest.to_tree(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct NamedRoot {
pub name: Vec<u8>,
pub tree: Tree,
}
impl NamedRoot {
pub fn new(name: Vec<u8>, tree: Tree) -> Self {
Self { name, tree }
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct NamedRootSelection {
pub roots: Vec<NamedRoot>,
pub missing_names: Vec<Vec<u8>>,
}
impl NamedRootSelection {
pub fn new(roots: Vec<NamedRoot>, missing_names: Vec<Vec<u8>>) -> Self {
Self {
roots,
missing_names,
}
}
pub fn is_complete(&self) -> bool {
self.missing_names.is_empty()
}
pub fn trees(&self) -> Vec<Tree> {
self.roots.iter().map(|root| root.tree.clone()).collect()
}
pub fn into_trees(self) -> Vec<Tree> {
self.roots.into_iter().map(|root| root.tree).collect()
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum NamedRootRetention {
All,
Exact {
names: Vec<Vec<u8>>,
},
Prefix {
prefix: Vec<u8>,
},
NewestByName {
prefix: Vec<u8>,
count: usize,
},
UpdatedSince {
prefix: Vec<u8>,
min_updated_at_millis: u64,
},
}
impl NamedRootRetention {
pub fn all() -> Self {
Self::All
}
pub fn exact<I, N>(names: I) -> Self
where
I: IntoIterator<Item = N>,
N: AsRef<[u8]>,
{
Self::Exact {
names: names
.into_iter()
.map(|name| name.as_ref().to_vec())
.collect(),
}
}
pub fn prefix(prefix: impl AsRef<[u8]>) -> Self {
Self::Prefix {
prefix: prefix.as_ref().to_vec(),
}
}
pub fn newest_by_name(prefix: impl AsRef<[u8]>, count: usize) -> Self {
Self::NewestByName {
prefix: prefix.as_ref().to_vec(),
count,
}
}
pub fn updated_since(prefix: impl AsRef<[u8]>, min_updated_at_millis: u64) -> Self {
Self::UpdatedSince {
prefix: prefix.as_ref().to_vec(),
min_updated_at_millis,
}
}
pub fn updated_within(prefix: impl AsRef<[u8]>, now_millis: u64, max_age: Duration) -> Self {
Self::updated_within_millis(prefix, now_millis, duration_millis_saturating(max_age))
}
pub fn updated_within_millis(
prefix: impl AsRef<[u8]>,
now_millis: u64,
window_millis: u64,
) -> Self {
Self::updated_since(prefix, now_millis.saturating_sub(window_millis))
}
pub fn updated_within_days(prefix: impl AsRef<[u8]>, now_millis: u64, days: u64) -> Self {
Self::updated_within_millis(prefix, now_millis, days.saturating_mul(86_400_000))
}
}
fn duration_millis_saturating(duration: Duration) -> u64 {
duration.as_millis().min(u128::from(u64::MAX)) as u64
}
pub(crate) fn sort_named_root_manifests(roots: &mut [NamedRootManifest]) {
roots.sort_by(|left, right| left.name.cmp(&right.name));
}
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum ManifestUpdate {
Applied,
Conflict {
current: Option<RootManifest>,
},
}
impl ManifestUpdate {
pub fn is_applied(&self) -> bool {
matches!(self, Self::Applied)
}
pub fn is_conflict(&self) -> bool {
matches!(self, Self::Conflict { .. })
}
pub fn current(&self) -> Option<&RootManifest> {
match self {
Self::Applied => None,
Self::Conflict { current } => current.as_ref(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum NamedRootUpdate {
Applied,
Conflict {
current: Option<Tree>,
},
}
impl NamedRootUpdate {
pub fn is_applied(&self) -> bool {
matches!(self, Self::Applied)
}
pub fn is_conflict(&self) -> bool {
matches!(self, Self::Conflict { .. })
}
pub fn current(&self) -> Option<&Tree> {
match self {
Self::Applied => None,
Self::Conflict { current } => current.as_ref(),
}
}
}
impl From<ManifestUpdate> for NamedRootUpdate {
fn from(update: ManifestUpdate) -> Self {
match update {
ManifestUpdate::Applied => Self::Applied,
ManifestUpdate::Conflict { current } => Self::Conflict {
current: current.map(RootManifest::into_tree),
},
}
}
}
pub trait ManifestStore: Send + Sync {
type Error: std::error::Error + Send + Sync + 'static;
fn get_root(&self, name: &[u8]) -> Result<Option<RootManifest>, Self::Error>;
fn put_root(&self, name: &[u8], manifest: &RootManifest) -> Result<(), Self::Error>;
fn delete_root(&self, name: &[u8]) -> Result<(), Self::Error>;
fn compare_and_swap_root(
&self,
name: &[u8],
expected: Option<&RootManifest>,
new: Option<&RootManifest>,
) -> Result<ManifestUpdate, Self::Error>;
}
pub trait ManifestStoreScan: ManifestStore {
fn list_roots(&self) -> Result<Vec<NamedRootManifest>, Self::Error>;
}
#[cfg(feature = "async-store")]
#[allow(async_fn_in_trait)]
pub trait AsyncManifestStore {
type Error: std::error::Error + 'static;
async fn get_root(&self, name: &[u8]) -> Result<Option<RootManifest>, Self::Error>;
async fn put_root(&self, name: &[u8], manifest: &RootManifest) -> Result<(), Self::Error>;
async fn delete_root(&self, name: &[u8]) -> Result<(), Self::Error>;
async fn compare_and_swap_root(
&self,
name: &[u8],
expected: Option<&RootManifest>,
new: Option<&RootManifest>,
) -> Result<ManifestUpdate, Self::Error>;
}
#[cfg(feature = "async-store")]
#[allow(async_fn_in_trait)]
pub trait AsyncManifestStoreScan: AsyncManifestStore {
async fn list_roots(&self) -> Result<Vec<NamedRootManifest>, Self::Error>;
}
impl<T: ManifestStore> ManifestStore for Arc<T> {
type Error = T::Error;
fn get_root(&self, name: &[u8]) -> Result<Option<RootManifest>, Self::Error> {
(**self).get_root(name)
}
fn put_root(&self, name: &[u8], manifest: &RootManifest) -> Result<(), Self::Error> {
(**self).put_root(name, manifest)
}
fn delete_root(&self, name: &[u8]) -> Result<(), Self::Error> {
(**self).delete_root(name)
}
fn compare_and_swap_root(
&self,
name: &[u8],
expected: Option<&RootManifest>,
new: Option<&RootManifest>,
) -> Result<ManifestUpdate, Self::Error> {
(**self).compare_and_swap_root(name, expected, new)
}
}
impl<T: ManifestStoreScan> ManifestStoreScan for Arc<T> {
fn list_roots(&self) -> Result<Vec<NamedRootManifest>, Self::Error> {
(**self).list_roots()
}
}
#[cfg(feature = "async-store")]
impl<T: AsyncManifestStore> AsyncManifestStore for Arc<T> {
type Error = T::Error;
async fn get_root(&self, name: &[u8]) -> Result<Option<RootManifest>, Self::Error> {
(**self).get_root(name).await
}
async fn put_root(&self, name: &[u8], manifest: &RootManifest) -> Result<(), Self::Error> {
(**self).put_root(name, manifest).await
}
async fn delete_root(&self, name: &[u8]) -> Result<(), Self::Error> {
(**self).delete_root(name).await
}
async fn compare_and_swap_root(
&self,
name: &[u8],
expected: Option<&RootManifest>,
new: Option<&RootManifest>,
) -> Result<ManifestUpdate, Self::Error> {
(**self).compare_and_swap_root(name, expected, new).await
}
}
#[cfg(feature = "async-store")]
impl<T: AsyncManifestStoreScan> AsyncManifestStoreScan for Arc<T> {
async fn list_roots(&self) -> Result<Vec<NamedRootManifest>, Self::Error> {
(**self).list_roots().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Config, Encoding};
use std::time::Duration;
#[derive(Serialize)]
struct LegacyRootManifestWire {
version: u64,
root: Option<Cid>,
config: Config,
}
#[test]
fn manifest_round_trips_through_bytes() {
let cid = Cid::from_bytes(b"root");
let manifest = RootManifest::new(
Some(cid),
Config::builder()
.min_chunk_size(2)
.max_chunk_size(8)
.chunking_factor(4)
.hash_seed(99)
.encoding(Encoding::Json)
.node_cache_max_nodes(128)
.build(),
)
.with_timestamps_millis(Some(100), Some(200));
let bytes = manifest.to_bytes().unwrap();
assert_eq!(RootManifest::from_bytes(&bytes).unwrap(), manifest);
}
#[test]
fn manifest_reads_legacy_bytes_without_timestamps() {
let legacy = LegacyRootManifestWire {
version: ROOT_MANIFEST_VERSION,
root: Some(Cid::from_bytes(b"legacy-root")),
config: Config::default(),
};
let bytes = serde_cbor::ser::to_vec_packed(&legacy).unwrap();
let manifest = RootManifest::from_bytes(&bytes).unwrap();
assert_eq!(manifest.root, legacy.root);
assert_eq!(manifest.config, legacy.config);
assert_eq!(manifest.created_at_millis, None);
assert_eq!(manifest.updated_at_millis, None);
}
#[test]
fn manifest_converts_to_and_from_tree() {
let tree = Tree {
root: Some(Cid::from_bytes(b"root")),
config: Config::default(),
};
let manifest = RootManifest::from_tree(&tree);
assert_eq!(manifest.to_tree(), tree);
assert_eq!(Tree::from(manifest), tree);
}
#[test]
fn named_root_update_converts_conflict_to_tree() {
let tree = Tree {
root: Some(Cid::from_bytes(b"root")),
config: Config::default(),
};
let update = ManifestUpdate::Conflict {
current: Some(RootManifest::from_tree(&tree)),
};
assert_eq!(
NamedRootUpdate::from(update),
NamedRootUpdate::Conflict {
current: Some(tree)
}
);
}
#[test]
fn retention_duration_helpers_build_cutoffs() {
assert_eq!(
NamedRootRetention::updated_within(b"checkpoint/", 1_000, Duration::from_millis(250)),
NamedRootRetention::updated_since(b"checkpoint/", 750)
);
assert_eq!(
NamedRootRetention::updated_within_millis(b"checkpoint/", 100, 250),
NamedRootRetention::updated_since(b"checkpoint/", 0)
);
assert_eq!(
NamedRootRetention::updated_within_days(b"checkpoint/", 172_800_050, 1),
NamedRootRetention::updated_since(b"checkpoint/", 86_400_050)
);
assert_eq!(
NamedRootRetention::updated_within_days(b"checkpoint/", 42, u64::MAX),
NamedRootRetention::updated_since(b"checkpoint/", 0)
);
}
}