1use std::io::Read;
2use std::path::{Path, PathBuf};
3
4use tokio_stream::wrappers::UnboundedReceiverStream;
5use tokio_util::sync::CancellationToken;
6
7use crate::engine::FileEngine;
8use crate::error::{FileEngineError, Result};
9use crate::handle::Handle;
10use crate::progress::Progress;
11
12#[derive(Debug, Clone)]
13pub enum FileKind {
14 File,
15 Directory,
16 Symlink,
17 Other,
18}
19
20#[derive(Debug, Clone)]
21pub struct FileInfo {
22 pub path: PathBuf,
23 pub kind: FileKind,
24 pub size: u64,
25 pub content_type: Option<&'static str>,
29 #[cfg(feature = "checksum")]
30 pub hash: Option<String>,
31}
32
33pub struct AnalyzeBuilder {
34 path: PathBuf,
35 recursive: bool,
36 follow_symlinks: bool,
37 #[cfg(feature = "checksum")]
38 with_hash: bool,
39 cancel_token: Option<CancellationToken>,
40}
41
42impl AnalyzeBuilder {
43 pub(crate) fn new(path: PathBuf, follow_symlinks: bool) -> Self {
44 Self {
45 path,
46 recursive: true,
47 follow_symlinks,
48 #[cfg(feature = "checksum")]
49 with_hash: false,
50 cancel_token: None,
51 }
52 }
53
54 pub fn recursive(mut self, enabled: bool) -> Self {
55 self.recursive = enabled;
56 self
57 }
58
59 pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
60 self.cancel_token = Some(token);
61 self
62 }
63
64 pub fn start(self) -> Result<Handle<Vec<FileInfo>>> {
65 let cancel_token = self.cancel_token.unwrap_or_default();
66 let (progress_tx, progress_rx) = tokio::sync::mpsc::unbounded_channel();
67
68 let task_cancel_token = cancel_token.clone();
69 let path = self.path;
70 let recursive = self.recursive;
71 let follow_symlinks = self.follow_symlinks;
72 #[cfg(feature = "checksum")]
73 let with_hash = self.with_hash;
74
75 let join = tokio::task::spawn_blocking(move || {
76 let mut results = Vec::new();
77
78 for (index, entry) in walk_blocking(&path, recursive, follow_symlinks).enumerate() {
79 if task_cancel_token.is_cancelled() {
80 return Err(FileEngineError::Cancelled);
81 }
82
83 let entry = entry.map_err(walkdir_error)?;
84 let metadata = entry.metadata().map_err(walkdir_error)?;
85
86 let kind = if metadata.file_type().is_symlink() {
87 FileKind::Symlink
88 } else if metadata.is_dir() {
89 FileKind::Directory
90 } else if metadata.is_file() {
91 FileKind::File
92 } else {
93 FileKind::Other
94 };
95
96 let size = metadata.len();
97 let is_regular_file = matches!(kind, FileKind::File);
98
99 let content_type = if is_regular_file {
100 sniff_content_type(entry.path())
101 } else {
102 None
103 };
104
105 #[cfg(feature = "checksum")]
106 let hash = if with_hash && is_regular_file {
107 Some(hash_file(entry.path())?)
108 } else {
109 None
110 };
111
112 let files_done = index as u64 + 1;
113 let _ = progress_tx.send(Progress {
114 bytes_done: files_done,
115 bytes_total: 0,
116 files_done,
117 files_total: 0,
118 current_file: Some(entry.path().to_path_buf()),
119 });
120
121 results.push(FileInfo {
122 path: entry.into_path(),
123 kind,
124 size,
125 content_type,
126 #[cfg(feature = "checksum")]
127 hash,
128 });
129 }
130
131 Ok(results)
132 });
133
134 Ok(Handle {
135 join,
136 progress_rx: UnboundedReceiverStream::new(progress_rx),
137 cancel_token,
138 })
139 }
140}
141
142#[cfg(feature = "checksum")]
143impl AnalyzeBuilder {
144 pub fn with_hash(mut self, enabled: bool) -> Self {
145 self.with_hash = enabled;
146 self
147 }
148}
149
150pub(crate) fn walk_blocking(
154 path: &Path,
155 recursive: bool,
156 follow_symlinks: bool,
157) -> impl Iterator<Item = walkdir::Result<walkdir::DirEntry>> {
158 let mut walker = walkdir::WalkDir::new(path).follow_links(follow_symlinks);
159 if !recursive {
160 walker = walker.max_depth(1);
161 }
162 walker.into_iter()
163}
164
165pub(crate) fn walkdir_error(err: walkdir::Error) -> FileEngineError {
166 let path = err.path().map(Path::to_path_buf).unwrap_or_default();
167 match err.into_io_error() {
168 Some(io_err) => crate::error::from_io(path, io_err),
169 None => FileEngineError::Io {
170 path,
171 source: std::io::Error::other("walk error with no underlying io::Error"),
172 },
173 }
174}
175
176fn sniff_content_type(path: &Path) -> Option<&'static str> {
177 let mut file = std::fs::File::open(path).ok()?;
178 let mut buf = [0u8; 8192];
179 let n = file.read(&mut buf).ok()?;
180 infer::get(&buf[..n]).map(|kind| kind.mime_type())
181}
182
183#[cfg(feature = "checksum")]
187pub(crate) fn hash_file(path: &Path) -> Result<String> {
188 let mut file =
189 std::fs::File::open(path).map_err(|e| crate::error::from_io(path.to_path_buf(), e))?;
190 let mut hasher = blake3::Hasher::new();
191 hasher
192 .update_reader(&mut file)
193 .map_err(|e| crate::error::from_io(path.to_path_buf(), e))?;
194 Ok(hasher.finalize().to_hex().to_string())
195}
196
197#[cfg(feature = "analyze")]
198impl FileEngine {
199 pub fn analyze(&self, path: impl Into<PathBuf>) -> AnalyzeBuilder {
200 AnalyzeBuilder::new(path.into(), self.options().follow_symlinks)
201 }
202}