1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
//! The [`NumberedDirBuilder`].

use std::ffi::OsString;
use std::fmt;
use std::fs;
use std::num::NonZeroU8;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;

use anyhow::{Context, Error, Result};

use crate::{NumberedDir, KEEP_DEFAULT, ROOT_DEFAULT};

/// Builder to create a [`NumberedDir`].
///
/// While you can use [`NumberedDir::create`] directly this provides functionality to
/// specific ways of constructing and re-using the [`NumberedDir`].
///
/// Primarily this builder adds the concept of a **root**, a directory in which to create
/// the [`NumberedDir`].  The concept of the **base** is the same as for [`NumberedDir`] and
/// is the prefix of the name of the [`NumberedDir`], thus a prefix of `myprefix` would
/// create directories numbered `myprefix-0`, `myprefix-1` etc.  Likewise the **count** is
/// also the same concept as for [`NumberedDir`] and specifies the maximum number of
/// numbered directories, older directories will be cleaned up.
///
/// # Configuring the builder
///
/// The basic constructor uses a *root* of `testdir-of-$USER` placed in the system's default
/// temporary director location as per [`std::env::temp_dir`].  To customise the root you
/// can use [`NumberedDirBuilder::root`] or [`NumberedDirBuilder::user_root].  The temporary
/// directory provider can also be changed using [`NumberedDirBuilder::tmpdir_provider`].
///
/// If you simply want an absolute path as parent directory for the numbered directory use
/// the [`NumberedDirBuilder::set_parent`] function.
///
/// Sometimes you may have some external condition which signals that an existing numbered
/// directory should be re-used.  The [`NumberedDirBuilder::reusefn] can be used for this.
/// This is useful for example when running tests using `cargo test` and you want to use the
/// same numbered directory for the unit, integration and doc tests even though they all run
/// in different processes.  The [`testdir`] macro does this by storing the process ID of
/// the `cargo test` process in the numbered directory and comparing that to the parent
/// process ID of the current process.
///
/// # Creating the [`NumberedDir`]
///
/// The [`NumberedDirBuilder::create`] method will create a new [`NumberedDir`].
#[derive(Clone)]
pub struct NumberedDirBuilder {
    /// The current absolute path of the parent directory.  The last component is the
    /// current root.  This is the parent directory in which we should create the
    /// NumberedDir.
    parent: PathBuf,
    /// The base of the numbered dir, its name without the number suffix.
    base: String,
    /// The number of numbered dirs to keep around **after** the new directory is created.
    count: NonZeroU8,
    /// Function to determine whether to re-use a numbered dir.
    #[allow(clippy::clippy::type_complexity)]
    reuse_fn: Option<Arc<Box<dyn Fn(&Path) -> bool + Send + Sync>>>,
}

impl fmt::Debug for NumberedDirBuilder {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("NumberedDirBuilder")
            .field("parent", &self.parent)
            .field("base", &self.base)
            .field("count", &self.count)
            .field("reusefn", &"<Fn(&Path) -> bool>")
            .finish()
    }
}

impl NumberedDirBuilder {
    /// Create a new builder for [`NumberedDir`].
    ///
    /// By default the *root* will be set to `testdir-of-$USER`. (using [`ROOT_DEFAULT`])
    /// and the count will be set to `8` ([`KEEP_DEFAULT`]).
    pub fn new(base: String) -> Self {
        if base.contains('/') || base.contains('\\') {
            panic!("base must not contain path separators");
        }
        let root = format!("{}-of-{}", ROOT_DEFAULT, whoami::username());
        Self {
            parent: std::env::temp_dir().join(root),
            base,
            count: KEEP_DEFAULT.unwrap(),
            reuse_fn: None,
        }
    }

    /// Resets the *base*-name of the [`NumberedDir`].
    pub fn base(&mut self, base: String) -> &mut Self {
        self.base = base;
        self
    }

    /// Sets a *root* in the system's temporary directory location.
    ///
    /// The [`NumberedDir`]'s parent will be the `root` subdirectory of the system's
    /// default temporary directory location.
    pub fn root(&mut self, root: impl Into<String>) -> &mut Self {
        self.parent.set_file_name(root.into());
        self
    }

    /// Sets a *root* with the username affixed.
    ///
    /// Like [`NumberedDirBuilder::root`] this sets a subdirectory of the system's default
    /// temporary directory location as the parent direcotry for the [`NumberedDir`].
    /// However it suffixes the username to the given `prefix` to use as *root*.
    pub fn user_root(&mut self, prefix: &str) -> &mut Self {
        let root = format!("{}{}", prefix, whoami::username());
        self.parent.set_file_name(root);
        self
    }

