Skip to content

Commit

Permalink
feat: provide os_limited::metadata().
Browse files Browse the repository at this point in the history
Metadata is currently limited to the amount of things, like bytes or entries,
in the metadata item, but there is potential for adding more later.
  • Loading branch information
Byron committed Jan 10, 2024
2 parents 916d769 + 8dad3df commit aa8e504
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 16 deletions.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
]
]
20 changes: 20 additions & 0 deletions examples/metadata.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
34 changes: 31 additions & 3 deletions src/freedesktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
use std::{
borrow::Borrow,
collections::HashSet,
fs::{File, OpenOptions},
fs::{self, File, OpenOptions},
io::{BufRead, BufReader, Write},
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
};

use log::{debug, warn};

use crate::{Error, TrashContext, TrashItem};
use crate::{Error, TrashContext, TrashItem, TrashItemMetadata, TrashItemSize};

type FsError = (PathBuf, std::io::Error);

Expand Down Expand Up @@ -218,6 +218,23 @@ pub fn list() -> Result<Vec<TrashItem>, Error> {
Ok(result)
}

pub fn metadata(item: &TrashItem) -> Result<TrashItemMetadata, Error> {
// 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)
Expand All @@ -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.
Expand Down Expand Up @@ -791,6 +807,18 @@ fn get_mount_points() -> Result<Vec<MountPoint>, 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<Vec<MountPoint>, 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;
Expand Down
51 changes: 50 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64> {
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<usize> {
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"))
Expand All @@ -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.
///
Expand All @@ -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<TrashItemMetadata, Error> {
platform::metadata(item)
}

/// Deletes all the provided [`TrashItem`]s permanently.
///
/// This function consumes the provided items.
Expand Down
61 changes: 50 additions & 11 deletions src/windows.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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
Expand Down Expand Up @@ -35,6 +38,10 @@ impl From<windows::core::Error> for Error {
}
}

fn to_wide_path(path: impl AsRef<OsStr>) -> Vec<u16> {
path.as_ref().encode_wide().chain(std::iter::once(0)).collect()
}

#[derive(Clone, Default, Debug)]
pub struct PlatformTrashContext;
impl PlatformTrashContext {
Expand All @@ -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 {
Expand All @@ -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<PathBuf>) -> Result<(), Error> {
let mut collection = Vec::new();
traverse_paths_recursively(full_paths, &mut collection)?;
Expand Down Expand Up @@ -124,6 +130,41 @@ pub fn list() -> Result<Vec<TrashItem>, Error> {
}
}

pub fn metadata(item: &TrashItem) -> Result<TrashItemMetadata, Error> {
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<I>(items: I) -> Result<(), Error>
where
I: IntoIterator,
Expand All @@ -136,7 +177,7 @@ where
let mut at_least_one = false;
for item in items {
at_least_one = true;
let id_as_wide: Vec<u16> = 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)?;
Expand Down Expand Up @@ -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<u16> = 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::<OsStr>::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)?;
}
Expand Down

0 comments on commit aa8e504

Please sign in to comment.