tempfs/
temp_file.rs

1#[cfg(feature = "rand_gen")]
2use crate::global_consts::{num_retry, rand_fn_len, valid_chars};
3#[cfg(feature = "mmap_support")]
4use memmap2::{Mmap, MmapMut, MmapOptions};
5#[cfg(feature = "rand_gen")]
6use rand::Rng;
7use std::env;
8use std::fmt::{Debug, Formatter};
9use std::fs::{File, OpenOptions};
10use std::io::{self, IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write};
11use std::ops::{Deref, DerefMut};
12#[cfg(unix)]
13use std::os::fd::{AsRawFd, RawFd};
14use std::path::{Path, PathBuf};
15
16use crate::error::{TempError, TempResult};
17
18/// A temporary file that is automatically deleted when dropped unless explicitly closed.
19///
20/// The file is opened with read and write permissions. When the instance is dropped,
21/// the underlying file is removed unless deletion is disarmed (for example, by calling
22/// [`close`](TempFile::close) or [`into_inner`](TempFile::into_inner)).
23pub struct TempFile {
24    /// The full path to the temporary file.
25    pub(crate) path: Option<PathBuf>,
26    /// The underlying file handle.
27    file: Option<File>,
28}
29
30impl TempFile {
31    /// Creates a new temporary file at the specified path.
32    ///
33    /// The file is created with read and write permissions.
34    ///
35    /// # Arguments
36    ///
37    /// * `path` - The path at which to create the file. If a relative path is provided, it is resolved relative to the system temporary directory.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if the file cannot be created.
42    pub fn new<P: AsRef<Path>>(path: P) -> TempResult<TempFile> {
43        let path_ref = path.as_ref();
44        let path_buf = if path_ref.is_absolute() {
45            path_ref.to_owned()
46        } else {
47            env::temp_dir().join(path_ref)
48        };
49        let file = Self::open(&path_buf)?;
50        Ok(Self {
51            path: Some(path_buf),
52            file: Some(file),
53        })
54    }
55
56    /// Creates a new temporary file at the specified path.
57    ///
58    /// The file is created with read and write permissions.
59    ///
60    /// # Arguments
61    ///
62    /// * `path` - The path at which to create the file. If a relative path is provided, it is resolved relative to the current working directory.
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if the file cannot be created.
67    pub fn new_here<P: AsRef<Path>>(path: P) -> TempResult<TempFile> {
68        if path.as_ref().is_relative() {
69            Self::new(env::current_dir()?.join(path))
70        } else {
71            Self::new(path)
72        }
73    }
74
75    /// Converts the `TempFile` into a permanent file.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if the inner file is `None`.
80    pub fn persist(&mut self) -> TempResult<File> {
81        self.path = None;
82        self.file.take().ok_or(TempError::FileIsNone)
83    }
84
85    /// Renames the temporary file and then persists it.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if renaming or persisting the file fails.
90    pub fn persist_name<S: AsRef<str>>(&mut self, name: S) -> TempResult<File> {
91        self.rename(name.as_ref())?;
92        self.persist()
93    }
94
95    /// Renames the temporary file (in the current directory) and persists it.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if renaming or persisting the file fails.
100    pub fn persist_here<S: AsRef<str>>(&mut self, name: S) -> TempResult<File> {
101        self.rename(env::current_dir()?.join(name.as_ref()))?;
102        self.persist()
103    }
104
105    #[cfg(feature = "rand_gen")]
106    /// Creates a new temporary file with a random name in the given directory.
107    ///
108    /// The file name is generated using random ASCII characters.
109    ///
110    /// # Arguments
111    ///
112    /// * `dir` - The directory in which to create the file. If `None`, the system temporary directory is used. If a relative directory is provided, it is resolved relative to the system temporary directory.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if a unique filename cannot be generated or if file creation fails.
117    pub fn new_random<P: AsRef<Path>>(dir: Option<P>) -> TempResult<Self> {
118        let dir_buf = if let Some(d) = dir {
119            let d_ref = d.as_ref();
120            if d_ref.is_absolute() {
121                d_ref.to_path_buf()
122            } else {
123                env::temp_dir().join(d_ref)
124            }
125        } else {
126            env::temp_dir()
127        };
128        let mut rng = rand::rng();
129        for _ in 0..num_retry() {
130            let name: String = (0..rand_fn_len())
131                .map(|_| {
132                    let idx = rng.random_range(0..valid_chars().len());
133                    valid_chars()[idx] as char
134                })
135                .collect();
136            let full_path = dir_buf.join(&name);
137            if !full_path.exists() {
138                let file = Self::open(&full_path)?;
139                return Ok(Self {
140                    path: Some(full_path),
141                    file: Some(file),
142                });
143            }
144        }
145        Err(io::Error::new(
146            io::ErrorKind::AlreadyExists,
147            "Could not generate a unique filename",
148        )
149        .into())
150    }
151
152    #[cfg(feature = "rand_gen")]
153    /// Creates a new temporary file with a random name in the given directory.
154    ///
155    /// The file name is generated using random ASCII characters.
156    ///
157    /// # Arguments
158    ///
159    /// * `dir` - The directory in which to create the file. If `None`, the current working directory is used. If a relative directory is provided, it is resolved relative to the current directory.
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if a unique filename cannot be generated or if file creation fails.
164    pub fn new_random_here<P: AsRef<Path>>(dir: Option<P>) -> TempResult<Self> {
165        if let Some(dir) = dir {
166            if dir.as_ref().is_absolute() {
167                Self::new_random(Some(dir))
168            } else {
169                Self::new_random(Some(env::current_dir()?.join(dir)))
170            }
171        } else {
172            Self::new_random(Some(env::current_dir()?))
173        }
174    }
175
176    /// Helper method to open file.
177    fn open(path: &Path) -> TempResult<File> {
178        OpenOptions::new()
179            .create_new(true)
180            .read(true)
181            .write(true)
182            .open(path)
183            .map_err(Into::into)
184    }
185
186    /// Returns a mutable reference to the file handle.
187    ///
188    /// # Errors
189    ///
190    /// Returns `Err(TempError::FileIsNone)` if the file handle is not available.
191    pub fn file_mut(&mut self) -> TempResult<&mut File> {
192        self.file.as_mut().ok_or(TempError::FileIsNone)
193    }
194
195    /// Returns an immutable reference to the file handle.
196    ///
197    /// # Errors
198    ///
199    /// Returns `Err(TempError::FileIsNone)` if the file handle is not available.
200    pub fn file(&self) -> TempResult<&File> {
201        self.file.as_ref().ok_or(TempError::FileIsNone)
202    }
203
204    /// Returns the path to the temporary file.
205    #[must_use]
206    pub fn path(&self) -> Option<&Path> {
207        self.path.as_deref()
208    }
209
210    /// Renames the temporary file.
211    ///
212    /// # Arguments
213    ///
214    /// * `new_path` - The new path for the file. If relative, its new path will be its old path's parent, followed by this. See [`rename_here`](TempFile::rename_here) for a method which renames relative paths to the current directory.
215    ///
216    /// # Errors
217    ///
218    /// Returns an error if the rename operation fails or the old path has no parent and the new path is relative.
219    pub fn rename<P: AsRef<Path>>(&mut self, new_path: P) -> TempResult<()> {
220        let mut new_path = new_path.as_ref().to_path_buf();
221        let pat = new_path.to_str().unwrap_or("");
222        let mut mod_path = false;
223        if !pat.contains('/') && !pat.contains('\\') {
224            mod_path = true;
225        }
226        if let Some(ref old_path) = self.path {
227            if mod_path {
228                new_path = old_path
229                    .parent()
230                    .ok_or(TempError::IO(io::Error::new(
231                        io::ErrorKind::NotFound,
232                        "Old path parent not found",
233                    )))?
234                    .to_path_buf()
235                    .join(new_path);
236            }
237            std::fs::copy(old_path, new_path.clone())?;
238            std::fs::remove_file(old_path)?;
239            self.path = Some(new_path);
240        }
241        Ok(())
242    }
243
244    /// Renames the temporary file using the current directory.
245    ///
246    /// # Arguments
247    ///
248    /// * `new_path` - The new path for the file. If relative, its new path will be the current directory, followed by this. See [`rename`](TempFile::rename) for a method which renames relative paths to the old directory.
249    ///
250    /// # Errors
251    ///
252    /// Returns an error if the rename operation fails.
253    pub fn rename_here<P: AsRef<Path>>(&mut self, new_path: P) -> TempResult<()> {
254        let mut new_path = new_path.as_ref().to_path_buf();
255        let pat = new_path.to_str().unwrap_or("");
256        let mut mod_path = false;
257        if !pat.contains('/') && !pat.contains('\\') {
258            mod_path = true;
259        }
260        if let Some(ref old_path) = self.path {
261            if mod_path {
262                new_path = env::current_dir()?.join(new_path);
263            }
264            std::fs::copy(old_path, new_path.clone())?;
265            std::fs::remove_file(old_path)?;
266            self.path = Some(new_path);
267        }
268        Ok(())
269    }
270
271    /// Synchronizes the file’s state with the storage device.
272    ///
273    /// This is generally not needed. [`File::sync_all`](File::sync_all) for its purpose.
274    ///
275    /// # Errors
276    ///
277    /// Returns `Err(TempError::FileIsNone)` if the file handle is not available, or if syncing fails.
278    pub fn sync_all(&self) -> TempResult<()> {
279        self.file()?.sync_all().map_err(Into::into)
280    }
281
282    /// Flushes the file and disarms automatic deletion.
283    ///
284    /// After calling this method, the file will not be deleted when the `TempFile` is dropped.
285    ///
286    /// # Errors
287    ///
288    /// Returns an error if flushing fails or if the file handle is not available.
289    pub fn disarm(mut self) -> TempResult<()> {
290        self.file_mut()?.flush().map_err(Into::<TempError>::into)?;
291        self.path = None;
292        Ok(())
293    }
294
295    /// Flushes and closes the file, disarming deletion.
296    ///
297    /// After calling this method, the file will not be deleted when the `TempFile` is dropped.
298    ///
299    /// # Errors
300    ///
301    /// Returns an error if flushing fails or if the file handle is not available.
302    pub fn close(mut self) -> TempResult<()> {
303        self.file_mut()?.flush().map_err(Into::<TempError>::into)?;
304        self.path = None;
305        self.file = None;
306        Ok(())
307    }
308
309    /// Consumes the `TempFile` and returns the inner file handle.
310    ///
311    /// This method disarms automatic deletion.
312    ///
313    /// # Errors
314    ///
315    /// Returns `Err(TempError::FileIsNone)` if the file handle has already been taken.
316    pub fn into_inner(mut self) -> TempResult<File> {
317        self.path = None;
318        self.file.take().ok_or(TempError::FileIsNone)
319    }
320
321    /// Checks if the file is still active.
322    #[must_use]
323    pub fn is_active(&self) -> bool {
324        self.path.is_some()
325    }
326
327    /// Deletes the temporary file immediately.
328    ///
329    /// This method flushes the file, removes it from the filesystem, and disarms automatic deletion.
330    ///
331    /// # Errors
332    ///
333    /// Returns an error if flushing fails, if the file handle is not available, or if file removal fails.
334    pub fn delete(mut self) -> TempResult<()> {
335        self.file_mut()?.flush().map_err(Into::<TempError>::into)?;
336        if let Some(ref path) = self.path {
337            std::fs::remove_file(path)?;
338            self.path = None;
339        }
340        Ok(())
341    }
342
343    /// Retrieves metadata of the file.
344    ///
345    /// # Errors
346    ///
347    /// Returns an error if the metadata cannot be accessed or if the file has been closed.
348    pub fn metadata(&self) -> TempResult<std::fs::Metadata> {
349        if let Some(ref path) = self.path {
350            std::fs::metadata(path).map_err(Into::into)
351        } else {
352            Err(Into::into(io::Error::new(
353                io::ErrorKind::NotFound,
354                "File has been closed",
355            )))
356        }
357    }
358
359    /// A function to convert a normal File and its path into a `TempFile`.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if the Path and File do not point to the same file.
364    #[cfg(unix)]
365    pub fn from_fp<P: AsRef<Path>>(file: File, path: P) -> TempResult<Self> {
366        if !Self::are_same_file(path.as_ref(), &file)? {
367            return Err(TempError::InvalidFileOrPath);
368        }
369        Ok(Self {
370            path: Some(path.as_ref().to_path_buf()),
371            file: Some(file),
372        })
373    }
374
375    /// Helper function to validate that a given &Path and File both point to the same file.
376    #[cfg(unix)]
377    fn are_same_file(path: &Path, file: &File) -> io::Result<bool> {
378        use std::fs::metadata;
379        use std::os::unix::fs::MetadataExt;
380        let path_metadata = metadata(path)?;
381        let file_metadata = file.metadata()?;
382
383        #[cfg(unix)]
384        {
385            // Compare device and inode
386            Ok(path_metadata.dev() == file_metadata.dev() && path_metadata.ino() == file_metadata.ino())
387        }
388    }
389}
390
391#[cfg(feature = "mmap_support")]
392impl TempFile {
393    /// Creates a read-only memory map of the file.
394    ///
395    /// # Safety
396    ///
397    /// This operation is unsafe because it relies on the underlying file not changing unexpectedly.
398    ///
399    /// # Errors
400    ///
401    /// Returns an error if mapping the file fails.
402    pub unsafe fn mmap(&self) -> TempResult<Mmap> {
403        let file = self.file()?;
404        unsafe { MmapOptions::new().map(file).map_err(Into::into) }
405    }
406
407    /// Creates a mutable memory map of the file.
408    ///
409    /// # Safety
410    ///
411    /// This operation is unsafe because it allows mutable access to the file's contents.
412    ///
413    /// # Errors
414    ///
415    /// Returns an error if mapping the file fails.
416    pub unsafe fn mmap_mut(&mut self) -> TempResult<MmapMut> {
417        let file = self.file_mut()?;
418        unsafe {
419            MmapOptions::new()
420                .map_mut(Self::immut(file))
421                .map_err(Into::into)
422        }
423    }
424
425    /// Workaround for memmap2 API quirks... but seriously, why does it work like this?
426    fn immut(file: &mut File) -> &File {
427        Box::leak(Box::new(file))
428    }
429}
430
431impl Write for TempFile {
432    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
433        if let Some(ref mut file) = self.file {
434            file.write(buf)
435        } else {
436            Err(io::Error::new(
437                io::ErrorKind::NotFound,
438                TempError::FileIsNone,
439            ))
440        }
441    }
442    fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
443        if let Some(ref mut file) = self.file {
444            file.write_vectored(bufs)
445        } else {
446            Err(io::Error::new(
447                io::ErrorKind::NotFound,
448                TempError::FileIsNone,
449            ))
450        }
451    }
452    fn flush(&mut self) -> io::Result<()> {
453        if let Some(ref mut file) = self.file {
454            file.flush()
455        } else {
456            Err(io::Error::new(
457                io::ErrorKind::NotFound,
458                TempError::FileIsNone,
459            ))
460        }
461    }
462}
463
464impl Read for TempFile {
465    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
466        if let Some(ref mut file) = self.file {
467            file.read(buf)
468        } else {
469            Err(io::Error::new(
470                io::ErrorKind::NotFound,
471                TempError::FileIsNone,
472            ))
473        }
474    }
475    fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
476        if let Some(ref mut file) = self.file {
477            file.read_vectored(bufs)
478        } else {
479            Err(io::Error::new(
480                io::ErrorKind::NotFound,
481                TempError::FileIsNone,
482            ))
483        }
484    }
485    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
486        if let Some(ref mut file) = self.file {
487            file.read_to_end(buf)
488        } else {
489            Err(io::Error::new(
490                io::ErrorKind::NotFound,
491                TempError::FileIsNone,
492            ))
493        }
494    }
495    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
496        if let Some(ref mut file) = self.file {
497            file.read_to_string(buf)
498        } else {
499            Err(io::Error::new(
500                io::ErrorKind::NotFound,
501                TempError::FileIsNone,
502            ))
503        }
504    }
505}
506
507impl Seek for TempFile {
508    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
509        if let Some(ref mut file) = self.file {
510            file.seek(pos)
511        } else {
512            Err(io::Error::new(
513                io::ErrorKind::NotFound,
514                TempError::FileIsNone,
515            ))
516        }
517    }
518}
519
520impl Debug for TempFile {
521    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
522        f.debug_struct("TempFile")
523            .field("path", &self.path)
524            .field("file", &self.file)
525            .finish()
526    }
527}
528
529impl Drop for TempFile {
530    fn drop(&mut self) {
531        if let Some(ref path) = self.path {
532            let _ = std::fs::remove_file(path);
533        }
534    }
535}
536
537impl AsRef<Path> for TempFile {
538    fn as_ref(&self) -> &Path {
539        // Instead of panicking if the path is None, we return an empty path.
540        self.path.as_deref().unwrap_or_else(|| Path::new(""))
541    }
542}
543
544#[cfg(unix)]
545impl AsRawFd for TempFile {
546    fn as_raw_fd(&self) -> RawFd {
547        // Return -1 if the file is not available.
548        self.file.as_ref().map_or(-1, AsRawFd::as_raw_fd)
549    }
550}
551
552impl Deref for TempFile {
553    type Target = File;
554    fn deref(&self) -> &Self::Target {
555        self.file.as_ref().expect("TempFile file is None")
556    }
557}
558
559impl DerefMut for TempFile {
560    fn deref_mut(&mut self) -> &mut Self::Target {
561        self.file.as_mut().expect("TempFile file is None")
562    }
563}
564
565#[cfg(windows)]
566impl std::os::windows::io::AsRawHandle for TempFile {
567    fn as_raw_handle(&self) -> std::os::windows::io::RawHandle {
568        // Return a null handle if the file is not available.
569        self.file
570            .as_ref()
571            .map(|f| f.as_raw_handle())
572            .unwrap_or(std::ptr::null_mut())
573    }
574}