trash/lib.rs
1//! This crate provides functions that allow moving files to the operating system's Recycle Bin or
2//! Trash, or the equivalent.
3//!
4//! Furthermore on Linux and on Windows additional functions are available from the `os_limited`
5//! module.
6//!
7//! ### Potential UB on Linux and FreeBSD
8//!
9//! When querying information about mount points, non-threadsafe versions of `libc::getmnt(info|ent)` are
10//! used which can cause UB if another thread calls into the same function, _probably_ only if the mountpoints
11//! changed as well.
12//!
13//! To neutralize the issue, the respective function in this crate has been made thread-safe with a Mutex.
14//!
15//! **If your crate calls into the aforementioned methods directly or indirectly from other threads,
16//! rather not use this crate.**
17//!
18//! As the handling of UB is clearly a trade-off and certainly goes against the zero-chance-of-UB goal
19//! of the Rust community, please interact with us [in the tracking issue](https://github.com/Byron/trash-rs/issues/42)
20//! to help find a more permanent solution.
21//!
22//! ### Notes on the Linux implementation
23//!
24//! This library implements version 1.0 of the [Freedesktop.org
25//! Trash](https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html) specification and
26//! aims to match the behaviour of Ubuntu 18.04 GNOME in cases of ambiguity. Most -if not all- Linux
27//! distributions that ship with a desktop environment follow this specification. For example
28//! GNOME, KDE, and XFCE all use this convention. This crate blindly assumes that the Linux
29//! distribution it runs on, follows this specification.
30//!
31
32use std::ffi::OsString;
33use std::hash::{Hash, Hasher};
34use std::path::{Path, PathBuf};
35
36use std::fmt;
37use std::{env::current_dir, error};
38
39use log::trace;
40
41#[cfg(test)]
42pub mod tests;
43
44#[cfg(target_os = "windows")]
45#[path = "windows.rs"]
46mod platform;
47
48#[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
49#[path = "freedesktop.rs"]
50mod platform;
51
52#[cfg(target_os = "macos")]
53pub mod macos;
54#[cfg(target_os = "macos")]
55use macos as platform;
56
57pub const DEFAULT_TRASH_CTX: TrashContext = TrashContext::new();
58
59/// A collection of preferences for trash operations.
60#[derive(Clone, Default, Debug)]
61pub struct TrashContext {
62 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
63 platform_specific: platform::PlatformTrashContext,
64}
65impl TrashContext {
66 pub const fn new() -> Self {
67 Self { platform_specific: platform::PlatformTrashContext::new() }
68 }
69
70 /// Removes a single file or directory.
71 ///
72 /// When a symbolic link is provided to this function, the symbolic link will be removed and the link
73 /// target will be kept intact.
74 ///
75 /// # Example
76 ///
77 /// ```
78 /// use std::fs::File;
79 /// use trash::delete;
80 /// File::create_new("delete_me").unwrap();
81 /// trash::delete("delete_me").unwrap();
82 /// assert!(File::open("delete_me").is_err());
83 /// ```
84 pub fn delete<T: AsRef<Path>>(&self, path: T) -> Result<(), Error> {
85 self.delete_all(&[path])
86 }
87
88 /// Removes all files/directories specified by the collection of paths provided as an argument.
89 ///
90 /// When a symbolic link is provided to this function, the symbolic link will be removed and the link
91 /// target will be kept intact.
92 ///
93 /// # Example
94 ///
95 /// ```
96 /// use std::fs::File;
97 /// use trash::delete_all;
98 /// File::create_new("delete_me_1").unwrap();
99 /// File::create_new("delete_me_2").unwrap();
100 /// delete_all(&["delete_me_1", "delete_me_2"]).unwrap();
101 /// assert!(File::open("delete_me_1").is_err());
102 /// assert!(File::open("delete_me_2").is_err());
103 /// ```
104 pub fn delete_all<I, T>(&self, paths: I) -> Result<(), Error>
105 where
106 I: IntoIterator<Item = T>,
107 T: AsRef<Path>,
108 {
109 trace!("Starting canonicalize_paths");
110 let full_paths = canonicalize_paths(paths)?;
111 trace!("Finished canonicalize_paths");
112 self.delete_all_canonicalized(full_paths)
113 }
114}
115
116/// Convenience method for `DEFAULT_TRASH_CTX.delete()`.
117///
118/// See: [`TrashContext::delete`](TrashContext::delete)
119pub fn delete<T: AsRef<Path>>(path: T) -> Result<(), Error> {
120 DEFAULT_TRASH_CTX.delete(path)
121}
122
123/// Convenience method for `DEFAULT_TRASH_CTX.delete_all()`.
124///
125/// See: [`TrashContext::delete_all`](TrashContext::delete_all)
126pub fn delete_all<I, T>(paths: I) -> Result<(), Error>
127where
128 I: IntoIterator<Item = T>,
129 T: AsRef<Path>,
130{
131 DEFAULT_TRASH_CTX.delete_all(paths)
132}
133
134/// Provides information about an error.
135#[derive(Debug)]
136pub enum Error {
137 Unknown {
138 description: String,
139 },
140
141 Os {
142 code: i32,
143 description: String,
144 },
145
146 /// **freedesktop only**
147 ///
148 /// Error coming from file system
149 #[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
150 FileSystem {
151 path: PathBuf,
152 source: std::io::Error,
153 },
154
155 /// One of the target items was a root folder.
156 /// If a list of items are requested to be removed by a single function call (e.g. `delete_all`)
157 /// and this error is returned, then it's guaranteed that none of the items is removed.
158 TargetedRoot,
159
160 /// The `target` does not exist or the process has insufficient permissions to access it.
161 CouldNotAccess {
162 target: String,
163 },
164
165 /// Error while canonicalizing path.
166 CanonicalizePath {
167 /// Path that triggered the error.
168 original: PathBuf,
169 },
170
171 /// Error while converting an [`OsString`] to a [`String`].
172 ///
173 /// This may also happen when converting a [`Path`] or [`PathBuf`] to an [`OsString`].
174 ConvertOsString {
175 /// The string that was attempted to be converted.
176 original: OsString,
177 },
178
179 /// This kind of error happens when a trash item's original parent already contains an item with
180 /// the same name and type (file or folder). In this case an error is produced and the
181 /// restoration of the files is halted meaning that there may be files that could be restored
182 /// but were left in the trash due to the error.
183 ///
184 /// One should not assume any relationship between the order that the items were supplied and
185 /// the list of remaining items. That is to say, it may be that the item that collided was in
186 /// the middle of the provided list but the remaining items' list contains all the provided
187 /// items.
188 ///
189 /// `path`: The path of the file that's blocking the trash item from being restored.
190 ///
191 /// `remaining_items`: All items that were not restored in the order they were provided,
192 /// starting with the item that triggered the error.
193 RestoreCollision {
194 path: PathBuf,
195 remaining_items: Vec<TrashItem>,
196 },
197
198 /// This sort of error is returned when multiple items with the same `original_path` were
199 /// requested to be restored. These items are referred to as twins here. If there are twins
200 /// among the items, then none of the items are restored.
201 ///
202 /// `path`: The `original_path` of the twins.
203 ///
204 /// `items`: The complete list of items that were handed over to the `restore_all` function.
205 RestoreTwins {
206 path: PathBuf,
207 items: Vec<TrashItem>,
208 },
209}
210impl fmt::Display for Error {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 write!(f, "Error during a `trash` operation: {self:?}")
213 }
214}
215impl error::Error for Error {
216 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
217 match self {
218 #[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
219 Self::FileSystem { path: _, source: e } => e.source(),
220 _ => None,
221 }
222 }
223}
224pub fn into_unknown<E: std::fmt::Display>(err: E) -> Error {
225 Error::Unknown { description: format!("{err}") }
226}
227
228pub(crate) fn canonicalize_paths<I, T>(paths: I) -> Result<Vec<PathBuf>, Error>
229where
230 I: IntoIterator<Item = T>,
231 T: AsRef<Path>,
232{
233 let paths = paths.into_iter();
234 paths
235 .map(|x| {
236 let target_ref = x.as_ref();
237 if target_ref.as_os_str().is_empty() {
238 return Err(Error::CanonicalizePath { original: target_ref.to_owned() });
239 }
240 let target = if target_ref.is_relative() {
241 let curr_dir = current_dir()
242 .map_err(|_| Error::CouldNotAccess { target: "[Current working directory]".into() })?;
243 curr_dir.join(target_ref)
244 } else {
245 target_ref.to_owned()
246 };
247 let parent = target.parent().ok_or(Error::TargetedRoot)?;
248 let canonical_parent =
249 parent.canonicalize().map_err(|_| Error::CanonicalizePath { original: parent.to_owned() })?;
250 if let Some(file_name) = target.file_name() {
251 Ok(canonical_parent.join(file_name))
252 } else {
253 // `file_name` is none if the path ends with `..`
254 Ok(canonical_parent)
255 }
256 })
257 .collect::<Result<Vec<_>, _>>()
258}
259
260/// This struct holds information about a single item within the trash.
261///
262/// A trash item can be a file or folder or any other object that the target
263/// operating system allows to put into the trash.
264#[derive(Debug, Clone)]
265pub struct TrashItem {
266 /// A system specific identifier of the item in the trash.
267 ///
268 /// On Windows it is the string returned by `IShellItem::GetDisplayName`
269 /// with the `SIGDN_DESKTOPABSOLUTEPARSING` flag.
270 ///
271 /// On Linux it is an absolute path to the `.trashinfo` file associated with
272 /// the item.
273 pub id: OsString,
274
275 /// The name of the item. For example if the folder '/home/user/New Folder'
276 /// was deleted, its `name` is 'New Folder'
277 pub name: OsString,
278
279 /// The path to the parent folder of this item before it was put inside the
280 /// trash. For example if the folder '/home/user/New Folder' is in the
281 /// trash, its `original_parent` is '/home/user'.
282 ///
283 /// To get the full path to the file in its original location use the
284 /// `original_path` function.
285 pub original_parent: PathBuf,
286
287 /// The number of non-leap seconds elapsed between the UNIX Epoch and the
288 /// moment the file was deleted.
289 /// Without the "chrono" feature, this will be a negative number on linux only.
290 pub time_deleted: i64,
291}
292
293impl TrashItem {
294 /// Joins the `original_parent` and `name` fields to obtain the full path to
295 /// the original file.
296 pub fn original_path(&self) -> PathBuf {
297 self.original_parent.join(&self.name)
298 }
299}
300impl PartialEq for TrashItem {
301 fn eq(&self, other: &Self) -> bool {
302 self.id == other.id
303 }
304}
305impl Eq for TrashItem {}
306impl Hash for TrashItem {
307 fn hash<H: Hasher>(&self, state: &mut H) {
308 self.id.hash(state);
309 }
310}
311
312/// Size of a [`TrashItem`] in bytes or entries
313#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]
314pub enum TrashItemSize {
315 /// Number of bytes in a file
316 Bytes(u64),
317 /// Number of entries in a directory, non-recursive
318 Entries(usize),
319}
320
321impl TrashItemSize {
322 /// The size of a file in bytes, if this item is a file.
323 pub fn size(&self) -> Option<u64> {
324 match self {
325 TrashItemSize::Bytes(s) => Some(*s),
326 TrashItemSize::Entries(_) => None,
327 }
328 }
329
330 /// The amount of entries in the directory, if this is a directory.
331 pub fn entries(&self) -> Option<usize> {
332 match self {
333 TrashItemSize::Bytes(_) => None,
334 TrashItemSize::Entries(e) => Some(*e),
335 }
336 }
337}
338
339/// Metadata about a [`TrashItem`]
340#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]
341pub struct TrashItemMetadata {
342 /// The size of the item, depending on whether or not it is a directory.
343 pub size: TrashItemSize,
344}
345
346#[cfg(any(
347 target_os = "windows",
348 all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android"))
349))]
350pub mod os_limited {
351 //! This module provides functionality which is only supported on Windows and
352 //! Linux or other Freedesktop Trash compliant environment.
353
354 use std::{
355 borrow::Borrow,
356 collections::HashSet,
357 hash::{Hash, Hasher},
358 };
359
360 use super::{platform, Error, TrashItem, TrashItemMetadata};
361
362 /// Returns all [`TrashItem`]s that are currently in the trash.
363 ///
364 /// The items are in no particular order and must be sorted when any kind of ordering is required.
365 ///
366 /// # Example
367 ///
368 /// ```
369 /// use trash::os_limited::list;
370 /// let trash_items = list().unwrap();
371 /// println!("{:#?}", trash_items);
372 /// ```
373 pub fn list() -> Result<Vec<TrashItem>, Error> {
374 platform::list()
375 }
376
377 /// Returns whether the trash is empty or has at least one item.
378 ///
379 /// Unlike calling [`list`], this function short circuits without evaluating every item.
380 ///
381 /// # Example
382 ///
383 /// ```
384 /// use trash::os_limited::is_empty;
385 /// if is_empty().unwrap_or(true) {
386 /// println!("Trash is empty");
387 /// } else {
388 /// println!("Trash contains at least one item");
389 /// }
390 /// ```
391 pub fn is_empty() -> Result<bool, Error> {
392 platform::is_empty()
393 }
394
395 /// Returns all valid trash bins on supported Unix platforms.
396 ///
397 /// Valid trash folders include the user's personal "home trash" as well as designated trash
398 /// bins across mount points. Some, or all of these, may not exist or be invalid in some way.
399 ///
400 /// # Example
401 ///
402 /// ```
403 /// # #[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))] {
404 /// use trash::os_limited::trash_folders;
405 /// let trash_bins = trash_folders()?;
406 /// println!("{trash_bins:#?}");
407 /// # }
408 /// # Ok::<(), trash::Error>(())
409 /// ```
410 #[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
411 pub fn trash_folders() -> Result<HashSet<std::path::PathBuf>, Error> {
412 platform::trash_folders()
413 }
414
415 /// Returns the [`TrashItemMetadata`] for a [`TrashItem`]
416 ///
417 /// # Example
418 ///
419 /// ```
420 /// use trash::os_limited::{list, metadata};
421 /// let trash_items = list().unwrap();
422 /// for item in trash_items {
423 /// println!("{:#?}", metadata(&item).unwrap());
424 /// }
425 /// ```
426 pub fn metadata(item: &TrashItem) -> Result<TrashItemMetadata, Error> {
427 platform::metadata(item)
428 }
429
430 /// Deletes all the provided [`TrashItem`]s permanently.
431 ///
432 /// This function consumes the provided items.
433 ///
434 /// # Example
435 ///
436 /// Taking items' ownership:
437 ///
438 /// ```
439 /// use std::fs::File;
440 /// use trash::{delete, os_limited::{list, purge_all}};
441 ///
442 /// let filename = "trash-purge_all-example-ownership";
443 /// File::create_new(filename).unwrap();
444 /// delete(filename).unwrap();
445 /// // Collect the filtered list just so that we can make sure there's exactly one element.
446 /// // There's no need to `collect` it otherwise.
447 /// let selected: Vec<_> = list().unwrap().into_iter().filter(|x| x.name == filename).collect();
448 /// assert_eq!(selected.len(), 1);
449 /// purge_all(selected).unwrap();
450 /// ```
451 ///
452 /// Taking items' reference:
453 ///
454 /// ```
455 /// use std::fs::File;
456 /// use trash::{delete, os_limited::{list, purge_all}};
457 ///
458 /// let filename = "trash-purge_all-example-reference";
459 /// File::create_new(filename).unwrap();
460 /// delete(filename).unwrap();
461 /// let mut selected = list().unwrap();
462 /// selected.retain(|x| x.name == filename);
463 /// assert_eq!(selected.len(), 1);
464 /// purge_all(&selected).unwrap();
465 /// ```
466 pub fn purge_all<I>(items: I) -> Result<(), Error>
467 where
468 I: IntoIterator,
469 <I as IntoIterator>::Item: Borrow<TrashItem>,
470 {
471 platform::purge_all(items)
472 }
473
474 /// Restores all the provided [`TrashItem`] to their original location.
475 ///
476 /// This function consumes the provided items.
477 ///
478 /// # Errors
479 ///
480 /// Errors this function may return include but are not limited to the following.
481 ///
482 /// It may be the case that when restoring a file or a folder, the `original_path` already has
483 /// a new item with the same name. When such a collision happens this function returns a
484 /// [`RestoreCollision`] kind of error.
485 ///
486 /// If two or more of the provided items have identical `original_path`s then a
487 /// [`RestoreTwins`] kind of error is returned.
488 ///
489 /// # Example
490 ///
491 /// Basic usage:
492 ///
493 /// ```
494 /// use std::fs::File;
495 /// use trash::os_limited::{list, restore_all};
496 ///
497 /// let filename = "trash-restore_all-example";
498 /// File::create_new(filename).unwrap();
499 /// restore_all(list().unwrap().into_iter().filter(|x| x.name == filename)).unwrap();
500 /// std::fs::remove_file(filename).unwrap();
501 /// ```
502 ///
503 /// Retry restoring when encountering [`RestoreCollision`] error:
504 ///
505 /// ```no_run
506 /// use trash::os_limited::{list, restore_all};
507 /// use trash::Error::RestoreCollision;
508 ///
509 /// let items = list().unwrap();
510 /// if let Err(RestoreCollision { path, mut remaining_items }) = restore_all(items) {
511 /// // keep all except the one(s) that couldn't be restored
512 /// remaining_items.retain(|e| e.original_path() != path);
513 /// restore_all(remaining_items).unwrap();
514 /// }
515 /// ```
516 ///
517 /// [`RestoreCollision`]: Error::RestoreCollision
518 /// [`RestoreTwins`]: Error::RestoreTwins
519 pub fn restore_all<I>(items: I) -> Result<(), Error>
520 where
521 I: IntoIterator<Item = TrashItem>,
522 {
523 // Check for twins here cause that's pretty platform independent.
524 struct ItemWrapper<'a>(&'a TrashItem);
525 impl PartialEq for ItemWrapper<'_> {
526 fn eq(&self, other: &Self) -> bool {
527 self.0.original_path() == other.0.original_path()
528 }
529 }
530 impl Eq for ItemWrapper<'_> {}
531 impl Hash for ItemWrapper<'_> {
532 fn hash<H: Hasher>(&self, state: &mut H) {
533 self.0.original_path().hash(state);
534 }
535 }
536 let items = items.into_iter().collect::<Vec<_>>();
537 let mut item_set = HashSet::with_capacity(items.len());
538 for item in items.iter() {
539 if !item_set.insert(ItemWrapper(item)) {
540 return Err(Error::RestoreTwins { path: item.original_path(), items });
541 }
542 }
543 platform::restore_all(items)
544 }
545}