Skip to main content

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