Skip to main content

cba/
bs.rs

1//! Filesystem set, check, read
2use crate::bait::ResultExt as _;
3use crate::bog::BogOkExt;
4use crate::{ebog, ibog, unwrap};
5use std::cmp::Ordering;
6use std::io;
7use std::path::{Component, PathBuf};
8use std::{
9    fs::{self, DirEntry},
10    path::Path,
11};
12
13// --------------- EXECUTABLE ---------------
14/// Check if executable
15///
16/// Prints error.
17pub fn is_executable(path: impl AsRef<Path>) -> bool {
18    let path = path.as_ref();
19    let error_prefix = format!("Failed to check executability of {path:?}");
20
21    #[cfg(unix)]
22    {
23        let metadata = unwrap!(std::fs::metadata(path).prefix(&error_prefix)._ebog());
24        use std::os::unix::fs::PermissionsExt;
25        metadata.permissions().mode() & 0o111 != 0
26    }
27
28    #[cfg(windows)]
29    {
30        let ext = path
31            .extension()
32            .and_then(|e| e.to_str())
33            .unwrap_or_default()
34            .to_ascii_lowercase();
35        matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com")
36    }
37
38    #[cfg(not(any(unix, windows)))]
39    {
40        ebog!("{error_prefix}: unsupported platform.");
41        false
42    }
43}
44
45/// Set executable.
46/// # Example
47/// ```rust,ignore
48///     let error_prefix = format!("Failed set executability of {path:?}");
49///     if symlink(src, dst)
50///         .prefix_err(&error_prefix)
51///         ._ebog()
52///         .is_some() {
53///     // success
54///     }
55///
56/// ```
57pub fn set_executable(path: &Path) -> Result<(), std::io::Error> {
58    #[cfg(windows)]
59    {
60        // determined by ext
61        // todo: improve
62        Ok(())
63    }
64    #[cfg(unix)]
65    {
66        use std::os::unix::fs::PermissionsExt;
67        let metadata = std::fs::metadata(path)?;
68
69        let mut perms = metadata.permissions();
70        perms.set_mode(perms.mode() | 0o111); // add executable bits
71        fs::set_permissions(path, perms)
72    }
73    #[cfg(not(any(unix, windows)))]
74    {
75        Ok(())
76    }
77}
78
79/// Get permissions: [read, write, exec]
80pub fn permissions(path: &Path) -> [bool; 3] {
81    #[cfg(unix)]
82    {
83        use std::os::unix::fs::PermissionsExt;
84        let metadata = match std::fs::metadata(path) {
85            Ok(m) => m,
86            Err(_) => return [false; 3],
87        };
88        let mode = metadata.permissions().mode();
89        [
90            mode & 0o400 != 0, // read
91            mode & 0o200 != 0, // write
92            mode & 0o100 != 0, // exec
93        ]
94    }
95    #[cfg(windows)]
96    {
97        let metadata = match std::fs::metadata(path) {
98            Ok(m) => m,
99            Err(_) => return [false; 3],
100        };
101        let readonly = metadata.permissions().readonly();
102        let ext = path
103            .extension()
104            .and_then(|e| e.to_str())
105            .unwrap_or_default()
106            .to_ascii_lowercase();
107        let executable = matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com");
108        [true, !readonly, executable]
109    }
110    #[cfg(not(any(unix, windows)))]
111    {
112        [false; 3]
113    }
114}
115
116/// False if could not determine
117///
118/// Prints error.
119pub fn is_symlink(path: impl AsRef<Path>) -> bool {
120    let path = path.as_ref();
121    let error_prefix = format!("Failed to check metadata of {path:?}");
122
123    let meta = unwrap!(fs::symlink_metadata(path).prefix(&error_prefix)._ebog());
124    meta.file_type().is_symlink()
125}
126
127/// Cross platform symlink creation (+ ancestors if needed).
128/// # Example
129/// ```rust,ignore
130///     let error_prefix = format!("Failed to symlink {src:?} to {dst:?}");
131///     if symlink(src, dst)
132///         .prefix_err(&error_prefix)
133///         ._ebog()
134///         .is_some() {
135///     // success
136///     }
137///
138/// ```
139pub fn symlink(
140    src: impl AsRef<Path>,
141    dst: impl AsRef<Path>,
142    relative: bool,
143) -> Result<(), std::io::Error> {
144    let src = src.as_ref();
145    let dst = dst.as_ref();
146
147    if let Some(parent) = dst.parent() {
148        std::fs::create_dir_all(parent)?;
149    }
150
151    let target = if relative {
152        unwrap!(
153            diff_paths(src, dst),
154            std::io::Error::other("Unable to determine relative path")
155        )
156    } else {
157        src.to_path_buf()
158    };
159
160    #[cfg(unix)]
161    {
162        std::os::unix::fs::symlink(&target, dst)?;
163    }
164
165    #[cfg(windows)]
166    {
167        use std::os::windows::fs::{symlink_dir, symlink_file};
168
169        let meta = src.metadata()?;
170        if meta.is_dir() {
171            symlink_dir(&target, dst)?;
172        } else {
173            symlink_file(&target, dst)?;
174        }
175    }
176
177    Ok(())
178}
179
180/// From https://docs.rs/pathdiff/latest/src/pathdiff/lib.rs.html#43-86
181/// Construct a relative path from a provided base directory path to the provided path.
182///
183/// ```rust
184/// use cba::bs::diff_paths;
185/// use std::path::*;
186///
187/// assert_eq!(diff_paths("/foo/bar",      "/foo/bar/baz"),  Some("../".into()));
188/// assert_eq!(diff_paths("/foo/bar/baz",  "/foo/bar"),      Some("baz".into()));
189/// assert_eq!(diff_paths("/foo/bar/quux", "/foo/bar/baz"),  Some("../quux".into()));
190/// assert_eq!(diff_paths("/foo/bar/baz",  "/foo/bar/quux"), Some("../baz".into()));
191/// assert_eq!(diff_paths("/foo/bar",      "/foo/bar/quux"), Some("../".into()));
192///
193/// assert_eq!(diff_paths("/foo/bar",      "baz"),           Some("/foo/bar".into()));
194/// assert_eq!(diff_paths("/foo/bar",      "/baz"),          Some("../foo/bar".into()));
195/// assert_eq!(diff_paths("foo",           "bar"),           Some("../foo".into()));
196///
197/// assert_eq!(
198///     diff_paths(&"/foo/bar/baz", "/foo/bar".to_string()),
199///     Some("baz".into())
200/// );
201/// assert_eq!(
202///     diff_paths(Path::new("/foo/bar/baz"), Path::new("/foo/bar").to_path_buf()),
203///     Some("baz".into())
204/// );
205/// ```
206pub fn diff_paths<P, B>(path: P, base: B) -> Option<PathBuf>
207where
208    P: AsRef<Path>,
209    B: AsRef<Path>,
210{
211    let path = path.as_ref();
212    let base = base.as_ref();
213
214    if path.is_absolute() != base.is_absolute() {
215        if path.is_absolute() {
216            Some(PathBuf::from(path))
217        } else {
218            None
219        }
220    } else {
221        let mut ita = path.components();
222        let mut itb = base.components();
223        let mut comps: Vec<Component> = vec![];
224        loop {
225            match (ita.next(), itb.next()) {
226                (None, None) => break,
227                (Some(a), None) => {
228                    comps.push(a);
229                    comps.extend(ita.by_ref());
230                    break;
231                }
232                (None, _) => comps.push(Component::ParentDir),
233                (Some(a), Some(b)) if comps.is_empty() && a == b => (),
234                (Some(a), Some(b)) if b == Component::CurDir => comps.push(a),
235                (Some(_), Some(b)) if b == Component::ParentDir => return None,
236                (Some(a), Some(_)) => {
237                    comps.push(Component::ParentDir);
238                    for _ in itb {
239                        comps.push(Component::ParentDir);
240                    }
241                    comps.push(a);
242                    comps.extend(ita.by_ref());
243                    break;
244                }
245            }
246        }
247        Some(comps.iter().map(|c| c.as_os_str()).collect())
248    }
249}
250
251// ---------- DIRECTORIES -----------------
252/// Create a directory if it doesn't exist.
253///
254/// Use case: initialize configuration directories.
255///
256/// Prints error and success.
257pub fn create_dir(dir: impl AsRef<Path>) -> bool {
258    let dir = dir.as_ref();
259    if dir.as_os_str().is_empty() {
260        ebog!("Failed to determine directory to create."); // i.e. state_dir().unwrap_or_default()
261        return false;
262    }
263
264    if !dir.exists() {
265        match std::fs::create_dir_all(dir) {
266            Ok(_) => {
267                ibog!("Created directory: {}", dir.display());
268                true
269            }
270            Err(e) => {
271                ebog!("Failed to create {:?}: {e}", dir);
272                false
273            }
274        }
275    } else {
276        true
277    }
278}
279
280/// Clear directory contents matching filter.
281/// Retusn Ok if dir has no contents or does not exist.
282///
283/// # Example
284/// ```rust,ignore
285/// let path = "/path/to/dir";
286/// let err_prefix = format!("Failed to clear directory at {path:?}");
287/// clear_dir(&path, |entry| {
288///    // filter condition
289///   true
290/// }).prefix_err(&err_prefix)._ebog();
291/// ```
292pub fn clear_dir(
293    dir: impl AsRef<Path>,
294    filter: impl Fn(&DirEntry) -> bool,
295) -> Result<(), io::Error> {
296    let path = dir.as_ref();
297
298    if !path.exists() {
299        return Ok(());
300    }
301
302    let entries = fs::read_dir(path)?;
303
304    for entry in entries {
305        let entry = entry?;
306        if !filter(&entry) {
307            continue;
308        }
309        let path = entry.path();
310
311        if path.is_dir() {
312            fs::remove_dir_all(&path)?;
313        } else {
314            fs::remove_file(&path)?;
315        }
316    }
317    Ok(())
318}
319
320#[easy_ext::ext(FsPathExt)]
321pub impl<T: AsRef<Path>> T {
322    /// Check if directory is empty
323    fn is_empty_dir(&self) -> bool {
324        let path = self.as_ref();
325        fs::read_dir(path)
326            .map(|mut entries| entries.next().is_none())
327            .unwrap_or(false)
328    }
329}
330
331/// Sort paths by modification time (newest first).
332pub fn sort_by_mtime(paths: &mut Vec<PathBuf>) {
333    paths.sort_by(|a, b| {
334        let ma = fs::metadata(a).and_then(|m| m.modified());
335        let mb = fs::metadata(b).and_then(|m| m.modified());
336        match (ma, mb) {
337            (Ok(a), Ok(b)) => b.cmp(&a),
338            (Ok(_), Err(_)) => Ordering::Less,
339            (Err(_), Ok(_)) => Ordering::Greater,
340            (Err(_), Err(_)) => Ordering::Equal,
341        }
342    });
343}