use crate::MULTIHASH_BLAKE3;
use anyhow::{Result, bail};
use chrono::{DateTime, TimeZone, Utc};
use ipld_core::ipld::Ipld;
use multihash::Multihash;
use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::{DeserializeOwned, Error as DeError},
};
use std::{collections::BTreeMap, fmt::Display};
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum NodeType {
PublicFile,
PublicDirectory,
PrivateFile,
PrivateDirectory,
TemporalSharePointer,
SnapshotSharePointer,
}
impl Display for NodeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
NodeType::PublicFile => "wnfs/pub/file",
NodeType::PublicDirectory => "wnfs/pub/dir",
NodeType::PrivateFile => "wnfs/priv/file",
NodeType::PrivateDirectory => "wnfs/priv/dir",
NodeType::TemporalSharePointer => "wnfs/share/temporal",
NodeType::SnapshotSharePointer => "wnfs/share/snapshot",
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Metadata(pub BTreeMap<String, Ipld>);
impl Metadata {
pub fn new(time: DateTime<Utc>) -> Self {
let time = time.timestamp();
Self(BTreeMap::from([
("created".into(), time.into()),
("modified".into(), time.into()),
]))
}
pub fn upsert_mtime(&mut self, time: DateTime<Utc>) {
self.0.insert("modified".into(), time.timestamp().into());
}
pub fn get_created(&self) -> Option<DateTime<Utc>> {
self.0.get("created").and_then(|ipld| match ipld {
Ipld::Integer(i) => Utc.timestamp_opt(i64::try_from(*i).ok()?, 0).single(),
_ => None,
})
}
pub fn get_modified(&self) -> Option<DateTime<Utc>> {
self.0.get("modified").and_then(|ipld| match ipld {
Ipld::Integer(i) => Utc.timestamp_opt(i64::try_from(*i).ok()?, 0).single(),
_ => None,
})
}
pub fn put(&mut self, key: &str, value: Ipld) -> Option<Ipld> {
self.0.insert(key.into(), value)
}
pub fn get(&self, key: &str) -> Option<&Ipld> {
self.0.get(key)
}
pub fn put_serializable(&mut self, key: &str, value: impl Serialize) -> Result<Option<Ipld>> {
let serialized = ipld_core::serde::to_ipld(value)?;
Ok(self.put(key, serialized))
}
pub fn get_deserializable<D: DeserializeOwned>(&self, key: &str) -> Option<Result<D>> {
self.get(key)
.map(|ipld| Ok(ipld_core::serde::from_ipld(ipld.clone())?))
}
pub fn delete(&mut self, key: &str) -> Option<Ipld> {
self.0.remove(key)
}
pub fn update(&mut self, other: &Self) {
for (key, value) in other.0.iter() {
self.0.insert(key.clone(), value.clone());
}
}
pub(crate) fn hash(&self) -> Result<Multihash<64>> {
let vec = serde_ipld_dagcbor::to_vec(self)?;
let hash = Multihash::wrap(MULTIHASH_BLAKE3, blake3::hash(&vec).as_bytes()).unwrap();
Ok(hash)
}
pub fn tie_break_with(&mut self, other: &Self) -> Result<()> {
if self.hash()?.digest() > other.hash()?.digest() {
self.0 = other.0.clone();
}
Ok(())
}
}
impl TryFrom<&Ipld> for NodeType {
type Error = anyhow::Error;
fn try_from(ipld: &Ipld) -> Result<Self> {
match ipld {
Ipld::String(s) => NodeType::try_from(s.as_str()),
other => bail!("Expected `Ipld::String` got {:#?}", other),
}
}
}
impl TryFrom<&str> for NodeType {
type Error = anyhow::Error;
fn try_from(name: &str) -> Result<Self> {
Ok(match name.to_lowercase().as_str() {
"wnfs/priv/dir" => NodeType::PrivateDirectory,
"wnfs/priv/file" => NodeType::PrivateFile,
"wnfs/pub/dir" => NodeType::PublicDirectory,
"wnfs/pub/file" => NodeType::PublicFile,
"wnfs/share/temporal" => NodeType::TemporalSharePointer,
"wnfs/share/snapshot" => NodeType::SnapshotSharePointer,
_ => bail!("Unknown UnixFsNodeKind: {}", name),
})
}
}
impl From<&NodeType> for String {
fn from(r#type: &NodeType) -> Self {
match r#type {
NodeType::PrivateDirectory => "wnfs/priv/dir".into(),
NodeType::PrivateFile => "wnfs/priv/file".into(),
NodeType::PublicDirectory => "wnfs/pub/dir".into(),
NodeType::PublicFile => "wnfs/pub/file".into(),
NodeType::TemporalSharePointer => "wnfs/share/temporal".into(),
NodeType::SnapshotSharePointer => "wnfs/share/snapshot".into(),
}
}
}
impl Serialize for NodeType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
String::from(self).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for NodeType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let r#type = String::deserialize(deserializer)?;
r#type.as_str().try_into().map_err(DeError::custom)
}
}
#[cfg(test)]
mod tests {
use crate::Metadata;
use chrono::Utc;
#[async_std::test]
async fn metadata_can_encode_decode_as_cbor() {
let metadata = Metadata::new(Utc::now());
let encoded_metadata = serde_ipld_dagcbor::to_vec(&metadata).unwrap();
let decoded_metadata: Metadata = serde_ipld_dagcbor::from_slice(&encoded_metadata).unwrap();
assert_eq!(metadata, decoded_metadata);
}
}