staged_file/
lib.rs

1//! `StagedFile` helps write data to a temporary file, and then gives the option
2//! to commit the temporary file to a desired final file path.
3//!
4//! First, a staged file is [created](`StagedFile::with_final_path()`) with the
5//! desired final path of the file. The staged file starts as a newly created
6//! temporary file.
7//!
8//! Then, data is written out by using the `StagedFile` as a `File` ref.
9//!
10//! Finally, the file is commited by calling [`StagedFile::commit()`]. If the
11//! temporary file contents are not to be committed, then let the `StagedFile` be
12//! dropped without calling `commit`.
13//!
14//! ```no_run
15//! use staged_file::StagedFile;
16//! use std::fs::File;
17//! use std::io::{prelude::*, LineWriter};
18//! use std::path::Path;
19//!
20//! # fn main() -> Result<(), staged_file::Error> {
21//! let final_path = Path::new("/a/file/path");
22//! let staged_file = StagedFile::with_final_path(&final_path)?;
23//!
24//! let text = b"Hello World!";
25//!
26//! {
27//!     let mut line_writer = LineWriter::new(&staged_file);
28//!     line_writer.write_all(text)?;
29//!     line_writer.flush()?;
30//! }
31//!
32//! staged_file.commit()?;
33//!
34//! assert_eq!(std::fs::read(final_path)?, text);
35//! # Ok(())
36//! # }
37//! ```
38use std::{
39    fmt::{self, Display, Formatter},
40    fs::File,
41    io,
42    path::{Path, PathBuf},
43    result,
44};
45
46/// A type erased boxed error.
47///
48/// Used for other implementation errors.
49#[derive(Debug)]
50pub struct BoxError(Box<dyn std::error::Error + Send + Sync + 'static>);
51
52impl Display for BoxError {
53    fn fmt(&self, f: &mut Formatter<'_>) -> result::Result<(), fmt::Error> {
54        self.0.fmt(f)
55    }
56}
57
58impl std::error::Error for BoxError {
59    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60        self.0.source()
61    }
62}
63
64/// Possible errors when creating and committing the staged file.
65#[derive(Debug)]
66pub enum Error {
67    /// The final path for the file is invalid.
68    InvalidFinalPath,
69    /// The parent directory of the final path is not valid (e.g. cannot be accessed or determined).
70    InvalidParentFinalPath,
71    /// An I/O error.
72    Io(io::Error),
73    /// All other errors.
74    Other(BoxError),
75}
76
77impl Display for Error {
78    fn fmt(&self, f: &mut Formatter<'_>) -> result::Result<(), fmt::Error> {
79        match self {
80            Error::InvalidFinalPath => write!(f, "invalid final path"),
81            Error::InvalidParentFinalPath => write!(f, "invalid parent final path"),
82            Error::Io(e) => e.fmt(f),
83            Error::Other(e) => e.fmt(f),
84        }
85    }
86}
87
88impl std::error::Error for Error {
89    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
90        match self {
91            Error::InvalidFinalPath | Error::InvalidParentFinalPath => None,
92            Error::Io(e) => Some(e),
93            Error::Other(e) => Some(e),
94        }
95    }
96}
97
98impl From<io::Error> for Error {
99    fn from(error: io::Error) -> Self {
100        Error::Io(error)
101    }
102}
103
104#[derive(Debug)]
105struct TempFilePath(PathBuf);
106
107#[derive(Debug)]
108struct FinalPath(PathBuf);
109
110fn final_path_parent(final_path: &Path) -> Result<&Path, Error> {
111    final_path
112        .parent()
113        .filter(|parent| !parent.as_os_str().is_empty())
114        .ok_or(Error::InvalidParentFinalPath)
115}
116
117#[derive(Debug)]
118enum State {
119    Staged {
120        temp_file: File,
121        temp_dir: tempfile::TempDir,
122        temp_file_path: TempFilePath,
123    },
124    Committed,
125}
126
127/// Creates a temporary file which can then be committed to a final path.
128#[derive(Debug)]
129pub struct StagedFile {
130    final_path: FinalPath,
131    state: State,
132}
133
134impl Drop for StagedFile {
135    fn drop(&mut self) {
136        let mut state = State::Committed;
137        std::mem::swap(&mut self.state, &mut state);
138        if let State::Staged {
139            temp_file,
140            temp_dir,
141            temp_file_path: _temp_file_path,
142        } = state
143        {
144            drop(temp_file);
145            drop(temp_dir);
146        }
147    }
148}
149
150impl StagedFile {
151    /// Instantiates a new staged file with the desired final path.
152    ///
153    /// The desired final path is where the file contents should be if the staged
154    /// file is [committed](`StagedFile::commit()`).
155    ///
156    /// A temporary directory and file are created during this function call.
157    ///
158    /// # Important
159    ///
160    /// If a file exists at the desired final file path, it will be overwritten.
161    ///
162    /// # Errors
163    ///
164    /// If the final path is invalid (e.g. is a directory) or if the final path's
165    /// parent directory cannot be determined, an [`Error`] will be returned.
166    ///
167    /// Any I/O error which occurs when creating the temporary directory or file
168    /// will also be returned.
169    pub fn with_final_path<P>(final_path: P) -> Result<Self, Error>
170    where
171        P: AsRef<Path>,
172    {
173        Self::with_final_path_and_temp_dir_prefix(final_path, None)
174    }
175
176    /// Instantiates a new staged file with the desired final path and a
177    /// temporary directory prefix.
178    ///
179    /// The desired final path is where the file contents should be if the staged
180    /// file is [committed](`StagedFile::commit()`).
181    ///
182    /// A temporary directory and file are created during this function call.
183    /// The temporary directory will have the prefix given or a default prefix
184    /// will be chosen.
185    ///
186    /// # Important
187    ///
188    /// If a file exists at the desired final file path, it will be overwritten.
189    ///
190    /// # Errors
191    ///
192    /// If the final path is invalid (e.g. is a directory) or if the final path's
193    /// parent directory cannot be determined, an [`Error`] will be returned.
194    ///
195    /// Any I/O error which occurs when creating the temporary directory or file
196    /// will also be returned.
197    pub fn with_final_path_and_temp_dir_prefix<P>(
198        final_path: P,
199        temp_dir_prefix: Option<&str>,
200    ) -> Result<Self, Error>
201    where
202        P: AsRef<Path>,
203    {
204        let final_path = final_path.as_ref();
205        if final_path.is_dir() {
206            return Err(Error::InvalidFinalPath);
207        }
208        let temp_dir = tempfile::Builder::new()
209            .prefix(temp_dir_prefix.unwrap_or(".staged"))
210            .tempdir_in(final_path_parent(final_path)?)?;
211        let temp_file_path = temp_dir
212            .path()
213            .join(final_path.file_name().ok_or(Error::InvalidFinalPath)?);
214        let temp_file = File::create(&temp_file_path)?;
215
216        Ok(Self {
217            final_path: FinalPath(final_path.to_path_buf()),
218            state: State::Staged {
219                temp_file,
220                temp_dir,
221                temp_file_path: TempFilePath(temp_file_path),
222            },
223        })
224    }
225
226    /// Commits the temporary file contents into the desired final path.
227    ///
228    /// If the contents should *not* be committed, then allow the `StagedFile` to
229    /// be dropped without calling commit.
230    ///
231    /// # Important
232    ///
233    /// If a file exists at the desired final file path, it will be overwritten.
234    ///
235    /// # Errors
236    ///
237    /// Any I/O errors encountered will be returned.
238    pub fn commit(mut self) -> Result<(), Error> {
239        let mut state = State::Committed;
240        std::mem::swap(&mut self.state, &mut state);
241        if let State::Staged {
242            temp_file,
243            temp_dir,
244            temp_file_path,
245        } = state
246        {
247            temp_file.sync_all()?;
248            // Explicit drop to remove any open file descriptors so temp dir can be deleted
249            drop(temp_file);
250
251            imp::commit(&temp_file_path, &self.final_path)?;
252
253            drop(temp_dir);
254
255            Ok(())
256        } else {
257            unreachable!()
258        }
259    }
260
261    #[inline]
262    fn as_file(&self) -> &File {
263        if let State::Staged { ref temp_file, .. } = self.state {
264            temp_file
265        } else {
266            unreachable!()
267        }
268    }
269}
270
271impl io::Write for StagedFile {
272    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
273        self.as_file().write(buf)
274    }
275
276    fn flush(&mut self) -> io::Result<()> {
277        self.as_file().flush()
278    }
279
280    fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
281        self.as_file().write_vectored(bufs)
282    }
283
284    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
285        self.as_file().write_all(buf)
286    }
287
288    fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> {
289        self.as_file().write_fmt(fmt)
290    }
291}
292
293impl io::Write for &StagedFile {
294    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
295        self.as_file().write(buf)
296    }
297
298    fn flush(&mut self) -> io::Result<()> {
299        self.as_file().flush()
300    }
301
302    fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
303        self.as_file().write_vectored(bufs)
304    }
305
306    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
307        self.as_file().write_all(buf)
308    }
309
310    fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> {
311        self.as_file().write_fmt(fmt)
312    }
313}
314
315impl io::Seek for StagedFile {
316    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
317        self.as_file().seek(pos)
318    }
319
320    fn stream_position(&mut self) -> io::Result<u64> {
321        self.as_file().stream_position()
322    }
323}
324
325impl io::Seek for &StagedFile {
326    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
327        self.as_file().seek(pos)
328    }
329
330    fn stream_position(&mut self) -> io::Result<u64> {
331        self.as_file().stream_position()
332    }
333}
334
335impl io::Read for StagedFile {
336    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
337        self.as_file().read(buf)
338    }
339
340    fn read_vectored(&mut self, bufs: &mut [io::IoSliceMut<'_>]) -> io::Result<usize> {
341        self.as_file().read_vectored(bufs)
342    }
343
344    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
345        self.as_file().read_to_end(buf)
346    }
347
348    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
349        self.as_file().read_to_string(buf)
350    }
351
352    fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
353        self.as_file().read_exact(buf)
354    }
355}
356
357impl io::Read for &StagedFile {
358    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
359        self.as_file().read(buf)
360    }
361
362    fn read_vectored(&mut self, bufs: &mut [io::IoSliceMut<'_>]) -> io::Result<usize> {
363        self.as_file().read_vectored(bufs)
364    }
365
366    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
367        self.as_file().read_to_end(buf)
368    }
369
370    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
371        self.as_file().read_to_string(buf)
372    }
373
374    fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
375        self.as_file().read_exact(buf)
376    }
377}
378
379pub(crate) mod imp;
380
381#[cfg(test)]
382mod test {
383    use super::*;
384
385    #[test]
386    fn commit_staged_file() {
387        use std::io::prelude::*;
388        use std::io::LineWriter;
389
390        let temp_dir = tempfile::tempdir().unwrap();
391        let final_path = temp_dir.path().join("test1");
392        let staged_file = StagedFile::with_final_path(&final_path).unwrap();
393
394        let text = b"Hello World!";
395
396        {
397            let mut line_writer = LineWriter::new(&staged_file);
398            line_writer.write_all(text).unwrap();
399            line_writer.flush().unwrap();
400        }
401
402        staged_file.commit().unwrap();
403
404        assert!(final_path.exists());
405        assert_eq!(std::fs::read(final_path).unwrap(), text);
406    }
407
408    #[test]
409    fn no_commit_staged_file() {
410        use std::io::prelude::*;
411        use std::io::LineWriter;
412
413        let temp_dir = tempfile::tempdir().unwrap();
414        let final_path = temp_dir.path().join("test2");
415        let staged_file = StagedFile::with_final_path(&final_path).unwrap();
416
417        let text = b"Hello World!";
418
419        let mut line_writer = LineWriter::new(&staged_file);
420        line_writer.write_all(text).unwrap();
421        line_writer.flush().unwrap();
422
423        assert!(!final_path.exists());
424    }
425}