evobench_tools/io_utils/
tempfile_utils.rs

1//! More composable/controllable utilities for handling temporary files?
2
3use std::{
4    fs::File,
5    io::{BufWriter, Write},
6    os::unix::fs::{MetadataExt, PermissionsExt},
7    path::{Path, PathBuf},
8};
9
10use cj_path_util::path_util::AppendToPath;
11use nix::{
12    errno::Errno,
13    unistd::{Gid, Uid, chown, getpid},
14};
15
16use crate::info;
17
18#[derive(Debug, thiserror::Error)]
19pub enum TempfileError {
20    #[error("path is missing parent directory part")]
21    MissingParent,
22    #[error("path is missing file name part")]
23    MissingFileName,
24    #[error("IO error while {0}: {1:#}")]
25    IOError(&'static str, std::io::Error),
26    #[error("IO error while {0}: {1:#}")]
27    IOErrno(&'static str, Errno),
28}
29
30/// Append a suffix `.tmp~..-..` where the numbers are pid and tid
31pub fn temp_path(target_path: impl AsRef<Path>) -> Result<PathBuf, TempfileError> {
32    let target_path = target_path.as_ref();
33    let dir = target_path
34        .parent()
35        .ok_or_else(|| TempfileError::MissingParent)?;
36    let file_name = target_path
37        .file_name()
38        .ok_or_else(|| TempfileError::MissingFileName)?;
39    let mut file_name: Vec<u8> = file_name.to_string_lossy().to_string().into();
40    let pid = getpid();
41    let tid = std::thread::current().id();
42    write!(&mut file_name, ".tmp~{pid}-{tid:?}").expect("nofail: no IO");
43    let file_name =
44        String::from_utf8(file_name).expect("nofail: was a string, with strings appended");
45    Ok(dir.append(file_name))
46}
47
48// #[test]
49// fn t() {
50//     assert_eq!("foo", temp_path("fun").expect("no err").to_string_lossy());
51// }
52
53#[derive(Debug, Clone)]
54pub struct TempfileOptions {
55    pub target_path: PathBuf,
56    pub retain_tempfile: bool,
57    pub migrate_access: bool,
58}
59
60impl TempfileOptions {
61    pub fn tempfile(self) -> Result<Tempfile, TempfileError> {
62        Tempfile::try_from(self)
63    }
64}
65
66#[derive(Debug)]
67pub struct Tempfile {
68    pub opts: TempfileOptions,
69    pub temp_path: PathBuf,
70}
71
72impl TryFrom<TempfileOptions> for Tempfile {
73    type Error = TempfileError;
74
75    fn try_from(opts: TempfileOptions) -> Result<Self, TempfileError> {
76        let temp_path = temp_path(&opts.target_path)?;
77        Ok(Tempfile { opts, temp_path })
78    }
79}
80
81impl Tempfile {
82    pub fn temp_path(&self) -> &Path {
83        &self.temp_path
84    }
85
86    pub fn target_path(&self) -> &Path {
87        &self.opts.target_path
88    }
89
90    pub fn finish(mut self) -> Result<(), TempfileError> {
91        self.opts.retain_tempfile = true; // tell Drop that it should do nothing
92        let Self {
93            opts:
94                TempfileOptions {
95                    ref target_path,
96                    retain_tempfile: _,
97                    migrate_access,
98                },
99            ref temp_path,
100        } = self;
101        let meta = if migrate_access {
102            match target_path.metadata() {
103                Ok(m) => Some(m),
104                Err(e) => match e.kind() {
105                    std::io::ErrorKind::NotFound => None,
106                    _ => return Err(TempfileError::IOError("getting metadata on target file", e)),
107                },
108            }
109        } else {
110            None
111        };
112        if let Some(meta) = meta {
113            // XX iirc when one sets setuid/setgid, user needs to be
114            // set first? Also, accessibility race, OK?
115            let uid = meta.uid();
116            let gid = meta.gid();
117            chown(
118                temp_path.into(),
119                Some(Uid::from_raw(uid)),
120                Some(Gid::from_raw(gid)),
121            )
122            .map_err(|e| TempfileError::IOErrno("copying owner/group to new file", e))?;
123
124            let perms = meta.permissions();
125            let mode = perms.mode();
126            // Is the way via mode really necessary, or pass perms directly??
127            std::fs::set_permissions(&temp_path, std::fs::Permissions::from_mode(mode))
128                .map_err(|e| TempfileError::IOError("copying permissions to new file", e))?;
129        }
130        std::fs::rename(temp_path, target_path)
131            .map_err(|e| TempfileError::IOError("renaming to target", e))?;
132        Ok(())
133    }
134}
135
136impl Drop for Tempfile {
137    fn drop(&mut self) {
138        let Self {
139            opts:
140                TempfileOptions {
141                    target_path: _,
142                    retain_tempfile,
143                    migrate_access: _,
144                },
145            temp_path,
146        } = self;
147        if !*retain_tempfile {
148            match std::fs::remove_file(&*temp_path) {
149                Ok(()) => (),
150                Err(e) => match e.kind() {
151                    std::io::ErrorKind::NotFound => (),
152                    _ => info!("error deleting temporary file {:?}: {e:#}", temp_path),
153                },
154            }
155        }
156    }
157}
158
159/// Usage of Tempfile made easier: opens a file handle to the
160/// temporary file and hands it back, too
161pub fn tempfile(
162    target_path: PathBuf,
163    migrate_access: bool,
164) -> Result<(Tempfile, BufWriter<File>), TempfileError> {
165    let tmp_file = TempfileOptions {
166        target_path,
167        retain_tempfile: false,
168        migrate_access,
169    }
170    .tempfile()?;
171    let temp_path = &tmp_file.temp_path;
172    let out = BufWriter::new(
173        File::create(temp_path)
174            .map_err(|e| TempfileError::IOError("creating temporary file", e))?,
175    );
176    Ok((tmp_file, out))
177}
178
179// XX todo?
180pub struct TempfileWithFlush {
181    pub tempfile: Tempfile,
182    pub file: File,
183}