common/bucket/
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 = Self::detect_mime_from_path(path);
59        let metadata = BTreeMap::new();
60
61        Self {
62            mime: MaybeMime(mime),
63            metadata: if metadata.is_empty() {
64                None
65            } else {
66                Some(metadata)
67            },
68        }
69    }
70
71    /// Detect MIME type from file extension
72    fn detect_mime_from_path(path: &Path) -> Option<Mime> {
73        let extension = path.extension()?.to_str()?.to_lowercase();
74
75        // Common file extensions to MIME types
76        let mime_str = match extension.as_str() {
77            // Text files
78            "txt" => "text/plain",
79            "html" | "htm" => "text/html",
80            "css" => "text/css",
81            "js" | "mjs" => "application/javascript",
82            "json" => "application/json",
83            "xml" => "application/xml",
84            "md" | "markdown" => "text/markdown",
85            "csv" => "text/csv",
86            "yaml" | "yml" => "application/x-yaml",
87            "toml" => "application/toml",
88
89            // Images
90            "jpg" | "jpeg" => "image/jpeg",
91            "png" => "image/png",
92            "gif" => "image/gif",
93            "bmp" => "image/bmp",
94            "svg" => "image/svg+xml",
95            "webp" => "image/webp",
96            "ico" => "image/x-icon",
97
98            // Videos
99            "mp4" => "video/mp4",
100            "webm" => "video/webm",
101            "avi" => "video/x-msvideo",
102            "mkv" => "video/x-matroska",
103            "mov" => "video/quicktime",
104
105            // Audio
106            "mp3" => "audio/mpeg",
107            "wav" => "audio/wav",
108            "ogg" => "audio/ogg",
109            "flac" => "audio/flac",
110            "aac" => "audio/aac",
111
112            // Archives
113            "zip" => "application/zip",
114            "tar" => "application/x-tar",
115            "gz" | "gzip" => "application/gzip",
116            "7z" => "application/x-7z-compressed",
117            "rar" => "application/x-rar-compressed",
118
119            // Documents
120            "pdf" => "application/pdf",
121            "doc" => "application/msword",
122            "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
123            "xls" => "application/vnd.ms-excel",
124            "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
125            "ppt" => "application/vnd.ms-powerpoint",
126            "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
127
128            // Programming
129            "rs" => "text/rust",
130            "py" => "text/x-python",
131            "c" => "text/x-c",
132            "cpp" | "cc" | "cxx" => "text/x-c++",
133            "h" | "hpp" => "text/x-c-header",
134            "java" => "text/x-java",
135            "go" => "text/x-go",
136            "sh" => "application/x-sh",
137
138            // Fonts
139            "ttf" => "font/ttf",
140            "otf" => "font/otf",
141            "woff" => "font/woff",
142            "woff2" => "font/woff2",
143
144            _ => "application/octet-stream",
145        };
146
147        mime_str.parse().ok()
148    }
149
150    /// Set custom metadata
151    pub fn set_metadata(&mut self, key: String, value: LinkedData) {
152        if let Some(ref mut metadata) = self.metadata {
153            metadata.insert(key, value);
154        } else {
155            let mut metadata = BTreeMap::new();
156            metadata.insert(key, value);
157            self.metadata = Some(metadata);
158        }
159    }
160
161    /// Get the MIME type if present
162    pub fn mime(&self) -> Option<&Mime> {
163        self.mime.0.as_ref()
164    }
165
166    /// Get the metadata if present
167    pub fn metadata(&self) -> Option<&BTreeMap<String, LinkedData>> {
168        self.metadata.as_ref()
169    }
170}
171
172// Lastly, we have a node, which is either a data link,
173//  or a link to another node.
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub enum NodeLink {
176    Data(Link, Secret, Data),
177    Dir(Link, Secret),
178}
179
180impl NodeLink {
181    /// Create a new Data node link with automatic metadata detection from path
182    pub fn new_data_from_path(link: Link, secret: Secret, path: &Path) -> Self {
183        let data = Data::from_path(path);
184        NodeLink::Data(link, secret, data)
185    }
186
187    /// Create a new Data node link without metadata
188    pub fn new_data(link: Link, secret: Secret) -> Self {
189        NodeLink::Data(link, secret, Data::new())
190    }
191
192    /// Create a new Dir node link
193    pub fn new_dir(link: Link, secret: Secret) -> Self {
194        NodeLink::Dir(link, secret)
195    }
196
197    pub fn link(&self) -> &Link {
198        match self {
199            NodeLink::Data(link, _, _) => link,
200            NodeLink::Dir(link, _) => link,
201        }
202    }
203
204    pub fn secret(&self) -> &Secret {
205        match self {
206            NodeLink::Data(_, secret, _) => secret,
207            NodeLink::Dir(_, secret) => secret,
208        }
209    }
210
211    /// Get data info if this is a Data link
212    pub fn data(&self) -> Option<&Data> {
213        match self {
214            NodeLink::Data(_, _, data) => Some(data),
215            NodeLink::Dir(_, _) => None,
216        }
217    }
218
219    /// Check if this is a directory link
220    pub fn is_dir(&self) -> bool {
221        matches!(self, NodeLink::Dir(_, _))
222    }
223
224    /// Check if this is a data/file link
225    pub fn is_data(&self) -> bool {
226        matches!(self, NodeLink::Data(_, _, _))
227    }
228}
229
230// And a node is just a map of names to links.
231//  When traversing the DAG, path names are just
232//  /-joined names of links in nodes.
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
234pub struct Node {
235    links: BTreeMap<String, NodeLink>,
236}
237
238#[derive(Debug, thiserror::Error)]
239pub enum NodeError {
240    #[error("link not found")]
241    LinkNotFound(String),
242}
243
244impl BlockEncoded<DagCborCodec> for Node {}
245
246impl Node {
247    pub fn new() -> Self {
248        Node {
249            links: BTreeMap::new(),
250        }
251    }
252
253    pub fn get_link(&self, name: &str) -> Option<&NodeLink> {
254        self.links.get(name)
255    }
256
257    pub fn insert(&mut self, name: String, link: NodeLink) -> Option<NodeLink> {
258        self.links.insert(name, link)
259    }
260
261    pub fn get_links(&self) -> &BTreeMap<String, NodeLink> {
262        &self.links
263    }
264
265    pub fn del(&mut self, name: &str) -> Option<NodeLink> {
266        // check if the link is an object
267        self.links.remove(name)
268    }
269
270    pub fn size(&self) -> usize {
271        self.links.len()
272    }
273}
274
275#[cfg(test)]
276mod test {
277    use super::*;
278
279    #[test]
280    fn test_node_encode_decode() {
281        let mut node = Node::default();
282        node.links.insert(
283            "example".to_string(),
284            NodeLink::Data(
285                Link::default(),
286                Secret::default(),
287                Data {
288                    metadata: None,
289                    mime: MaybeMime(None),
290                },
291            ),
292        );
293
294        let encoded = node.encode().unwrap();
295        let decoded = Node::decode(&encoded).unwrap();
296
297        assert_eq!(node, decoded);
298    }
299
300    #[test]
301    fn test_data_from_path() {
302        use std::path::PathBuf;
303
304        // Test with .json file
305        let path = PathBuf::from("/test/file.json");
306        let data = Data::from_path(&path);
307        assert_eq!(data.mime().map(|m| m.as_ref()), Some("application/json"));
308
309        // Test with .rs file
310        let path = PathBuf::from("/src/main.rs");
311        let data = Data::from_path(&path);
312        assert_eq!(data.mime().map(|m| m.as_ref()), Some("text/rust"));
313
314        // Test with unknown extension
315        let path = PathBuf::from("/test/file.xyz");
316        let data = Data::from_path(&path);
317        assert_eq!(
318            data.mime().map(|m| m.as_ref()),
319            Some("application/octet-stream")
320        );
321
322        // Test with no extension
323        let path = PathBuf::from("/test/README");
324        let data = Data::from_path(&path);
325        assert_eq!(data.mime(), None);
326    }
327
328    #[test]
329    fn test_node_link_constructors() {
330        use std::path::PathBuf;
331
332        let link = Link::default();
333        let secret = Secret::default();
334        let path = PathBuf::from("/test/image.png");
335
336        // Test new_data_from_path
337        let node_link = NodeLink::new_data_from_path(link.clone(), secret.clone(), &path);
338        assert!(node_link.is_data());
339        assert!(!node_link.is_dir());
340        let data = node_link.data().unwrap();
341        assert_eq!(data.mime().map(|m| m.as_ref()), Some("image/png"));
342
343        // Test new_data without path
344        let node_link = NodeLink::new_data(link.clone(), secret.clone());
345        assert!(node_link.is_data());
346        let data = node_link.data().unwrap();
347        assert_eq!(data.mime(), None);
348        assert_eq!(data.metadata(), None);
349
350        // Test new_dir
351        let node_link = NodeLink::new_dir(link.clone(), secret.clone());
352        assert!(node_link.is_dir());
353        assert!(!node_link.is_data());
354        assert!(node_link.data().is_none());
355    }
356}