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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ description = "A library for moving files and folders to the Recycle Bin"
keywords = ["remove", "trash", "rubbish", "recycle", "bin"]
repository = "https://github.com/ArturKovacs/trash"
edition = "2021"
rust-version = "1.85.0"
include = ["src/**/*", "LICENSE.txt", "README.md", "CHANGELOG.md", "build.rs"]

[features]
Expand All @@ -23,7 +24,6 @@ log = "0.4"
[dev-dependencies]
serial_test = { version = "2.0.0", default-features = false }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
rand = "0.8.5"
once_cell = "1.18.0"
env_logger = "0.10.0"
tempfile = "3.8.0"
Expand Down
52 changes: 49 additions & 3 deletions src/freedesktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::{
collections::HashSet,
ffi::{OsStr, OsString},
fs::{self, File, OpenOptions},
io::{BufRead, BufReader, Write},
io::{BufRead, BufReader, ErrorKind, Write},
os::unix::{
ffi::{OsStrExt, OsStringExt},
fs::PermissionsExt,
Expand Down Expand Up @@ -551,12 +551,31 @@ fn move_items_no_replace(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result
let dst = dst.as_ref();

try_creating_placeholders(src, dst)?;
std::fs::rename(src, dst).map_err(|e| (src.to_owned(), e))?;

// Once everything is moved, lets recursively remove the directory
// Try to rename first (fastest option for same filesystem)
let Err(e) = std::fs::rename(src, dst) else { return Ok(()) };

let needs_cross_device_copy = e.kind() == ErrorKind::CrossesDevices;
if !needs_cross_device_copy {
return Err((src.to_owned(), e));
}

debug!("Cross-device move detected, falling back to copy+delete for {:?}", src);

// Copy the file/directory
if src.is_dir() {
copy_dir_all(src, dst)?;
} else {
std::fs::copy(src, dst).map_err(|e| (src.to_owned(), e))?;
}

// Remove the source
if src.is_dir() {
std::fs::remove_dir_all(src).map_err(|e| (src.to_owned(), e))?;
} else {
std::fs::remove_file(src).map_err(|e| (src.to_owned(), e))?;
}

Ok(())
}

Expand All @@ -574,6 +593,33 @@ fn try_creating_placeholders(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Re
Ok(())
}

/// Helper function to recursively copy a directory
fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), FsError> {
let src = src.as_ref();
let dst = dst.as_ref();

std::fs::create_dir_all(dst).map_err(|e| (dst.to_owned(), e))?;

for entry in std::fs::read_dir(src).map_err(|e| (src.to_owned(), e))? {
let entry = entry.map_err(|e| (src.to_owned(), e))?;
let file_type = entry.file_type().map_err(|e| (entry.path(), e))?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());

if file_type.is_dir() {
copy_dir_all(&src_path, &dst_path)?;
} else if file_type.is_symlink() {
// Handle symlinks by copying the symlink itself, not the target
let target = std::fs::read_link(&src_path).map_err(|e| (src_path.clone(), e))?;
std::os::unix::fs::symlink(&target, &dst_path).map_err(|e| (dst_path.clone(), e))?;
} else {
std::fs::copy(&src_path, &dst_path).map_err(|e| (src_path.clone(), e))?;
}
}

Ok(())
}

fn decode_uri_path(path: impl AsRef<Path>) -> PathBuf {
// Paths may be invalid Unicode on most Unixes so they should be treated as byte strings
// A higher level crate, such as `url`, can't be used directly since its API intakes valid Rust
Expand Down
6 changes: 3 additions & 3 deletions src/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ impl TrashContext {

fn delete_using_file_mgr<P: AsRef<Path>>(full_paths: &[P]) -> Result<(), Error> {
trace!("Starting delete_using_file_mgr");
let file_mgr = unsafe { NSFileManager::defaultManager() };
let file_mgr = NSFileManager::defaultManager();
for path in full_paths {
let path = path.as_ref().as_os_str().as_encoded_bytes();
let path = match std::str::from_utf8(path) {
Expand All @@ -93,11 +93,11 @@ fn delete_using_file_mgr<P: AsRef<Path>>(full_paths: &[P]) -> Result<(), Error>
};

trace!("Starting fileURLWithPath");
let url = unsafe { NSURL::fileURLWithPath(&path) };
let url = NSURL::fileURLWithPath(&path);
trace!("Finished fileURLWithPath");

trace!("Calling trashItemAtURL");
let res = unsafe { file_mgr.trashItemAtURL_resultingItemURL_error(&url, None) };
let res = file_mgr.trashItemAtURL_resultingItemURL_error(&url, None);
trace!("Finished trashItemAtURL");

if let Err(err) = res {
Expand Down
2 changes: 2 additions & 0 deletions tests/trash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ mod unix {
// use crate::init_logging;

#[test]
#[ignore = "permission denied in more recent macOS versions"]
fn test_delete_symlink() {
init_logging();
trace!("Started test_delete_symlink");
Expand All @@ -107,6 +108,7 @@ mod unix {
}

#[test]
#[ignore = "permission denied in more recent macOS versions"]
fn test_delete_symlink_in_folder() {
init_logging();
trace!("Started test_delete_symlink_in_folder");
Expand Down
Loading