Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/freedesktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -952,8 +952,9 @@ mod tests {

use crate::{
canonicalize_paths, delete, delete_all,
os_limited::{list, purge_all, restore_all},
os_limited::{list, purge_all},
platform::encode_uri_path,
restore_all,
tests::get_unique_name,
Error,
};
Expand Down
166 changes: 86 additions & 80 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,8 @@ pub enum Error {
description: String,
},

/// **freedesktop only**
///
/// Error coming from file system
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
#[cfg(all(unix, not(target_os = "ios"), not(target_os = "android")))]
FileSystem {
path: PathBuf,
source: std::io::Error,
Expand Down Expand Up @@ -268,6 +266,11 @@ pub fn into_unknown<E: std::fmt::Display>(err: E) -> Error {
Error::Unknown { description: format!("{err}") }
}

#[cfg(all(unix, not(target_os = "ios"), not(target_os = "android")))]
pub fn fs_error(path: impl Into<PathBuf>, source: std::io::Error) -> Error {
Error::FileSystem { path: path.into(), source }
}

pub(crate) fn canonicalize_paths<I, T>(paths: I) -> Result<Vec<PathBuf>, Error>
where
I: IntoIterator<Item = T>,
Expand Down Expand Up @@ -386,6 +389,82 @@ pub struct TrashItemMetadata {
pub size: TrashItemSize,
}

#[cfg(all(not(target_os = "ios"), not(target_os = "android")))]
/// Restores all the provided [`TrashItem`] to their original location.
///
/// This function consumes the provided items.
///
/// # Errors
///
/// Errors this function may return include but are not limited to the following.
///
/// It may be the case that when restoring a file or a folder, the `original_path` already has
/// a new item with the same name. When such a collision happens this function returns a
/// [`RestoreCollision`] kind of error.
///
/// If two or more of the provided items have identical `original_path`s then a
/// [`RestoreTwins`] kind of error is returned.
///
/// # Example
///
/// Basic usage:
///
/// ```
/// use std::fs::File;
/// use trash::{delete_with_info, restore_all};
///
/// let filename = "trash-restore_all-example";
/// File::create_new(filename).unwrap();
/// let item = delete_with_info(filename).unwrap();
/// restore_all([item]).unwrap();
/// std::fs::remove_file(filename).unwrap();
/// ```
///
/// Retry restoring when encountering [`RestoreCollision`] error:
///
/// ```no_run
/// use trash::{delete_with_info, restore_all};
/// use trash::Error::RestoreCollision;
///
/// let items = vec![delete_with_info("some-file").unwrap()];
/// if let Err(RestoreCollision { path, mut remaining_items }) = restore_all(items) {
/// // keep all except the one(s) that couldn't be restored
/// remaining_items.retain(|e| e.original_path() != path);
/// restore_all(remaining_items).unwrap();
/// }
/// ```
///
/// [`RestoreCollision`]: Error::RestoreCollision
/// [`RestoreTwins`]: Error::RestoreTwins
pub fn restore_all<I>(items: I) -> Result<(), Error>
where
I: IntoIterator<Item = TrashItem>,
{
use std::collections::HashSet;

// Check for twins here cause that's pretty platform independent.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could race here again, as in: this check can pass then something else uses trash and now the actual restore fails with a (maybe?) confusing error.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I just saw that was already in the crate.... no need to change anything then :)

struct ItemWrapper<'a>(&'a TrashItem);
impl PartialEq for ItemWrapper<'_> {
fn eq(&self, other: &Self) -> bool {
self.0.original_path() == other.0.original_path()
}
}
impl Eq for ItemWrapper<'_> {}
impl Hash for ItemWrapper<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.original_path().hash(state);
}
}
let items = items.into_iter().collect::<Vec<_>>();
let mut item_set = HashSet::with_capacity(items.len());
for item in items.iter() {
if !item_set.insert(ItemWrapper(item)) {
return Err(Error::RestoreTwins { path: item.original_path(), items });
}
}
platform::restore_all(items)
}

