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/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..53c79d7 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, TrashItemSize}; 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; + + 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 { + TrashItemSize::Entries(fs::read_dir(&file).map_err(|e| fs_error(&file, e))?.count()) + } else { + TrashItemSize::Bytes(metadata.len()) + }; + Ok(TrashItemMetadata { size }) +} + /// The path points to: /// - existing file | directory | symlink => Ok(true) /// - broken symlink => Ok(true) @@ -242,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. @@ -791,6 +807,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, return an error + Err(Error::Unknown { description: "Mount points cannot be determined on this operating system".into() }) +} + #[cfg(test)] mod tests { use serial_test::serial; diff --git a/src/lib.rs b/src/lib.rs index 1e3d0f5..a039985 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -304,6 +304,40 @@ impl Hash for TrashItem { } } +/// Size of a [`TrashItem`] in bytes or entries +#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum TrashItemSize { + /// Number of bytes in a file + Bytes(u64), + /// Number of entries in a directory, non-recursive + 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, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct TrashItemMetadata { + /// The size of the item, depending on whether or not it is a directory. + pub size: TrashItemSize, +} + #[cfg(any( target_os = "windows", all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")) @@ -318,7 +352,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 +369,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. diff --git a/src/windows.rs b/src/windows.rs index e617647..12c1a82 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -1,4 +1,4 @@ -use crate::{Error, TrashContext, TrashItem}; +use crate::{Error, TrashContext, TrashItem, TrashItemMetadata, TrashItemSize}; 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 @@ -35,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 { @@ -53,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 { @@ -70,7 +76,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 +130,41 @@ pub fn list() -> Result, Error> { } } +pub fn metadata(item: &TrashItem) -> Result { + ensure_com_initialized(); + 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; + 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; + } + } + } + TrashItemSize::Entries(size) + } else { + let item2: IShellItem2 = item.cast()?; + TrashItemSize::Bytes(unsafe { item2.GetUInt64(&PKEY_Size)? }) + }; + Ok(TrashItemMetadata { size }) +} + pub fn purge_all(items: I) -> Result<(), Error> where I: IntoIterator, @@ -136,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)?; @@ -172,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)?; }