Implement setting permissions, fixes #325

This commit is contained in:
Jeremy Soller 2025-05-16 09:39:53 -06:00
parent 2c8060f93b
commit 051001b9ea
No known key found for this signature in database
GPG key ID: 670FDFB5428E05CA
6 changed files with 186 additions and 81 deletions

7
Cargo.lock generated
View file

@ -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"

View file

@ -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.

View file

@ -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

View file

@ -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));

View file

@ -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);

View file

@ -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<Vec<String>> = 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<HashMap<PathBuf, &'static str>> = 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);
}
}
}
}
}