    /// Uses a different temporary direcotry to place the *root* into.
    ///
    /// By default [`std::env::temp_dir`] is used to get the system's temporary directory
    /// location to place the *root* into.  This allows you to provide an alternate function
    /// which will be called to get the location of the directory where *root* will be
    /// placed.  You provider should probably return an absolute path but this is not
    /// enforced.
    pub fn tmpdir_provider(&mut self, provider: impl FnOnce() -> PathBuf) -> &mut Self {
        let default_root = OsString::from_str(ROOT_DEFAULT).unwrap();
        let root = self.parent.file_name().unwrap_or(&default_root);
        self.parent = provider().join(root);
        self
    }

    /// Sets the parent directory for the [`NumberedDir`].
    ///
    /// This does not follow the *root* concept anymore, instead it directly sets the full
    /// path for the parent directory in which the [`NumberedDir`] will be created.  You
    /// probably want this to be an absolute path but this is not enforced.
    ///
    /// Be aware that it is a requirement that the last component of the parent directory is
    /// valid UTF-8.
    pub fn set_parent(&mut self, path: PathBuf) -> &mut Self {
        if path.file_name().and_then(|name| name.to_str()).is_none() {
            panic!("Last component of parent is not UTF-8");
        }
        self.parent = path;
        self
    }

    /// Sets the total number of [`NumberedDir`] directories to keep.
    ///
    /// If creating the new [`NumberedDir`] would exceed this number, older directories will
    /// be removed.
    pub fn count(&mut self, count: NonZeroU8) -> &mut Self {
        self.count = count;
        self
    }

    /// Enables [`NumberedDir`] re-use if `f` returns `true`.
    ///
    /// The provided function will be called with each existing numbered directory and if it
    /// returns `true` this directory will be re-used instead of a new one being created.
    pub fn reusefn<F>(&mut self, f: F) -> &mut Self
    where
        F: Fn(&Path) -> bool + Send + Sync + 'static,
    {
        self.reuse_fn = Some(Arc::new(Box::new(f)));
        self
    }

    /// Disables any previous call to [`NumberedDirBuilder::reusefn`].
    pub fn disable_reuse(&mut self) -> &mut Self {
        self.reuse_fn = None;
        self
    }

    /// Creates a new [`NumberedDir`] as configured.
    pub fn create(&self) -> Result<NumberedDir> {
        if !self.parent.exists() {
            fs::create_dir_all(&self.parent).context("Failed to create root directory")?;
        }
        if !self.parent.is_dir() {
            return Err(Error::msg("Path for root is not a directory"));
        }
        if let Some(ref reuse_fn) = self.reuse_fn {
            for numdir in NumberedDir::iterate(&self.parent, &self.base)? {
                if reuse_fn(&numdir.path()) {
                    return Ok(numdir);
                }
            }
        }
        NumberedDir::create(&self.parent, &self.base, self.count)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_builder_create() {
        let parent = tempfile::tempdir().unwrap();
        let dir = NumberedDirBuilder::new(String::from("base"))
            .tmpdir_provider(|| parent.path().to_path_buf())
            .create()
            .unwrap();
        assert!(dir.path().is_dir());
        let root = dir
            .path()
            .parent()
            .unwrap()
            .file_name()
            .unwrap()
            .to_string_lossy();
        assert!(root.starts_with("testdir-of-"));
    }

    #[test]
    fn test_builder_root() {
        let parent = tempfile::tempdir().unwrap();
        let dir = NumberedDirBuilder::new(String::from("base"))
            .tmpdir_provider(|| parent.path().to_path_buf())
            .root("myroot")
            .create()
            .unwrap();
        assert!(dir.path().is_dir());
        let root = parent.path().join("myroot");
        assert_eq!(dir.path(), root.join("base-0"));
    }

    #[test]
    fn test_builder_user_root() {
        let parent = tempfile::tempdir().unwrap();
        let dir = NumberedDirBuilder::new(String::from("base"))
            .tmpdir_provider(|| parent.path().to_path_buf())
            .root("myroot-")
            .create()
            .unwrap();
        assert!(dir.path().is_dir());
        let root = dir
            .path()
            .parent()
            .unwrap()
            .file_name()
            .unwrap()
            .to_string_lossy();
        assert!(root.starts_with("myroot-"));
    }

    #[test]
    fn test_builder_set_parent() {
        let temp = tempfile::tempdir().unwrap();
        let parent = temp.path().join("myparent");
        let dir = NumberedDirBuilder::new(String::from("base"))
            .set_parent(parent.clone())
            .create()
            .unwrap();
        assert!(dir.path().is_dir());
        assert_eq!(dir.path(), parent.join("base-0"));
    }

    #[test]
    fn test_builder_count() {
        let temp = tempfile::tempdir().unwrap();
        let parent = temp.path();
        let mut builder = NumberedDirBuilder::new(String::from("base"));
        builder.tmpdir_provider(|| parent.to_path_buf());
        builder.count(NonZeroU8::new(1).unwrap());

        let dir0 = builder.create().unwrap();
        assert!(dir0.path().is_dir());

        let dir1 = builder.create().unwrap();
        assert!(!dir0.path().is_dir());
        assert!(dir1.path().is_dir());
    }
}