Skip to content
Open
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
110 changes: 66 additions & 44 deletions src/freedesktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -341,56 +341,77 @@ fn restorable_file_in_trash_from_info_file(info_file: impl AsRef<std::ffi::OsStr
trash_folder.join("files").join(name_in_trash)
}

pub fn restore_all<I>(items: I) -> Result<(), Error>
pub fn restore_single(item: TrashItem, destination: impl AsRef<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;

// 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<I>(items: I, mode: RestoreMode) -> Result<(), Error>
where
I: IntoIterator<Item = TrashItem>,
{
// 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(())
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
22 changes: 15 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,13 @@ pub struct TrashItemMetadata {
pub size: TrashItemSize,
}

#[derive(Debug, Clone, Copy, Default)]
pub enum RestoreMode {
Copy link
Owner

Choose a reason for hiding this comment

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

This isn't documented at all. Could you add #![deny(missing_docs)] to lib.rs? That way this would light up in CI.

Force,
Copy link
Owner

Choose a reason for hiding this comment

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

The variants could probably be less abstract by spelling out more closely what they do.

#[default]
Soft,
}

#[cfg(any(
target_os = "windows",
all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android"))
Expand All @@ -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.
///
Expand Down Expand Up @@ -493,30 +500,31 @@ 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();
/// ```
///
/// Retry restoring when encountering [`RestoreCollision`] error:
///
/// ```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<I>(items: I) -> Result<(), Error>
pub fn restore_all<I>(items: I, mode: RestoreMode) -> Result<(), Error>
Copy link
Owner

Choose a reason for hiding this comment

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

I don't think this can be a breaking change, after all this is only implemented on one platform.
Further, I was expecting that restore_single() is exposed on a single platform, right now it doesn't seem available at all.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, I totally agree. I went this route because I noticed that the same functions on Windows and Freedesktop are routed to os_limited. Since I’m not super familiar with Windows, I didn’t feel confident implementing restore_single() for it, so I decided to change the argument instead to keep things consistent.

I did consider adding configuration attributes specifically for Freedesktop, but since there wasn’t a precedent for that, I wasn’t quite sure how to approach it.

Some guidance on this would be much appreciated!

Copy link
Owner

Choose a reason for hiding this comment

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

Some guidance on this would be much appreciated!

Undo the public API change and instead expose restore_single() in an os-limited way, just for the platform it's actually implemented in. This is new to this crate and you'd have to add such capability and probably a test as well (for the public API) just to be sure it's actually accessible.

where
I: IntoIterator<Item = TrashItem>,
{
Expand All @@ -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)
}
}
9 changes: 5 additions & 4 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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<trash::TrashItem>, name: &String| {
for curr in v.iter() {
Expand Down Expand Up @@ -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();
Expand Down
5 changes: 3 additions & 2 deletions src/windows.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -200,7 +200,8 @@ where
}
}

pub fn restore_all<I>(items: I) -> Result<(), Error>
// TODO: implment force restore for windows
pub fn restore_all<I>(items: I, _mode: RestoreMode) -> Result<(), Error>
where
I: IntoIterator<Item = TrashItem>,
{
Expand Down
Loading