lb_rs/model/
path_ops.rs

1use crate::model::access_info::UserAccessMode;
2use crate::model::errors::{LbErrKind, LbResult};
3use crate::model::file_like::FileLike;
4use crate::model::file_metadata::{FileType, Owner};
5use crate::model::lazy::{LazyStaged1, LazyTree};
6use crate::model::signed_file::SignedFile;
7use crate::model::staged::StagedTreeLike;
8use crate::model::tree_like::{TreeLike, TreeLikeMut};
9use crate::model::{symkey, validate};
10use crate::service::keychain::Keychain;
11use std::collections::HashSet;
12use uuid::Uuid;
13
14use super::ValidationFailure;
15
16impl<Base, Local, Staged> LazyTree<Staged>
17where
18    Base: TreeLike<F = SignedFile>,
19    Local: TreeLike<F = Base::F>,
20    Staged: StagedTreeLike<Base = Base, Staged = Local>,
21{
22    pub fn path_to_id(&mut self, path: &str, root: &Uuid, keychain: &Keychain) -> LbResult<Uuid> {
23        let mut current = *root;
24        'path: for name in split_path(path) {
25            let id = if let FileType::Link { target } = self.find(&current)?.file_type() {
26                target
27            } else {
28                current
29            };
30            'child: for child in self.children(&id)? {
31                if self.calculate_deleted(&child)? {
32                    continue 'child;
33                }
34
35                if self.name_using_links(&child, keychain)? == name {
36                    current = match self.find(&child)?.file_type() {
37                        FileType::Link { target } => target,
38                        _ => child,
39                    };
40
41                    continue 'path;
42                }
43            }
44
45            return Err(LbErrKind::FileNonexistent.into());
46        }
47
48        Ok(current)
49    }
50
51    pub fn id_to_path(&mut self, id: &Uuid, keychain: &Keychain) -> LbResult<String> {
52        let meta = self.find(id)?;
53
54        if meta.is_root() {
55            return Ok("/".to_string());
56        }
57
58        let mut path = match meta.file_type() {
59            FileType::Document => "",
60            FileType::Folder => "/",
61            FileType::Link { target } => match self.find(&target)?.file_type() {
62                FileType::Document | FileType::Link { .. } => "",
63                FileType::Folder => "/",
64            },
65        }
66        .to_string();
67
68        let mut current = *meta.id();
69        loop {
70            let current_meta = if let Some(link) = self.linked_by(&current)? {
71                self.find(&link)?
72            } else {
73                self.find(&current)?
74            };
75            if self.maybe_find(current_meta.parent()).is_none() {
76                return Err(LbErrKind::FileParentNonexistent.into());
77            }
78            if current_meta.is_root() {
79                return Ok(path);
80            }
81            let next = *current_meta.parent();
82            let current_name = self.name_using_links(&current, keychain)?;
83            path = format!("/{}{}", current_name, path);
84            current = next;
85        }
86    }
87
88    pub fn list_paths(
89        &mut self, filter: Option<Filter>, keychain: &Keychain,
90    ) -> LbResult<Vec<String>> {
91        // Deal with filter
92        let filtered = match filter {
93            Some(Filter::DocumentsOnly) => {
94                let mut ids = HashSet::new();
95                for id in self.ids() {
96                    if self.find(&id)?.is_document() {
97                        ids.insert(id);
98                    }
99                }
100                ids
101            }
102            Some(Filter::FoldersOnly) => {
103                let mut ids = HashSet::new();
104                for id in self.ids() {
105                    if self.find(&id)?.is_folder() {
106                        ids.insert(id);
107                    }
108                }
109                ids
110            }
111            Some(Filter::LeafNodesOnly) => {
112                let mut retained: HashSet<Uuid> = self.ids().into_iter().collect();
113                for id in self.ids() {
114                    retained.remove(self.find(&id)?.parent());
115                }
116                retained
117            }
118            None => self.ids().into_iter().collect(),
119        };
120
121        // remove deleted; include links not linked files
122        let mut paths = vec![];
123        for id in filtered.clone() {
124            let id = match self.linked_by(&id)? {
125                None => id,
126                Some(link) => {
127                    if filtered.contains(&link) {
128                        continue;
129                    }
130                    link
131                }
132            };
133
134            if !self.calculate_deleted(&id)? && !self.in_pending_share(&id)? {
135                paths.push(self.id_to_path(&id, keychain)?);
136            }
137        }
138
139        Ok(paths)
140    }
141}
142
143impl<Base, Local> LazyStaged1<Base, Local>
144where
145    Base: TreeLike<F = SignedFile>,
146    Local: TreeLikeMut<F = Base::F>,
147{
148    pub fn create_link_at_path(
149        &mut self, path: &str, target_id: Uuid, root: &Uuid, keychain: &Keychain,
150    ) -> LbResult<Uuid> {
151        validate::path(path)?;
152        let file_type = FileType::Link { target: target_id };
153        let path_components = split_path(path);
154        self.create_at_path_helper(file_type, path_components, root, keychain)
155    }
156
157    pub fn create_at_path(
158        &mut self, path: &str, root: &Uuid, keychain: &Keychain,
159    ) -> LbResult<Uuid> {
160        validate::path(path)?;
161        let file_type = if path.ends_with('/') { FileType::Folder } else { FileType::Document };
162        let path_components = split_path(path);
163        self.create_at_path_helper(file_type, path_components, root, keychain)
164    }
165
166    fn create_at_path_helper(
167        &mut self, file_type: FileType, path_components: Vec<&str>, root: &Uuid,
168        keychain: &Keychain,
169    ) -> LbResult<Uuid> {
170        let mut current = *root;
171
172        'path: for index in 0..path_components.len() {
173            'child: for child in self.children(&current)? {
174                if self.calculate_deleted(&child)? {
175                    continue 'child;
176                }
177
178                if self.name_using_links(&child, keychain)? == path_components[index] {
179                    if index == path_components.len() - 1 {
180                        return Err(LbErrKind::Validation(ValidationFailure::PathConflict(
181                            HashSet::from([child]),
182                        )))?;
183                    }
184
185                    current = match self.find(&child)?.file_type() {
186                        FileType::Document => {
187                            return Err(LbErrKind::Validation(
188                                ValidationFailure::NonFolderWithChildren(child),
189                            ))?;
190                        }
191                        FileType::Folder => child,
192                        FileType::Link { target } => {
193                            let current = self.find(&target)?;
194                            if current.access_mode(&Owner(keychain.get_pk()?))
195                                < Some(UserAccessMode::Write)
196                            {
197                                return Err(LbErrKind::InsufficientPermission.into());
198                            }
199                            *current.id()
200                        }
201                    };
202                    continue 'path;
203                }
204            }
205
206            // Child does not exist, create it
207            let this_file_type =
208                if index != path_components.len() - 1 { FileType::Folder } else { file_type };
209
210            current = self.create(
211                Uuid::new_v4(),
212                symkey::generate_key(),
213                &current,
214                path_components[index],
215                this_file_type,
216                keychain,
217            )?;
218        }
219
220        Ok(current)
221    }
222}
223
224#[derive(Debug)]
225pub enum Filter {
226    DocumentsOnly,
227    FoldersOnly,
228    LeafNodesOnly,
229}
230
231fn split_path(path: &str) -> Vec<&str> {
232    path.split('/')
233        .collect::<Vec<&str>>()
234        .into_iter()
235        .filter(|s| !s.is_empty()) // Remove the trailing empty element in the case this is a folder
236        .collect::<Vec<&str>>()
237}