#[cfg(any(
target_os = "windows",
all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android"))
Expand All @@ -394,11 +473,10 @@ pub mod os_limited {
//! This module provides functionality which is only supported on Windows and
//! Linux or other Freedesktop Trash compliant environment.

use std::{
borrow::Borrow,
collections::HashSet,
hash::{Hash, Hasher},
};
use std::borrow::Borrow;

#[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))]
use std::collections::HashSet;

use super::{platform, Error, TrashItem, TrashItemMetadata};

Expand Down Expand Up @@ -513,76 +591,4 @@ pub mod os_limited {
{
platform::purge_all(items)
}

/// Restores all the provided [`TrashItem`] to their original location.
///
/// This function consumes the provided items.
///
/// # Errors
///
/// Errors this function may return include but are not limited to the following.
///
/// It may be the case that when restoring a file or a folder, the `original_path` already has
/// a new item with the same name. When such a collision happens this function returns a
/// [`RestoreCollision`] kind of error.
///
/// If two or more of the provided items have identical `original_path`s then a
/// [`RestoreTwins`] kind of error is returned.
///
/// # Example
///
/// Basic usage:
///
/// ```
/// use std::fs::File;
/// use trash::os_limited::{list, restore_all};
///
/// let filename = "trash-restore_all-example";
/// File::create_new(filename).unwrap();
/// restore_all(list().unwrap().into_iter().filter(|x| x.name == filename)).unwrap();
/// std::fs::remove_file(filename).unwrap();
/// ```
///
/// Retry restoring when encountering [`RestoreCollision`] error:
///
/// ```no_run
/// use trash::os_limited::{list, restore_all};
/// use trash::Error::RestoreCollision;
///
/// let items = list().unwrap();
/// if let Err(RestoreCollision { path, mut remaining_items }) = restore_all(items) {
/// // keep all except the one(s) that couldn't be restored
/// remaining_items.retain(|e| e.original_path() != path);
/// restore_all(remaining_items).unwrap();
/// }
/// ```
///
/// [`RestoreCollision`]: Error::RestoreCollision
/// [`RestoreTwins`]: Error::RestoreTwins
pub fn restore_all<I>(items: I) -> Result<(), Error>
where
I: IntoIterator<Item = TrashItem>,
{
// Check for twins here cause that's pretty platform independent.
struct ItemWrapper<'a>(&'a TrashItem);
impl PartialEq for ItemWrapper<'_> {
fn eq(&self, other: &Self) -> bool {
self.0.original_path() == other.0.original_path()
}
}
impl Eq for ItemWrapper<'_> {}
impl Hash for ItemWrapper<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.original_path().hash(state);
}
}
let items = items.into_iter().collect::<Vec<_>>();
let mut item_set = HashSet::with_capacity(items.len());
for item in items.iter() {
if !item_set.insert(ItemWrapper(item)) {
return Err(Error::RestoreTwins { path: item.original_path(), items });
}
}
platform::restore_all(items)
}
}
36 changes: 35 additions & 1 deletion src/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::{
use log::trace;
use objc2_foundation::{NSFileManager, NSString, NSURL};

use crate::{into_unknown, Error, TrashContext, TrashItem};
use crate::{fs_error, into_unknown, Error, TrashContext, TrashItem};

#[derive(Copy, Clone, Debug)]
/// There are 2 ways to trash files: via the ≝Finder app or via the OS NsFileManager call
Expand Down Expand Up @@ -287,5 +287,39 @@ fn esc_quote(s: &str) -> Cow<'_, str> {
}
}

/// Does a basic restore using file renaming, ignoring whether the
/// `DeleteMethod::NSFileManager` or `DeleteMethod::Finder` was used when
/// deleting the file, which means that files deleted with
/// `DeleteMethod::Finder` will not correctly update the `.DS_Store` file that
/// is kept in macOS' trash.
pub fn restore_all<I>(items: I) -> Result<(), Error>
where
I: IntoIterator<Item = TrashItem>,
{
let mut iter = items.into_iter();
while let Some(item) = iter.next() {
let original_path = item.original_path();
let trash_path = Path::new(&item.id);

// Ensure that both the trash item still exists, as well as that the
// there's no collision on the original path before proceeding.
if !std::fs::exists(&item.id).map_err(into_unknown)? {
return Err(Error::Unknown { description: format!("Trash item not found at {:?}", item.id) });
}

if std::fs::exists(&original_path).map_err(|error| fs_error(&original_path, error))? {
return Err(Error::RestoreCollision {
path: original_path,
remaining_items: std::iter::once(item).chain(iter).collect::<Vec<_>>(),
});
}

std::fs::create_dir_all(&item.original_parent).map_err(|error| fs_error(&original_path, error))?;
Copy link
Copy Markdown
Member

@yara-blue yara-blue Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could race between fs::exists and this call. Might be better to just try it and then investigate the returned error.

std::fs::rename(trash_path, &original_path).map_err(|error| fs_error(&original_path, error))?;
}

Ok(())
}

#[cfg(test)]
mod tests;
Loading
Loading