wnfs_common/
metadata.rs

1//! File system metadata.
2
3use crate::MULTIHASH_BLAKE3;
4use anyhow::{Result, bail};
5use chrono::{DateTime, TimeZone, Utc};
6use ipld_core::ipld::Ipld;
7use multihash::Multihash;
8use serde::{
9    Deserialize, Deserializer, Serialize, Serializer,
10    de::{DeserializeOwned, Error as DeError},
11};
12use std::{collections::BTreeMap, fmt::Display};
13
14//--------------------------------------------------------------------------------------------------
15// Type Definitions
16//--------------------------------------------------------------------------------------------------
17
18/// The type of file system node.
19#[derive(Debug, Clone, PartialEq, Eq, Copy)]
20pub enum NodeType {
21    PublicFile,
22    PublicDirectory,
23    PrivateFile,
24    PrivateDirectory,
25    TemporalSharePointer,
26    SnapshotSharePointer,
27}
28
29impl Display for NodeType {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.write_str(match self {
32            NodeType::PublicFile => "wnfs/pub/file",
33            NodeType::PublicDirectory => "wnfs/pub/dir",
34            NodeType::PrivateFile => "wnfs/priv/file",
35            NodeType::PrivateDirectory => "wnfs/priv/dir",
36            NodeType::TemporalSharePointer => "wnfs/share/temporal",
37            NodeType::SnapshotSharePointer => "wnfs/share/snapshot",
38        })
39    }
40}
41
42/// The metadata of a node in the WNFS file system.
43///
44/// # Examples
45///
46/// ```
47/// use wnfs_common::Metadata;
48/// use chrono::Utc;
49///
50/// let metadata = Metadata::new(Utc::now());
51///
52/// println!("{:?}", metadata);
53/// ```
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct Metadata(pub BTreeMap<String, Ipld>);
56
57//--------------------------------------------------------------------------------------------------
58// Implementations
59//--------------------------------------------------------------------------------------------------
60
61impl Metadata {
62    /// Creates a new metadata.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use wnfs_common::Metadata;
68    /// use chrono::Utc;
69    ///
70    /// let metadata = Metadata::new(Utc::now());
71    ///
72    /// println!("{:?}", metadata);
73    /// ```
74    pub fn new(time: DateTime<Utc>) -> Self {
75        let time = time.timestamp();
76        Self(BTreeMap::from([
77            ("created".into(), time.into()),
78            ("modified".into(), time.into()),
79        ]))
80    }
81
82    /// Updates modified time.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use wnfs_common::Metadata;
88    /// use chrono::{Utc, TimeZone, Duration};
89    ///
90    /// let mut metadata = Metadata::new(Utc::now());
91    /// let time = Utc::now() + Duration::days(1);
92    ///
93    /// metadata.upsert_mtime(time);
94    ///
95    /// let imprecise_time = Utc.timestamp_opt(time.timestamp(), 0).single();
96    /// assert_eq!(metadata.get_modified(), imprecise_time);
97    /// ```
98    pub fn upsert_mtime(&mut self, time: DateTime<Utc>) {
99        self.0.insert("modified".into(), time.timestamp().into());
100    }
101
102    /// Returns the created time.
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use wnfs_common::Metadata;
108    /// use chrono::{Utc, TimeZone};
109    ///
110    /// let time = Utc::now();
111    /// let metadata = Metadata::new(time);
112    ///
113    /// let imprecise_time = Utc.timestamp_opt(time.timestamp(), 0).single();
114    /// assert_eq!(metadata.get_created(), imprecise_time);
115    /// ```
116    ///
117    /// Will return `None` if there's no created metadata on the
118    /// node or if it's not a second-based POSIX timestamp integer.
119    pub fn get_created(&self) -> Option<DateTime<Utc>> {
120        self.0.get("created").and_then(|ipld| match ipld {
121            Ipld::Integer(i) => Utc.timestamp_opt(i64::try_from(*i).ok()?, 0).single(),
122            _ => None,
123        })
124    }
125
126    /// Returns the modified time.
127    ///
128    /// # Examples
129    ///
130    /// ```
131    /// use wnfs_common::Metadata;
132    /// use chrono::{Utc, TimeZone};
133    ///
134    /// let time = Utc::now();
135    /// let metadata = Metadata::new(time);
136    ///
137    /// let imprecise_time = Utc.timestamp_opt(time.timestamp(), 0).single();
138    /// assert_eq!(metadata.get_modified(), imprecise_time);
139    /// ```
140    ///
141    /// Will return `None` if there's no created metadata on the
142    /// node or if it's not a second-based POSIX timestamp integer.
143    pub fn get_modified(&self) -> Option<DateTime<Utc>> {
144        self.0.get("modified").and_then(|ipld| match ipld {
145            Ipld::Integer(i) => Utc.timestamp_opt(i64::try_from(*i).ok()?, 0).single(),
146            _ => None,
147        })
148    }
149
150    /// Inserts a key-value pair into the metadata.
151    /// If the key already existed, the value is updated, and the old value is returned.
152    ///
153    /// # Examples
154    /// ```
155    /// use wnfs_common::Metadata;
156    /// use chrono::Utc;
157    /// use ipld_core::ipld::Ipld;
158    ///
159    /// let mut metadata = Metadata::new(Utc::now());
160    /// metadata.put("foo", Ipld::String("bar".into()));
161    /// assert_eq!(metadata.0.get("foo"), Some(&Ipld::String("bar".into())));
162    /// metadata.put("foo", Ipld::String("baz".into()));
163    /// assert_eq!(metadata.0.get("foo"), Some(&Ipld::String("baz".into())));
164    /// ```
165    ///
166    /// Returns (self, old_value), where old_value is `None` if the key did not exist prior to this call.
167    pub fn put(&mut self, key: &str, value: Ipld) -> Option<Ipld> {
168        self.0.insert(key.into(), value)
169    }
170
171    /// Returns metadata value behind given key.
172    pub fn get(&self, key: &str) -> Option<&Ipld> {
173        self.0.get(key)
174    }
175
176    /// Serializes and inserts given value at given key in metadata.
177    pub fn put_serializable(&mut self, key: &str, value: impl Serialize) -> Result<Option<Ipld>> {
178        let serialized = ipld_core::serde::to_ipld(value)?;
179        Ok(self.put(key, serialized))
180    }
181
182    /// Returns deserialized metadata value behind given key.
183    pub fn get_deserializable<D: DeserializeOwned>(&self, key: &str) -> Option<Result<D>> {
184        self.get(key)
185            .map(|ipld| Ok(ipld_core::serde::from_ipld(ipld.clone())?))
186    }
187
188    /// Deletes a key from the metadata.
189    ///
190    /// # Examples
191    /// ```
192    /// use wnfs_common::Metadata;
193    /// use chrono::Utc;
194    /// use ipld_core::ipld::Ipld;
195    ///
196    /// let mut metadata = Metadata::new(Utc::now());
197    /// metadata.put("foo", Ipld::String("bar".into()));
198    /// assert_eq!(metadata.0.get("foo"), Some(&Ipld::String("bar".into())));
199    /// metadata.delete("foo");
200    /// assert_eq!(metadata.0.get("foo"), None);
201    /// ```
202    ///
203    /// Returns `Some<Ipld>` if the key existed prior to this call, otherwise None.
204    pub fn delete(&mut self, key: &str) -> Option<Ipld> {
205        self.0.remove(key)
206    }
207
208    /// Updates this metadata with the contents of another metadata. merge strategy is to take theirs.
209    ///
210    /// # Examples
211    /// ```
212    /// use wnfs_common::Metadata;
213    /// use chrono::Utc;
214    /// use ipld_core::ipld::Ipld;
215    ///
216    /// let mut metadata1 = Metadata::new(Utc::now());
217    /// metadata1.put("foo", Ipld::String("bar".into()));
218    /// let mut metadata2 = Metadata::new(Utc::now());
219    /// metadata2.put("foo", Ipld::String("baz".into()));
220    /// metadata1.update(&metadata2);
221    /// assert_eq!(metadata1.0.get("foo"), Some(&Ipld::String("baz".into())));
222    /// ```
223    pub fn update(&mut self, other: &Self) {
224        for (key, value) in other.0.iter() {
225            self.0.insert(key.clone(), value.clone());
226        }
227    }
228
229    pub(crate) fn hash(&self) -> Result<Multihash<64>> {
230        let vec = serde_ipld_dagcbor::to_vec(self)?;
231        let hash = Multihash::wrap(MULTIHASH_BLAKE3, blake3::hash(&vec).as_bytes()).unwrap();
232        Ok(hash)
233    }
234
235    /// Tie break this node with another one.
236    /// Used for conflict reconciliation. We don't merge the two metadata maps
237    /// together (yet), instead we compare their hashes. The one with the lower hash
238    /// survives.
239    pub fn tie_break_with(&mut self, other: &Self) -> Result<()> {
240        if self.hash()?.digest() > other.hash()?.digest() {
241            self.0 = other.0.clone();
242        }
243
244        Ok(())
245    }
246}
247
248impl TryFrom<&Ipld> for NodeType {
249    type Error = anyhow::Error;
250
251    fn try_from(ipld: &Ipld) -> Result<Self> {
252        match ipld {
253            Ipld::String(s) => NodeType::try_from(s.as_str()),
254            other => bail!("Expected `Ipld::String` got {:#?}", other),
255        }
256    }
257}
258
259impl TryFrom<&str> for NodeType {
260    type Error = anyhow::Error;
261
262    fn try_from(name: &str) -> Result<Self> {
263        Ok(match name.to_lowercase().as_str() {
264            "wnfs/priv/dir" => NodeType::PrivateDirectory,
265            "wnfs/priv/file" => NodeType::PrivateFile,
266            "wnfs/pub/dir" => NodeType::PublicDirectory,
267            "wnfs/pub/file" => NodeType::PublicFile,
268            "wnfs/share/temporal" => NodeType::TemporalSharePointer,
269            "wnfs/share/snapshot" => NodeType::SnapshotSharePointer,
270            _ => bail!("Unknown UnixFsNodeKind: {}", name),
271        })
272    }
273}
274
275impl From<&NodeType> for String {
276    fn from(r#type: &NodeType) -> Self {
277        match r#type {
278            NodeType::PrivateDirectory => "wnfs/priv/dir".into(),
279            NodeType::PrivateFile => "wnfs/priv/file".into(),
280            NodeType::PublicDirectory => "wnfs/pub/dir".into(),
281            NodeType::PublicFile => "wnfs/pub/file".into(),
282            NodeType::TemporalSharePointer => "wnfs/share/temporal".into(),
283            NodeType::SnapshotSharePointer => "wnfs/share/snapshot".into(),
284        }
285    }
286}
287
288impl Serialize for NodeType {
289    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
290    where
291        S: Serializer,
292    {
293        String::from(self).serialize(serializer)
294    }
295}
296
297impl<'de> Deserialize<'de> for NodeType {
298    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
299    where
300        D: Deserializer<'de>,
301    {
302        let r#type = String::deserialize(deserializer)?;
303        r#type.as_str().try_into().map_err(DeError::custom)
304    }
305}
306//--------------------------------------------------------------------------------------------------
307// Tests
308//--------------------------------------------------------------------------------------------------
309
310#[cfg(test)]
311mod tests {
312    use crate::Metadata;
313    use chrono::Utc;
314
315    #[async_std::test]
316    async fn metadata_can_encode_decode_as_cbor() {
317        let metadata = Metadata::new(Utc::now());
318
319        let encoded_metadata = serde_ipld_dagcbor::to_vec(&metadata).unwrap();
320        let decoded_metadata: Metadata = serde_ipld_dagcbor::from_slice(&encoded_metadata).unwrap();
321
322        assert_eq!(metadata, decoded_metadata);
323    }
324}