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 = 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 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 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 #[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 #[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 #[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 #[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 pub fn dir_name(&self) -> &str {
343 self.name.as_ref()
344 }
345
346 pub fn dir_path(&self) -> &Path {
348 self.path.as_ref()
349 }
350
351 pub fn directories(&self) -> &[PathBuf] {
353 self.directories.as_ref()
354 }
355
356 pub fn files(&'a self) -> &'a [FileMetadata<'a>] {
358 self.files.as_ref()
359 }
360
361 #[cfg(feature = "extra")]
363 pub fn size(&self) -> usize {
364 self.size
365 }
366
367 #[cfg(feature = "size")]
369 pub fn size_formatted(&self) -> String {
370 crate::FsUtils::size_to_bytes(self.size)
371 }
372
373 pub fn errors(&'a self) -> &'a [DirError<'a>] {
375 self.errors.as_ref()
376 }
377}
378
379#[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 pub fn name(&self) -> &str {
403 self.name.as_ref()
404 }
405
406 pub fn path(&self) -> &Path {
408 self.path.as_ref()
409 }
410
411 #[cfg(feature = "extra")]
413 pub fn size(&self) -> usize {
414 self.size
415 }
416
417 #[cfg(feature = "size")]
419 pub fn formatted_size(&self) -> String {
420 crate::FsUtils::size_to_bytes(self.size)
421 }
422
423 #[cfg(feature = "time")]
425 pub fn accessed(&self) -> Option<Tai64N> {
426 self.accessed
427 }
428
429 #[cfg(feature = "time")]
431 pub fn modified(&self) -> Option<Tai64N> {
432 self.modified
433 }
434
435 #[cfg(feature = "time")]
437 pub fn created(&self) -> Option<Tai64N> {
438 self.created
439 }
440
441 #[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 #[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 #[cfg(feature = "time")]
455 pub fn accessed_humatime(&self) -> Option<String> {
456 crate::FsUtils::tai64_now_duration_to_humantime(&self.accessed?)
457 }
458
459 #[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 #[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 #[cfg(feature = "time")]
473 pub fn modified_humatime(&self) -> Option<String> {
474 crate::FsUtils::tai64_now_duration_to_humantime(&self.modified?)
475 }
476
477 #[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 #[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 #[cfg(feature = "time")]
491 pub fn created_humatime(&self) -> Option<String> {
492 crate::FsUtils::tai64_now_duration_to_humantime(&self.created?)
493 }
494
495 #[cfg(feature = "extra")]
497 pub fn read_only(&self) -> bool {
498 self.read_only
499 }
500
501 #[cfg(feature = "extra")]
503 pub fn symlink(&self) -> bool {
504 self.symlink
505 }
506
507 #[cfg(feature = "file-type")]
509 pub fn file_format(&self) -> &FileFormat {
510 &self.file_format
511 }
512}
513
514#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
516pub struct DirError<'a> {
517 pub path: PathBuf,
519 pub error: std::io::ErrorKind,
521 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}