From 051001b9ea46d54e5387bf16384f7326636cd881 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 16 May 2025 09:39:53 -0600 Subject: [PATCH] Implement setting permissions, fixes #325 --- Cargo.lock | 7 -- Cargo.toml | 1 - i18n/en/cosmic_files.ftl | 2 + src/app.rs | 3 + src/operation/mod.rs | 41 +++++++- src/tab.rs | 213 ++++++++++++++++++++++++++------------- 6 files changed, 186 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33b2e64..28eb018 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1549,7 +1549,6 @@ dependencies = [ "tikv-jemallocator", "tokio", "trash", - "unix_permissions_ext", "url", "uzers", "vergen", @@ -7231,12 +7230,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unix_permissions_ext" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7497808a85e03f612f13e9c5061e4c81cdee86e6c00adfa1096690990ccd08e9" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index fbb9d70..d94b88b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,6 @@ rust-embed = "8" slotmap = "1.0.7" recently-used-xbel = "1.1.0" zip = "2.2.2" -unix_permissions_ext = "0.1.2" uzers = "0.12.1" # Completion-based IO runtime to enable io_uring / IOCP file IO support. diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 78dd002..69d1653 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -227,6 +227,8 @@ extracted = Extracted {$items} {$items -> } from "{$from}" to "{$to}" setting-executable-and-launching = Setting "{$name}" as executable and launching set-executable-and-launched = Set "{$name}" as executable and launched +setting-permissions = Setting permissions for "{$name}" to {$mode} +set-permissions = Set permissions for "{$name}" to {$mode} moving = Moving {$items} {$items -> [one] item *[other] items diff --git a/src/app.rs b/src/app.rs index c8f9372..e00a9ae 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3546,6 +3546,9 @@ impl Application for App { //TODO: this will block for a few ms, run in background? self.mime_app_cache.set_default(mime, id); } + tab::Command::SetPermissions(path, mode) => { + commands.push(self.operation(Operation::SetPermissions { path, mode })); + } tab::Command::WindowDrag => { if let Some(window_id) = &self.window_id_opt { commands.push(window::drag(*window_id)); diff --git a/src/operation/mod.rs b/src/operation/mod.rs index 483a553..6ec3483 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -530,6 +530,11 @@ pub enum Operation { SetExecutableAndLaunch { path: PathBuf, }, + /// Set permissions + SetPermissions { + path: PathBuf, + mode: u32, + }, } #[derive(Clone, Debug)] @@ -629,6 +634,13 @@ impl Operation { Self::SetExecutableAndLaunch { path } => { fl!("setting-executable-and-launching", name = file_name(path)) } + Self::SetPermissions { path, mode } => { + fl!( + "setting-permissions", + name = file_name(path), + mode = format!("{:#03o}", mode) + ) + } } } @@ -686,6 +698,13 @@ impl Operation { Self::SetExecutableAndLaunch { path } => { fl!("set-executable-and-launched", name = file_name(path)) } + Self::SetPermissions { path, mode } => { + fl!( + "set-permissions", + name = file_name(path), + mode = format!("{:#03o}", mode) + ) + } } } @@ -704,7 +723,8 @@ impl Operation { Self::NewFile { .. } | Self::NewFolder { .. } | Self::Rename { .. } - | Self::SetExecutableAndLaunch { .. } => false, + | Self::SetExecutableAndLaunch { .. } + | Self::SetPermissions { .. } => false, } } @@ -1196,6 +1216,25 @@ impl Operation { .map_err(OperationError::from_str)?; Ok(OperationSelection::default()) } + Self::SetPermissions { path, mode } => { + controller.check().await.map_err(OperationError::from_str)?; + + compio::runtime::spawn_blocking(move || -> Result<(), OperationError> { + //TODO: what to do on non-Unix systems? + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let perms = fs::Permissions::from_mode(mode); + fs::set_permissions(&path, perms).map_err(OperationError::from_str)?; + } + + Ok(()) + }) + .await + .map_err(wrap_compio_spawn_error)? + .map_err(OperationError::from_str)?; + Ok(OperationSelection::default()) + } }; controller_clone.set_progress(1.0); diff --git a/src/tab.rs b/src/tab.rs index f74a43a..64ea73b 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -76,7 +76,6 @@ use crate::{ operation::Controller, thumbnailer::thumbnailer, }; -use unix_permissions_ext::UNIXPermissionsExt; use uzers::{get_group_by_gid, get_user_by_uid}; pub const DOUBLE_CLICK_DURATION: Duration = Duration::from_millis(500); @@ -89,6 +88,27 @@ const THUMBNAIL_SIZE: u32 = (ICON_SIZE_GRID as u32) * (ICON_SCALE_MAX as u32); const DRAG_SCROLL_DISTANCE: f32 = 15.0; +static MODE_NAMES: Lazy> = Lazy::new(|| { + vec![ + // Mode 0 + fl!("none"), + // Mode 1 + fl!("execute-only"), + // Mode 2 + fl!("write-only"), + // Mode 3 + fl!("write-execute"), + // Mode 4 + fl!("read-only"), + // Mode 5 + fl!("read-execute"), + // Mode 6 + fl!("read-write"), + // Mode 7 + fl!("read-write-execute"), + ] +}); + static SPECIAL_DIRS: Lazy> = Lazy::new(|| { let mut special_dirs = HashMap::new(); if let Some(dir) = dirs::document_dir() { @@ -348,58 +368,18 @@ fn format_size(size: u64) -> String { format!("{} B", size) } } -enum PermissionOwner { - Owner, - Group, - Other, + +const MODE_SHIFT_USER: u32 = 6; +const MODE_SHIFT_GROUP: u32 = 3; +const MODE_SHIFT_OTHER: u32 = 0; + +fn get_mode_part(mode: u32, shift: u32) -> u32 { + (mode >> shift) & 0o7 } -fn format_permissions_owner(metadata: &Metadata, owner: PermissionOwner) -> String { - match owner { - PermissionOwner::Owner => get_user_by_uid(metadata.uid()) - .and_then(|user| user.name().to_str().map(ToOwned::to_owned)) - .unwrap_or_default(), - PermissionOwner::Group => get_group_by_gid(metadata.gid()) - .and_then(|group| group.name().to_str().map(ToOwned::to_owned)) - .unwrap_or_default(), - PermissionOwner::Other => String::from(""), - } -} - -fn format_permissions(metadata: &Metadata, owner: PermissionOwner) -> String { - let mut mode = 0; - if match owner { - PermissionOwner::Owner => metadata.permissions().readable_by_owner(), - PermissionOwner::Group => metadata.permissions().readable_by_group(), - PermissionOwner::Other => metadata.permissions().readable_by_other(), - } { - mode |= 4; - } - if match owner { - PermissionOwner::Owner => metadata.permissions().writable_by_owner(), - PermissionOwner::Group => metadata.permissions().writable_by_group(), - PermissionOwner::Other => metadata.permissions().writable_by_other(), - } { - mode |= 2; - } - if match owner { - PermissionOwner::Owner => metadata.permissions().executable_by_owner(), - PermissionOwner::Group => metadata.permissions().executable_by_group(), - PermissionOwner::Other => metadata.permissions().executable_by_other(), - } { - mode |= 1; - } - match mode { - 0 => fl!("none"), - 1 => fl!("execute-only"), - 2 => fl!("write-only"), - 3 => fl!("write-execute"), - 4 => fl!("read-only"), - 5 => fl!("read-execute"), - 6 => fl!("read-write"), - 7 => fl!("read-write-execute"), - _ => unreachable!(), - } +fn set_mode_part(mode: u32, shift: u32, bits: u32) -> u32 { + assert!(bits <= 0o7); + (mode & !(0o7 << shift)) | (bits << shift) } fn date_time_formatter(military_time: bool) -> DateTimeFormatter { @@ -1362,6 +1342,7 @@ pub enum Command { OpenTrash, Preview(PreviewKind), SetOpenWith(Mime, String), + SetPermissions(PathBuf, u32), WindowDrag, WindowToggleMaximize, } @@ -1415,6 +1396,7 @@ pub enum Message { SelectFirst, SelectLast, SetOpenWith(Mime, String), + SetPermissions(PathBuf, u32), SetSort(HeadingOptions, bool), TabComplete(PathBuf, Vec<(String, PathBuf)>), Thumbnail(PathBuf, ItemThumbnail), @@ -1842,34 +1824,74 @@ impl Item { ))); } - #[cfg(not(target_os = "windows"))] - { + #[cfg(unix)] + if let Some(path) = self.path_opt() { + use std::os::unix::fs::MetadataExt; + + let mode = metadata.mode(); + + let user_name = get_user_by_uid(metadata.uid()) + .and_then(|user| user.name().to_str().map(ToOwned::to_owned)) + .unwrap_or_default(); + let user_path = path.clone(); settings.push( - widget::settings::item::builder(format_permissions_owner( - metadata, - PermissionOwner::Owner, - )) - .description(fl!("owner")) - .control(widget::text::body(format_permissions( - metadata, - PermissionOwner::Owner, - ))), + widget::settings::item::builder(user_name) + .description(fl!("owner")) + .control(widget::dropdown( + &MODE_NAMES, + Some(get_mode_part(mode, MODE_SHIFT_USER).try_into().unwrap()), + move |selected| { + Message::SetPermissions( + user_path.clone(), + set_mode_part( + mode, + MODE_SHIFT_USER, + selected.try_into().unwrap(), + ), + ) + }, + )), ); + let group_name = get_group_by_gid(metadata.gid()) + .and_then(|group| group.name().to_str().map(ToOwned::to_owned)) + .unwrap_or_default(); + let group_path = path.clone(); settings.push( - widget::settings::item::builder(format_permissions_owner( - metadata, - PermissionOwner::Group, - )) - .description(fl!("group")) - .control(widget::text::body(format_permissions( - metadata, - PermissionOwner::Group, - ))), + widget::settings::item::builder(group_name) + .description(fl!("group")) + .control(widget::dropdown( + &MODE_NAMES, + Some(get_mode_part(mode, MODE_SHIFT_GROUP).try_into().unwrap()), + move |selected| { + Message::SetPermissions( + group_path.clone(), + set_mode_part( + mode, + MODE_SHIFT_GROUP, + selected.try_into().unwrap(), + ), + ) + }, + )), ); + let other_path = path.clone(); settings.push(widget::settings::item::builder(fl!("other")).control( - widget::text::body(format_permissions(metadata, PermissionOwner::Other)), + widget::dropdown( + &MODE_NAMES, + Some(get_mode_part(mode, MODE_SHIFT_OTHER).try_into().unwrap()), + move |selected| { + Message::SetPermissions( + other_path.clone(), + set_mode_part( + mode, + MODE_SHIFT_OTHER, + selected.try_into().unwrap(), + ), + ) + }, + ), )); } } @@ -3428,6 +3450,9 @@ impl Tab { Message::SetOpenWith(mime, id) => { commands.push(Command::SetOpenWith(mime, id)); } + Message::SetPermissions(path, mode) => { + commands.push(Command::SetPermissions(path, mode)); + } Message::SetSort(heading_option, dir) => { if !matches!(self.location, Location::Search(..)) { self.sort_name = heading_option; @@ -5963,4 +5988,48 @@ mod tests { Ok(()) } + + #[test] + fn mode_calculations() { + use super::{ + get_mode_part, set_mode_part, MODE_SHIFT_GROUP, MODE_SHIFT_OTHER, MODE_SHIFT_USER, + }; + for user in 0..=7 { + for group in 0..=7 { + for other in 0..=7 { + let mode = (user << MODE_SHIFT_USER) + | (group << MODE_SHIFT_GROUP) + | (other << MODE_SHIFT_OTHER); + assert_eq!( + format!("{:03o}", mode), + format!("{:o}{:o}{:o}", user, group, other), + ); + assert_eq!(get_mode_part(mode, MODE_SHIFT_USER), user); + assert_eq!(get_mode_part(mode, MODE_SHIFT_GROUP), group); + assert_eq!(get_mode_part(mode, MODE_SHIFT_OTHER), other); + + let mode_no_user = (group << MODE_SHIFT_GROUP) | (other << MODE_SHIFT_OTHER); + assert_eq!( + format!("{:03o}", mode_no_user), + format!("0{:o}{:o}", group, other) + ); + assert_eq!(set_mode_part(mode_no_user, MODE_SHIFT_USER, user), mode); + + let mode_no_group = (user << MODE_SHIFT_USER) | (other << MODE_SHIFT_OTHER); + assert_eq!( + format!("{:03o}", mode_no_group), + format!("{:o}0{:o}", user, other) + ); + assert_eq!(set_mode_part(mode_no_group, MODE_SHIFT_GROUP, group), mode); + + let mode_no_other = (user << MODE_SHIFT_USER) | (group << MODE_SHIFT_GROUP); + assert_eq!( + format!("{:03o}", mode_no_other), + format!("{:o}{:o}0", user, group) + ); + assert_eq!(set_mode_part(mode_no_other, MODE_SHIFT_OTHER, other), mode); + } + } + } + } }