From 292910dd9892518ed7552fb18c3ce9dd548b2e09 Mon Sep 17 00:00:00 2001 From: jx Date: Mon, 17 Feb 2025 01:21:58 -0500 Subject: [PATCH 1/6] added: restore_single TrashItem, restore to arbitrary destination, force restore --- src/freedesktop.rs | 93 ++++++++++++++++++++++++++-------------------- src/lib.rs | 2 +- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/freedesktop.rs b/src/freedesktop.rs index 73d5066..de44864 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -341,56 +341,67 @@ fn restorable_file_in_trash_from_info_file(info_file: impl AsRef(items: I) -> Result<(), Error> +pub fn restore_single(item: &TrashItem, destination: &Path, force: bool) -> Result<(), Error> { + // The "in-trash" filename must be parsed from the trashinfo filename + // which is the filename in the `id` field. + let info_file = &item.id; + + // A bunch of unwraps here. This is fine because if any of these fail that means + // that either there's a bug in this code or the target system didn't follow + // the specification. + let file = restorable_file_in_trash_from_info_file(info_file); + assert!(virtually_exists(&file).map_err(|e| fs_error(&file, e))?); + // Make sure the parent exists so that `create_dir` doesn't faile due to that. + std::fs::create_dir_all(&item.original_parent).map_err(|e| fs_error(&item.original_parent, e))?; + let mut collision = false; + if file.is_dir() { + // NOTE create_dir_all succeeds when the path already exist but create_dir + // fails with `std::io::ErrorKind::AlreadyExists`. + if let Err(e) = std::fs::create_dir(&destination) { + if e.kind() == std::io::ErrorKind::AlreadyExists { + collision = true; + } else { + return Err(fs_error(&destination, e)); + } + } + } else { + // File or symlink + if let Err(e) = OpenOptions::new().create_new(true).write(true).open(&destination) { + if e.kind() == std::io::ErrorKind::AlreadyExists { + collision = true; + } else { + return Err(fs_error(&destination, e)); + } + } + } + if !force && collision { + return Err(Error::RestoreCollision { path: destination.to_path_buf(), remaining_items: vec![] }); + } + std::fs::rename(&file, &destination).map_err(|e| fs_error(&file, e))?; + std::fs::remove_file(info_file).map_err(|e| fs_error(info_file, e))?; + Ok(()) +} + +pub fn restore_all(items: I, force: bool) -> Result<(), Error> where I: IntoIterator, { // Simply read the items' original location from the infofile and attemp to move the items there // and delete the infofile if the move operation was sucessful. - let mut iter = items.into_iter(); while let Some(item) = iter.next() { - // The "in-trash" filename must be parsed from the trashinfo filename - // which is the filename in the `id` field. - let info_file = &item.id; - - // A bunch of unwraps here. This is fine because if any of these fail that means - // that either there's a bug in this code or the target system didn't follow - // the specification. - let file = restorable_file_in_trash_from_info_file(info_file); - assert!(virtually_exists(&file).map_err(|e| fs_error(&file, e))?); - // TODO add option to forcefully replace any target at the restore location - // if it already exists. - let original_path = item.original_path(); - // Make sure the parent exists so that `create_dir` doesn't faile due to that. - std::fs::create_dir_all(&item.original_parent).map_err(|e| fs_error(&item.original_parent, e))?; - let mut collision = false; - if file.is_dir() { - // NOTE create_dir_all succeeds when the path already exist but create_dir - // fails with `std::io::ErrorKind::AlreadyExists`. - if let Err(e) = std::fs::create_dir(&original_path) { - if e.kind() == std::io::ErrorKind::AlreadyExists { - collision = true; - } else { - return Err(fs_error(&original_path, e)); - } - } - } else { - // File or symlink - if let Err(e) = OpenOptions::new().create_new(true).write(true).open(&original_path) { - if e.kind() == std::io::ErrorKind::AlreadyExists { - collision = true; - } else { - return Err(fs_error(&original_path, e)); - } + let destination = item.original_path(); + match restore_single(&item, &destination, force) { + Ok(()) => (), + Err(e) => { + return Err(match e { + Error::RestoreCollision { path, .. } => { + Error::RestoreCollision { path, remaining_items: std::iter::once(item).chain(iter).collect() } + } + other => other, + }) } } - if collision { - let remaining: Vec<_> = std::iter::once(item).chain(iter).collect(); - return Err(Error::RestoreCollision { path: original_path, remaining_items: remaining }); - } - std::fs::rename(&file, &original_path).map_err(|e| fs_error(&file, e))?; - std::fs::remove_file(info_file).map_err(|e| fs_error(info_file, e))?; } Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index adccaea..8a24d51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -540,6 +540,6 @@ pub mod os_limited { return Err(Error::RestoreTwins { path: item.original_path(), items }); } } - platform::restore_all(items) + platform::restore_all(items, false) } } From 0331e20af75b69343d055ec85ae71cc6a2ccef3a Mon Sep 17 00:00:00 2001 From: jx Date: Sun, 16 Mar 2025 10:30:55 -0400 Subject: [PATCH 2/6] removed unnecessary reference --- src/freedesktop.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/freedesktop.rs b/src/freedesktop.rs index de44864..817dc29 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -357,27 +357,27 @@ pub fn restore_single(item: &TrashItem, destination: &Path, force: bool) -> Resu if file.is_dir() { // NOTE create_dir_all succeeds when the path already exist but create_dir // fails with `std::io::ErrorKind::AlreadyExists`. - if let Err(e) = std::fs::create_dir(&destination) { + if let Err(e) = std::fs::create_dir(destination) { if e.kind() == std::io::ErrorKind::AlreadyExists { collision = true; } else { - return Err(fs_error(&destination, e)); + return Err(fs_error(destination, e)); } } } else { // File or symlink - if let Err(e) = OpenOptions::new().create_new(true).write(true).open(&destination) { + if let Err(e) = OpenOptions::new().create_new(true).write(true).open(destination) { if e.kind() == std::io::ErrorKind::AlreadyExists { collision = true; } else { - return Err(fs_error(&destination, e)); + return Err(fs_error(destination, e)); } } } if !force && collision { return Err(Error::RestoreCollision { path: destination.to_path_buf(), remaining_items: vec![] }); } - std::fs::rename(&file, &destination).map_err(|e| fs_error(&file, e))?; + std::fs::rename(&file, destination).map_err(|e| fs_error(&file, e))?; std::fs::remove_file(info_file).map_err(|e| fs_error(info_file, e))?; Ok(()) } From 04eff70d49ad9f3c1a60a770d1d5c579d1b57ab3 Mon Sep 17 00:00:00 2001 From: jx Date: Sat, 22 Mar 2025 16:07:58 -0400 Subject: [PATCH 3/6] use enum instead of boolean for restore operation --- src/freedesktop.rs | 18 ++++++++++++------ src/lib.rs | 10 ++++++++-- src/windows.rs | 5 +++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/freedesktop.rs b/src/freedesktop.rs index 817dc29..6ac59ae 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -21,7 +21,7 @@ use std::{ use log::{debug, warn}; -use crate::{Error, TrashContext, TrashItem, TrashItemMetadata, TrashItemSize}; +use crate::{Error, RestoreMode, TrashContext, TrashItem, TrashItemMetadata, TrashItemSize}; type FsError = (PathBuf, std::io::Error); @@ -341,7 +341,7 @@ fn restorable_file_in_trash_from_info_file(info_file: impl AsRef Result<(), Error> { +pub fn restore_single(item: &TrashItem, destination: &Path, mode: RestoreMode) -> Result<(), Error> { // The "in-trash" filename must be parsed from the trashinfo filename // which is the filename in the `id` field. let info_file = &item.id; @@ -374,15 +374,21 @@ pub fn restore_single(item: &TrashItem, destination: &Path, force: bool) -> Resu } } } - if !force && collision { - return Err(Error::RestoreCollision { path: destination.to_path_buf(), remaining_items: vec![] }); + + if collision { + match mode { + RestoreMode::Force => (), + RestoreMode::Soft => { + return Err(Error::RestoreCollision { path: destination.to_path_buf(), remaining_items: vec![] }); + } + } } std::fs::rename(&file, destination).map_err(|e| fs_error(&file, e))?; std::fs::remove_file(info_file).map_err(|e| fs_error(info_file, e))?; Ok(()) } -pub fn restore_all(items: I, force: bool) -> Result<(), Error> +pub fn restore_all(items: I, mode: RestoreMode) -> Result<(), Error> where I: IntoIterator, { @@ -391,7 +397,7 @@ where let mut iter = items.into_iter(); while let Some(item) = iter.next() { let destination = item.original_path(); - match restore_single(&item, &destination, force) { + match restore_single(&item, &destination, mode) { Ok(()) => (), Err(e) => { return Err(match e { diff --git a/src/lib.rs b/src/lib.rs index 8a24d51..6f9a0bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -343,6 +343,12 @@ pub struct TrashItemMetadata { pub size: TrashItemSize, } +#[derive(Debug, Clone, Copy)] +pub enum RestoreMode { + Force, + Soft, +} + #[cfg(any( target_os = "windows", all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")) @@ -357,7 +363,7 @@ pub mod os_limited { hash::{Hash, Hasher}, }; - use super::{platform, Error, TrashItem, TrashItemMetadata}; + use super::{platform, Error, RestoreMode, TrashItem, TrashItemMetadata}; /// Returns all [`TrashItem`]s that are currently in the trash. /// @@ -540,6 +546,6 @@ pub mod os_limited { return Err(Error::RestoreTwins { path: item.original_path(), items }); } } - platform::restore_all(items, false) + platform::restore_all(items, RestoreMode::Soft) } } diff --git a/src/windows.rs b/src/windows.rs index 8727706..6ae0b50 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -1,4 +1,4 @@ -use crate::{Error, TrashContext, TrashItem, TrashItemMetadata, TrashItemSize}; +use crate::{Error, RestoreMode, TrashContext, TrashItem, TrashItemMetadata, TrashItemSize}; use std::{ borrow::Borrow, ffi::{c_void, OsStr, OsString}, @@ -200,7 +200,8 @@ where } } -pub fn restore_all(items: I) -> Result<(), Error> +// TODO: implment force restore for windows +pub fn restore_all(items: I, _mode: RestoreMode) -> Result<(), Error> where I: IntoIterator, { From 209f5003b9d34d7117502d4b8b54178bdc3218e2 Mon Sep 17 00:00:00 2001 From: jx Date: Sat, 22 Mar 2025 16:33:07 -0400 Subject: [PATCH 4/6] expose RestoreMode to top-level restore_all --- src/freedesktop.rs | 5 +++-- src/lib.rs | 16 +++++++++------- src/tests.rs | 9 +++++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/freedesktop.rs b/src/freedesktop.rs index 6ac59ae..1354af9 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -897,7 +897,7 @@ mod tests { os_limited::{list, purge_all, restore_all}, platform::encode_uri_path, tests::get_unique_name, - Error, + Error, RestoreMode, }; use super::decode_uri_path; @@ -981,7 +981,8 @@ mod tests { delete(symlink).unwrap(); let items = list().unwrap(); let item = items.into_iter().find(|it| it.name == *symlink).unwrap(); - restore_all([item.clone()]).expect("The broken symbolic link should be restored successfully."); + restore_all([item.clone()], RestoreMode::Soft) + .expect("The broken symbolic link should be restored successfully."); // Delete and Purge it without errors delete(symlink).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 6f9a0bf..26552a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -343,9 +343,10 @@ pub struct TrashItemMetadata { pub size: TrashItemSize, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub enum RestoreMode { Force, + #[default] Soft, } @@ -499,10 +500,11 @@ pub mod os_limited { /// ``` /// use std::fs::File; /// use trash::os_limited::{list, restore_all}; + /// use trash::RestoreMode; /// /// let filename = "trash-restore_all-example"; /// File::create_new(filename).unwrap(); - /// restore_all(list().unwrap().into_iter().filter(|x| x.name == filename)).unwrap(); + /// restore_all(list().unwrap().into_iter().filter(|x| x.name == filename), RestoreMode::Soft).unwrap(); /// std::fs::remove_file(filename).unwrap(); /// ``` /// @@ -510,19 +512,19 @@ pub mod os_limited { /// /// ```no_run /// use trash::os_limited::{list, restore_all}; - /// use trash::Error::RestoreCollision; + /// use trash::{RestoreMode, Error::RestoreCollision}; /// /// let items = list().unwrap(); - /// if let Err(RestoreCollision { path, mut remaining_items }) = restore_all(items) { + /// if let Err(RestoreCollision { path, mut remaining_items }) = restore_all(items, RestoreMode::Soft) { /// // keep all except the one(s) that couldn't be restored /// remaining_items.retain(|e| e.original_path() != path); - /// restore_all(remaining_items).unwrap(); + /// restore_all(remaining_items, RestoreMode::Soft).unwrap(); /// } /// ``` /// /// [`RestoreCollision`]: Error::RestoreCollision /// [`RestoreTwins`]: Error::RestoreTwins - pub fn restore_all(items: I) -> Result<(), Error> + pub fn restore_all(items: I, mode: RestoreMode) -> Result<(), Error> where I: IntoIterator, { @@ -546,6 +548,6 @@ pub mod os_limited { return Err(Error::RestoreTwins { path: item.original_path(), items }); } } - platform::restore_all(items, RestoreMode::Soft) + platform::restore_all(items, mode) } } diff --git a/src/tests.rs b/src/tests.rs index 41ad08a..9003732 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -27,6 +27,7 @@ pub use utils::{get_unique_name, init_logging}; ))] mod os_limited { use super::{get_unique_name, init_logging}; + use crate::RestoreMode; use serial_test::serial; use std::collections::{hash_map::Entry, HashMap}; use std::ffi::{OsStr, OsString}; @@ -123,7 +124,7 @@ mod os_limited { #[test] fn restore_empty() { init_logging(); - trash::os_limited::restore_all(vec![]).unwrap(); + trash::os_limited::restore_all(vec![], RestoreMode::Soft).unwrap(); } #[test] @@ -176,7 +177,7 @@ mod os_limited { .filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes())) .collect(); assert_eq!(targets.len(), file_count); - trash::os_limited::restore_all(targets).unwrap(); + trash::os_limited::restore_all(targets, RestoreMode::Soft).unwrap(); let remaining = trash::os_limited::list() .unwrap() .into_iter() @@ -220,7 +221,7 @@ mod os_limited { .collect(); targets.sort_by(|a, b| a.name.cmp(&b.name)); assert_eq!(targets.len(), file_count); - let remaining_count = match trash::os_limited::restore_all(targets) { + let remaining_count = match trash::os_limited::restore_all(targets, RestoreMode::Soft) { Err(trash::Error::RestoreCollision { remaining_items, .. }) => { let contains = |v: &Vec, name: &String| { for curr in v.iter() { @@ -279,7 +280,7 @@ mod os_limited { .collect(); targets.sort_by(|a, b| a.name.cmp(&b.name)); assert_eq!(targets.len(), file_count + 1); // plus one for one of the twins - match trash::os_limited::restore_all(targets) { + match trash::os_limited::restore_all(targets, RestoreMode::Soft) { Err(trash::Error::RestoreTwins { path, items }) => { assert_eq!(path.file_name().unwrap().to_str().unwrap(), twin_name); trash::os_limited::purge_all(items).unwrap(); From 84e492ea2a2d1ee0fb78b7c2e038a2296598044d Mon Sep 17 00:00:00 2001 From: jx Date: Sat, 22 Mar 2025 19:00:05 -0400 Subject: [PATCH 5/6] take ownership over trashitem like restore_all --- src/freedesktop.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/freedesktop.rs b/src/freedesktop.rs index 1354af9..96b2fdc 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -341,7 +341,7 @@ fn restorable_file_in_trash_from_info_file(info_file: impl AsRef Result<(), Error> { +pub fn restore_single(item: TrashItem, destination: &Path, mode: RestoreMode) -> Result<(), Error> { // The "in-trash" filename must be parsed from the trashinfo filename // which is the filename in the `id` field. let info_file = &item.id; @@ -379,7 +379,7 @@ pub fn restore_single(item: &TrashItem, destination: &Path, mode: RestoreMode) - match mode { RestoreMode::Force => (), RestoreMode::Soft => { - return Err(Error::RestoreCollision { path: destination.to_path_buf(), remaining_items: vec![] }); + return Err(Error::RestoreCollision { path: destination.to_path_buf(), remaining_items: vec![item] }); } } } @@ -397,13 +397,14 @@ where let mut iter = items.into_iter(); while let Some(item) = iter.next() { let destination = item.original_path(); - match restore_single(&item, &destination, mode) { + match restore_single(item, &destination, mode) { Ok(()) => (), Err(e) => { return Err(match e { - Error::RestoreCollision { path, .. } => { - Error::RestoreCollision { path, remaining_items: std::iter::once(item).chain(iter).collect() } - } + Error::RestoreCollision { path, remaining_items } => Error::RestoreCollision { + path, + remaining_items: remaining_items.into_iter().chain(iter).collect(), + }, other => other, }) } From d552156b903e2854a73181515ed4bb87d702b99d Mon Sep 17 00:00:00 2001 From: jx Date: Sat, 22 Mar 2025 19:55:09 -0400 Subject: [PATCH 6/6] argument change from Path to impl AsRef for restore_single --- src/freedesktop.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/freedesktop.rs b/src/freedesktop.rs index 96b2fdc..3a44fae 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -341,7 +341,7 @@ fn restorable_file_in_trash_from_info_file(info_file: impl AsRef Result<(), Error> { +pub fn restore_single(item: TrashItem, destination: impl AsRef, mode: RestoreMode) -> Result<(), Error> { // The "in-trash" filename must be parsed from the trashinfo filename // which is the filename in the `id` field. let info_file = &item.id; @@ -357,20 +357,20 @@ pub fn restore_single(item: TrashItem, destination: &Path, mode: RestoreMode) -> if file.is_dir() { // NOTE create_dir_all succeeds when the path already exist but create_dir // fails with `std::io::ErrorKind::AlreadyExists`. - if let Err(e) = std::fs::create_dir(destination) { + if let Err(e) = std::fs::create_dir(destination.as_ref()) { if e.kind() == std::io::ErrorKind::AlreadyExists { collision = true; } else { - return Err(fs_error(destination, e)); + return Err(fs_error(destination.as_ref(), e)); } } } else { // File or symlink - if let Err(e) = OpenOptions::new().create_new(true).write(true).open(destination) { + if let Err(e) = OpenOptions::new().create_new(true).write(true).open(destination.as_ref()) { if e.kind() == std::io::ErrorKind::AlreadyExists { collision = true; } else { - return Err(fs_error(destination, e)); + return Err(fs_error(destination.as_ref(), e)); } } } @@ -379,11 +379,14 @@ pub fn restore_single(item: TrashItem, destination: &Path, mode: RestoreMode) -> match mode { RestoreMode::Force => (), RestoreMode::Soft => { - return Err(Error::RestoreCollision { path: destination.to_path_buf(), remaining_items: vec![item] }); + return Err(Error::RestoreCollision { + path: destination.as_ref().to_path_buf(), + remaining_items: vec![item], + }); } } } - std::fs::rename(&file, destination).map_err(|e| fs_error(&file, e))?; + std::fs::rename(&file, destination.as_ref()).map_err(|e| fs_error(&file, e))?; std::fs::remove_file(info_file).map_err(|e| fs_error(info_file, e))?; Ok(()) }