Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mac: Return trash items that are created by delete #128

Open
wants to merge 41 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
69e74a5
Return trash items that are created by delete (old FR)
eugenesvk Dec 4, 2024
8e5cb9d
fix mac return signatures
eugenesvk Dec 4, 2024
029e5e9
update doc
eugenesvk Dec 4, 2024
3cd198e
dep: add chrono to macOS
eugenesvk Dec 4, 2024
6cc803b
add support for getting trashed items' data
eugenesvk Dec 4, 2024
ffe57a4
return a single TrashItem for a single trashed item
eugenesvk Dec 4, 2024
9a56e8b
update docs
eugenesvk Dec 4, 2024
db0868b
__TODO why is this needed?
eugenesvk Dec 4, 2024
251e786
mac: split delete functions into 2 categories: those that return tras…
eugenesvk Dec 4, 2024
d20a453
fmt
eugenesvk Dec 5, 2024
a2db980
fix wrong delete arg order
eugenesvk Dec 5, 2024
bcb755e
add stub arguments for Windows/Linux
eugenesvk Dec 5, 2024
34fe204
fmt
eugenesvk Dec 5, 2024
3c393de
upd comment
eugenesvk Dec 5, 2024
17f212b
test: add test_delete_binary_path_with_ns_file_manager_with_info
eugenesvk Dec 6, 2024
e9717d4
dep: add osakit to parse AppleScript's output
eugenesvk Dec 6, 2024
1609ec1
update description
eugenesvk Dec 6, 2024
5576cf7
mac: add support for Finder returned url to trashed items
eugenesvk Dec 6, 2024
5b1c2c6
test: add test_delete_with_finder_with_info
eugenesvk Dec 6, 2024
d11b8e0
test: add test_delete_with_finder_with_info_invalid
eugenesvk Dec 6, 2024
5680417
test: add test_delete_binary_with_finder_with_info
eugenesvk Dec 6, 2024
99ae323
fix 1 item not being returned from AppleScript
eugenesvk Dec 6, 2024
9021350
test: add 1-file delete test to test_delete_with_finder_with_info
eugenesvk Dec 6, 2024
cb7550a
fmt
eugenesvk Dec 6, 2024
7637858
fix variable type detection in AppleScript
eugenesvk Dec 6, 2024
91b0bc6
set an empty return to AS with no info so it can be parsed by osakit
eugenesvk Dec 6, 2024
bc43ab2
test: add test_delete_with_finder
eugenesvk Dec 7, 2024
d2eaf19
add ScriptMethod
eugenesvk Dec 7, 2024
401d5e3
add back cli applesript runs
eugenesvk Dec 7, 2024
b55ddca
test: update
eugenesvk Dec 7, 2024
4e5c6e6
test: add test_delete_with_finder_osakit_with_info
eugenesvk Dec 7, 2024
29c7a62
update doc
eugenesvk Dec 8, 2024
8c69c26
update doc to reflect the need to run Finder scripts on the main thread
eugenesvk Dec 8, 2024
1ec7904
test: move osakit to a separate test with no harness
eugenesvk Dec 9, 2024
674d7be
test: update osakit
eugenesvk Dec 9, 2024
bbb1289
test: run threading tests in a short loop
eugenesvk Dec 9, 2024
2ee8045
dep: add custom test harness supporting forcing main thread
eugenesvk Dec 9, 2024
369c4c8
move tests behind macos cfg compilation
eugenesvk Dec 9, 2024
41acbf1
update comment
eugenesvk Dec 9, 2024
a166605
test: fail unless on main thread
eugenesvk Dec 9, 2024
75a23d2
workflow: split running tests required to be on the main thread into …
eugenesvk Dec 9, 2024
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ objc2-foundation = { version = "0.2.0", features = [
"NSURL",
] }
percent-encoding = "2.3.1"
chrono = { version = "0.4.31", optional = true, default-features = false, features = ["clock",] }
osakit = "0.2.4"

