From 3bea3e2f11d5def136455e7bc2377cb05b80147e Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Sat, 6 Jan 2024 12:38:01 -0700 Subject: [PATCH 1/5] Add metadata function, implement for freedesktop --- examples/metadata.rs | 20 ++++++++++++++++++++ src/freedesktop.rs | 21 +++++++++++++++++++-- src/lib.rs | 26 +++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 examples/metadata.rs diff --git a/examples/metadata.rs b/examples/metadata.rs new file mode 100644 index 0000000..32c99a6 --- /dev/null +++ b/examples/metadata.rs @@ -0,0 +1,20 @@ +#[cfg(not(any( + target_os = "windows", + all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")) +)))] +fn main() { + println!("This is currently only supported on Windows, Linux, and other Freedesktop.org compliant OSes"); +} + +#[cfg(any( + target_os = "windows", + all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")) +))] +fn main() { + let trash_items = trash::os_limited::list().unwrap(); + + for item in trash_items { + let metadata = trash::os_limited::metadata(&item).unwrap(); + println!("{:?}: {:?}", item, metadata); + } +} diff --git a/src/freedesktop.rs b/src/freedesktop.rs index 04c46cf..402e8fa 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -9,7 +9,7 @@ use std::{ borrow::Borrow, collections::HashSet, - fs::{File, OpenOptions}, + fs::{self, File, OpenOptions}, io::{BufRead, BufReader, Write}, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, @@ -17,7 +17,7 @@ use std::{ use log::{debug, warn}; -use crate::{Error, TrashContext, TrashItem}; +use crate::{Error, TrashContext, TrashItem, TrashItemMetadata}; type FsError = (PathBuf, std::io::Error); @@ -218,6 +218,23 @@ pub fn list() -> Result, Error> { Ok(result) } +pub fn metadata(item: &TrashItem) -> Result { + // When purging an item 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))?); + let metadata = fs::symlink_metadata(&file).map_err(|e| fs_error(&file, e))?; + let is_dir = metadata.is_dir(); + let size = + if is_dir { fs::read_dir(&file).map_err(|e| fs_error(&file, e))?.count() as u64 } else { metadata.len() }; + Ok(TrashItemMetadata { is_dir, size }) +} + /// The path points to: /// - existing file | directory | symlink => Ok(true) /// - broken symlink => Ok(true) diff --git a/src/lib.rs b/src/lib.rs index 1e3d0f5..586c05c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -304,6 +304,15 @@ impl Hash for TrashItem { } } +/// Metadata about a [`TrashItem`] +#[derive(Debug, Clone)] +pub struct TrashItemMetadata { + /// True if the [`TrashItem`] is a directory, false if it is a file + pub is_dir: bool, + /// Number of entries for directories, number of bytes for files + pub size: u64, +} + #[cfg(any( target_os = "windows", all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")) @@ -318,7 +327,7 @@ pub mod os_limited { hash::{Hash, Hasher}, }; - use super::{platform, Error, TrashItem}; + use super::{platform, Error, TrashItem, TrashItemMetadata}; /// Returns all [`TrashItem`]s that are currently in the trash. /// @@ -335,6 +344,21 @@ pub mod os_limited { platform::list() } + /// Returns the [`TrashItemMetadata`] for a [`TrashItem`] + /// + /// # Example + /// + /// ``` + /// use trash::os_limited::{list, metadata}; + /// let trash_items = list().unwrap(); + /// for item in trash_items { + /// println!("{:#?}", metadata(&item).unwrap()); + /// } + /// ``` + pub fn metadata(item: &TrashItem) -> Result { + platform::metadata(item) + } + /// Deletes all the provided [`TrashItem`]s permanently. /// /// This function consumes the provided items. From 1a1f75e59b4c18abdf6bc8790a4e54b53dff50df Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Sat, 6 Jan 2024 20:15:26 -0700 Subject: [PATCH 2/5] Windows implementation --- Cargo.toml | 4 +++- src/windows.rs | 46 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 783f46e..7e1598c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,9 @@ once_cell = "1.7.2" [target.'cfg(windows)'.dependencies] windows = { version = "0.44.0", features = [ "Win32_Foundation", + "Win32_Storage_EnhancedStorage", "Win32_System_Com_StructuredStorage", + "Win32_System_SystemServices", "Win32_UI_Shell_PropertiesSystem", ] } scopeguard = "1.2.0" @@ -61,4 +63,4 @@ pre-build = [ "cp /tmp/netbsd/usr/lib/libexecinfo.so /usr/local/x86_64-unknown-netbsd/lib", "rm base.tar.xz", "rm -rf /tmp/netbsd", -] \ No newline at end of file +] diff --git a/src/windows.rs b/src/windows.rs index e617647..dd56749 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -1,4 +1,4 @@ -use crate::{Error, TrashContext, TrashItem}; +use crate::{Error, TrashContext, TrashItem, TrashItemMetadata}; use std::{ borrow::Borrow, ffi::{c_void, OsStr, OsString}, @@ -7,7 +7,10 @@ use std::{ path::PathBuf, }; use windows::core::{Interface, GUID, PCWSTR, PWSTR}; -use windows::Win32::{Foundation::*, System::Com::*, UI::Shell::PropertiesSystem::*, UI::Shell::*}; +use windows::Win32::{ + Foundation::*, Storage::EnhancedStorage::*, System::Com::*, System::SystemServices::*, + UI::Shell::PropertiesSystem::*, UI::Shell::*, +}; /////////////////////////////////////////////////////////////////////////// // These don't have bindings in windows-rs for some reason @@ -70,7 +73,7 @@ impl TrashContext { } } - /// Removes all files and folder paths recursively. + /// Removes all files and folder paths recursively. pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec) -> Result<(), Error> { let mut collection = Vec::new(); traverse_paths_recursively(full_paths, &mut collection)?; @@ -124,6 +127,43 @@ pub fn list() -> Result, Error> { } } +pub fn metadata(item: &TrashItem) -> Result { + ensure_com_initialized(); + unsafe { + let id_as_wide: Vec = item.id.encode_wide().chain(std::iter::once(0)).collect(); + let parsing_name = PCWSTR(id_as_wide.as_ptr()); + let item: IShellItem = SHCreateItemFromParsingName(parsing_name, None)?; + let is_dir = item.GetAttributes(SFGAO_FOLDER)? == SFGAO_FOLDER; + let size = if is_dir { + let pesi: IEnumShellItems = item.BindToHandler(None, &BHID_EnumItems)?; + let mut size = 0; + loop { + let mut fetched_count: u32 = 0; + let mut arr = [None]; + pesi.Next(&mut arr, Some(&mut fetched_count as *mut u32))?; + + if fetched_count == 0 { + break; + } + + match &arr[0] { + Some(_item) => { + size += 1; + } + None => { + break; + } + } + } + size + } else { + let item2: IShellItem2 = item.cast()?; + item2.GetUInt64(&PKEY_Size)? + }; + Ok(TrashItemMetadata { is_dir, size }) + } +} + pub fn purge_all(items: I) -> Result<(), Error> where I: IntoIterator, From fd89ea5d780fa111d12fbe6644dc4153a78565c5 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Sun, 7 Jan 2024 09:10:47 -0700 Subject: [PATCH 3/5] Stub for get_mount_points on unsupported targets --- src/freedesktop.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/freedesktop.rs b/src/freedesktop.rs index 402e8fa..220bf43 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -808,6 +808,18 @@ fn get_mount_points() -> Result, Error> { Ok(result) } +#[cfg(not(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd" +)))] +fn get_mount_points() -> Result, Error> { + // On platforms that don't have support yet, simply return no mount points + Ok(Vec::new()) +} + #[cfg(test)] mod tests { use serial_test::serial; From 63639c3337cc282a1aaa69ef5afd00f8516e3dcd Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 9 Jan 2024 14:53:21 -0700 Subject: [PATCH 4/5] Address review comments --- src/freedesktop.rs | 17 +++++++------ src/lib.rs | 13 ++++++++-- src/windows.rs | 60 ++++++++++++++++++++++------------------------ 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/src/freedesktop.rs b/src/freedesktop.rs index 220bf43..02fdcb9 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -17,7 +17,7 @@ use std::{ use log::{debug, warn}; -use crate::{Error, TrashContext, TrashItem, TrashItemMetadata}; +use crate::{Error, TrashContext, TrashItem, TrashItemMetadata, TrashItemSize}; type FsError = (PathBuf, std::io::Error); @@ -223,15 +223,15 @@ pub fn metadata(item: &TrashItem) -> Result { // 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))?); let metadata = fs::symlink_metadata(&file).map_err(|e| fs_error(&file, e))?; let is_dir = metadata.is_dir(); - let size = - if is_dir { fs::read_dir(&file).map_err(|e| fs_error(&file, e))?.count() as u64 } else { metadata.len() }; + let size = if is_dir { + TrashItemSize::Entries(fs::read_dir(&file).map_err(|e| fs_error(&file, e))?.count()) + } else { + TrashItemSize::Bytes(metadata.len()) + }; Ok(TrashItemMetadata { is_dir, size }) } @@ -259,7 +259,6 @@ where // 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))?); if file.is_dir() { std::fs::remove_dir_all(&file).map_err(|e| fs_error(&file, e))?; // TODO Update directory size cache if there's one. @@ -816,8 +815,8 @@ fn get_mount_points() -> Result, Error> { target_os = "netbsd" )))] fn get_mount_points() -> Result, Error> { - // On platforms that don't have support yet, simply return no mount points - Ok(Vec::new()) + // On platforms that don't have support yet, return an error + Err(Error::Unknown { description: "Mount points cannot be determined on this operating system".into() }) } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 586c05c..2a36df7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -304,13 +304,22 @@ impl Hash for TrashItem { } } +/// Size of a [`TrashItem`] in bytes or entries +#[derive(Debug, Clone)] +pub enum TrashItemSize { + /// Number of bytes in a file + Bytes(u64), + /// Number of entries in a directory, non-recursive + Entries(usize), +} + /// Metadata about a [`TrashItem`] #[derive(Debug, Clone)] pub struct TrashItemMetadata { /// True if the [`TrashItem`] is a directory, false if it is a file pub is_dir: bool, - /// Number of entries for directories, number of bytes for files - pub size: u64, + /// The size of the item, as a [`TrashItemSize`] enum + pub size: TrashItemSize, } #[cfg(any( diff --git a/src/windows.rs b/src/windows.rs index dd56749..cbe1c1c 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -1,4 +1,4 @@ -use crate::{Error, TrashContext, TrashItem, TrashItemMetadata}; +use crate::{Error, TrashContext, TrashItem, TrashItemMetadata, TrashItemSize}; use std::{ borrow::Borrow, ffi::{c_void, OsStr, OsString}, @@ -129,39 +129,37 @@ pub fn list() -> Result, Error> { pub fn metadata(item: &TrashItem) -> Result { ensure_com_initialized(); - unsafe { - let id_as_wide: Vec = item.id.encode_wide().chain(std::iter::once(0)).collect(); - let parsing_name = PCWSTR(id_as_wide.as_ptr()); - let item: IShellItem = SHCreateItemFromParsingName(parsing_name, None)?; - let is_dir = item.GetAttributes(SFGAO_FOLDER)? == SFGAO_FOLDER; - let size = if is_dir { - let pesi: IEnumShellItems = item.BindToHandler(None, &BHID_EnumItems)?; - let mut size = 0; - loop { - let mut fetched_count: u32 = 0; - let mut arr = [None]; - pesi.Next(&mut arr, Some(&mut fetched_count as *mut u32))?; - - if fetched_count == 0 { - break; - } + let id_as_wide: Vec = item.id.encode_wide().chain(std::iter::once(0)).collect(); + let parsing_name = PCWSTR(id_as_wide.as_ptr()); + let item: IShellItem = unsafe { SHCreateItemFromParsingName(parsing_name, None)? }; + let is_dir = unsafe { item.GetAttributes(SFGAO_FOLDER)? } == SFGAO_FOLDER; + let size = if is_dir { + let pesi: IEnumShellItems = unsafe { item.BindToHandler(None, &BHID_EnumItems)? }; + let mut size = 0; + loop { + let mut fetched_count: u32 = 0; + let mut arr = [None]; + unsafe { pesi.Next(&mut arr, Some(&mut fetched_count as *mut u32))? }; + + if fetched_count == 0 { + break; + } - match &arr[0] { - Some(_item) => { - size += 1; - } - None => { - break; - } + match &arr[0] { + Some(_item) => { + size += 1; + } + None => { + break; } } - size - } else { - let item2: IShellItem2 = item.cast()?; - item2.GetUInt64(&PKEY_Size)? - }; - Ok(TrashItemMetadata { is_dir, size }) - } + } + TrashItemSize::Entries(size) + } else { + let item2: IShellItem2 = item.cast()?; + TrashItemSize::Bytes(unsafe { item2.GetUInt64(&PKEY_Size)? }) + }; + Ok(TrashItemMetadata { is_dir, size }) } pub fn purge_all(items: I) -> Result<(), Error> From 8dad3dfc45657962a57a932c40bc37ea1ebe0d7f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 10 Jan 2024 07:10:25 +0100 Subject: [PATCH 5/5] refactor - introduce function to create a wide path - remove redundant `is_dir` field, but provide utilities to figure out if it is a directory or not. --- src/freedesktop.rs | 2 +- src/lib.rs | 26 +++++++++++++++++++++----- src/windows.rs | 21 +++++++++++---------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/freedesktop.rs b/src/freedesktop.rs index 02fdcb9..53c79d7 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -232,7 +232,7 @@ pub fn metadata(item: &TrashItem) -> Result { } else { TrashItemSize::Bytes(metadata.len()) }; - Ok(TrashItemMetadata { is_dir, size }) + Ok(TrashItemMetadata { size }) } /// The path points to: diff --git a/src/lib.rs b/src/lib.rs index 2a36df7..a039985 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -305,7 +305,7 @@ impl Hash for TrashItem { } /// Size of a [`TrashItem`] in bytes or entries -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)] pub enum TrashItemSize { /// Number of bytes in a file Bytes(u64), @@ -313,12 +313,28 @@ pub enum TrashItemSize { Entries(usize), } +impl TrashItemSize { + /// The size of a file in bytes, if this item is a file. + pub fn size(&self) -> Option { + match self { + TrashItemSize::Bytes(s) => Some(*s), + TrashItemSize::Entries(_) => None, + } + } + + /// The amount of entries in the directory, if this is a directory. + pub fn entries(&self) -> Option { + match self { + TrashItemSize::Bytes(_) => None, + TrashItemSize::Entries(e) => Some(*e), + } + } +} + /// Metadata about a [`TrashItem`] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct TrashItemMetadata { - /// True if the [`TrashItem`] is a directory, false if it is a file - pub is_dir: bool, - /// The size of the item, as a [`TrashItemSize`] enum + /// The size of the item, depending on whether or not it is a directory. pub size: TrashItemSize, } diff --git a/src/windows.rs b/src/windows.rs index cbe1c1c..12c1a82 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -38,6 +38,10 @@ impl From for Error { } } +fn to_wide_path(path: impl AsRef) -> Vec { + path.as_ref().encode_wide().chain(std::iter::once(0)).collect() +} + #[derive(Clone, Default, Debug)] pub struct PlatformTrashContext; impl PlatformTrashContext { @@ -56,8 +60,7 @@ impl TrashContext { for full_path in full_paths.iter() { let path_prefix = ['\\' as u16, '\\' as u16, '?' as u16, '\\' as u16]; - let wide_path_container: Vec<_> = - full_path.as_os_str().encode_wide().chain(std::iter::once(0)).collect(); + let wide_path_container = to_wide_path(full_path); let wide_path_slice = if wide_path_container.starts_with(&path_prefix) { &wide_path_container[path_prefix.len()..] } else { @@ -129,7 +132,7 @@ pub fn list() -> Result, Error> { pub fn metadata(item: &TrashItem) -> Result { ensure_com_initialized(); - let id_as_wide: Vec = item.id.encode_wide().chain(std::iter::once(0)).collect(); + let id_as_wide = to_wide_path(&item.id); let parsing_name = PCWSTR(id_as_wide.as_ptr()); let item: IShellItem = unsafe { SHCreateItemFromParsingName(parsing_name, None)? }; let is_dir = unsafe { item.GetAttributes(SFGAO_FOLDER)? } == SFGAO_FOLDER; @@ -159,7 +162,7 @@ pub fn metadata(item: &TrashItem) -> Result { let item2: IShellItem2 = item.cast()?; TrashItemSize::Bytes(unsafe { item2.GetUInt64(&PKEY_Size)? }) }; - Ok(TrashItemMetadata { is_dir, size }) + Ok(TrashItemMetadata { size }) } pub fn purge_all(items: I) -> Result<(), Error> @@ -174,7 +177,7 @@ where let mut at_least_one = false; for item in items { at_least_one = true; - let id_as_wide: Vec = item.borrow().id.encode_wide().chain(std::iter::once(0)).collect(); + let id_as_wide = to_wide_path(&item.borrow().id); let parsing_name = PCWSTR(id_as_wide.as_ptr()); let trash_item: IShellItem = SHCreateItemFromParsingName(parsing_name, None)?; pfo.DeleteItem(&trash_item, None)?; @@ -210,14 +213,12 @@ where let pfo: IFileOperation = CoCreateInstance(&FileOperation as *const _, None, CLSCTX_ALL)?; pfo.SetOperationFlags(FOF_NO_UI | FOFX_EARLYFAILURE)?; for item in items.iter() { - let id_as_wide: Vec = item.id.encode_wide().chain(std::iter::once(0)).collect(); + let id_as_wide = to_wide_path(&item.id); let parsing_name = PCWSTR(id_as_wide.as_ptr()); let trash_item: IShellItem = SHCreateItemFromParsingName(parsing_name, None)?; - let parent_path_wide: Vec<_> = - item.original_parent.as_os_str().encode_wide().chain(std::iter::once(0)).collect(); + let parent_path_wide = to_wide_path(&item.original_parent); let orig_folder_shi: IShellItem = SHCreateItemFromParsingName(PCWSTR(parent_path_wide.as_ptr()), None)?; - let name_wstr: Vec<_> = - AsRef::::as_ref(&item.name).encode_wide().chain(std::iter::once(0)).collect(); + let name_wstr = to_wide_path(&item.name); pfo.MoveItem(&trash_item, &orig_folder_shi, PCWSTR(name_wstr.as_ptr()), None)?; }