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#[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 pub fn new(path: &'a str) -> Self {
55 Self::new_path_buf(path.into())
56 }
57
58 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 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 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 #[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 #[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 #[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 #[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 pub fn dir_name(&self) -> &str {
338 self.name.as_ref()
339 }
340
341 pub fn dir_path(&self) -> &Path {
343 self.path.as_ref()
344 }
345
346 pub fn directories(&self) -> &[PathBuf] {
348 self.directories.as_ref()
349 }
350
351 pub fn files(&'a self) -> &'a [FileMetadata<'a>] {
353 self.files.as_ref()
354 }
355
356 #[cfg(feature = "extra")]
358 pub fn size(&self) -> usize {
359 self.size
360 }
361
362 #[cfg(feature = "size")]
364 pub fn size_formatted(&self) -> String {
365 crate::FsUtils::size_to_bytes(self.size)
366 }
367
368 pub fn errors(&'a self) -> &'a [DirError<'a>] {
370 self.errors.as_ref()
371 }
372}
373
374#[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 pub fn name(&self) -> &str {
398 self.name.as_ref()
399 }
400
401 pub fn path(&self) -> &Path {
403 self.path.as_ref()
404 }
405
406 #[cfg(feature = "extra")]
408 pub fn size(&self) -> usize {
409 self.size
410 }
411
412 #[cfg(feature = "size")]
414 pub fn formatted_size(&self) -> String {
415 crate::FsUtils::size_to_bytes(self.size)
416 }
417
418 #[cfg(feature = "time")]
420 pub fn accessed(&self) -> Option<Tai64N> {
421 self.accessed
422 }
423
424 #[cfg(feature = "time")]
426 pub fn modified(&self) -> Option<Tai64N> {
427 self.modified
428 }
429
430 #[cfg(feature = "time")]
432 pub fn created(&self) -> Option<Tai64N> {
433 self.created
434 }
435
436 #[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 #[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 #[cfg(feature = "time")]
450 pub fn accessed_humatime(&self) -> Option<String> {
451 crate::FsUtils::tai64_now_duration_to_humantime(&self.accessed?)
452 }
453
454 #[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 #[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 #[cfg(feature = "time")]
468 pub fn modified_humatime(&self) -> Option<String> {
469 crate::FsUtils::tai64_now_duration_to_humantime(&self.modified?)
470 }
471
472 #[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 #[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 #[cfg(feature = "time")]
486 pub fn created_humatime(&self) -> Option<String> {
487 crate::FsUtils::tai64_now_duration_to_humantime(&self.created?)
488 }
489
490 #[cfg(feature = "extra")]
492 pub fn read_only(&self) -> bool {
493 self.read_only
494 }
495
496 #[cfg(feature = "extra")]
498 pub fn symlink(&self) -> bool {
499 self.symlink
500 }
501
502 #[cfg(feature = "file-type")]
504 pub fn file_format(&self) -> &FileFormat {
505 &self.file_format
506 }
507}
508
509#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
511pub struct DirError<'a> {
512 pub path: PathBuf,
514 pub error: std::io::ErrorKind,
516 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}