Skip to main content

over_there/core/server/fs/
dir.rs

1use std::io;
2use std::path::{Path, PathBuf};
3use tokio::fs;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct LocalDirEntry {
7    pub path: PathBuf,
8    pub is_file: bool,
9    pub is_dir: bool,
10    pub is_symlink: bool,
11}
12
13impl LocalDirEntry {
14    pub fn path_to_string(&self) -> String {
15        self.path.to_string_lossy().to_string()
16    }
17}
18
19pub async fn entries(path: impl AsRef<Path>) -> io::Result<Vec<LocalDirEntry>> {
20    let mut entries = Vec::new();
21    let mut dir_stream = fs::read_dir(path).await?;
22    while let Some(entry) = dir_stream.next_entry().await? {
23        let file_type = entry.file_type().await?;
24        entries.push(LocalDirEntry {
25            path: entry.path(),
26            is_file: file_type.is_file(),
27            is_dir: file_type.is_dir(),
28            is_symlink: file_type.is_symlink(),
29        });
30    }
31    Ok(entries)
32}
33
34pub async fn rename(
35    from: impl AsRef<Path>,
36    to: impl AsRef<Path>,
37) -> io::Result<()> {
38    let metadata = fs::metadata(from.as_ref()).await?;
39
40    if metadata.is_dir() {
41        fs::rename(from, to).await
42    } else {
43        Err(io::Error::new(io::ErrorKind::Other, "Not a directory"))
44    }
45}
46
47pub async fn create(
48    path: impl AsRef<Path>,
49    create_components: bool,
50) -> io::Result<()> {
51    if create_components {
52        fs::create_dir_all(path).await
53    } else {
54        fs::create_dir(path).await
55    }
56}
57
58pub async fn remove(path: impl AsRef<Path>, non_empty: bool) -> io::Result<()> {
59    if non_empty {
60        fs::remove_dir_all(path).await
61    } else {
62        fs::remove_dir(path).await
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[tokio::test]
71    async fn entries_should_yield_error_if_not_a_directory() {
72        let result = {
73            let file = tempfile::NamedTempFile::new().unwrap();
74            entries(file.as_ref()).await
75        };
76
77        match result {
78            Err(x) if x.kind() == io::ErrorKind::Other => (),
79            x => panic!("Unexpected result: {:?}", x),
80        }
81    }
82
83    #[tokio::test]
84    async fn entries_should_return_immediate_entries_within_dir() {
85        let (dir_path, result) = {
86            let dir = tempfile::tempdir().unwrap();
87
88            fs::File::create(dir.as_ref().join("test-file"))
89                .await
90                .expect("Failed to create file");
91
92            fs::create_dir(dir.as_ref().join("test-dir"))
93                .await
94                .expect("Failed to create dir");
95
96            let result = entries(dir.as_ref()).await;
97
98            (dir.into_path(), result)
99        };
100
101        match result {
102            Ok(entries) => {
103                assert_eq!(entries.len(), 2, "Unexpected number of entries");
104
105                assert!(
106                    entries.contains(&LocalDirEntry {
107                        path: dir_path.join("test-file"),
108                        is_file: true,
109                        is_dir: false,
110                        is_symlink: false,
111                    }),
112                    "No test-file found"
113                );
114
115                assert!(
116                    entries.contains(&LocalDirEntry {
117                        path: dir_path.join("test-dir"),
118                        is_file: false,
119                        is_dir: true,
120                        is_symlink: false,
121                    }),
122                    "No test-dir found"
123                );
124            }
125            x => panic!("Unexpected result: {:?}", x),
126        }
127    }
128
129    #[tokio::test]
130    async fn rename_should_yield_error_if_not_a_directory() {
131        let result = {
132            let from_file = tempfile::NamedTempFile::new().unwrap();
133            let from = from_file.as_ref();
134            let to_dir = tempfile::tempdir().unwrap();
135            let to = to_dir.as_ref();
136
137            rename(from, to).await
138        };
139
140        match result {
141            Err(x) if x.kind() == io::ErrorKind::Other => (),
142            x => panic!("Unexpected result: {:?}", x),
143        }
144    }
145
146    #[tokio::test]
147    async fn rename_should_return_success_if_able_to_rename_directory() {
148        let result = {
149            let from_dir = tempfile::tempdir().unwrap();
150            let from = from_dir.as_ref();
151            let to_dir = tempfile::tempdir().unwrap();
152            let to = to_dir.as_ref();
153
154            rename(from, to).await
155        };
156
157        match result {
158            Ok(_) => (),
159            x => panic!("Unexpected result: {:?}", x),
160        }
161    }
162
163    #[tokio::test]
164    async fn create_should_return_success_if_able_to_make_an_empty_directory() {
165        let result = {
166            let parent_dir = tempfile::tempdir().unwrap();
167
168            create(parent_dir.as_ref().join("test-dir"), false).await
169        };
170
171        assert!(result.is_ok(), "Failed unexpectedly: {:?}", result);
172
173        let result = {
174            let parent_dir = tempfile::tempdir().unwrap();
175
176            create(parent_dir.as_ref().join("test-dir"), true).await
177        };
178
179        assert!(result.is_ok(), "Failed unexpectedly: {:?}", result);
180    }
181
182    #[tokio::test]
183    async fn create_should_yield_error_if_some_components_dont_exist_and_flag_not_set(
184    ) {
185        let result = {
186            let parent_dir = tempfile::tempdir().unwrap();
187            let new_dir = parent_dir.as_ref().join(
188                ["does", "not", "exist"]
189                    .iter()
190                    .collect::<PathBuf>()
191                    .as_path(),
192            );
193
194            create(new_dir, false).await
195        };
196
197        assert!(result.is_err(), "Unexpectedly succeeded: {:?}", result);
198    }
199
200    #[tokio::test]
201    async fn create_should_return_success_if_able_to_make_nested_empty_directory(
202    ) {
203        let parent_dir = tempfile::tempdir().unwrap();
204
205        create(parent_dir.as_ref().join("test-dir"), false)
206            .await
207            .expect("Failed to create directory");
208
209        create(parent_dir.as_ref().join("test-dir"), true)
210            .await
211            .expect("Failed to create directory");
212    }
213
214    #[tokio::test]
215    async fn remove_should_yield_error_if_not_a_directory() {
216        let result = {
217            let file = tempfile::NamedTempFile::new().unwrap();
218            remove(file.as_ref(), false).await
219        };
220
221        match result {
222            Err(x) if x.kind() == io::ErrorKind::Other => (),
223            x => panic!("Unexpected result: {:?}", x),
224        }
225    }
226
227    #[tokio::test]
228    async fn remove_should_return_success_if_able_to_remove_empty_directory() {
229        // Remove an empty directory with non-empty flag not set
230        let result = {
231            let dir = tempfile::tempdir().unwrap();
232            remove(dir.as_ref(), false).await
233        };
234
235        match result {
236            Ok(_) => (),
237            x => panic!("Unexpected result: {:?}", x),
238        }
239
240        // Remove an empty directory with non-empty flag set
241        let result = {
242            let dir = tempfile::tempdir().unwrap();
243            remove(dir.as_ref(), true).await
244        };
245
246        match result {
247            Ok(_) => (),
248            x => panic!("Unexpected result: {:?}", x),
249        }
250    }
251
252    #[tokio::test]
253    async fn remove_should_yield_error_if_removing_nonempty_directory_and_flag_not_set(
254    ) {
255        let result = {
256            let dir = tempfile::tempdir().unwrap();
257
258            fs::File::create(dir.as_ref().join("test-file"))
259                .await
260                .expect("Failed to create file");
261
262            remove(dir.as_ref(), false).await
263        };
264
265        match result {
266            Err(x) if x.kind() == io::ErrorKind::Other => (),
267            x => panic!("Unexpected result: {:?}", x),
268        }
269    }
270
271    #[tokio::test]
272    async fn remove_should_return_success_if_able_to_remove_nonempty_directory_if_flag_set(
273    ) {
274        let result = {
275            let dir = tempfile::tempdir().unwrap();
276
277            fs::File::create(dir.as_ref().join("test-file"))
278                .await
279                .expect("Failed to create file");
280
281            remove(dir.as_ref(), true).await
282        };
283
284        match result {
285            Ok(_) => (),
286            x => panic!("Unexpected result: {:?}", x),
287        }
288    }
289}