diff --git a/src/freedesktop.rs b/src/freedesktop.rs index 73d5066..3a44fae 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,56 +341,77 @@ fn restorable_file_in_trash_from_info_file(info_file: impl AsRef(items: I) -> 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; + + // 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.as_ref()) { + if e.kind() == std::io::ErrorKind::AlreadyExists { + collision = true; + } else { + 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.as_ref()) { + if e.kind() == std::io::ErrorKind::AlreadyExists { + collision = true; + } else { + return Err(fs_error(destination.as_ref(), e)); + } + } + } + + if collision { + match mode { + RestoreMode::Force => (), + RestoreMode::Soft => { + return Err(Error::RestoreCollision { + path: destination.as_ref().to_path_buf(), + remaining_items: vec![item], + }); + } + } + } + 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(()) +} + +pub fn restore_all(items: I, mode: RestoreMode) -> 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, mode) { + Ok(()) => (), + Err(e) => { + return Err(match e { + Error::RestoreCollision { path, remaining_items } => Error::RestoreCollision { + path, + remaining_items: remaining_items.into_iter().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(()) } @@ -880,7 +901,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; @@ -964,7 +985,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 adccaea..26552a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -343,6 +343,13 @@ pub struct TrashItemMetadata { pub size: TrashItemSize, } +#[derive(Debug, Clone, Copy, Default)] +pub enum RestoreMode { + Force, + #[default] + Soft, +} + #[cfg(any( target_os = "windows", all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")) @@ -357,7 +364,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. /// @@ -493,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(); /// ``` /// @@ -504,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, { @@ -540,6 +548,6 @@ pub mod os_limited { return Err(Error::RestoreTwins { path: item.original_path(), items }); } } - platform::restore_all(items) + 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(); 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, {