common/bucket/
mount.rs

1use std::collections::BTreeMap;
2use std::io::Read;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use parking_lot::Mutex;
7use uuid::Uuid;
8
9use crate::crypto::{PublicKey, Secret, SecretError, SecretKey, Share};
10use crate::linked_data::{BlockEncoded, CodecError, Link};
11use crate::peer::{BlobsStore, BlobsStoreError};
12
13use super::manifest::Manifest;
14use super::node::{Node, NodeError, NodeLink};
15use super::pins::Pins;
16
17pub fn clean_path(path: &Path) -> PathBuf {
18    if !path.is_absolute() {
19        panic!("path is not absolute");
20    }
21    path.iter()
22        .skip(1)
23        .map(|part| part.to_string_lossy().to_string())
24        .collect::<PathBuf>()
25}
26
27#[derive(Clone)]
28pub struct MountInner {
29    // link to the manifest
30    pub link: Link,
31    // the loaded manifest
32    pub manifest: Manifest,
33    // the loaded, decrypted entry node
34    pub entry: Node,
35    // the loaded pins
36    pub pins: Pins,
37}
38
39impl MountInner {
40    pub fn link(&self) -> &Link {
41        &self.link
42    }
43    pub fn entry(&self) -> &Node {
44        &self.entry
45    }
46    pub fn manifest(&self) -> &Manifest {
47        &self.manifest
48    }
49    pub fn pins(&self) -> &Pins {
50        &self.pins
51    }
52}
53
54#[derive(Clone)]
55pub struct Mount(Arc<Mutex<MountInner>>, BlobsStore);
56
57#[derive(Debug, thiserror::Error)]
58pub enum MountError {
59    #[error("default error: {0}")]
60    Default(#[from] anyhow::Error),
61    #[error("link not found")]
62    LinkNotFound(Link),
63    #[error("path not found: {0}")]
64    PathNotFound(PathBuf),
65    #[error("path is not a node: {0}")]
66    PathNotNode(PathBuf),
67    #[error("blobs store error: {0}")]
68    BlobsStore(#[from] BlobsStoreError),
69    #[error("secret error: {0}")]
70    Secret(#[from] SecretError),
71    #[error("node error: {0}")]
72    Node(#[from] NodeError),
73    #[error("codec error: {0}")]
74    Codec(#[from] CodecError),
75    #[error("share error: {0}")]
76    Share(#[from] crate::crypto::ShareError),
77    #[error("peers share was not found. this should be impossible")]
78    ShareNotFound,
79}
80
81impl Mount {
82    pub fn inner(&self) -> MountInner {
83        self.0.lock().clone()
84    }
85
86    pub fn blobs(&self) -> BlobsStore {
87        self.1.clone()
88    }
89
90    pub fn link(&self) -> Link {
91        let inner = self.0.lock();
92        inner.link.clone()
93    }
94
95    /// Save the current mount state to the blobs store
96    #[allow(clippy::await_holding_lock)]
97    pub async fn save(&self, blobs: &BlobsStore) -> Result<Link, MountError> {
98        let mut inner = self.0.lock();
99        // Create a new secret for the updated root
100        let secret = Secret::generate();
101        // get the now previous link to the bucket
102        let previous = inner.link.clone();
103        // Put the current root node into blobs with the new secret
104        let entry = Self::_put_node_in_blobs(&inner.entry, &secret, blobs).await?;
105        // Serialize current pins to blobs
106        // put the new root link into the pins, as well as the previous link
107        inner.pins.insert(*entry.clone().hash());
108        inner.pins.insert(*previous.hash());
109        let pins_link = Self::_put_pins_in_blobs(&inner.pins, blobs).await?;
110        // Update the bucket's share with the new root link
111        // (add_share creates the Share internally)
112        let mut manifest = inner.manifest.clone();
113        let _m = manifest.clone();
114        let shares = _m.shares();
115        manifest.unset_shares();
116        for share in shares.values() {
117            let public_key = share.principal().identity;
118            manifest.add_share(public_key, secret.clone())?;
119        }
120        // Update the bucket's pins field
121        manifest.set_pins(pins_link.clone());
122        manifest.set_previous(previous);
123        manifest.set_entry(entry.clone());
124        // Put the updated manifest into blobs to determine the new link
125        let link = Self::_put_manifest_in_blobs(&manifest, blobs).await?;
126
127        // update the internal state
128        inner.manifest = manifest;
129
130        Ok(link)
131    }
132
133    pub async fn init(
134        id: Uuid,
135        name: String,
136        owner: &SecretKey,
137        blobs: &BlobsStore,
138    ) -> Result<Self, MountError> {
139        // create a new root node for the bucket
140        let entry = Node::default();
141        // create a new secret for the owner
142        let secret = Secret::generate();
143        // put the node in the blobs store for the secret
144        let entry_link = Self::_put_node_in_blobs(&entry, &secret, blobs).await?;
145        // share the secret with the owner
146        let share = Share::new(&secret, &owner.public())?;
147        // Initialize pins with root node hash
148        let mut pins = Pins::new();
149        pins.insert(*entry_link.hash());
150        // Put the pins in blobs to get a pins link
151        let pins_link = Self::_put_pins_in_blobs(&pins, blobs).await?;
152        // construct the new manifest
153        let manifest = Manifest::new(
154            id,
155            name.clone(),
156            owner.public(),
157            share,
158            entry_link.clone(),
159            pins_link.clone(),
160        );
161        let link = Self::_put_manifest_in_blobs(&manifest, blobs).await?;
162
163        // return the new mount
164        Ok(Mount(
165            Arc::new(Mutex::new(MountInner {
166                link,
167                manifest,
168                entry,
169                pins,
170            })),
171            blobs.clone(),
172        ))
173    }
174
175    pub async fn load(
176        link: &Link,
177        secret_key: &SecretKey,
178        blobs: &BlobsStore,
179    ) -> Result<Self, MountError> {
180        let public_key = &secret_key.public();
181        let manifest = Self::_get_manifest_from_blobs(link, blobs).await?;
182
183        let _share = manifest.get_share(public_key);
184
185        let share = match _share {
186            Some(share) => share.share(),
187            None => return Err(MountError::ShareNotFound),
188        };
189
190        let secret = share.recover(secret_key)?;
191
192        let pins = Self::_get_pins_from_blobs(manifest.pins(), blobs).await?;
193        let entry =
194            Self::_get_node_from_blobs(&NodeLink::Dir(manifest.entry().clone(), secret), blobs)
195                .await?;
196
197        Ok(Mount(
198            Arc::new(Mutex::new(MountInner {
199                link: link.clone(),
200                manifest,
201                entry,
202                pins,
203            })),
204            blobs.clone(),
205        ))
206    }
207
208    #[allow(clippy::await_holding_lock)]
209    pub async fn share(&mut self, peer: PublicKey) -> Result<(), MountError> {
210        let mut inner = self.0.lock();
211        inner.manifest.add_share(peer, Secret::default())?;
212        Ok(())
213    }
214
215    #[allow(clippy::await_holding_lock)]
216    pub async fn add<R>(
217        &mut self,
218        path: &Path,
219        data: R,
220        blobs: &BlobsStore,
221    ) -> Result<(), MountError>
222    where
223        R: Read + Send + Sync + 'static + Unpin,
224    {
225        let secret = Secret::generate();
226
227        let encrypted_reader = secret.encrypt_reader(data)?;
228
229        // TODO (amiller68): this is incredibly dumb
230        use bytes::Bytes;
231        use futures::stream;
232        let encrypted_bytes = {
233            let mut buf = Vec::new();
234            let mut reader = encrypted_reader;
235            reader.read_to_end(&mut buf).map_err(SecretError::Io)?;
236            buf
237        };
238
239        let stream = Box::pin(stream::once(async move {
240            Ok::<_, std::io::Error>(Bytes::from(encrypted_bytes))
241        }));
242
243        let hash = blobs.put_stream(stream).await?;
244
245        let link = Link::new(
246            crate::linked_data::LD_RAW_CODEC,
247            hash,
248            iroh_blobs::BlobFormat::Raw,
249        );
250
251        let node_link = NodeLink::new_data_from_path(link.clone(), secret, path);
252
253        let mut inner = self.0.lock();
254        let root_node = inner.entry.clone();
255        let (updated_link, node_hashes) =
256            Self::_set_node_link_at_path(root_node, node_link, path, blobs).await?;
257
258        // Track pins: data blob + all created node hashes
259        inner.pins.insert(hash);
260        inner.pins.extend(node_hashes);
261
262        if let NodeLink::Dir(new_root_link, new_secret) = updated_link {
263            inner.entry = Self::_get_node_from_blobs(
264                &NodeLink::Dir(new_root_link.clone(), new_secret),
265                blobs,
266            )
267            .await?;
268        }
269
270        Ok(())
271    }
272
273    #[allow(clippy::await_holding_lock)]
274    pub async fn rm(&mut self, path: &Path, blobs: &BlobsStore) -> Result<(), MountError> {
275        let path = clean_path(path);
276        let parent_path = path
277            .parent()
278            .ok_or_else(|| MountError::Default(anyhow::anyhow!("Cannot remove root")))?;
279
280        let inner = self.0.lock();
281        let entry = inner.entry.clone();
282        drop(inner);
283
284        let mut parent_node = if parent_path == Path::new("") {
285            entry.clone()
286        } else {
287            Self::_get_node_at_path(&entry, parent_path, blobs).await?
288        };
289
290        let file_name = path.file_name().unwrap().to_string_lossy().to_string();
291
292        if parent_node.del(&file_name).is_none() {
293            return Err(MountError::PathNotFound(path.to_path_buf()));
294        }
295
296        if parent_path == Path::new("") {
297            let secret = Secret::generate();
298            let link = Self::_put_node_in_blobs(&parent_node, &secret, blobs).await?;
299
300            let mut inner = self.0.lock();
301            // Track the new root node hash
302            inner.pins.insert(*link.hash());
303            inner.entry = parent_node;
304        } else {
305            // Save the modified parent node to blobs
306            let secret = Secret::generate();
307            let parent_link = Self::_put_node_in_blobs(&parent_node, &secret, blobs).await?;
308            let node_link = NodeLink::new_dir(parent_link.clone(), secret);
309
310            let mut inner = self.0.lock();
311            // Convert parent_path back to absolute for _set_node_link_at_path
312            let abs_parent_path = Path::new("/").join(parent_path);
313            let (updated_link, node_hashes) =
314                Self::_set_node_link_at_path(entry, node_link, &abs_parent_path, blobs).await?;
315
316            // Track the parent node hash and all created node hashes
317            inner.pins.insert(*parent_link.hash());
318            inner.pins.extend(node_hashes);
319
320            if let NodeLink::Dir(new_root_link, new_secret) = updated_link {
321                inner.entry = Self::_get_node_from_blobs(
322                    &NodeLink::Dir(new_root_link.clone(), new_secret),
323                    blobs,
324                )
325                .await?;
326            }
327        }
328
329        Ok(())
330    }
331
332    #[allow(clippy::await_holding_lock)]
333    pub async fn ls(
334        &self,
335        path: &Path,
336        blobs: &BlobsStore,
337    ) -> Result<BTreeMap<PathBuf, NodeLink>, MountError> {
338        let mut items = BTreeMap::new();
339        let path = clean_path(path);
340
341        let inner = self.0.lock();
342        let root_node = inner.entry.clone();
343        drop(inner);
344
345        let node = if path == Path::new("") {
346            root_node
347        } else {
348            match Self::_get_node_at_path(&root_node, &path, blobs).await {
349                Ok(node) => node,
350                Err(MountError::LinkNotFound(_)) => {
351                    return Err(MountError::PathNotNode(path.to_path_buf()))
352                }
353                Err(err) => return Err(err),
354            }
355        };
356
357        for (name, link) in node.get_links() {
358            let mut full_path = path.clone();
359            full_path.push(name);
360            items.insert(full_path, link.clone());
361        }
362
363        Ok(items)
364    }
365
366    pub async fn ls_deep(
367        &self,
368        path: &Path,
369        blobs: &BlobsStore,
370    ) -> Result<BTreeMap<PathBuf, NodeLink>, MountError> {
371        let base_path = clean_path(path);
372        self._ls_deep(path, &base_path, blobs).await
373    }
374
375    async fn _ls_deep(
376        &self,
377        path: &Path,
378        base_path: &Path,
379        blobs: &BlobsStore,
380    ) -> Result<BTreeMap<PathBuf, NodeLink>, MountError> {
381        let mut all_items = BTreeMap::new();
382
383        // get the initial items at the given path
384        let items = self.ls(path, blobs).await?;
385
386        for (item_path, link) in items {
387            // Make path relative to the base_path
388            let relative_path = if base_path == Path::new("") {
389                item_path.clone()
390            } else {
391                item_path
392                    .strip_prefix(base_path)
393                    .unwrap_or(&item_path)
394                    .to_path_buf()
395            };
396            all_items.insert(relative_path.clone(), link.clone());
397
398            if link.is_dir() {
399                // Recurse using the absolute path
400                let abs_item_path = Path::new("/").join(&item_path);
401                let sub_items = Box::pin(self._ls_deep(&abs_item_path, base_path, blobs)).await?;
402
403                // Sub items already have correct relative paths from base_path
404                for (sub_path, sub_link) in sub_items {
405                    all_items.insert(sub_path, sub_link);
406                }
407            }
408        }
409
410        Ok(all_items)
411    }
412
413    #[allow(clippy::await_holding_lock)]
414    pub async fn cat(&self, path: &Path, blobs: &BlobsStore) -> Result<Vec<u8>, MountError> {
415        let path = clean_path(path);
416
417        let inner = self.0.lock();
418        let root_node = inner.entry.clone();
419        drop(inner);
420
421        let (parent_path, file_name) = if let Some(parent) = path.parent() {
422            (
423                parent,
424                path.file_name().unwrap().to_string_lossy().to_string(),
425            )
426        } else {
427            return Err(MountError::PathNotFound(path.to_path_buf()));
428        };
429
430        let parent_node = if parent_path == Path::new("") {
431            root_node
432        } else {
433            Self::_get_node_at_path(&root_node, parent_path, blobs).await?
434        };
435
436        let link = parent_node
437            .get_link(&file_name)
438            .ok_or_else(|| MountError::PathNotFound(path.to_path_buf()))?;
439
440        match link {
441            NodeLink::Data(link, secret, _) => {
442                let encrypted_data = blobs.get(link.hash()).await?;
443                let data = secret.decrypt(&encrypted_data)?;
444                Ok(data)
445            }
446            NodeLink::Dir(_, _) => Err(MountError::PathNotNode(path.to_path_buf())),
447        }
448    }
449
450    /// Get the NodeLink for a file at a given path
451    #[allow(clippy::await_holding_lock)]
452    pub async fn get(&self, path: &Path, blobs: &BlobsStore) -> Result<NodeLink, MountError> {
453        let path = clean_path(path);
454
455        let inner = self.0.lock();
456        let root_node = inner.entry.clone();
457        drop(inner);
458
459        let (parent_path, file_name) = if let Some(parent) = path.parent() {
460            (
461                parent,
462                path.file_name().unwrap().to_string_lossy().to_string(),
463            )
464        } else {
465            return Err(MountError::PathNotFound(path.to_path_buf()));
466        };
467
468        let parent_node = if parent_path == Path::new("") {
469            root_node
470        } else {
471            Self::_get_node_at_path(&root_node, parent_path, blobs).await?
472        };
473
474        parent_node
475            .get_link(&file_name)
476            .cloned()
477            .ok_or_else(|| MountError::PathNotFound(path.to_path_buf()))
478    }
479
480    async fn _get_node_at_path(
481        node: &Node,
482        path: &Path,
483        blobs: &BlobsStore,
484    ) -> Result<Node, MountError> {
485        let mut current_node = node.clone();
486        let mut consumed_path = PathBuf::from("/");
487
488        for part in path.iter() {
489            consumed_path.push(part);
490            let next = part.to_string_lossy().to_string();
491            let next_link = current_node
492                .get_link(&next)
493                .ok_or(MountError::PathNotFound(consumed_path.clone()))?;
494            current_node = Self::_get_node_from_blobs(next_link, blobs).await?
495        }
496        Ok(current_node)
497    }
498
499    pub async fn _set_node_link_at_path(
500        node: Node,
501        node_link: NodeLink,
502        path: &Path,
503        blobs: &BlobsStore,
504    ) -> Result<(NodeLink, Vec<crate::linked_data::Hash>), MountError> {
505        let path = clean_path(path);
506        let mut visited_nodes = Vec::new();
507        let mut name = path.file_name().unwrap().to_string_lossy().to_string();
508        let parent_path = path.parent().unwrap_or(Path::new(""));
509
510        let mut consumed_path = PathBuf::from("/");
511        let mut node = node;
512        visited_nodes.push((consumed_path.clone(), node.clone()));
513
514        for part in parent_path.iter() {
515            let next = part.to_string_lossy().to_string();
516            let next_link = node.get_link(&next);
517            if let Some(next_link) = next_link {
518                consumed_path.push(part);
519                match next_link {
520                    NodeLink::Dir(..) => {
521                        node = Self::_get_node_from_blobs(next_link, blobs).await?
522                    }
523                    NodeLink::Data(..) => {
524                        return Err(MountError::PathNotNode(consumed_path.clone()));
525                    }
526                }
527                visited_nodes.push((consumed_path.clone(), node.clone()));
528            } else {
529                // Create a new directory node
530                node = Node::default();
531                consumed_path.push(part);
532                visited_nodes.push((consumed_path.clone(), node.clone()));
533            }
534        }
535
536        let mut node_link = node_link;
537        let mut created_hashes = Vec::new();
538        for (path, mut node) in visited_nodes.into_iter().rev() {
539            node.insert(name, node_link.clone());
540            let secret = Secret::generate();
541            let link = Self::_put_node_in_blobs(&node, &secret, blobs).await?;
542            created_hashes.push(*link.hash());
543            node_link = NodeLink::Dir(link, secret);
544            name = path
545                .file_name()
546                .unwrap_or_default()
547                .to_string_lossy()
548                .to_string();
549        }
550
551        Ok((node_link, created_hashes))
552    }
553
554    async fn _get_manifest_from_blobs(
555        link: &Link,
556        blobs: &BlobsStore,
557    ) -> Result<Manifest, MountError> {
558        tracing::debug!(
559            "_get_bucket_from_blobs: Checking for bucket data at link {:?}",
560            link
561        );
562        let hash = link.hash();
563        tracing::debug!("_get_bucket_from_blobs: Bucket hash: {}", hash);
564
565        match blobs.stat(hash).await {
566            Ok(true) => {
567                tracing::debug!(
568                    "_get_bucket_from_blobs: Bucket hash {} exists in blobs",
569                    hash
570                );
571            }
572            Ok(false) => {
573                tracing::error!("_get_bucket_from_blobs: Bucket hash {} NOT FOUND in blobs - LinkNotFound error!", hash);
574                return Err(MountError::LinkNotFound(link.clone()));
575            }
576            Err(e) => {
577                tracing::error!(
578                    "_get_bucket_from_blobs: Error checking bucket hash {}: {}",
579                    hash,
580                    e
581                );
582                return Err(e.into());
583            }
584        }
585
586        tracing::debug!("_get_bucket_from_blobs: Reading bucket data from blobs");
587        let data = blobs.get(hash).await?;
588        tracing::debug!(
589            "_get_bucket_from_blobs: Got {} bytes of bucket data",
590            data.len()
591        );
592
593        let bucket_data = Manifest::decode(&data)?;
594        tracing::debug!(
595            "_get_bucket_from_blobs: Successfully decoded BucketData for bucket '{}'",
596            bucket_data.name()
597        );
598
599        Ok(bucket_data)
600    }
601
602    async fn _get_pins_from_blobs(link: &Link, blobs: &BlobsStore) -> Result<Pins, MountError> {
603        tracing::debug!("_get_pins_from_blobs: Checking for pins at link {:?}", link);
604        let hash = link.hash();
605        tracing::debug!("_get_pins_from_blobs: Pins hash: {}", hash);
606
607        match blobs.stat(hash).await {
608            Ok(true) => {
609                tracing::debug!("_get_pins_from_blobs: Pins hash {} exists in blobs", hash);
610            }
611            Ok(false) => {
612                tracing::error!(
613                    "_get_pins_from_blobs: Pins hash {} NOT FOUND in blobs - LinkNotFound error!",
614                    hash
615                );
616                return Err(MountError::LinkNotFound(link.clone()));
617            }
618            Err(e) => {
619                tracing::error!(
620                    "_get_pins_from_blobs: Error checking pins hash {}: {}",
621                    hash,
622                    e
623                );
624                return Err(e.into());
625            }
626        }
627
628        tracing::debug!("_get_pins_from_blobs: Reading hash list from blobs");
629        // Read hashes from the hash list blob
630        let hashes = blobs.read_hash_list(*hash).await?;
631        tracing::debug!(
632            "_get_pins_from_blobs: Successfully read {} hashes from pinset",
633            hashes.len()
634        );
635
636        Ok(Pins::from_vec(hashes))
637    }
638
639    async fn _get_node_from_blobs(
640        node_link: &NodeLink,
641        blobs: &BlobsStore,
642    ) -> Result<Node, MountError> {
643        let link = node_link.link();
644        let secret = node_link.secret();
645        let hash = link.hash();
646
647        tracing::debug!("_get_node_from_blobs: Checking for node at hash {}", hash);
648
649        match blobs.stat(hash).await {
650            Ok(true) => {
651                tracing::debug!("_get_node_from_blobs: Node hash {} exists in blobs", hash);
652            }
653            Ok(false) => {
654                tracing::error!(
655                    "_get_node_from_blobs: Node hash {} NOT FOUND in blobs - LinkNotFound error!",
656                    hash
657                );
658                return Err(MountError::LinkNotFound(link.clone()));
659            }
660            Err(e) => {
661                tracing::error!(
662                    "_get_node_from_blobs: Error checking node hash {}: {}",
663                    hash,
664                    e
665                );
666                return Err(e.into());
667            }
668        }
669
670        tracing::debug!("_get_node_from_blobs: Reading encrypted node blob");
671        let blob = blobs.get(hash).await?;
672        tracing::debug!(
673            "_get_node_from_blobs: Got {} bytes of encrypted node data",
674            blob.len()
675        );
676
677        tracing::debug!("_get_node_from_blobs: Decrypting node data");
678        let data = secret.decrypt(&blob)?;
679        tracing::debug!("_get_node_from_blobs: Decrypted {} bytes", data.len());
680
681        let node = Node::decode(&data)?;
682        tracing::debug!("_get_node_from_blobs: Successfully decoded Node");
683
684        Ok(node)
685    }
686
687    // TODO (amiller68): you should inline a Link
688    //  into the node when we store encrypt it,
689    //  so that we have an integrity check
690    async fn _put_node_in_blobs(
691        node: &Node,
692        secret: &Secret,
693        blobs: &BlobsStore,
694    ) -> Result<Link, MountError> {
695        let _data = node.encode()?;
696        let data = secret.encrypt(&_data)?;
697        let hash = blobs.put(data).await?;
698        // NOTE (amiller68): nodes are always stored as raw
699        //  since they are encrypted blobs
700        let link = Link::new(
701            crate::linked_data::LD_RAW_CODEC,
702            hash,
703            iroh_blobs::BlobFormat::Raw,
704        );
705        Ok(link)
706    }
707
708    pub async fn _put_manifest_in_blobs(
709        bucket_data: &Manifest,
710        blobs: &BlobsStore,
711    ) -> Result<Link, MountError> {
712        let data = bucket_data.encode()?;
713        let hash = blobs.put(data).await?;
714        // NOTE (amiller68): buckets are unencrypted, so they can inherit
715        //  the codec of the bucket itself (which is currently always cbor)
716        let link = Link::new(bucket_data.codec(), hash, iroh_blobs::BlobFormat::Raw);
717        Ok(link)
718    }
719
720    pub async fn _put_pins_in_blobs(pins: &Pins, blobs: &BlobsStore) -> Result<Link, MountError> {
721        // Create a hash list blob from the pins (raw bytes: 32 bytes per hash, concatenated)
722        let hash = blobs.create_hash_list(pins.iter().copied()).await?;
723        // Pins are stored as raw blobs containing concatenated hashes
724        let link = Link::new(
725            crate::linked_data::LD_RAW_CODEC,
726            hash,
727            iroh_blobs::BlobFormat::HashSeq,
728        );
729        Ok(link)
730    }
731}
732
733#[cfg(test)]
734mod test {
735    use super::*;
736    use std::io::Cursor;
737    use tempfile::TempDir;
738
739    async fn setup_test_env() -> (Mount, BlobsStore, crate::crypto::SecretKey, TempDir) {
740        let temp_dir = TempDir::new().unwrap();
741        let blob_path = temp_dir.path().join("blobs");
742
743        let secret_key = SecretKey::generate();
744        // Generate iroh secret key from random bytes
745        let blobs = BlobsStore::load(&blob_path).await.unwrap();
746
747        let mount = Mount::init(Uuid::new_v4(), "test".to_string(), &secret_key, &blobs)
748            .await
749            .unwrap();
750
751        (mount, blobs, secret_key, temp_dir)
752    }
753
754    #[tokio::test]
755    async fn test_add_and_cat() {
756        let (mut mount, blobs, _, _temp) = setup_test_env().await;
757
758        let data = b"Hello, world!";
759        let path = PathBuf::from("/test.txt");
760
761        mount
762            .add(&path, Cursor::new(data.to_vec()), &blobs)
763            .await
764            .unwrap();
765
766        let result = mount.cat(&path, &blobs).await.unwrap();
767        assert_eq!(result, data);
768    }
769
770    #[tokio::test]
771    async fn test_add_with_metadata() {
772        let (mut mount, blobs, _, _temp) = setup_test_env().await;
773
774        let data = b"{ \"key\": \"value\" }";
775        let path = PathBuf::from("/data.json");
776
777        mount
778            .add(&path, Cursor::new(data.to_vec()), &blobs)
779            .await
780            .unwrap();
781
782        let items = mount.ls(&PathBuf::from("/"), &blobs).await.unwrap();
783        assert_eq!(items.len(), 1);
784
785        let (file_path, link) = items.iter().next().unwrap();
786        assert_eq!(file_path, &PathBuf::from("data.json"));
787
788        if let Some(data_info) = link.data() {
789            assert!(data_info.mime().is_some());
790            assert_eq!(data_info.mime().unwrap().as_ref(), "application/json");
791        } else {
792            panic!("Expected data link with metadata");
793        }
794    }
795
796    #[tokio::test]
797    async fn test_ls() {
798        let (mut mount, blobs, _, _temp) = setup_test_env().await;
799
800        mount
801            .add(
802                &PathBuf::from("/file1.txt"),
803                Cursor::new(b"data1".to_vec()),
804                &blobs,
805            )
806            .await
807            .unwrap();
808        mount
809            .add(
810                &PathBuf::from("/file2.txt"),
811                Cursor::new(b"data2".to_vec()),
812                &blobs,
813            )
814            .await
815            .unwrap();
816        mount
817            .add(
818                &PathBuf::from("/dir/file3.txt"),
819                Cursor::new(b"data3".to_vec()),
820                &blobs,
821            )
822            .await
823            .unwrap();
824
825        let items = mount.ls(&PathBuf::from("/"), &blobs).await.unwrap();
826        assert_eq!(items.len(), 3);
827
828        assert!(items.contains_key(&PathBuf::from("file1.txt")));
829        assert!(items.contains_key(&PathBuf::from("file2.txt")));
830        assert!(items.contains_key(&PathBuf::from("dir")));
831
832        let sub_items = mount.ls(&PathBuf::from("/dir"), &blobs).await.unwrap();
833        assert_eq!(sub_items.len(), 1);
834        assert!(sub_items.contains_key(&PathBuf::from("dir/file3.txt")));
835    }
836
837    #[tokio::test]
838    async fn test_ls_deep() {
839        let (mut mount, blobs, _, _temp) = setup_test_env().await;
840
841        mount
842            .add(&PathBuf::from("/a.txt"), Cursor::new(b"a".to_vec()), &blobs)
843            .await
844            .unwrap();
845        mount
846            .add(
847                &PathBuf::from("/dir1/b.txt"),
848                Cursor::new(b"b".to_vec()),
849                &blobs,
850            )
851            .await
852            .unwrap();
853        mount
854            .add(
855                &PathBuf::from("/dir1/dir2/c.txt"),
856                Cursor::new(b"c".to_vec()),
857                &blobs,
858            )
859            .await
860            .unwrap();
861        mount
862            .add(
863                &PathBuf::from("/dir1/dir2/dir3/d.txt"),
864                Cursor::new(b"d".to_vec()),
865                &blobs,
866            )
867            .await
868            .unwrap();
869
870        let all_items = mount.ls_deep(&PathBuf::from("/"), &blobs).await.unwrap();
871
872        assert!(all_items.contains_key(&PathBuf::from("a.txt")));
873        assert!(all_items.contains_key(&PathBuf::from("dir1")));
874        assert!(all_items.contains_key(&PathBuf::from("dir1/b.txt")));
875        assert!(all_items.contains_key(&PathBuf::from("dir1/dir2")));
876        assert!(all_items.contains_key(&PathBuf::from("dir1/dir2/c.txt")));
877        assert!(all_items.contains_key(&PathBuf::from("dir1/dir2/dir3")));
878        assert!(all_items.contains_key(&PathBuf::from("dir1/dir2/dir3/d.txt")));
879    }
880
881    #[tokio::test]
882    async fn test_rm() {
883        let (mut mount, blobs, _, _temp) = setup_test_env().await;
884
885        mount
886            .add(
887                &PathBuf::from("/file1.txt"),
888                Cursor::new(b"data1".to_vec()),
889                &blobs,
890            )
891            .await
892            .unwrap();
893        mount
894            .add(
895                &PathBuf::from("/file2.txt"),
896                Cursor::new(b"data2".to_vec()),
897                &blobs,
898            )
899            .await
900            .unwrap();
901
902        let items = mount.ls(&PathBuf::from("/"), &blobs).await.unwrap();
903        assert_eq!(items.len(), 2);
904
905        mount
906            .rm(&PathBuf::from("/file1.txt"), &blobs)
907            .await
908            .unwrap();
909
910        let items = mount.ls(&PathBuf::from("/"), &blobs).await.unwrap();
911        assert_eq!(items.len(), 1);
912        assert!(items.contains_key(&PathBuf::from("file2.txt")));
913        assert!(!items.contains_key(&PathBuf::from("file1.txt")));
914
915        let result = mount.cat(&PathBuf::from("/file1.txt"), &blobs).await;
916        assert!(result.is_err());
917    }
918
919    #[tokio::test]
920    async fn test_nested_operations() {
921        let (mut mount, blobs, _, _temp) = setup_test_env().await;
922
923        let files = vec![
924            ("/root.txt", b"root" as &[u8]),
925            ("/docs/readme.md", b"readme" as &[u8]),
926            ("/docs/guide.pdf", b"guide" as &[u8]),
927            ("/src/main.rs", b"main" as &[u8]),
928            ("/src/lib.rs", b"lib" as &[u8]),
929            ("/src/tests/unit.rs", b"unit" as &[u8]),
930            ("/src/tests/integration.rs", b"integration" as &[u8]),
931        ];
932
933        for (path, data) in &files {
934            mount
935                .add(&PathBuf::from(path), Cursor::new(data.to_vec()), &blobs)
936                .await
937                .unwrap();
938        }
939
940        for (path, expected_data) in &files {
941            let data = mount.cat(&PathBuf::from(path), &blobs).await.unwrap();
942            assert_eq!(data, expected_data.to_vec());
943        }
944
945        mount
946            .rm(&PathBuf::from("/src/tests/unit.rs"), &blobs)
947            .await
948            .unwrap();
949
950        let result = mount
951            .cat(&PathBuf::from("/src/tests/unit.rs"), &blobs)
952            .await;
953        assert!(result.is_err());
954
955        let data = mount
956            .cat(&PathBuf::from("/src/tests/integration.rs"), &blobs)
957            .await
958            .unwrap();
959        assert_eq!(data, b"integration");
960    }
961
962    #[tokio::test]
963    async fn test_various_file_types() {
964        let (mut mount, blobs, _, _temp) = setup_test_env().await;
965
966        let test_files = vec![
967            ("/image.png", "image/png"),
968            ("/video.mp4", "video/mp4"),
969            ("/style.css", "text/css"),
970            ("/script.js", "application/javascript"),
971            ("/data.json", "application/json"),
972            ("/archive.zip", "application/zip"),
973            ("/document.pdf", "application/pdf"),
974            ("/code.rs", "text/rust"),
975        ];
976
977        for (path, expected_mime) in test_files {
978            mount
979                .add(&PathBuf::from(path), Cursor::new(b"test".to_vec()), &blobs)
980                .await
981                .unwrap();
982
983            let items = mount.ls(&PathBuf::from("/"), &blobs).await.unwrap();
984            let link = items.values().find(|l| l.is_data()).unwrap();
985
986            if let Some(data_info) = link.data() {
987                assert!(data_info.mime().is_some());
988                assert_eq!(data_info.mime().unwrap().as_ref(), expected_mime);
989            }
990
991            mount.rm(&PathBuf::from(path), &blobs).await.unwrap();
992        }
993    }
994
995    #[tokio::test]
996    async fn test_error_cases() {
997        let (mount, blobs, _, _temp) = setup_test_env().await;
998
999        let result = mount
1000            .cat(&PathBuf::from("/does_not_exist.txt"), &blobs)
1001            .await;
1002        assert!(result.is_err());
1003
1004        let result = mount.ls(&PathBuf::from("/does_not_exist"), &blobs).await;
1005        assert!(result.is_err() || result.unwrap().is_empty());
1006
1007        let (mut mount, blobs, _, _temp) = setup_test_env().await;
1008        mount
1009            .add(
1010                &PathBuf::from("/dir/file.txt"),
1011                Cursor::new(b"data".to_vec()),
1012                &blobs,
1013            )
1014            .await
1015            .unwrap();
1016
1017        let result = mount.cat(&PathBuf::from("/dir"), &blobs).await;
1018        assert!(result.is_err());
1019    }
1020
1021    #[tokio::test]
1022    async fn test_save_load() {
1023        let (mount, blobs, secret_key, _temp) = setup_test_env().await;
1024        let link = mount.save(&blobs).await.unwrap();
1025        let _mount = Mount::load(&link, &secret_key, &blobs).await.unwrap();
1026    }
1027}