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
10fn 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
26pub trait Object {
28 fn create(&self) -> Result<(), std::io::Error>;
30
31 fn remove(&self) -> Result<(), std::io::Error>;
33
34 fn path(&self) -> &Path;
36
37 fn name(&self) -> &str;
39
40 fn full(&self) -> PathBuf;
42}
43
44pub trait BuilderCreate {
46 fn new(path: PathBuf, name: String) -> Self;
47}
48
49pub 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
84pub 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
117pub struct Temp {
128 object: Box<dyn Object>,
129 associated: Vec<Temp>,
130
131 #[cfg(feature = "user")]
132 mode: user::Mode,
133}
134impl Temp {
135 pub fn associate(&mut self, temp: Temp) {
138 self.associated.push(temp)
139 }
140
141 pub fn name(&self) -> &str {
143 self.object.name()
144 }
145
146 pub fn path(&self) -> &Path {
148 self.object.path()
149 }
150
151 pub fn full(&self) -> PathBuf {
153 self.object.full()
154 }
155
156 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#[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 pub fn new() -> Self {
236 Self {
237 make: true,
238 ..Default::default()
239 }
240 }
241
242 #[cfg(feature = "user")]
243 pub fn owner(mut self, mode: user::Mode) -> Self {
245 self.mode = Some(mode);
246 self
247 }
248
249 pub fn within(mut self, path: impl Into<PathBuf>) -> Self {
252 self.path = Some(path.into());
253 self
254 }
255
256 pub fn name(mut self, name: impl Into<String>) -> Self {
259 self.name = Some(name.into());
260 self
261 }
262
263 pub fn extension(mut self, extension: impl Into<String>) -> Self {
265 self.extension = Some(extension.into());
266 self
267 }
268
269 pub fn make(mut self, make_object: bool) -> Self {
272 self.make = make_object;
273 self
274 }
275
276 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}