about summary refs log tree commit diff
path: root/util/src/fs.rs
blob: 6a7a5b490b4e5764b8f7ac3c6e5dc04655d88a63 (plain) (blame)
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
use std::io::{self, Result};
use std::path::{Path, PathBuf};
use rand::{Rng, distributions::Alphanumeric};
use tokio::fs;

/// Create a temporary file named `temp.[a-zA-Z0-9]{length}` in
/// the given location and immediately open it. Returns the
/// filename and the corresponding file handle. It is the caller's
/// responsibility to clean up the temporary file when it is no
/// longer needed.
///
/// Uses [`OpenOptions::create_new`][fs::OpenOptions::create_new]
/// to detect filename collisions, in which case it will
/// automatically retry until the operation succeeds.
///
/// # Errors
///
/// Returns the underlying [`io::Error`] if the operation fails
/// due to reasons other than filename collision.
pub async fn mktemp<T, B>(dir: T, basename: B, length: usize) -> Result<(PathBuf, fs::File)>
where
    T: AsRef<Path>,
    B: Into<Option<&'static str>>
{
    let dir = dir.as_ref();
    let basename = basename.into().unwrap_or("");
    fs::create_dir_all(dir).await?;

    loop {
        let filename = dir.join(format!(
            "{}{}{}",
            basename,
            if basename.is_empty() { "" } else { "." },
            {
                let string = rand::thread_rng()
                .sample_iter(&Alphanumeric)
                .take(length)
                .collect::<Vec<u8>>();
                String::from_utf8(string).unwrap()
            }
        ));

        match fs::OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(&filename)
            .await
        {
            Ok(file) => return Ok((filename, file)),
            Err(err) => match err.kind() {
                io::ErrorKind::AlreadyExists => continue,
                _ => return Err(err)
            }
        }
    }
}