monofs/filesystem/dir/
find.rs

1use std::fmt::Debug;
2
3use monoutils_store::IpldStore;
4use typed_path::{Utf8UnixComponent, Utf8UnixPath};
5
6use crate::filesystem::{entity::Entity, FsError, FsResult};
7
8use super::{Dir, Utf8UnixPathSegment};
9
10//--------------------------------------------------------------------------------------------------
11// Types
12//--------------------------------------------------------------------------------------------------
13
14/// Result type for `find_dir*` functions.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub enum FindResult<T> {
17    /// The directory was found.
18    Found {
19        /// The directory containing the entity.
20        dir: T,
21    },
22
23    /// The directory was not found.
24    NotFound {
25        /// The last found directory in the path.
26        dir: T,
27
28        /// The depth of the path to the entity.
29        depth: usize,
30    },
31
32    /// Intermediate path is not a directory.
33    NotADir {
34        /// The depth of the path to the entity.
35        depth: usize,
36    },
37}
38
39/// Result type for `find_dir` function.
40pub type FindResultDir<'a, S> = FindResult<&'a Dir<S>>;
41
42/// Result type for `find_dir_mut` function.
43pub type FindResultDirMut<'a, S> = FindResult<&'a mut Dir<S>>;
44
45//--------------------------------------------------------------------------------------------------
46// Functions
47//--------------------------------------------------------------------------------------------------
48
49/// Looks for a directory at the specified path.
50///
51/// This function navigates through the directory structure starting from the given directory,
52/// following the path specified by `path`. It attempts to resolve each component of the path
53/// until it either finds the target directory, encounters an error, or determines that the path
54/// is not found or invalid.
55///
56/// ## Examples
57///
58/// ```
59/// use monofs::filesystem::{Dir, find_dir};
60/// use monoutils_store::MemoryStore;
61///
62/// # #[tokio::main]
63/// # async fn main() -> anyhow::Result<()> {
64/// let store = MemoryStore::default();
65/// let root_dir = Dir::new(store);
66/// let result = find_dir(&root_dir, "some/path/to/entity").await?;
67/// # Ok(())
68/// # }
69/// ```
70///
71/// ## Note
72///
73/// The function does not support the following path components:
74/// - `.`
75/// - `..`
76/// - `/`
77///
78/// If any of these components are present in the path, the function will return an error.
79pub async fn find_dir<S>(mut dir: &Dir<S>, path: impl AsRef<str>) -> FsResult<FindResultDir<S>>
80where
81    S: IpldStore + Send + Sync,
82{
83    let path = Utf8UnixPath::new(path.as_ref());
84
85    // Convert path components to Utf8UnixPathSegment and collect them
86    let components = path
87        .components()
88        .map(|ref c| match c {
89            Utf8UnixComponent::RootDir => Err(FsError::InvalidSearchPath(path.to_string())),
90            Utf8UnixComponent::CurDir => Err(FsError::InvalidSearchPath(path.to_string())),
91            Utf8UnixComponent::ParentDir => Err(FsError::InvalidSearchPath(path.to_string())),
92            _ => Ok(Utf8UnixPathSegment::try_from(c)?),
93        })
94        .collect::<Result<Vec<_>, _>>()?;
95
96    // Process intermediate components (if any)
97    for (depth, segment) in components.iter().enumerate() {
98        match dir.get_entity(segment).await? {
99            Some(Entity::Dir(d)) => {
100                dir = d;
101            }
102            Some(Entity::SoftLink(_)) => {
103                // SoftLinks are not supported yet, so we return an error
104                return Err(FsError::SoftLinkNotSupportedYet(components.clone()));
105            }
106            Some(_) => {
107                // If we encounter a non-directory entity in the middle of the path,
108                // we return NotADir result
109                return Ok(FindResult::NotADir { depth });
110            }
111            None => {
112                // If an intermediate component doesn't exist,
113                // we return NotFound result
114                return Ok(FindResult::NotFound { dir, depth });
115            }
116        }
117    }
118
119    Ok(FindResult::Found { dir })
120}
121
122/// Looks for a directory at the specified path. This is a mutable version of `find_dir`.
123///
124/// This function navigates through the directory structure starting from the given directory,
125/// following the path specified by `path`. It attempts to resolve each component of the path
126/// until it either finds the target directory, encounters an error, or determines that the path
127/// is not found or invalid.
128///
129/// ## Examples
130///
131/// ```
132/// use monofs::filesystem::{Dir, find_dir_mut};
133/// use monoutils_store::MemoryStore;
134///
135/// # #[tokio::main]
136/// # async fn main() -> anyhow::Result<()> {
137/// let store = MemoryStore::default();
138/// let mut root_dir = Dir::new(store);
139/// let result = find_dir_mut(&mut root_dir, "some/path/to/entity").await?;
140/// # Ok(())
141/// # }
142/// ```
143///
144/// ## Note
145///
146/// The function does not support the following path components:
147/// - `.`
148/// - `..`
149/// - `/`
150///
151/// If any of these components are present in the path, the function will return an error.
152pub async fn find_dir_mut<S>(
153    mut dir: &mut Dir<S>,
154    path: impl AsRef<str>,
155) -> FsResult<FindResultDirMut<S>>
156where
157    S: IpldStore + Send + Sync,
158{
159    let path = Utf8UnixPath::new(path.as_ref());
160
161    // Convert path components to Utf8UnixPathSegment and collect them
162    let components = path
163        .components()
164        .map(|ref c| match c {
165            Utf8UnixComponent::RootDir => Err(FsError::InvalidSearchPath(path.to_string())),
166            Utf8UnixComponent::CurDir => Err(FsError::InvalidSearchPath(path.to_string())),
167            Utf8UnixComponent::ParentDir => Err(FsError::InvalidSearchPath(path.to_string())),
168            _ => Ok(Utf8UnixPathSegment::try_from(c)?),
169        })
170        .collect::<Result<Vec<_>, _>>()?;
171
172    // Process intermediate components (if any)
173    for (depth, segment) in components.iter().enumerate() {
174        match dir.get_entity(segment).await? {
175            Some(Entity::Dir(_)) => {
176                // A hack to get a mutable reference to the directory
177                dir = dir.get_dir_mut(segment).await?.unwrap();
178            }
179            Some(Entity::SoftLink(_)) => {
180                // SoftLinks are not supported yet, so we return an error
181                return Err(FsError::SoftLinkNotSupportedYet(components.clone()));
182            }
183            Some(_) => {
184                // If we encounter a non-directory entity in the middle of the path,
185                // we return NotADir result
186                return Ok(FindResult::NotADir { depth });
187            }
188            None => {
189                // If an intermediate component doesn't exist,
190                // we return NotFound result
191                return Ok(FindResult::NotFound { dir, depth });
192            }
193        }
194    }
195
196    Ok(FindResult::Found { dir })
197}
198
199/// Retrieves an existing entity or creates a new one at the specified path.
200///
201/// This function checks the existence of an entity at the given path. If the entity
202/// exists, it returns the entity. If the entity does not exist, it creates a new
203/// directory hierarchy and returns the new entity.
204///
205/// ## Examples
206///
207/// ```
208/// use monofs::filesystem::{Dir, find_or_create_dir};
209/// use monoutils_store::MemoryStore;
210///
211/// # #[tokio::main]
212/// # async fn main() -> anyhow::Result<()> {
213/// let store = MemoryStore::default();
214/// let mut root_dir = Dir::new(store);
215/// let new_dir = find_or_create_dir(&mut root_dir, "new/nested/directory").await?;
216/// assert!(new_dir.is_empty());
217/// # Ok(())
218/// # }
219/// ```
220pub async fn find_or_create_dir<S>(dir: &mut Dir<S>, path: impl AsRef<str>) -> FsResult<&mut Dir<S>>
221where
222    S: IpldStore + Send + Sync,
223{
224    let path = Utf8UnixPath::new(path.as_ref());
225
226    match find_dir_mut(dir, path).await {
227        Ok(FindResult::Found { dir }) => Ok(dir),
228        Ok(FindResult::NotFound { mut dir, depth }) => {
229            for component in path.components().skip(depth) {
230                let new_dir = Dir::new(dir.get_store().clone());
231                let segment = Utf8UnixPathSegment::try_from(&component)?;
232
233                dir.put_dir(segment.clone(), new_dir)?;
234                dir = dir.get_dir_mut(&segment).await?.unwrap();
235            }
236
237            Ok(dir)
238        }
239        Ok(FindResult::NotADir { depth }) => {
240            let components = path
241                .components()
242                .take(depth + 1)
243                .map(|c| c.to_string())
244                .collect::<Vec<_>>();
245
246            Err(FsError::NotADirectory(components.join("/")))
247        }
248        Err(e) => Err(e),
249    }
250}
251
252//--------------------------------------------------------------------------------------------------
253// Tests
254//--------------------------------------------------------------------------------------------------
255
256#[cfg(test)]
257mod tests {
258    use monoutils_store::MemoryStore;
259
260    use crate::filesystem::File;
261
262    use super::*;
263
264    mod fixtures {
265        use monoutils_store::Storable;
266
267        use super::*;
268
269        pub(super) async fn setup_test_filesystem() -> anyhow::Result<Dir<MemoryStore>> {
270            let store = MemoryStore::default();
271            let mut root = Dir::new(store.clone());
272
273            let mut subdir1 = Dir::new(store.clone());
274            let mut subdir2 = Dir::new(store.clone());
275
276            let file1 = File::new(store.clone());
277            let file2 = File::new(store.clone());
278
279            let file1_cid = file1.store().await?;
280            subdir1.put_entry("file1.txt", file1_cid.into())?;
281
282            let file2_cid = file2.store().await?;
283            subdir2.put_entry("file2.txt", file2_cid.into())?;
284
285            let subdir2_cid = subdir2.store().await?;
286            subdir1.put_entry("subdir2", subdir2_cid.into())?;
287
288            let subdir1_cid = subdir1.store().await?;
289            root.put_entry("subdir1", subdir1_cid.into())?;
290
291            Ok(root)
292        }
293    }
294
295    #[tokio::test]
296    async fn test_find_dir() -> anyhow::Result<()> {
297        let root = fixtures::setup_test_filesystem().await?;
298
299        // Test finding existing directories
300        let result = find_dir(&root, "subdir1").await?;
301        assert!(matches!(result, FindResult::Found { .. }));
302
303        let result = find_dir(&root, "subdir1/subdir2").await?;
304        assert!(matches!(result, FindResult::Found { .. }));
305
306        // Test finding non-existent directories
307        let result = find_dir(&root, "nonexistent").await?;
308        assert!(matches!(result, FindResult::NotFound { depth: 0, .. }));
309
310        let result = find_dir(&root, "subdir1/nonexistent").await?;
311        assert!(matches!(result, FindResult::NotFound { depth: 1, .. }));
312
313        // Test finding a path that contains a file
314        let result = find_dir(&root, "subdir1/file1.txt/invalid").await?;
315        assert!(matches!(result, FindResult::NotADir { depth: 1 }));
316
317        // Test invalid paths
318        let result = find_dir(&root, "/invalid/path").await;
319        assert!(matches!(result, Err(FsError::InvalidSearchPath(_))));
320
321        let result = find_dir(&root, "invalid/../path").await;
322        assert!(matches!(result, Err(FsError::InvalidSearchPath(_))));
323
324        let result = find_dir(&root, "./invalid/path").await;
325        assert!(matches!(result, Err(FsError::InvalidSearchPath(_))));
326
327        Ok(())
328    }
329
330    #[tokio::test]
331    async fn test_find_dir_mut() -> anyhow::Result<()> {
332        let mut root = fixtures::setup_test_filesystem().await?;
333
334        // Test finding existing directories
335        let result = find_dir_mut(&mut root, "subdir1").await?;
336        assert!(matches!(result, FindResult::Found { .. }));
337
338        let result = find_dir_mut(&mut root, "subdir1/subdir2").await?;
339        assert!(matches!(result, FindResult::Found { .. }));
340
341        // Test finding non-existent directories
342        let result = find_dir_mut(&mut root, "nonexistent").await?;
343        assert!(matches!(result, FindResult::NotFound { depth: 0, .. }));
344
345        let result = find_dir_mut(&mut root, "subdir1/nonexistent").await?;
346        assert!(matches!(result, FindResult::NotFound { depth: 1, .. }));
347
348        // Test finding a path that contains a file
349        let result = find_dir_mut(&mut root, "subdir1/file1.txt/invalid").await?;
350        assert!(matches!(result, FindResult::NotADir { depth: 1 }));
351
352        Ok(())
353    }
354
355    #[tokio::test]
356    async fn test_find_or_create_dir() -> anyhow::Result<()> {
357        let mut root = fixtures::setup_test_filesystem().await?;
358
359        // Test creating a new directory
360        let new_dir = find_or_create_dir(&mut root, "new_dir").await?;
361        assert!(new_dir.is_empty());
362
363        // Verify the new directory exists
364        let result = find_dir(&root, "new_dir").await?;
365        assert!(matches!(result, FindResult::Found { .. }));
366
367        // Test creating a nested structure
368        let nested_dir = find_or_create_dir(&mut root, "parent/child/grandchild").await?;
369        assert!(nested_dir.is_empty());
370
371        // Verify the nested structure exists
372        let result = find_dir(&root, "parent/child/grandchild").await?;
373        assert!(matches!(result, FindResult::Found { .. }));
374
375        // Test getting an existing directory
376        let existing_dir = find_or_create_dir(&mut root, "subdir1").await?;
377        assert!(!existing_dir.is_empty());
378
379        // Test creating a directory where a file already exists
380        let result = find_or_create_dir(&mut root, "subdir1/file1.txt").await;
381        assert!(matches!(result, Err(FsError::NotADirectory(_))));
382
383        Ok(())
384    }
385}