tempfile_fast/
sponge.rs

1use std::env;
2use std::fs;
3use std::io;
4use std::path::Path;
5use std::path::PathBuf;
6
7use super::PersistableTempFile;
8
9/// A safer abstraction for atomic overwrites of files.
10///
11/// A `Sponge` will "soak up" writes, and eventually, when you're ready, write them to the destination file.
12/// This is atomic, so the destination file will never be left in an intermediate state. This is
13/// error, panic, and crash safe.
14///
15/// Ownership and permission is preserved, where appropriate for the platform.
16///
17/// Space is needed to soak up these writes: If you are overwriting a large file, you may need
18/// disk space for the entire file to be stored twice.
19///
20/// For performance and correctness reasons, many of the things that can go wrong will go wrong at
21/// `commit()` time, not on creation. This might not be what you want if you are doing a very
22/// expensive operation. Most of the failures are permissions errors, however. If you are operating
23/// as a single user inside the user's directory, the chance of failure (except for disk space) is
24/// negligible.
25///
26/// # Example
27///
28/// ```rust
29/// # use std::io::Write;
30/// let mut temp = tempfile_fast::Sponge::new_for("example.txt").unwrap();
31/// temp.write_all(b"hello").unwrap();
32/// temp.commit().unwrap();
33/// ```
34pub struct Sponge {
35    dest: PathBuf,
36    temp: io::BufWriter<PersistableTempFile>,
37}
38
39impl Sponge {
40    /// Create a `Sponge` which will eventually overwrite the named file.
41    /// The file does not have to exist.
42    ///
43    /// This will be resolved to an absolute path relative to the current directory immediately.
44    ///
45    /// The path is *not* run through [`fs::canonicalize`], so other oddities will resolve
46    /// at `commit()` time. Notably, a `symlink` (or `hardlink`, or `reflink`) will be converted
47    /// into a regular file, using the target's [`fs::metadata`].
48    ///
49    /// Intermediate directories will be created using the platform defaults (e.g. permissions),
50    /// if this is not what you want, create them in advance.
51    pub fn new_for<P: AsRef<Path>>(path: P) -> Result<Sponge, io::Error> {
52        let path = path.as_ref();
53
54        let path = if path.is_absolute() {
55            path.to_path_buf()
56        } else {
57            let mut absolute = env::current_dir()?;
58            absolute.push(path);
59            absolute
60        };
61
62        let parent = path
63            .parent()
64            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "path must have a parent"))?;
65
66        fs::create_dir_all(parent)?;
67
68        Ok(Sponge {
69            temp: io::BufWriter::new(PersistableTempFile::new_in(parent)?),
70            dest: path,
71        })
72    }
73
74    /// Write the `Sponge` out to the destination file.
75    ///
76    /// Ownership and permission is preserved, where appropriate for the platform. The permissions
77    /// and ownership are resolved now, using the (absolute) path provided. i.e. changes to the
78    /// destination's file's permissions since the creation of the `Sponge` will be included.
79    ///
80    /// The aim is to transfer all ownership and permission information, but not timestamps.
81    /// The implementation, and what information is transferred, is subject to change in minor
82    /// versions.
83    ///
84    /// The file is `flush()`ed correctly, but not `fsync()`'d. The update is atomic against
85    /// anything that happens to the current process, including erroring, panicking, or crashing.
86    ///
87    /// If you need the update to survive power loss, or OS/kernel issues, you should additionally
88    /// follow the platform recommendations for `fsync()`, which may involve calling `fsync()` on
89    /// at least the new file, and probably on the parent directory. Note that this is the same as
90    /// every other file API, but is being called out here as a reminder, if you are building
91    /// certain types of application.
92    ///
93    /// ## Platform-specific behavior
94    ///
95    /// Metadata:
96    /// * `unix` (including `linux`): At least `chown(uid, gid)` and `chmod(mode_t)`
97    /// * `windows`: At least the `readonly` flag.
98    /// * all: See [`fs::set_permissions`]
99    ///
100    /// ## Error
101    ///
102    /// If any underlying operation fails the system error will be returned directly. This method
103    /// consumes `self`, so these errors are not recoverable. Failing to set the ownership
104    /// information on the temporary file is an error, not ignored, unlike in many implementations.
105    pub fn commit(self) -> Result<(), io::Error> {
106        let temp = self.temp.into_inner()?;
107        copy_metadata(&self.dest, temp.as_ref())?;
108        temp.persist_by_rename(self.dest)
109            .map_err(|persist_error| persist_error.error)?;
110        Ok(())
111    }
112}
113
114/// A `Sponge` is a `BufWriter`.
115impl io::Write for Sponge {
116    /// `write` to the intermediate file, without touching the destination.
117    fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> {
118        self.temp.write(buf)
119    }
120
121    /// `flush` to the intermediate file, without touching the destination.
122    /// This has no real purpose, as these writes should not be observable.
123    fn flush(&mut self) -> Result<(), io::Error> {
124        self.temp.flush()
125    }
126}
127
128fn copy_metadata(source: &Path, dest: &fs::File) -> Result<(), io::Error> {
129    let metadata = match source.metadata() {
130        Ok(metadata) => metadata,
131        Err(ref e) if io::ErrorKind::NotFound == e.kind() => {
132            return Ok(());
133        }
134        Err(e) => Err(e)?,
135    };
136
137    dest.set_permissions(metadata.permissions())?;
138
139    #[cfg(unix)]
140    unix_chown::chown(metadata, dest)?;
141
142    Ok(())
143}
144
145#[cfg(unix)]
146mod unix_chown {
147    use std::fs;
148    use std::io;
149    use std::os::unix::fs::MetadataExt;
150    use std::os::unix::io::AsRawFd;
151
152    pub fn chown(source: fs::Metadata, dest: &fs::File) -> Result<(), io::Error> {
153        let fd = dest.as_raw_fd();
154        zero_success(unsafe { libc::fchown(fd, source.uid(), source.gid()) })?;
155        Ok(())
156    }
157
158    fn zero_success(err: libc::c_int) -> Result<(), io::Error> {
159        if 0 == err {
160            return Ok(());
161        }
162
163        Err(io::Error::last_os_error())
164    }
165}