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}