1use std::collections::BTreeMap;
20use std::fmt::Debug;
21use std::fs::{read_link, symlink_metadata, DirEntry, Metadata};
22use std::io::ErrorKind;
23use std::os::unix::fs::MetadataExt;
24use std::path::Path;
25
26use anyhow::anyhow;
27use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
28use log::warn;
29use serde::{Deserialize, Serialize};
30
31pub use yama::definitions::FilesystemOwnership;
32pub use yama::definitions::FilesystemPermissions;
33
34#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
35pub enum FileTree<NMeta, DMeta, SMeta, Other>
36where
37 NMeta: Debug + Clone + Eq + PartialEq,
38 DMeta: Debug + Clone + Eq + PartialEq,
39 SMeta: Debug + Clone + Eq + PartialEq,
40 Other: Debug + Clone + Eq + PartialEq,
41{
42 NormalFile {
43 mtime: u64,
45 ownership: FilesystemOwnership,
46 permissions: FilesystemPermissions,
47 meta: NMeta,
48 },
49 Directory {
50 ownership: FilesystemOwnership,
51 permissions: FilesystemPermissions,
52 children: BTreeMap<String, FileTree<NMeta, DMeta, SMeta, Other>>,
53 meta: DMeta,
54 },
55 SymbolicLink {
56 ownership: FilesystemOwnership,
57 target: String,
58 meta: SMeta,
59 },
60 Other(Other),
61}
62
63pub type FileTree1<A> = FileTree<A, A, A, ()>;
64
65impl<NMeta, DMeta, SMeta, Other> FileTree<NMeta, DMeta, SMeta, Other>
66where
67 NMeta: Debug + Clone + Eq + PartialEq,
68 DMeta: Debug + Clone + Eq + PartialEq,
69 SMeta: Debug + Clone + Eq + PartialEq,
70 Other: Debug + Clone + Eq + PartialEq,
71{
72 pub fn is_dir(&self) -> bool {
73 match self {
74 FileTree::NormalFile { .. } => false,
75 FileTree::Directory { .. } => true,
76 FileTree::SymbolicLink { .. } => false,
77 FileTree::Other(_) => false,
78 }
79 }
80
81 pub fn is_symlink(&self) -> bool {
82 match self {
83 FileTree::NormalFile { .. } => false,
84 FileTree::Directory { .. } => false,
85 FileTree::SymbolicLink { .. } => true,
86 FileTree::Other(_) => false,
87 }
88 }
89
90 pub fn get_by_path(&self, path: &String) -> Option<&FileTree<NMeta, DMeta, SMeta, Other>> {
91 let mut node = self;
92 for piece in path.split('/') {
93 if piece.is_empty() {
94 continue;
95 }
96 match node {
97 FileTree::Directory { children, .. } => match children.get(piece) {
98 None => {
99 return None;
100 }
101 Some(new_node) => {
102 node = new_node;
103 }
104 },
105 _ => {
106 return None;
107 }
108 }
109 }
110 Some(node)
111 }
112
113 pub fn replace_meta<Replacement: Clone + Debug + Eq + PartialEq>(
114 &self,
115 replacement: &Replacement,
116 ) -> FileTree<Replacement, Replacement, Replacement, Other> {
117 match self {
118 FileTree::NormalFile {
119 mtime,
120 ownership,
121 permissions,
122 ..
123 } => FileTree::NormalFile {
124 mtime: *mtime,
125 ownership: *ownership,
126 permissions: *permissions,
127 meta: replacement.clone(),
128 },
129 FileTree::Directory {
130 ownership,
131 permissions,
132 children,
133 ..
134 } => {
135 let children = children
136 .iter()
137 .map(|(str, ft)| (str.clone(), ft.replace_meta(replacement)))
138 .collect();
139
140 FileTree::Directory {
141 ownership: ownership.clone(),
142 permissions: permissions.clone(),
143 children,
144 meta: replacement.clone(),
145 }
146 }
147 FileTree::SymbolicLink {
148 ownership, target, ..
149 } => FileTree::SymbolicLink {
150 ownership: ownership.clone(),
151 target: target.clone(),
152 meta: replacement.clone(),
153 },
154 FileTree::Other(other) => FileTree::Other(other.clone()),
155 }
156 }
157
158 pub fn filter_inclusive<F>(&mut self, predicate: &mut F) -> bool
165 where
166 F: FnMut(&Self) -> bool,
167 {
168 match self {
169 FileTree::Directory { children, .. } => {
170 let mut to_remove = Vec::new();
171 for (name, child) in children.iter_mut() {
172 if !child.filter_inclusive(predicate) {
173 to_remove.push(name.clone());
174 }
175 }
176 for name in to_remove {
177 children.remove(&name);
178 }
179 !children.is_empty() || predicate(&self)
180 }
181 _ => predicate(&self),
182 }
183 }
184}
185
186impl<X: Debug + Clone + Eq, YAny: Debug + Clone + Eq> FileTree<X, X, X, YAny> {
187 pub fn get_metadata(&self) -> Option<&X> {
188 match self {
189 FileTree::NormalFile { meta, .. } => Some(meta),
190 FileTree::Directory { meta, .. } => Some(meta),
191 FileTree::SymbolicLink { meta, .. } => Some(meta),
192 FileTree::Other(_) => None,
193 }
194 }
195
196 pub fn set_metadata(&mut self, new_meta: X) {
197 match self {
198 FileTree::NormalFile { meta, .. } => {
199 *meta = new_meta;
200 }
201 FileTree::Directory { meta, .. } => {
202 *meta = new_meta;
203 }
204 FileTree::SymbolicLink { meta, .. } => {
205 *meta = new_meta;
206 }
207 FileTree::Other(_) => {
208 }
210 }
211 }
212}
213
214pub fn mtime_msec(metadata: &Metadata) -> u64 {
216 (metadata.mtime() * 1000 + metadata.mtime_nsec() / 1_000_000) as u64
217}
218
219pub fn scan(path: &Path) -> anyhow::Result<Option<FileTree<(), (), (), ()>>> {
221 let pbar = ProgressBar::with_draw_target(0, ProgressDrawTarget::stdout_with_hz(2));
222 pbar.set_style(ProgressStyle::default_spinner().template("{spinner} {pos:7} {msg}"));
223 pbar.set_message("dir scan");
224
225 let result = scan_with_progress_bar(path, &pbar);
226 pbar.finish_at_current_pos();
227 result
228}
229
230pub fn scan_with_progress_bar(
232 path: &Path,
233 progress_bar: &ProgressBar,
234) -> anyhow::Result<Option<FileTree<(), (), (), ()>>> {
235 let metadata_res = symlink_metadata(path);
236 progress_bar.inc(1);
237 if let Err(e) = &metadata_res {
238 match e.kind() {
239 ErrorKind::NotFound => {
240 warn!("vanished: {:?}", path);
241 return Ok(None);
242 }
243 ErrorKind::PermissionDenied => {
244 warn!("permission denied: {:?}", path);
245 return Ok(None);
246 }
247 _ => { }
248 }
249 }
250 let metadata = metadata_res?;
251 let filetype = metadata.file_type();
252
253 let ownership = FilesystemOwnership {
261 uid: metadata.uid() as u16,
262 gid: metadata.gid() as u16,
263 };
264
265 let permissions = FilesystemPermissions {
266 mode: metadata.mode(),
267 };
268
269 if filetype.is_file() {
270 Ok(Some(FileTree::NormalFile {
272 mtime: mtime_msec(&metadata),
273 ownership,
274 permissions,
275 meta: (),
276 }))
277 } else if filetype.is_dir() {
278 let mut children = BTreeMap::new();
279 progress_bar.set_message(&format!("{:?}", path));
280 let dir_read = path.read_dir();
281
282 if let Err(e) = &dir_read {
283 match e.kind() {
284 ErrorKind::NotFound => {
285 warn!("vanished/: {:?}", path);
286 return Ok(None);
287 }
288 ErrorKind::PermissionDenied => {
289 warn!("permission denied/: {:?}", path);
290 return Ok(None);
291 }
292 _ => { }
293 }
294 }
295
296 for entry in dir_read? {
297 let entry: DirEntry = entry?;
298 let scanned = scan_with_progress_bar(&entry.path(), progress_bar)?;
299 if let Some(scanned) = scanned {
300 children.insert(
301 entry
302 .file_name()
303 .into_string()
304 .expect("OsString not String"),
305 scanned,
306 );
307 }
308 }
309
310 Ok(Some(FileTree::Directory {
311 ownership,
312 permissions,
313 children,
314 meta: (),
315 }))
316 } else if filetype.is_symlink() {
317 let target = read_link(path)?
318 .to_str()
319 .ok_or(anyhow!("target path cannot be to_str()d"))?
320 .to_owned();
321
322 Ok(Some(FileTree::SymbolicLink {
323 ownership,
324 target,
325 meta: (),
326 }))
327 } else {
328 Ok(None)
329 }
330}