common/mount/
node.rs

1#![allow(clippy::doc_lazy_continuation)]
2
3use std::collections::BTreeMap;
4use std::path::Path;
5
6use mime::Mime;
7use serde::{Deserialize, Serialize};
8
9use crate::crypto::Secret;
10use crate::linked_data::{BlockEncoded, DagCborCodec, Link, LinkedData};
11
12use super::maybe_mime::MaybeMime;
13
14/**
15 * Nodes
16 * =====
17 * Nodes are the building blocks of a bucket's file structure.
18 *  (Maybe bucket is not a good name for this project, since Nodes are
19 *   *NOT* just key / value pairs, but a full DAG structure)
20 *  At a high level, a node is just a description of links to
21 *   to other nodes, which fall into two categories:
22 *  - Data Links: links to terminal nodes in the DAG i.e. actual files
23 *  - Dir Links: links to other nodes in the DAG i.e. directories
24 * Nodes are always DAG-CBOR encoded, and may be encrypted
25 */
26
27// Describes links to terminal nodes in the DAG i.e. actual
28//  files
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct Data {
31    // NOTE (amiller68): this is its own type st we can implement
32    //  sed / de for the option cleanly
33    // Data Links may have a MIME type associated with them,
34    //  if it can be determined
35    mime: MaybeMime,
36    // Data Links may have metadata built for them, which are parsed
37    //  from the links data at inclusion time
38    metadata: Option<BTreeMap<String, LinkedData>>,
39}
40
41impl Default for Data {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl Data {
48    /// Create a new Data with no metadata
49    pub fn new() -> Self {
50        Self {
51            mime: MaybeMime(None),
52            metadata: None,
53        }
54    }
55
56    /// Create a Data with mime type detected from file path
57    pub fn from_path(path: &Path) -> Self {
58        let mime = MaybeMime::from_path(path);
59        let metadata = BTreeMap::new();
60
61        Self {
62            mime,
63            metadata: if metadata.is_empty() {
64                None
65            } else {
66                Some(metadata)
67            },
68        }
69    }
70
71    /// Set custom metadata
72    pub fn set_metadata(&mut self, key: String, value: LinkedData) {
73        if let Some(ref mut metadata) = self.metadata {
74            metadata.insert(key, value);
75        } else {
76            let mut metadata = BTreeMap::new();
77            metadata.insert(key, value);
78            self.metadata = Some(metadata);
79        }
80    }
81
82    /// Get the MIME type if present
83    pub fn mime(&self) -> Option<&Mime> {
84        self.mime.0.as_ref()
85    }
86
87    /// Get the metadata if present
88    pub fn metadata(&self) -> Option<&BTreeMap<String, LinkedData>> {
89        self.metadata.as_ref()
90    }
91}
92
93// Lastly, we have a node, which is either a data link,
94//  or a link to another node.
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub enum NodeLink {
97    Data(Link, Secret, Data),
98    Dir(Link, Secret),
99}
100
101impl NodeLink {
102    /// Create a new Data node link with automatic metadata detection from path
103    pub fn new_data_from_path(link: Link, secret: Secret, path: &Path) -> Self {
104        let data = Data::from_path(path);
105        NodeLink::Data(link, secret, data)
106    }
107
108    /// Create a new Data node link without metadata
109    pub fn new_data(link: Link, secret: Secret) -> Self {
110        NodeLink::Data(link, secret, Data::new())
111    }
112
113    /// Create a new Dir node link
114    pub fn new_dir(link: Link, secret: Secret) -> Self {
115        NodeLink::Dir(link, secret)
116    }
117
118    pub fn link(&self) -> &Link {
119        match self {
120            NodeLink::Data(link, _, _) => link,
121            NodeLink::Dir(link, _) => link,
122        }
123    }
124
125    pub fn secret(&self) -> &Secret {
126        match self {
127            NodeLink::Data(_, secret, _) => secret,
128            NodeLink::Dir(_, secret) => secret,
129        }
130    }
131
132    /// Get data info if this is a Data link
133    pub fn data(&self) -> Option<&Data> {
134        match self {
135            NodeLink::Data(_, _, data) => Some(data),
136            NodeLink::Dir(_, _) => None,
137        }
138    }
139
140    /// Check if this is a directory link
141    pub fn is_dir(&self) -> bool {
142        matches!(self, NodeLink::Dir(_, _))
143    }
144
145    /// Check if this is a data/file link
146    pub fn is_data(&self) -> bool {
147        matches!(self, NodeLink::Data(_, _, _))
148    }
149}
150
151// And a node is just a map of names to links.
152//  When traversing the DAG, path names are just
153//  /-joined names of links in nodes.
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
155pub struct Node {
156    links: BTreeMap<String, NodeLink>,
157}
158
159#[derive(Debug, thiserror::Error)]
160pub enum NodeError {
161    #[error("link not found")]
162    LinkNotFound(String),
163}
164
165impl BlockEncoded<DagCborCodec> for Node {}
166
167impl Node {
168    pub fn new() -> Self {
169        Node {
170            links: BTreeMap::new(),
171        }
172    }
173
174    pub fn get_link(&self, name: &str) -> Option<&NodeLink> {
175        self.links.get(name)
176    }
177
178    pub fn insert(&mut self, name: String, link: NodeLink) -> Option<NodeLink> {
179        self.links.insert(name, link)
180    }
181
182    pub fn get_links(&self) -> &BTreeMap<String, NodeLink> {
183        &self.links
184    }
185
186    pub fn del(&mut self, name: &str) -> Option<NodeLink> {
187        // check if the link is an object
188        self.links.remove(name)
189    }
190
191    pub fn size(&self) -> usize {
192        self.links.len()
193    }
194}
195
196#[cfg(test)]
197mod test {
198    use super::*;
199
200    #[test]
201    fn test_node_encode_decode() {
202        let mut node = Node::default();
203        node.links.insert(
204            "example".to_string(),
205            NodeLink::Data(
206                Link::default(),
207                Secret::default(),
208                Data {
209                    metadata: None,
210                    mime: MaybeMime(None),
211                },
212            ),
213        );
214
215        let encoded = node.encode().unwrap();
216        let decoded = Node::decode(&encoded).unwrap();
217
218        assert_eq!(node, decoded);
219    }
220
221    #[test]
222    fn test_data_from_path() {
223        use std::path::PathBuf;
224
225        // Test with .json file
226        let path = PathBuf::from("/test/file.json");
227        let data = Data::from_path(&path);
228        assert_eq!(data.mime().map(|m| m.as_ref()), Some("application/json"));
229
230        // Test with .rs file
231        let path = PathBuf::from("/src/main.rs");
232        let data = Data::from_path(&path);
233        assert_eq!(data.mime().map(|m| m.as_ref()), Some("text/x-rust"));
234
235        // Test with .m4a audio file
236        let path = PathBuf::from("/audio/song.m4a");
237        let data = Data::from_path(&path);
238        assert_eq!(data.mime().map(|m| m.as_ref()), Some("audio/m4a"));
239
240        // Test with unknown extension (mime_guess returns None for truly unknown extensions)
241        let path = PathBuf::from("/test/file.unknownext");
242        let data = Data::from_path(&path);
243        assert_eq!(data.mime(), None);
244
245        // Test with no extension
246        let path = PathBuf::from("/test/README");
247        let data = Data::from_path(&path);
248        assert_eq!(data.mime(), None);
249    }
250
251    #[test]
252    fn test_node_link_constructors() {
253        use std::path::PathBuf;
254
255        let link = Link::default();
256        let secret = Secret::default();
257        let path = PathBuf::from("/test/image.png");
258
259        // Test new_data_from_path
260        let node_link = NodeLink::new_data_from_path(link.clone(), secret.clone(), &path);
261        assert!(node_link.is_data());
262        assert!(!node_link.is_dir());
263        let data = node_link.data().unwrap();
264        assert_eq!(data.mime().map(|m| m.as_ref()), Some("image/png"));
265
266        // Test new_data without path
267        let node_link = NodeLink::new_data(link.clone(), secret.clone());
268        assert!(node_link.is_data());
269        let data = node_link.data().unwrap();
270        assert_eq!(data.mime(), None);
271        assert_eq!(data.metadata(), None);
272
273        // Test new_dir
274        let node_link = NodeLink::new_dir(link.clone(), secret.clone());
275        assert!(node_link.is_dir());
276        assert!(!node_link.is_data());
277        assert!(node_link.data().is_none());
278    }
279}