temp/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    env::temp_dir,
5    iter::repeat_with,
6    os::unix::fs::symlink,
7    path::{Path, PathBuf},
8};
9
10/// Generate a unique object in the provided directory.
11fn unique(dir: &Path) -> String {
12    let mut rng = fastrand::Rng::new();
13    loop {
14        let mut instance = String::with_capacity(16);
15        repeat_with(|| rng.u8(..))
16            .take(8)
17            .map(|byte| format!("{byte:02x?}"))
18            .for_each(|byte| instance.push_str(&byte));
19
20        if !dir.join(&instance).exists() {
21            break instance;
22        }
23    }
24}
25
26/// An object is something that exists in the filesystem.
27pub trait Object {
28    /// Create the object
29    fn create(&self) -> Result<(), std::io::Error>;
30
31    /// Remove the object
32    fn remove(&self) -> Result<(), std::io::Error>;
33
34    /// Get the parent of the object
35    fn path(&self) -> &Path;
36
37    /// Get the name of the object.
38    fn name(&self) -> &str;
39
40    /// Get the full path of the object, IE path + name
41    fn full(&self) -> PathBuf;
42}
43
44/// A trait for Objects that can be created in the `temp::Builder`
45pub trait BuilderCreate {
46    fn new(path: PathBuf, name: String) -> Self;
47}
48
49/// A temporary file.
50pub struct File {
51    parent: PathBuf,
52    name: String,
53}
54impl Object for File {
55    fn create(&self) -> Result<(), std::io::Error> {
56        if !self.parent.exists() {
57            std::fs::create_dir_all(&self.parent)?;
58        }
59        std::fs::File::create_new(self.parent.join(&self.name)).map(|_| ())
60    }
61    fn remove(&self) -> Result<(), std::io::Error> {
62        std::fs::remove_file(self.full()).map(|_| ())?;
63        Ok(())
64    }
65
66    fn path(&self) -> &Path {
67        &self.parent
68    }
69
70    fn name(&self) -> &str {
71        &self.name
72    }
73
74    fn full(&self) -> PathBuf {
75        self.parent.join(&self.name)
76    }
77}
78impl BuilderCreate for File {
79    fn new(path: PathBuf, name: String) -> Self {
80        Self { parent: path, name }
81    }
82}
83
84/// A temporary directory
85pub struct Directory {
86    path: PathBuf,
87    name: String,
88}
89impl Object for Directory {
90    fn create(&self) -> Result<(), std::io::Error> {
91        std::fs::create_dir_all(self.path.join(&self.name)).map(|_| ())
92    }
93
94    fn remove(&self) -> Result<(), std::io::Error> {
95        std::fs::remove_dir_all(self.full()).map(|_| ())?;
96        Ok(())
97    }
98
99    fn path(&self) -> &Path {
100        &self.path
101    }
102
103    fn name(&self) -> &str {
104        &self.name
105    }
106
107    fn full(&self) -> PathBuf {
108        self.path.join(&self.name)
109    }
110}
111impl BuilderCreate for Directory {
112    fn new(path: PathBuf, name: String) -> Self {
113        Self { path, name }
114    }
115}
116
117/// An instance of a Temporary Object. The Object will be deleted
118/// when the Object is dropped.
119///
120/// Additional Temporary Objects can be associated to an instance,
121/// such that they will be tied to the main Object's lifetime and
122/// dropped together.
123///
124/// There is no distinction between Files/Directories/other objects
125/// in a Temp. The distinction is only made within the Builder, and
126/// it is up to the program to know what kind of object is stored.
127pub struct Temp {
128    object: Box<dyn Object>,
129    associated: Vec<Temp>,
130
131    #[cfg(feature = "user")]
132    mode: user::Mode,
133}
134impl Temp {
135    /// Associate another Temporary Object to the caller. It will
136    /// be tied to the caller's lifetime, and dropped with it.
137    pub fn associate(&mut self, temp: Temp) {
138        self.associated.push(temp)
139    }
140
141    /// Get the name of the temporary object.
142    pub fn name(&self) -> &str {
143        self.object.name()
144    }
145
146    /// Get the path the Temporary Object is located within.
147    pub fn path(&self) -> &Path {
148        self.object.path()
149    }
150
151    /// The full path to the object, including its name.
152    pub fn full(&self) -> PathBuf {
153        self.object.full()
154    }
155
156    /// Make a Symlink from the Temporary Object. This link will be associated
157    /// with the caller, such that the Link will be deleted with the Object.
158    pub fn link(
159        &mut self,
160        link: impl Into<PathBuf>,
161
162        #[cfg(feature = "user")] mode: user::Mode,
163    ) -> Result<(), std::io::Error> {
164        let link = link.into();
165        if let Some(parent) = link.parent()
166            && let Some(name) = link.file_name()
167        {
168            #[cfg(feature = "user")]
169            user::run_as!(mode, { symlink(self.object.full(), &link) })??;
170
171            #[cfg(not(feature = "user"))]
172            symlink(self.object.full(), &link)?;
173
174            self.associated.push(Temp {
175                object: Box::new(File {
176                    parent: parent.to_path_buf(),
177                    name: name.to_string_lossy().into_owned(),
178                }),
179                associated: Vec::new(),
180
181                #[cfg(feature = "user")]
182                mode,
183            });
184            Ok(())
185        } else {
186            Err(std::io::ErrorKind::NotFound.into())
187        }
188    }
189}
190impl Drop for Temp {
191    #[cfg(feature = "user")]
192    fn drop(&mut self) {
193        let mode = self.mode;
194        if let Err(e) = user::run_as!(mode, { self.object.remove() }) {
195            log::error!("Failed to remove temporary file: {e}");
196        }
197    }
198
199    #[cfg(not(feature = "user"))]
200    fn drop(&mut self) {
201        log::trace!("Dropping temporary file: {}", self.full().display());
202        if let Err(e) = self.object.remove() {
203            log::error!("Failed to remove temporary file: {e}");
204        }
205    }
206}
207unsafe impl Send for Temp {}
208unsafe impl Sync for Temp {}
209
210/// Build a new Temporary Object.
211///
212/// ## Example
213///
214/// ```rust
215/// use std::io::Write;
216/// let temp = temp::Builder::new().create::<temp::File>().unwrap();
217/// let path = temp.full();
218/// let mut file = std::fs::File::open(&path).unwrap();
219/// write!(file, "Hello!");
220/// drop(temp);
221/// assert!(!path.exists());
222/// ```
223#[derive(Default)]
224pub struct Builder {
225    name: Option<String>,
226    path: Option<PathBuf>,
227    extension: Option<String>,
228
229    #[cfg(feature = "user")]
230    mode: Option<user::Mode>,
231    make: bool,
232}
233impl Builder {
234    /// Create a new Builder.
235    pub fn new() -> Self {
236        Self {
237            make: true,
238            ..Default::default()
239        }
240    }
241
242    #[cfg(feature = "user")]
243    /// Set the owner of the Temporary Object.
244    pub fn owner(mut self, mode: user::Mode) -> Self {
245        self.mode = Some(mode);
246        self
247    }
248
249    /// The directory the Temporary Object should reside in.
250    /// If not set, defaults to /tmp.
251    pub fn within(mut self, path: impl Into<PathBuf>) -> Self {
252        self.path = Some(path.into());
253        self
254    }
255
256    /// The name of the Temporary Object. If not set, uses a
257    /// randomized, unique string.
258    pub fn name(mut self, name: impl Into<String>) -> Self {
259        self.name = Some(name.into());
260        self
261    }
262
263    /// Set an optional extension to the Object.
264    pub fn extension(mut self, extension: impl Into<String>) -> Self {
265        self.extension = Some(extension.into());
266        self
267    }
268
269    /// Whether to create the Object on create(). By default,
270    /// the object is created.
271    pub fn make(mut self, make_object: bool) -> Self {
272        self.make = make_object;
273        self
274    }
275
276    /// Create the Object, consuming the Builder. The Object is passed as a Template to this
277    /// function.
278    ///
279    /// ## Examples
280    ///
281    /// Create a new temporary file as /tmp/new_file
282    ///
283    /// ```rust
284    /// let file = temp::Builder::new().name("new_file").create::<temp::File>().unwrap();
285    /// assert!(std::path::Path::new("/tmp/new_file").exists());
286    /// ```
287    ///
288    /// Create a new temporary directory in the current directory
289    ///
290    /// ```rust
291    /// temp::Builder::new().within("").create::<temp::Directory>().unwrap();
292    /// ```
293    pub fn create<T: BuilderCreate + Object + 'static>(self) -> Result<Temp, std::io::Error> {
294        let parent = self.path.unwrap_or(temp_dir());
295        let mut name = self.name.unwrap_or(unique(&parent));
296
297        #[cfg(feature = "user")]
298        let mode = self.mode.unwrap_or(user::current()?);
299
300        if let Some(extension) = &self.extension {
301            name.push_str(&format!(".{extension}"));
302        }
303
304        let object = T::new(parent, name);
305        if self.make {
306            #[cfg(feature = "user")]
307            user::run_as!(mode, object.create())??;
308
309            #[cfg(not(feature = "user"))]
310            object.create()?;
311        }
312        Ok(Temp {
313            object: Box::new(object),
314            associated: Vec::new(),
315
316            #[cfg(feature = "user")]
317            mode,
318        })
319    }
320}