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}