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}