dir_meta/
fs.rs

1use crate::CowStr;
2
3use std::{
4    borrow::Cow,
5    path::{Path, PathBuf},
6};
7
8#[cfg(feature = "async")]
9use async_recursion::async_recursion;
10
11#[cfg(feature = "async")]
12use futures_lite::StreamExt;
13
14#[cfg(feature = "file-type")]
15use file_format::FileFormat;
16
17#[cfg(feature = "time")]
18use tai64::Tai64N;
19
20#[cfg(feature = "time")]
21use crate::DateTimeString;
22
23/// The Metadata of all directories and files in the current directory
24/// #### Example
25/// ```rust
26/// use dir_meta::DirMetadata;
27///
28/// // With feature `async` enabled using `cargo add dir-meta --features async`
29/// #[cfg(feature = "async")]
30/// {
31///     let dir = DirMetadata::new("/path/to/directory").async_dir_metadata();
32/// }
33///
34/// // With feature `sync` enabled using `cargo add dir-meta --features sync`
35/// #[cfg(feature = "sync")]
36/// {
37///     let dir = DirMetadata::new("/path/to/directory").sync_dir_metadata();
38/// }
39/// ```
40#[derive(Debug, PartialEq, Eq, Default, Clone)]
41pub struct DirMetadata<'a> {
42    name: CowStr<'a>,
43    path: PathBuf,
44    directories: Vec<PathBuf>,
45    files: Vec<FileMetadata<'a>>,
46    #[cfg(feature = "extra")]
47    size: usize,
48    errors: Vec<DirError<'a>>,
49}
50
51impl<'a> DirMetadata<'_> {
52    /// Create a new instance of [Self]
53    /// but with the path as a `&str`
54    pub fn new(path: &'a str) -> Self {
55        Self::new_path_buf(path.into())
56    }
57
58    /// Create a new instance of [Self]
59    pub fn new_path_buf(path: PathBuf) -> Self {
60        let name = Cow::Owned(path.file_name().unwrap().to_string_lossy().to_string());
61
62        Self {
63            path,
64            name,
65            ..Default::default()
66        }
67    }
68
69    /// Multiple files can have the same name if they are in different dirs
70    /// so using this method returns a [vector](Vec) of [FileMetadata]
71    pub fn get_file(&self, file_name: &'a str) -> Vec<&'a FileMetadata> {
72        self.files()
73            .iter()
74            .filter(|file| file.name() == file_name)
75            .collect()
76    }
77
78    /// Get a file by it's absolute path (from root)
79    pub fn get_file_by_path(&self, path: &'a str) -> Option<&'a FileMetadata> {
80        self.files()
81            .iter()
82            .find(|file| file.path() == Path::new(path))
83    }
84
85    /// Returns an error if the directory cannot be accessed
86    /// Read all the directories and files in the given path in async fashion
87    #[cfg(feature = "async")]
88    pub async fn async_dir_metadata(mut self) -> Result<Self, std::io::Error> {
89        use async_fs::read_dir;
90
91        let mut dir = read_dir(&self.path).await?;
92
93        self.async_iter_dir(&mut dir).await;
94
95        Ok(self)
96    }
97
98    /// Returns an error if the directory cannot be accessed
99    /// Read all the directories and files in the given path
100    #[cfg(feature = "sync")]
101    pub fn sync_dir_metadata(mut self) -> Result<Self, std::io::Error> {
102        use std::fs::read_dir;
103        let mut dir = read_dir(&self.path)?;
104
105        self.sync_iter_dir(&mut dir);
106
107        Ok(self)
108    }
109
110    /// Recursively iterate over directories inside directories
111    #[cfg(feature = "async")]
112    #[async_recursion]
113    pub async fn async_iter_dir(
114        &'a mut self,
115        prepared_dir: &mut async_fs::ReadDir,
116    ) -> &'a mut Self {
117        use async_fs::read_dir;
118
119        let mut directories = Vec::<PathBuf>::new();
120
121        while let Some(entry_result) = prepared_dir.next().await {
122            match entry_result {
123                Err(error) => {
124                    self.errors.push(DirError {
125                        path: self.path.clone(),
126                        error: error.kind(),
127                        display: error.to_string().into(),
128                    });
129                }
130                Ok(entry) => {
131                    let mut is_dir = false;
132
133                    match entry.file_type().await {
134                        Ok(file_type) => is_dir = file_type.is_dir(),
135                        Err(error) => {
136                            let inner_path = entry.path();
137
138                            self.errors.push(DirError {
139                                path: inner_path.clone(),
140                                error: error.kind(),
141                                display: Cow::Owned(format!(
142                                    "Unable to check if `{}` is a directory",
143                                    inner_path.display()
144                                )),
145                            });
146                        }
147                    }
148
149                    if is_dir {
150                        directories.push(entry.path())
151                    } else {
152                        let mut file_meta = FileMetadata::default();
153
154                        #[cfg(all(feature = "file-type", feature = "async"))]
155                        {
156                            let cloned_path = entry.path().clone();
157                            let get_file_format =
158                                blocking::unblock(move || FileFormat::from_file(cloned_path));
159                            let format = (get_file_format.await).unwrap_or_default();
160                            file_meta.file_format = format;
161                        }
162
163                        file_meta.name =
164                            CowStr::Owned(entry.file_name().to_string_lossy().to_string());
165                        file_meta.path = entry.path();
166
167                        #[cfg(any(feature = "size", feature = "time", feature = "extra"))]
168                        match entry.metadata().await {
169                            Ok(meta) => {
170                                #[cfg(feature = "extra")]
171                                {
172                                    let current_file_size = meta.len() as usize;
173                                    self.size += current_file_size;
174                                    file_meta.size = current_file_size;
175                                }
176
177                                #[cfg(feature = "time")]
178                                {
179                                    file_meta.accessed =
180                                        crate::FsUtils::maybe_time(meta.accessed().ok());
181                                    file_meta.modified =
182                                        crate::FsUtils::maybe_time(meta.modified().ok());
183                                    file_meta.created =
184                                        crate::FsUtils::maybe_time(meta.created().ok());
185                                }
186                            }
187                            Err(error) => {
188                                self.errors.push(DirError {
189                                    path: entry.path(),
190                                    error: error.kind(),
191                                    display: Cow::Owned(format!(
192                                        "Unable to access metadata of file `{}`",
193                                        entry.path().display()
194                                    )),
195                                });
196                            }
197                        }
198
199                        self.files.push(file_meta);
200                    }
201                }
202            }
203        }
204
205        let mut dir_iter = futures_lite::stream::iter(&directories);
206
207        while let Some(path) = dir_iter.next().await {
208            match read_dir(path.clone()).await {
209                Ok(mut prepared_dir) => {
210                    self.async_iter_dir(&mut prepared_dir).await;
211                }
212                Err(error) => self.errors.push(DirError {
213                    path: path.to_owned(),
214                    error: error.kind(),
215                    display: Cow::Owned(format!(
216                        "Unable to access metadata of file `{}`",
217                        path.display()
218                    )),
219                }),
220            }
221        }
222
223        self.directories.extend_from_slice(&directories);
224
225        self
226    }
227
228    /// Recursively iterate over directories inside directories
229    #[cfg(feature = "sync")]
230    pub fn sync_iter_dir(&mut self, prepared_dir: &mut std::fs::ReadDir) -> &mut Self {
231        let mut directories = Vec::<PathBuf>::new();
232
233        prepared_dir
234            .by_ref()
235            .for_each(|entry_result| match entry_result {
236                Err(error) => {
237                    self.errors.push(DirError {
238                        path: self.path.clone(),
239                        error: error.kind(),
240                        display: error.to_string().into(),
241                    });
242                }
243                Ok(entry) => {
244                    let mut is_dir = false;
245
246                    match entry.file_type() {
247                        Ok(file_type) => is_dir = file_type.is_dir(),
248                        Err(error) => {
249                            let inner_path = entry.path();
250
251                            self.errors.push(DirError {
252                                path: inner_path.clone(),
253                                error: error.kind(),
254                                display: Cow::Owned(format!(
255                                    "Unable to check if `{}` is a directory",
256                                    inner_path.display()
257                                )),
258                            });
259                        }
260                    }
261
262                    if is_dir {
263                        directories.push(entry.path())
264                    } else {
265                        let mut file_meta = FileMetadata::default();
266
267                        #[cfg(all(feature = "file-type", feature = "sync"))]
268                        {
269                            let cloned_path = entry.path().clone();
270                            let get_file_format = FileFormat::from_file(cloned_path);
271                            let format = (get_file_format).unwrap_or_default();
272                            file_meta.file_format = format;
273                        }
274
275                        file_meta.name =
276                            CowStr::Owned(entry.file_name().to_string_lossy().to_string());
277                        file_meta.path = entry.path();
278                        #[cfg(any(feature = "size", feature = "time", feature = "extra"))]
279                        match entry.metadata() {
280                            Ok(meta) => {
281                                #[cfg(feature = "extra")]
282                                {
283                                    let current_file_size = meta.len() as usize;
284                                    self.size += current_file_size;
285                                    file_meta.size = current_file_size;
286                                }
287
288                                #[cfg(feature = "time")]
289                                {
290                                    file_meta.accessed =
291                                        crate::FsUtils::maybe_time(meta.accessed().ok());
292                                    file_meta.modified =
293                                        crate::FsUtils::maybe_time(meta.modified().ok());
294                                    file_meta.created =
295                                        crate::FsUtils::maybe_time(meta.created().ok());
296                                }
297                            }
298                            Err(error) => {
299                                self.errors.push(DirError {
300                                    path: entry.path(),
301                                    error: error.kind(),
302                                    display: Cow::Owned(format!(
303                                        "Unable to access metadata of file `{}`",
304                                        entry.path().display()
305                                    )),
306                                });
307                            }
308                        }
309
310                        self.files.push(file_meta);
311                    }
312                }
313            });
314
315        directories
316            .iter()
317            .for_each(|path| match std::fs::read_dir(path.clone()) {
318                Ok(mut prepared_dir) => {
319                    self.sync_iter_dir(&mut prepared_dir);
320                }
321                Err(error) => self.errors.push(DirError {
322                    path: path.to_owned(),
323                    error: error.kind(),
324                    display: Cow::Owned(format!(
325                        "Unable to access metadata of file `{}`",
326                        path.display()
327                    )),
328                }),
329            });
330
331        self.directories.extend_from_slice(&directories);
332
333        self
334    }
335
336    /// Get the name of the current directory
337    pub fn dir_name(&self) -> &str {
338        self.name.as_ref()
339    }
340
341    /// Get the path of the current directory
342    pub fn dir_path(&self) -> &Path {
343        self.path.as_ref()
344    }
345
346    /// Get all the sub-directories of the current directory
347    pub fn directories(&self) -> &[PathBuf] {
348        self.directories.as_ref()
349    }
350
351    /// Get all the files in the current directory and all the files in it's sub-directory
352    pub fn files(&'a self) -> &'a [FileMetadata<'a>] {
353        self.files.as_ref()
354    }
355
356    /// Get the size of the directory including the  size of all files in the sub-directories
357    #[cfg(feature = "extra")]
358    pub fn size(&self) -> usize {
359        self.size
360    }
361
362    /// Get the size of the directory including the  size of all files in the sub-directories in human readable format
363    #[cfg(feature = "size")]
364    pub fn size_formatted(&self) -> String {
365        crate::FsUtils::size_to_bytes(self.size)
366    }
367
368    /// Get all the errors encountered while opening the sub-directories and files
369    pub fn errors(&'a self) -> &'a [DirError<'a>] {
370        self.errors.as_ref()
371    }
372}
373
374/// The file metadata like file name, file type, file size, file path etc
375#[derive(Debug, PartialEq, Eq, Default, Clone)]
376pub struct FileMetadata<'a> {
377    name: CowStr<'a>,
378    path: PathBuf,
379    #[cfg(feature = "extra")]
380    size: usize,
381    #[cfg(feature = "extra")]
382    read_only: bool,
383    #[cfg(feature = "time")]
384    created: Option<Tai64N>,
385    #[cfg(feature = "time")]
386    accessed: Option<Tai64N>,
387    #[cfg(feature = "time")]
388    modified: Option<Tai64N>,
389    #[cfg(feature = "extra")]
390    symlink: bool,
391    #[cfg(feature = "file-type")]
392    file_format: FileFormat,
393}
394
395impl<'a> FileMetadata<'a> {
396    /// Get the name of the file
397    pub fn name(&self) -> &str {
398        self.name.as_ref()
399    }
400
401    /// Get the path of the file
402    pub fn path(&self) -> &Path {
403        self.path.as_ref()
404    }
405
406    /// Get the size of the file
407    #[cfg(feature = "extra")]
408    pub fn size(&self) -> usize {
409        self.size
410    }
411
412    /// Get the size of the file in human readable format
413    #[cfg(feature = "size")]
414    pub fn formatted_size(&self) -> String {
415        crate::FsUtils::size_to_bytes(self.size)
416    }
417
418    /// Get the TAI64N timestamp when the file was last accessed
419    #[cfg(feature = "time")]
420    pub fn accessed(&self) -> Option<Tai64N> {
421        self.accessed
422    }
423
424    /// Get the TAI64N timestamp when the file was last modified
425    #[cfg(feature = "time")]
426    pub fn modified(&self) -> Option<Tai64N> {
427        self.modified
428    }
429
430    /// Get the TAI64N timestamp when the file was last created
431    #[cfg(feature = "time")]
432    pub fn created(&self) -> Option<Tai64N> {
433        self.created
434    }
435
436    /// Get the timestamp in local time in 24 hour format when the file was last accessed
437    #[cfg(feature = "time")]
438    pub fn accessed_24hr(&self) -> Option<DateTimeString<'a>> {
439        Some(crate::FsUtils::tai64_to_local_hrs(&self.accessed?))
440    }
441
442    /// Get the timestamp in local time in 12 hour format when the file was last accessed
443    #[cfg(feature = "time")]
444    pub fn accessed_am_pm(&self) -> Option<DateTimeString<'a>> {
445        Some(crate::FsUtils::tai64_to_local_am_pm(&self.accessed?))
446    }
447
448    /// Get the time passed since access of a file eg `3 sec ago`
449    #[cfg(feature = "time")]
450    pub fn accessed_humatime(&self) -> Option<String> {
451        crate::FsUtils::tai64_now_duration_to_humantime(&self.accessed?)
452    }
453
454    /// Get the timestamp in local time in 24 hour format when the file was last modified
455    #[cfg(feature = "time")]
456    pub fn modified_24hr(&self) -> Option<DateTimeString<'a>> {
457        Some(crate::FsUtils::tai64_to_local_hrs(&self.modified?))
458    }
459
460    /// Get the timestamp in local time in 12 hour format when the file was last modified
461    #[cfg(feature = "time")]
462    pub fn modified_am_pm(&self) -> Option<DateTimeString<'a>> {
463        Some(crate::FsUtils::tai64_to_local_am_pm(&self.modified?))
464    }
465
466    /// Get the time passed since modification of a file eg `3 sec ago`
467    #[cfg(feature = "time")]
468    pub fn modified_humatime(&self) -> Option<String> {
469        crate::FsUtils::tai64_now_duration_to_humantime(&self.modified?)
470    }
471
472    /// Get the timestamp in local time in 24 hour format when the file was created
473    #[cfg(feature = "time")]
474    pub fn created_24hr(&self) -> Option<DateTimeString<'a>> {
475        Some(crate::FsUtils::tai64_to_local_hrs(&self.created?))
476    }
477
478    /// Get the timestamp in local time in 12 hour format when the file was created
479    #[cfg(feature = "time")]
480    pub fn created_am_pm(&self) -> Option<DateTimeString<'a>> {
481        Some(crate::FsUtils::tai64_to_local_am_pm(&self.created?))
482    }
483
484    /// Get the time passed since file was created of a file eg `3 sec ago`
485    #[cfg(feature = "time")]
486    pub fn created_humatime(&self) -> Option<String> {
487        crate::FsUtils::tai64_now_duration_to_humantime(&self.created?)
488    }
489
490    /// Is the file read only
491    #[cfg(feature = "extra")]
492    pub fn read_only(&self) -> bool {
493        self.read_only
494    }
495
496    /// Is the file a symbolic link
497    #[cfg(feature = "extra")]
498    pub fn symlink(&self) -> bool {
499        self.symlink
500    }
501
502    /// Get the format of the current file
503    #[cfg(feature = "file-type")]
504    pub fn file_format(&self) -> &FileFormat {
505        &self.file_format
506    }
507}
508
509/// An error encountered while accessing a file or sub-directory
510#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
511pub struct DirError<'a> {
512    /// The path to the sub-directory or file where the error occurred
513    pub path: PathBuf,
514    /// The kind of error that occurred based on [std::io::ErrorKind]
515    pub error: std::io::ErrorKind,
516    /// The formatted error as a [String]
517    pub display: CowStr<'a>,
518}
519
520#[cfg(test)]
521mod sanity_checks {
522
523    #[cfg(all(feature = "async", feature = "size", feature = "extra"))]
524    #[test]
525    fn async_features() {
526        smol::block_on(async {
527            let dir = String::from(env!("CARGO_MANIFEST_DIR")) + "/src";
528
529            let outcome = crate::DirMetadata::new(&dir)
530                .async_dir_metadata()
531                .await
532                .unwrap();
533
534            {
535                #[cfg(feature = "time")]
536                for file in outcome.files() {
537                    assert_ne!("", file.name());
538                    assert_ne!(Option::None, file.accessed_24hr());
539                    assert_ne!(Option::None, file.accessed_am_pm());
540                    assert_ne!(Option::None, file.accessed_humatime());
541                    assert_ne!(Option::None, file.created_24hr());
542                    assert_ne!(Option::None, file.created_am_pm());
543                    assert_ne!(Option::None, file.created_humatime());
544                    assert_ne!(Option::None, file.modified_24hr());
545                    assert_ne!(Option::None, file.modified_am_pm());
546                    assert_ne!(Option::None, file.modified_humatime());
547                    assert_ne!(String::default(), file.formatted_size());
548                }
549            }
550        })
551    }
552
553    #[cfg(all(feature = "sync", feature = "size", feature = "extra"))]
554    #[test]
555    fn sync_features() {
556        use file_format::FileFormat;
557
558        let dir = String::from(env!("CARGO_MANIFEST_DIR")) + "/src";
559
560        let outcome = crate::DirMetadata::new(&dir).sync_dir_metadata().unwrap();
561
562        {
563            #[cfg(feature = "time")]
564            for file in outcome.files() {
565                assert_ne!("", file.name());
566                assert_ne!(Option::None, file.accessed_24hr());
567                assert_ne!(Option::None, file.accessed_am_pm());
568                assert_ne!(Option::None, file.accessed_humatime());
569                assert_ne!(Option::None, file.created_24hr());
570                assert_ne!(Option::None, file.created_am_pm());
571                assert_ne!(Option::None, file.created_humatime());
572                assert_ne!(Option::None, file.modified_24hr());
573                assert_ne!(Option::None, file.modified_am_pm());
574                assert_ne!(Option::None, file.modified_humatime());
575                assert_ne!(String::default(), file.formatted_size());
576            }
577        }
578
579        #[cfg(feature = "extra")]
580        {
581            assert!(outcome.size() > 0usize);
582        }
583
584        #[cfg(feature = "file-type")]
585        {
586            let path = dir.clone() + "/lib.rs";
587            let file = outcome.get_file_by_path(&path);
588            assert!(file.is_some());
589            assert_eq!(file.unwrap().file_format(), &FileFormat::PlainText);
590        }
591    }
592}