[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))'.dependencies]
chrono = { version = "0.4.31", optional = true, default-features = false, features = [
Expand Down
33 changes: 24 additions & 9 deletions src/freedesktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,17 @@ impl PlatformTrashContext {
}
}
impl TrashContext {
pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec<PathBuf>) -> Result<(), Error> {
pub(crate) fn delete_all_canonicalized(
&self,
full_paths: Vec<PathBuf>,
_with_info: bool,
) -> Result<Option<Vec<TrashItem>>, Error> {
let home_trash = home_trash()?;
let sorted_mount_points = get_sorted_mount_points()?;
let home_topdir = home_topdir(&sorted_mount_points)?;
debug!("The home topdir is {:?}", home_topdir);
let uid = unsafe { libc::getuid() };
let mut items = Vec::with_capacity(full_paths.len());
for path in full_paths {
debug!("Deleting {:?}", path);
let topdir = get_first_topdir_containing_path(&path, &sorted_mount_points);
Expand All @@ -47,18 +52,19 @@ impl TrashContext {
debug!("The topdir was identical to the home topdir, so moving to the home trash.");
// Note that the following function creates the trash folder
// and its required subfolders in case they don't exist.
move_to_trash(path, &home_trash, topdir).map_err(|(p, e)| fs_error(p, e))?;
items.push(move_to_trash(path, &home_trash, topdir).map_err(|(p, e)| fs_error(p, e))?);
} else if topdir.to_str() == Some("/var/home") && home_topdir.to_str() == Some("/") {
debug!("The topdir is '/var/home' but the home_topdir is '/', moving to the home trash anyway.");
move_to_trash(path, &home_trash, topdir).map_err(|(p, e)| fs_error(p, e))?;
items.push(move_to_trash(path, &home_trash, topdir).map_err(|(p, e)| fs_error(p, e))?);
} else {
execute_on_mounted_trash_folders(uid, topdir, true, true, |trash_path| {
move_to_trash(&path, trash_path, topdir)
items.push(move_to_trash(&path, trash_path, topdir)?);
Ok(())
})
.map_err(|(p, e)| fs_error(p, e))?;
}
}
Ok(())
Ok(Some(items))
}
}

Expand Down Expand Up @@ -450,7 +456,7 @@ fn move_to_trash(
src: impl AsRef<Path>,
trash_folder: impl AsRef<Path>,
_topdir: impl AsRef<Path>,
) -> Result<(), FsError> {
) -> Result<TrashItem, FsError> {
let src = src.as_ref();
let trash_folder = trash_folder.as_ref();
let files_folder = trash_folder.join("files");
Expand Down Expand Up @@ -491,6 +497,7 @@ fn move_to_trash(
info_name.push(".trashinfo");
let info_file_path = info_folder.join(&info_name);
let info_result = OpenOptions::new().create_new(true).write(true).open(&info_file_path);
let mut time_deleted = -1;
match info_result {
Err(error) => {
if error.kind() == std::io::ErrorKind::AlreadyExists {
Expand All @@ -510,10 +517,12 @@ fn move_to_trash(
#[cfg(feature = "chrono")]
{
let now = chrono::Local::now();
time_deleted = now.timestamp();
writeln!(file, "DeletionDate={}", now.format("%Y-%m-%dT%H:%M:%S"))
}
#[cfg(not(feature = "chrono"))]
{
time_deleted = -1;
Ok(())
}
})
Expand All @@ -537,12 +546,18 @@ fn move_to_trash(
}
Ok(_) => {
// We did it!
break;
return Ok(TrashItem {
id: info_file_path.into(),
name: filename.into(),
original_parent: src
.parent()
.expect("Absolute path to trashed item should have a parent")
.to_path_buf(),
time_deleted,
});
}
}
}

Ok(())
}

/// An error may mean that a collision was found.
Expand Down
73 changes: 65 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl TrashContext {
/// Removes a single file or directory.
///
/// When a symbolic link is provided to this function, the symbolic link will be removed and the link
/// target will be kept intact.
/// target will be kept intact. Successful results will have always have None trash items.
///
/// # Example
///
Expand All @@ -81,14 +81,26 @@ impl TrashContext {
/// trash::delete("delete_me").unwrap();
/// assert!(File::open("delete_me").is_err());
/// ```
pub fn delete<T: AsRef<Path>>(&self, path: T) -> Result<(), Error> {
pub fn delete<T: AsRef<Path>>(&self, path: T) -> Result<Option<Vec<TrashItem>>, Error> {
self.delete_all(&[path])
}

/// Same as `delete`, but returns `TrashItem` if available.
pub fn delete_with_info<T: AsRef<Path>>(&self, path: T) -> Result<Option<TrashItem>, Error> {
match self.delete_all_with_info(&[path]) {
// Result<Option<Vec<TrashItem>>>
Ok(maybe_items) => match maybe_items {
Some(mut items) => Ok(items.pop()), // no need to check that vec.len=2?
None => Ok(None),
},
Err(e) => Err(e),
}
}

/// Removes all files/directories specified by the collection of paths provided as an argument.
///
/// When a symbolic link is provided to this function, the symbolic link will be removed and the link
/// target will be kept intact.
/// target will be kept intact. Successful results will have always have None trash items.
///
/// # Example
///
Expand All @@ -101,36 +113,66 @@ impl TrashContext {
/// assert!(File::open("delete_me_1").is_err());
/// assert!(File::open("delete_me_2").is_err());
/// ```
pub fn delete_all<I, T>(&self, paths: I) -> Result<(), Error>
pub fn delete_all<I, T>(&self, paths: I) -> Result<Option<Vec<TrashItem>>, Error>
where
I: IntoIterator<Item = T>,
T: AsRef<Path>,
{
trace!("Starting canonicalize_paths");
let full_paths = canonicalize_paths(paths)?;
trace!("Finished canonicalize_paths");
self.delete_all_canonicalized(full_paths, false)
}

/// Same as `delete_all, but returns `TrashItem`s if available.
pub fn delete_all_with_info<I, T>(&self, paths: I) -> Result<Option<Vec<TrashItem>>, Error>
where
I: IntoIterator<Item = T>,
T: AsRef<Path>,
{
trace!("Starting canonicalize_paths");
let full_paths = canonicalize_paths(paths)?;
trace!("Finished canonicalize_paths");
self.delete_all_canonicalized(full_paths)
self.delete_all_canonicalized(full_paths, true)
}
}

/// Convenience method for `DEFAULT_TRASH_CTX.delete()`.
///
/// See: [`TrashContext::delete`](TrashContext::delete)
pub fn delete<T: AsRef<Path>>(path: T) -> Result<(), Error> {
pub fn delete<T: AsRef<Path>>(path: T) -> Result<Option<Vec<TrashItem>>, Error> {
DEFAULT_TRASH_CTX.delete(path)
}

/// Convenience method for `DEFAULT_TRASH_CTX.delete_with_info()`.
///
/// See: [`TrashContext::delete`](TrashContext::delete_with_info)
pub fn delete_with_info<T: AsRef<Path>>(path: T) -> Result<Option<TrashItem>, Error> {
DEFAULT_TRASH_CTX.delete_with_info(path)
}

/// Convenience method for `DEFAULT_TRASH_CTX.delete_all()`.
///
/// See: [`TrashContext::delete_all`](TrashContext::delete_all)
pub fn delete_all<I, T>(paths: I) -> Result<(), Error>
pub fn delete_all<I, T>(paths: I) -> Result<Option<Vec<TrashItem>>, Error>
where
I: IntoIterator<Item = T>,
T: AsRef<Path>,
{
DEFAULT_TRASH_CTX.delete_all(paths)
}

/// Convenience method for `DEFAULT_TRASH_CTX.delete_all_with_info()`.
///
/// See: [`TrashContext::delete_all`](TrashContext::delete_all_with_info)
pub fn delete_all_with_info<I, T>(paths: I) -> Result<Option<Vec<TrashItem>>, Error>
where
I: IntoIterator<Item = T>,
T: AsRef<Path>,
{
DEFAULT_TRASH_CTX.delete_all_with_info(paths)
}

/// Provides information about an error.
#[derive(Debug)]
pub enum Error {
Expand Down Expand Up @@ -270,10 +312,16 @@ pub struct TrashItem {
///
/// On Linux it is an absolute path to the `.trashinfo` file associated with
/// the item.
///
/// On macOS it is the string returned by the `.path()` method on the `NSURL` item
/// returned by the `trashItemAtURL_resultingItemURL_error` call.
pub id: OsString,

/// The name of the item. For example if the folder '/home/user/New Folder'
/// was deleted, its `name` is 'New Folder'
/// macOS: when trashing with DeleteMethod::Finder, files are passed to Finder in a single batch,
/// so if the size of the returned list of trashed paths is different from the list of items we sent
/// to trash, we can't match input to output, so will leave this field "" blank
pub name: OsString,

/// The path to the parent folder of this item before it was put inside the
Expand All @@ -282,11 +330,20 @@ pub struct TrashItem {
///
/// To get the full path to the file in its original location use the
/// `original_path` function.
/// macOS: when trashing with DeleteMethod::Finder, files are passed to Finder in a single batch,
/// so if the size of the returned list of trashed paths is different from the list of items we sent
/// to trash, we can't match input to output, so will leave this field "" blank
pub original_parent: PathBuf,

/// The number of non-leap seconds elapsed between the UNIX Epoch and the
/// moment the file was deleted.
/// Without the "chrono" feature, this will be a negative number on linux only.
/// Without the "chrono" feature, this will be a negative number on linux/macOS only.
/// macOS has the number, but there is no information on how to get it,
/// the usual 'kMDItemDateAdded' attribute doesn't exist for files @ trash
/// apple.stackexchange.com/questions/437475/how-can-i-find-out-when-a-file-had-been-moved-to-trash
/// stackoverflow.com/questions/53341670/access-the-file-date-added-in-terminal
/// macOS: when trashing with DeleteMethod::Finder, files are passed to Finder in a single batch, so the timing will be
/// set to the time before a batch is trashed and is the same for all files in a batch
pub time_deleted: i64,
}

Expand Down
Loading
Loading