datman/
tree.rs

1/*
2This file is part of Yama.
3
4Yama is free software: you can redistribute it and/or modify
5it under the terms of the GNU General Public License as published by
6the Free Software Foundation, either version 3 of the License, or
7(at your option) any later version.
8
9Yama is distributed in the hope that it will be useful,
10but WITHOUT ANY WARRANTY; without even the implied warranty of
11MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12GNU General Public License for more details.
13
14You should have received a copy of the GNU General Public License
15along with Yama.  If not, see <https://www.gnu.org/licenses/>.
16*/
17
18
19use 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        /// modification time in ms
44        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    /// Filters the tree in-place by removing nodes that do not satisfy the predicate.
159    /// 'Inclusive' in the sense that if a directory does not satisfy the predicate but one of its
160    /// descendants does, then the directory will be included anyway.
161    /// (So nodes that satisfy the predicate will never be excluded because of a parent not doing so.)
162    ///
163    /// Returns true if this node should be included, and false if it should not be.
164    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                // nop
209            }
210        }
211    }
212}
213
214/// Given a file's metadata, returns the mtime in milliseconds.
215pub fn mtime_msec(metadata: &Metadata) -> u64 {
216    (metadata.mtime() * 1000 + metadata.mtime_nsec() / 1_000_000) as u64
217}
218
219/// Scan the filesystem to produce a Tree, using a default progress bar.
220pub 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
230/// Scan the filesystem to produce a Tree, using the specified progress bar.
231pub 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            _ => { /* nop */ }
248        }
249    }
250    let metadata = metadata_res?;
251    let filetype = metadata.file_type();
252
253    /*let name = path
254    .file_name()
255    .ok_or(anyhow!("No filename, wat"))?
256    .to_str()
257    .ok_or(anyhow!("Filename can't be to_str()d"))?
258    .to_owned();*/
259
260    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        // Leave an unpopulated file node. It's not my responsibility to chunk it right now.
271        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                _ => { /* nop */ }
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}