acick_util/
abs_path.rs

1use std::env::current_dir;
2use std::fmt;
3use std::fs;
4use std::io::{self, Seek as _, SeekFrom, Write};
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7
8use anyhow::{anyhow, Context as _};
9use serde::{de, Deserialize, Deserializer, Serialize};
10
11use crate::{Error, Result};
12
13/// Wraps `shellexpand::full` method.
14fn expand<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
15    Ok(shellexpand::full(&path.as_ref().to_string_lossy())?.parse()?)
16}
17
18/// An absolute (not necessarily canonicalized) path that may or may not exist.
19#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)]
20pub struct AbsPathBuf(PathBuf);
21
22impl AbsPathBuf {
23    /// Construct an absolute path.
24    ///
25    /// Returns error if `path` is not absolute.
26    ///
27    /// If path need to be shell-expanded, use `AbsPathBuf::from_shell_path` instead.
28    pub fn try_new<P: AsRef<Path>>(path: P) -> Result<Self> {
29        let path = path.as_ref();
30        if !path.is_absolute() {
31            return Err(anyhow!("Path is not absolute : {}", path.display()));
32        }
33        let mut ret = Self(PathBuf::new());
34        ret.push(path);
35        Ok(ret)
36    }
37
38    /// Constructs an absolute path whilte expanding leading tilde and environment variables.
39    ///
40    /// Returns error if expanded `path` is not absolute.
41    pub fn from_shell_path<P: AsRef<Path>>(path: P) -> Result<Self> {
42        Self::try_new(expand(path)?)
43    }
44
45    /// Returns current directory as an absolute path.
46    pub fn cwd() -> Result<Self> {
47        Ok(Self(current_dir()?))
48    }
49
50    /// Joins path.
51    pub fn join<P: AsRef<Path>>(&self, path: P) -> Self {
52        Self(self.0.join(path))
53    }
54
55    /// Joins path while expanding leading tilde and environment variables.
56    pub fn join_expand<P: AsRef<Path>>(&self, path: P) -> Result<Self> {
57        Ok(self.join(expand(path)?))
58    }
59
60    fn push<P: AsRef<Path>>(&mut self, path: P) {
61        self.0.push(path)
62    }
63
64    /// Returns parent path.
65    pub fn parent(&self) -> Option<Self> {
66        self.0.parent().map(|parent| Self(parent.to_owned()))
67    }
68
69    pub fn search_dir_contains(&self, file_name: &str) -> Option<Self> {
70        for dir in self.0.ancestors() {
71            let mut file_path = dir.join(file_name);
72            if file_path.is_file() {
73                file_path.pop();
74                return Some(Self(file_path));
75            }
76        }
77        None
78    }
79
80    pub fn save_pretty(
81        &self,
82        save: impl FnOnce(fs::File) -> Result<()>,
83        overwrite: bool,
84        base_dir: Option<&AbsPathBuf>,
85        cnsl: &mut dyn Write,
86    ) -> Result<Option<bool>> {
87        write!(
88            cnsl,
89            "Saving {} ... ",
90            self.strip_prefix_if(base_dir).display()
91        )?;
92        let result = self.save(save, overwrite);
93        let msg = match result {
94            Ok(Some(true)) => "overwritten",
95            Ok(Some(false)) => "saved",
96            Ok(None) => "already exists",
97            Err(_) => "failed",
98        };
99        writeln!(cnsl, "{}", msg)?;
100        result
101    }
102
103    // returns Some(true): overwritten, Some(false): created, None: skipped
104    pub fn save(
105        &self,
106        save: impl FnOnce(fs::File) -> Result<()>,
107        overwrite: bool,
108    ) -> Result<Option<bool>> {
109        let is_existed = self.as_ref().is_file();
110        if !overwrite && is_existed {
111            return Ok(None);
112        }
113        self.create_dir_all_and_open(false, true)
114            .with_context(|| format!("Could not open file : {}", self))
115            .and_then(|mut file| {
116                // truncate file before write
117                file.seek(SeekFrom::Start(0))?;
118                file.set_len(0)?;
119                Ok(file)
120            })
121            .and_then(save)?;
122        Ok(Some(is_existed))
123    }
124
125    pub fn load_pretty<T>(
126        &self,
127        load: impl FnOnce(fs::File) -> Result<T>,
128        base_dir: Option<&AbsPathBuf>,
129        cnsl: &mut dyn Write,
130    ) -> Result<T> {
131        write!(
132            cnsl,
133            "Loading {} ... ",
134            self.strip_prefix_if(base_dir).display()
135        )?;
136        let result = self.load(load);
137        let msg = match result {
138            Ok(_) => "loaded",
139            Err(_) => "failed",
140        };
141        writeln!(cnsl, "{}", msg)?;
142        result
143    }
144
145    pub fn load<T>(&self, load: impl FnOnce(fs::File) -> Result<T>) -> Result<T> {
146        fs::OpenOptions::new()
147            .read(true)
148            .open(&self.0)
149            .with_context(|| format!("Could not open file : {}", self))
150            .and_then(load)
151    }
152
153    pub fn remove_dir_all_pretty(
154        &self,
155        base_dir: Option<&AbsPathBuf>,
156        cnsl: &mut dyn Write,
157    ) -> Result<bool> {
158        write!(
159            cnsl,
160            "Removing {} ... ",
161            self.strip_prefix_if(base_dir).display()
162        )?;
163        let result = self.remove_dir_all();
164        let msg = match result {
165            Ok(true) => "removed",
166            Ok(false) => "not existed",
167            Err(_) => "failed",
168        };
169        writeln!(cnsl, "{}", msg)?;
170        result
171    }
172
173    fn remove_dir_all(&self) -> Result<bool> {
174        if !self.as_ref().exists() {
175            return Ok(false);
176        }
177        fs::remove_dir_all(self.as_ref())?;
178        Ok(true)
179    }
180
181    pub fn remove_file_pretty(
182        &self,
183        base_dir: Option<&AbsPathBuf>,
184        cnsl: &mut dyn Write,
185    ) -> Result<bool> {
186        write!(
187            cnsl,
188            "Removing {} ... ",
189            self.strip_prefix_if(base_dir).display()
190        )?;
191        let result = if self.as_ref().exists() {
192            self.remove_file().map(|_| true)
193        } else {
194            Ok(false)
195        };
196        let msg = match result {
197            Ok(true) => "removed",
198            Ok(false) => "not existed",
199            Err(_) => "failed",
200        };
201        writeln!(cnsl, "{}", msg)?;
202        result
203    }
204
205    fn remove_file(&self) -> Result<()> {
206        fs::remove_file(self.as_ref())?;
207        Ok(())
208    }
209
210    pub fn move_from_pretty(
211        &self,
212        from: &AbsPathBuf,
213        base_dir: Option<&AbsPathBuf>,
214        cnsl: &mut dyn Write,
215    ) -> Result<()> {
216        write!(
217            cnsl,
218            "Moving {} to {} ... ",
219            from.strip_prefix_if(base_dir).display(),
220            self.strip_prefix_if(base_dir).display()
221        )?;
222        let result = self.move_from(from);
223        let msg = match result {
224            Ok(_) => "moved",
225            Err(_) => "failed",
226        };
227        writeln!(cnsl, "{}", msg)?;
228        result
229    }
230
231    fn move_from(&self, from: &AbsPathBuf) -> Result<()> {
232        fs::rename(from.as_ref(), self.as_ref())?;
233        Ok(())
234    }
235
236    pub fn create_dir_all_and_open(&self, is_read: bool, is_write: bool) -> io::Result<fs::File> {
237        if let Some(dir) = self.parent() {
238            dir.create_dir_all()?
239        }
240        self.open(is_read, is_write)
241    }
242
243    pub fn create_dir_all(&self) -> io::Result<()> {
244        fs::create_dir_all(self.as_ref())
245    }
246
247    fn open(&self, is_read: bool, is_write: bool) -> io::Result<fs::File> {
248        fs::OpenOptions::new()
249            .read(is_read)
250            .write(is_write)
251            .create(true)
252            .open(&self.0)
253    }
254
255    pub fn strip_prefix(&self, base: &AbsPathBuf) -> &Path {
256        self.0
257            .strip_prefix(&base.0)
258            .unwrap_or_else(|_| self.0.as_path())
259    }
260
261    fn strip_prefix_if(&self, base: Option<&AbsPathBuf>) -> &Path {
262        if let Some(base) = base {
263            self.strip_prefix(base)
264        } else {
265            self.0.as_path()
266        }
267    }
268}
269
270impl AsRef<PathBuf> for AbsPathBuf {
271    fn as_ref(&self) -> &PathBuf {
272        &self.0
273    }
274}
275
276impl FromStr for AbsPathBuf {
277    type Err = Error;
278
279    /// Converts str to AbsPathBuf.
280    ///
281    /// Note that this method expands leading tilde and environment variables.
282    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
283        Self::from_shell_path(s)
284    }
285}
286
287impl<'de> Deserialize<'de> for AbsPathBuf {
288    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
289    where
290        D: Deserializer<'de>,
291    {
292        String::deserialize(deserializer)?
293            .parse()
294            .map_err(de::Error::custom)
295    }
296}
297
298impl fmt::Display for AbsPathBuf {
299    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
300        self.0.display().fmt(f)
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use lazy_static::lazy_static;
307
308    use super::*;
309
310    use crate::assert_matches;
311
312    lazy_static! {
313        static ref DRIVE: String = std::env::var("ACICK_TEST_DRIVE").unwrap_or_else(|_| String::from("C"));
314        static ref SHELL_PATH_SUCCESS_TESTS: Vec<(String, PathBuf)> = {
315            let mut tests = vec![
316                (prefix("/a/b"), PathBuf::from(prefix("/a/b"))),
317                ("~/a/b".into(), dirs::home_dir().unwrap().join("a/b")),
318                if cfg!(windows) {
319                    ("$APPDATA/a/b".into(), PathBuf::from(std::env::var("APPDATA").unwrap()).join("a/b"))
320                } else {
321                    ("$HOME/a/b".into(), dirs::home_dir().unwrap().join("a/b"))
322                },
323                (prefix("/a//b"), PathBuf::from(prefix("/a/b"))),
324                (prefix("/a/./b"), PathBuf::from(prefix("/a/b"))),
325                (prefix("/a/b/"), PathBuf::from(prefix("/a/b"))),
326                (prefix("/a/../b"), PathBuf::from(prefix("/a/../b"))),
327            ];
328            if cfg!(windows) {
329                tests.extend_from_slice(&[(
330                    format!("{}:\\a\\b", &*DRIVE),
331                    PathBuf::from(format!("{}:\\a\\b", &*DRIVE)),
332                )]);
333            }
334            tests
335        };
336        static ref SHELL_PATH_FAILURE_TESTS: Vec<&'static str> = {
337            let mut tests = vec!["./a/b/", "a/b", "$ACICK_UNKNOWN_VAR"];
338            if cfg!(windows) {
339                tests.extend_from_slice(&[
340                    "%APPDATA%", // do not expand windows style env var
341                    "/a/b", // not absolute in windows
342                ]);
343            }
344            tests
345        };
346    }
347
348    #[derive(Serialize, Deserialize, Debug)]
349    struct TestData {
350        abs_path: AbsPathBuf,
351    }
352
353    fn prefix(path: &str) -> String {
354        if cfg!(windows) {
355            format!("{}:{}", &*DRIVE, path)
356        } else {
357            path.to_string()
358        }
359    }
360
361    #[test]
362    fn test_try_new_success() -> anyhow::Result<()> {
363        let tests = &[
364            (prefix("/a/b"), prefix("/a/b")),
365            (prefix("/a//b"), prefix("/a/b")),
366            (prefix("/a/./b"), prefix("/a/b")),
367            (prefix("/a/b/"), prefix("/a/b")),
368            (prefix("/a/../b"), prefix("/a/../b")),
369        ];
370        for (actual, expected) in tests {
371            let actual = AbsPathBuf::try_new(actual)?;
372            let expected = PathBuf::from(expected);
373            assert_eq!(actual.as_ref(), &expected);
374        }
375        Ok(())
376    }
377
378    #[test]
379    fn test_try_new_failure() -> anyhow::Result<()> {
380        let tests = &[
381            "~/a/b",
382            if cfg!(windows) {
383                "$APPDATA/a/b"
384            } else {
385                "$HOME/a/b"
386            },
387            "./a/b/",
388            "a/b",
389            "$ACICK_UNKNOWN_VAR",
390        ];
391        for test in tests {
392            assert_matches!(AbsPathBuf::try_new(test) => Err(_));
393        }
394        Ok(())
395    }
396
397    #[test]
398    fn test_parent() -> anyhow::Result<()> {
399        let tests = &[(prefix("/a/b"), Some(prefix("/a"))), (prefix("/"), None)];
400        for (left, right) in tests {
401            let actual = AbsPathBuf::try_new(left)?.parent();
402            let expected = right
403                .as_ref()
404                .map(|path| AbsPathBuf::try_new(path).unwrap());
405            assert_eq!(actual, expected);
406        }
407        Ok(())
408    }
409
410    #[test]
411    fn test_from_str_success() -> anyhow::Result<()> {
412        for (actual, expected) in SHELL_PATH_SUCCESS_TESTS.iter() {
413            let actual: AbsPathBuf = actual.parse()?;
414            assert_eq!(actual.as_ref(), expected);
415        }
416        Ok(())
417    }
418
419    #[test]
420    fn test_from_str_failure() -> anyhow::Result<()> {
421        for test in SHELL_PATH_FAILURE_TESTS.iter() {
422            assert_matches!(AbsPathBuf::from_str(test) => Err(_));
423        }
424        Ok(())
425    }
426
427    #[cfg(not(windows))]
428    #[test]
429    fn test_serialize_success_unix() -> anyhow::Result<()> {
430        let test_data = TestData {
431            abs_path: AbsPathBuf::try_new("/a/b")?,
432        };
433        let actual = serde_yaml::to_string(&test_data)?;
434        let expected = format!("---\nabs_path: {}\n", "/a/b");
435        assert_eq!(actual, expected);
436        Ok(())
437    }
438
439    #[cfg(windows)]
440    #[test]
441    fn test_serialize_success_windows() -> anyhow::Result<()> {
442        let tests = &[
443            (
444                format!(r#"{}:\a\b"#, &*DRIVE),
445                format!(r#""{}:\\a\\b""#, &*DRIVE),
446            ),
447            (
448                format!(r#"{}:/a/b"#, &*DRIVE),
449                format!(r#""{}:/a/b""#, &*DRIVE),
450            ),
451        ];
452        for (left, right) in tests {
453            let test_data = TestData {
454                abs_path: AbsPathBuf::try_new(left)?,
455            };
456            let actual = serde_yaml::to_string(&test_data)?;
457            let expected = format!(
458                r#"---
459abs_path: {}
460"#,
461                right
462            );
463            assert_eq!(actual, expected);
464        }
465        Ok(())
466    }
467
468    #[test]
469    fn test_deserialize_success() -> anyhow::Result<()> {
470        for (actual, expected) in SHELL_PATH_SUCCESS_TESTS.iter() {
471            let yaml_str = format!("---\nabs_path: {}", actual);
472            let test_data: TestData = serde_yaml::from_str(&yaml_str)?;
473            assert_eq!(test_data.abs_path.as_ref(), expected);
474        }
475        Ok(())
476    }
477
478    #[test]
479    fn test_deserialize_failure() -> anyhow::Result<()> {
480        for test in SHELL_PATH_FAILURE_TESTS.iter() {
481            let yaml_str = format!("abs_path: {}", test);
482            let result = serde_yaml::from_str::<TestData>(&yaml_str);
483            assert_matches!(result => Err(_));
484        }
485        Ok(())
486    }
487
488    #[test]
489    fn test_display() -> anyhow::Result<()> {
490        let actual: AbsPathBuf = "~/a".parse()?;
491        let expected = PathBuf::from(format!("{}/a", dirs::home_dir().unwrap().display()));
492        assert_eq!(actual.as_ref(), &expected);
493        assert_eq!(format!("{}", actual), format!("{}", expected.display()));
494        Ok(())
495    }
496}