gix_tempfile/
handle.rs

1//!
2#![allow(clippy::empty_docs)]
3use std::{io, path::Path};
4
5use tempfile::{NamedTempFile, TempPath};
6
7use crate::{AutoRemove, ContainingDirectory, ForksafeTempfile, Handle, NEXT_MAP_INDEX, REGISTRY};
8
9/// Marker to signal the Registration is an open file able to be written to.
10#[derive(Debug)]
11pub struct Writable;
12
13/// Marker to signal the Registration is a closed file that consumes no additional process resources.
14///
15/// It can't ever be written to unless reopened after persisting it.
16#[derive(Debug)]
17pub struct Closed;
18
19pub(crate) enum Mode {
20    Writable,
21    Closed,
22}
23
24/// Utilities
25impl Handle<()> {
26    fn at_path(
27        path: &Path,
28        directory: ContainingDirectory,
29        cleanup: AutoRemove,
30        mode: Mode,
31        permissions: Option<std::fs::Permissions>,
32    ) -> io::Result<usize> {
33        let tempfile = {
34            let mut builder = tempfile::Builder::new();
35            let dot_ext_storage;
36            match path.file_stem() {
37                Some(stem) => builder.prefix(stem),
38                None => builder.prefix(""),
39            };
40            if let Some(ext) = path.extension() {
41                dot_ext_storage = format!(".{}", ext.to_string_lossy());
42                builder.suffix(&dot_ext_storage);
43            }
44            if let Some(permissions) = permissions {
45                builder.permissions(permissions);
46            }
47            let parent_dir = path.parent().expect("parent directory is present");
48            let parent_dir = directory.resolve(parent_dir)?;
49            ForksafeTempfile::new(builder.rand_bytes(0).tempfile_in(parent_dir)?, cleanup, mode)
50        };
51        let id = NEXT_MAP_INDEX.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
52        expect_none(REGISTRY.insert(id, Some(tempfile)));
53        Ok(id)
54    }
55
56    fn new_writable_inner(
57        containing_directory: &Path,
58        directory: ContainingDirectory,
59        cleanup: AutoRemove,
60        mode: Mode,
61    ) -> io::Result<usize> {
62        let containing_directory = directory.resolve(containing_directory)?;
63        let id = NEXT_MAP_INDEX.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
64        expect_none(REGISTRY.insert(
65            id,
66            Some(ForksafeTempfile::new(
67                NamedTempFile::new_in(containing_directory)?,
68                cleanup,
69                mode,
70            )),
71        ));
72        Ok(id)
73    }
74}
75
76/// Creation and ownership transfer
77impl Handle<Closed> {
78    /// Create a registered tempfile at the given `path`, where `path` includes the desired filename and close it immediately.
79    ///
80    /// Depending on the `directory` configuration, intermediate directories will be created, and depending on `cleanup` empty
81    /// intermediate directories will be removed.
82    ///
83    /// ### Warning of potential leaks
84    ///
85    /// Without [signal handlers](crate::signal) installed, tempfiles will remain once a termination
86    /// signal is encountered as destructors won't run. See [the top-level documentation](crate) for more.
87    pub fn at(path: impl AsRef<Path>, directory: ContainingDirectory, cleanup: AutoRemove) -> io::Result<Self> {
88        Ok(Handle {
89            id: Handle::<()>::at_path(path.as_ref(), directory, cleanup, Mode::Closed, None)?,
90            _marker: Default::default(),
91        })
92    }
93
94    /// Like [`at`](Self::at()), but with support for filesystem `permissions`.
95    pub fn at_with_permissions(
96        path: impl AsRef<Path>,
97        directory: ContainingDirectory,
98        cleanup: AutoRemove,
99        permissions: std::fs::Permissions,
100    ) -> io::Result<Self> {
101        Ok(Handle {
102            id: Handle::<()>::at_path(path.as_ref(), directory, cleanup, Mode::Closed, Some(permissions))?,
103            _marker: Default::default(),
104        })
105    }
106
107    /// Take ownership of the temporary file path, which deletes it when dropped without persisting it beforehand.
108    ///
109    /// It's a theoretical possibility that the file isn't present anymore if signals interfere, hence the `Option`
110    pub fn take(self) -> Option<TempPath> {
111        let res = REGISTRY.remove(&self.id);
112        std::mem::forget(self);
113        res.and_then(|(_k, v)| v.map(ForksafeTempfile::into_temppath))
114    }
115}
116
117/// Creation and ownership transfer
118impl Handle<Writable> {
119    /// Create a registered tempfile at the given `path`, where `path` includes the desired filename.
120    ///
121    /// Depending on the `directory` configuration, intermediate directories will be created, and depending on `cleanup` empty
122    /// intermediate directories will be removed.
123    ///
124    /// ### Warning of potential leaks
125    ///
126    /// Without [signal handlers](crate::signal) installed, tempfiles will remain once a termination
127    /// signal is encountered as destructors won't run. See [the top-level documentation](crate) for more.
128    pub fn at(path: impl AsRef<Path>, directory: ContainingDirectory, cleanup: AutoRemove) -> io::Result<Self> {
129        Ok(Handle {
130            id: Handle::<()>::at_path(path.as_ref(), directory, cleanup, Mode::Writable, None)?,
131            _marker: Default::default(),
132        })
133    }
134
135    /// Like [`at`](Self::at()), but with support for filesystem `permissions`.
136    pub fn at_with_permissions(
137        path: impl AsRef<Path>,
138        directory: ContainingDirectory,
139        cleanup: AutoRemove,
140        permissions: std::fs::Permissions,
141    ) -> io::Result<Self> {
142        Ok(Handle {
143            id: Handle::<()>::at_path(path.as_ref(), directory, cleanup, Mode::Writable, Some(permissions))?,
144            _marker: Default::default(),
145        })
146    }
147
148    /// Create a registered tempfile within `containing_directory` with a name that won't clash, and clean it up as specified with `cleanup`.
149    /// Control how to deal with intermediate directories with `directory`.
150    /// The temporary file is opened and can be written to using the [`with_mut()`][Handle::with_mut()] method.
151    ///
152    /// ### Warning of potential leaks
153    ///
154    /// Without [signal handlers](crate::signal) installed, tempfiles will remain once a termination
155    /// signal is encountered as destructors won't run. See [the top-level documentation](crate) for more.
156    pub fn new(
157        containing_directory: impl AsRef<Path>,
158        directory: ContainingDirectory,
159        cleanup: AutoRemove,
160    ) -> io::Result<Self> {
161        Ok(Handle {
162            id: Handle::<()>::new_writable_inner(containing_directory.as_ref(), directory, cleanup, Mode::Writable)?,
163            _marker: Default::default(),
164        })
165    }
166
167    /// Take ownership of the temporary file.
168    ///
169    /// It's a theoretical possibility that the file isn't present anymore if signals interfere, hence the `Option`
170    pub fn take(self) -> Option<NamedTempFile> {
171        let res = REGISTRY.remove(&self.id);
172        std::mem::forget(self);
173        res.and_then(|(_k, v)| v.map(|v| v.into_tempfile().expect("correct runtime typing")))
174    }
175
176    /// Close the underlying file handle but keep track of the temporary file as before for automatic cleanup.
177    ///
178    /// This saves system resources in situations where one opens a tempfile file at a time, writes a new value, and closes
179    /// it right after to perform more updates of this kind in other tempfiles. When all succeed, they can be renamed one after
180    /// another.
181    pub fn close(self) -> std::io::Result<Handle<Closed>> {
182        match REGISTRY.remove(&self.id) {
183            Some((id, Some(t))) => {
184                std::mem::forget(self);
185                expect_none(REGISTRY.insert(id, Some(t.close())));
186                Ok(Handle::<Closed> {
187                    id,
188                    _marker: Default::default(),
189                })
190            }
191            None | Some((_, None)) => Err(std::io::Error::new(
192                std::io::ErrorKind::NotFound,
193                format!("The tempfile with id {} wasn't available anymore", self.id),
194            )),
195        }
196    }
197}
198
199/// Mutation
200impl Handle<Writable> {
201    /// Obtain a mutable handler to the underlying named tempfile and call `f(&mut named_tempfile)` on it.
202    ///
203    /// Note that for the duration of the call, a signal interrupting the operation will cause the tempfile not to be cleaned up
204    /// as it is not visible anymore to the signal handler.
205    ///
206    /// # Assumptions
207    /// The caller must assure that the signal handler for cleanup will be followed by an abort call so that
208    /// this code won't run again on a removed instance. An error will occur otherwise.
209    pub fn with_mut<T>(&mut self, once: impl FnOnce(&mut NamedTempFile) -> T) -> std::io::Result<T> {
210        match REGISTRY.remove(&self.id) {
211            Some((id, Some(mut t))) => {
212                let res = once(t.as_mut_tempfile().expect("correct runtime typing"));
213                expect_none(REGISTRY.insert(id, Some(t)));
214                Ok(res)
215            }
216            None | Some((_, None)) => Err(std::io::Error::new(
217                std::io::ErrorKind::NotFound,
218                format!("The tempfile with id {} wasn't available anymore", self.id),
219            )),
220        }
221    }
222}
223
224mod io_impls {
225    use std::{io, io::SeekFrom};
226
227    use super::{Handle, Writable};
228
229    impl io::Write for Handle<Writable> {
230        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
231            self.with_mut(|f| f.write(buf))?
232        }
233
234        fn flush(&mut self) -> io::Result<()> {
235            self.with_mut(io::Write::flush)?
236        }
237    }
238
239    impl io::Seek for Handle<Writable> {
240        fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
241            self.with_mut(|f| f.seek(pos))?
242        }
243    }
244
245    impl io::Read for Handle<Writable> {
246        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
247            self.with_mut(|f| f.read(buf))?
248        }
249    }
250}
251
252///
253pub mod persist {
254    use std::path::Path;
255
256    use crate::{
257        handle::{expect_none, Closed, Writable},
258        Handle, REGISTRY,
259    };
260
261    mod error {
262        use std::fmt::{self, Debug, Display};
263
264        use crate::Handle;
265
266        /// The error returned by various [`persist(…)`][Handle<crate::handle::Writable>::persist()] methods
267        #[derive(Debug)]
268        pub struct Error<T: Debug> {
269            /// The io error that prevented the attempt to succeed
270            pub error: std::io::Error,
271            /// The registered handle to the tempfile which couldn't be persisted.
272            pub handle: Handle<T>,
273        }
274
275        impl<T: Debug> Display for Error<T> {
276            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277                Display::fmt(&self.error, f)
278            }
279        }
280
281        impl<T: Debug> std::error::Error for Error<T> {
282            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
283                self.error.source()
284            }
285        }
286    }
287    pub use error::Error;
288
289    impl Handle<Writable> {
290        /// Persist this tempfile to replace the file at the given `path` if necessary, in a way that recovers the original instance
291        /// on error or returns the open now persisted former tempfile.
292        /// Note that it might not exist anymore if an interrupt handler managed to steal it and allowed the program to return to
293        /// its normal flow.
294        pub fn persist(self, path: impl AsRef<Path>) -> Result<Option<std::fs::File>, Error<Writable>> {
295            let res = REGISTRY.remove(&self.id);
296
297            match res.and_then(|(_k, v)| v.map(|v| v.persist(path))) {
298                Some(Ok(Some(file))) => {
299                    std::mem::forget(self);
300                    Ok(Some(file))
301                }
302                None => {
303                    std::mem::forget(self);
304                    Ok(None)
305                }
306                Some(Err((err, tempfile))) => {
307                    expect_none(REGISTRY.insert(self.id, Some(tempfile)));
308                    Err(Error::<Writable> {
309                        error: err,
310                        handle: self,
311                    })
312                }
313                Some(Ok(None)) => unreachable!("no open files in an open handle"),
314            }
315        }
316    }
317
318    impl Handle<Closed> {
319        /// Persist this tempfile to replace the file at the given `path` if necessary, in a way that recovers the original instance
320        /// on error.
321        pub fn persist(self, path: impl AsRef<Path>) -> Result<(), Error<Closed>> {
322            let res = REGISTRY.remove(&self.id);
323
324            match res.and_then(|(_k, v)| v.map(|v| v.persist(path))) {
325                None | Some(Ok(None)) => {
326                    std::mem::forget(self);
327                    Ok(())
328                }
329                Some(Err((err, tempfile))) => {
330                    expect_none(REGISTRY.insert(self.id, Some(tempfile)));
331                    Err(Error::<Closed> {
332                        error: err,
333                        handle: self,
334                    })
335                }
336                Some(Ok(Some(_file))) => unreachable!("no open files in a closed handle"),
337            }
338        }
339    }
340}
341
342impl ContainingDirectory {
343    fn resolve(self, dir: &Path) -> std::io::Result<&Path> {
344        match self {
345            ContainingDirectory::Exists => Ok(dir),
346            ContainingDirectory::CreateAllRaceProof(retries) => crate::create_dir::all(dir, retries),
347        }
348    }
349}
350
351fn expect_none<T>(v: Option<T>) {
352    assert!(
353        v.is_none(),
354        "there should never be conflicts or old values as ids are never reused."
355    );
356}
357
358impl<T: std::fmt::Debug> Drop for Handle<T> {
359    fn drop(&mut self) {
360        if let Some((_id, Some(tempfile))) = REGISTRY.remove(&self.id) {
361            tempfile.drop_impl();
362        }
363    }
364}