bak/
lib.rs

1//! `bak` is a Rust library for safely moving files out of the way.
2//!
3//! The API has a few methods, but the one to start with is
4//! `bak::move_aside(PATH)`.
5//!
6//! `move_aside("foo")` will move the file or directory "foo" to
7//! "foo.bak", if there isn't already something there. If there is
8//! already a file called "foo.bak", it will move it to "foo.bak.0", and
9//! so on.
10//!
11//! `move_aside()` returns an `io::Result<PathBuf>` containing the path
12//! to the renamed file.
13//!
14//! You can call `move_aside_with_extension(PATH, EXTENSION)` if you'd
15//! like to use an extension other than "bak". To see where a file would
16//! be moved without actually moving it, call `destination_path(PATH)`
17//! or `destination_with_extension(PATH, EXTENSION)`.
18//!
19//! ## caveats
20//!
21//! - If `bak` is in the middle of renaming a file from `foo` to
22//!   `foo.bak`, and another process or thread concurrently creates a
23//!   file called `foo.bak`, `bak` will silently overwrite the newly
24//!   created `foo.bak` with `foo`. This is because `bak` uses
25//!   `std::fs::rename`, which clobbers destination files.
26#![deny(missing_docs)]
27
28mod common;
29mod error;
30mod template;
31
32#[cfg(test)]
33mod testing;
34
35const DEFAULT_EXTENSION: &str = "bak";
36
37use crate::common::*;
38
39/// Move aside `path` using the default extension, "bak".
40pub fn move_aside(path: impl AsRef<Path>) -> io::Result<PathBuf> {
41  move_aside_with_extension(path, DEFAULT_EXTENSION)
42}
43
44/// Move aside `path` using `extension`.
45pub fn move_aside_with_extension(
46  path: impl AsRef<Path>,
47  extension: impl AsRef<OsStr>,
48) -> io::Result<PathBuf> {
49  let template = Template::new(path.as_ref())?;
50
51  let source = template.source();
52
53  let destination = template.destination(extension.as_ref())?;
54
55  fs::rename(source, &destination)?;
56
57  Ok(destination)
58}
59
60/// Get the destination that `path` would be moved to by `move_aside(path)`
61/// without actually moving it.
62pub fn destination(path: impl AsRef<Path>) -> io::Result<PathBuf> {
63  destination_with_extension(path, DEFAULT_EXTENSION)
64}
65
66/// Get the destination that `path` would be moved to by
67/// `move_aside(path, extension)` without actually moving it.
68pub fn destination_with_extension(
69  path: impl AsRef<Path>,
70  extension: impl AsRef<OsStr>,
71) -> io::Result<PathBuf> {
72  Template::new(path.as_ref())?.destination(extension.as_ref())
73}
74
75#[cfg(test)]
76mod tests {
77  use super::*;
78
79  use std::fs::File;
80
81  macro_rules! test {
82    {
83      name:        $name:ident,
84      files:       [$($file:expr),*],
85      source:      $source:expr,
86      extension:   $extension:expr,
87      destination: $destination:expr,
88    } => {
89      #[test]
90      fn $name() -> io::Result<()> {
91        let mut files = Vec::new();
92        $(
93          {
94            files.push(PathBuf::from($file));
95          }
96        )*;
97
98        let source = PathBuf::from($source);
99
100        let extension: Option<&OsStr> = $extension.map(|extension: &str| extension.as_ref());
101
102        let desired_destination = PathBuf::from($destination);
103
104        let tempdir = tempfile::tempdir()?;
105
106        let base = tempdir.path();
107
108        for file in &files {
109          File::create(base.join(file))?;
110        }
111
112        let planned_destination = match extension {
113          Some(extension) => destination_with_extension(base.join(&source), extension)?,
114          None => destination(base.join(&source))?,
115        };
116
117        let planned_destination = planned_destination.strip_prefix(base.canonicalize()?).unwrap();
118
119        assert_eq!(planned_destination, desired_destination);
120
121        let actual_destination = match extension {
122          Some(extension) => move_aside_with_extension(base.join(&source), extension)?,
123          None => move_aside(base.join(&source))?,
124        };
125
126        let actual_destination = actual_destination.strip_prefix(base.canonicalize()?).unwrap();
127
128        assert_eq!(actual_destination, desired_destination);
129
130        let mut want = files.clone();
131        want.retain(|file| file != &source);
132        want.push(desired_destination);
133        want.sort();
134
135        let mut have = tempdir.path()
136          .read_dir()?
137          .map(|result| result.map(|entry| PathBuf::from(entry.file_name())))
138          .collect::<io::Result<Vec<PathBuf>>>()?;
139        have.sort();
140
141        assert_eq!(have, want, "{:?} != {:?}", have, want);
142
143        Ok(())
144      }
145    }
146  }
147
148  test! {
149    name:        no_conflicts,
150    files:       ["foo"],
151    source:      "foo",
152    extension:   None,
153    destination: "foo.bak",
154  }
155
156  test! {
157    name:        one_conflict,
158    files:       ["foo", "foo.bak"],
159    source:      "foo",
160    extension:   None,
161    destination: "foo.bak.0",
162  }
163
164  test! {
165    name:        two_conflicts,
166    files:       ["foo", "foo.bak", "foo.bak.0"],
167    source:      "foo",
168    extension:   None,
169    destination: "foo.bak.1",
170  }
171
172  test! {
173    name:        three_conflicts,
174    files:       ["foo", "foo.bak", "foo.bak.0", "foo.bak.1"],
175    source:      "foo",
176    extension:   None,
177    destination: "foo.bak.2",
178  }
179
180  test! {
181    name:        no_conflicts_ext,
182    files:       ["foo"],
183    source:      "foo",
184    extension:   Some("bar"),
185    destination: "foo.bar",
186  }
187
188  test! {
189    name:        one_conflict_ext,
190    files:       ["foo", "foo.bar"],
191    source:      "foo",
192    extension:   Some("bar"),
193    destination: "foo.bar.0",
194  }
195
196  test! {
197    name:        two_conflicts_ext,
198    files:       ["foo", "foo.bar", "foo.bar.0"],
199    source:      "foo",
200    extension:   Some("bar"),
201    destination: "foo.bar.1",
202  }
203
204  test! {
205    name:        three_conflicts_ext,
206    files:       ["foo", "foo.bar", "foo.bar.0", "foo.bar.1"],
207    source:      "foo",
208    extension:   Some("bar"),
209    destination: "foo.bar.2",
210  }
211}