use super::{Cmd, Feedback}; use crate::{args, pw}; use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command, ValueHint}; use std::{ error::Error, fs::File, io, os::{fd::AsRawFd, unix::prelude::MetadataExt}, path::PathBuf, }; use walkdir::{DirEntry, WalkDir}; mod chgrp; pub use chgrp::Chgrp; #[derive(Debug, Default)] pub struct Chown; impl Cmd for Chown { fn cli(&self) -> clap::Command { Command::new("chown") .about("change file owner and group") .author("Nathan Fisher") .version(env!("CARGO_PKG_VERSION")) .arg( Arg::new("user") .value_name("OWNER[:GROUP]") .value_hint(ValueHint::Username) .num_args(1) .required(true), ) .args(args()) .group( ArgGroup::new("links") .args(["cli-traverse", "full-traverse", "no-traverse"]) .requires("recursive") .multiple(false), ) } fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box> { let Some(matches) = matches else { return Err(Box::new(io::Error::new(io::ErrorKind::Other, "no input"))); }; let recurse = Recurse::from_matches(matches); let feedback = Feedback::from_matches(matches); let (user, group) = if let Some(who) = matches.get_one::("user") { if let Some((u, g)) = who.split_once(':') { let uid = pw::get_uid_for_name(u) .ok_or(io::Error::new(io::ErrorKind::Other, "cannot get uid"))?; let gid = pw::get_gid_for_groupname(g) .ok_or(io::Error::new(io::ErrorKind::Other, "cannot get gid"))?; (User { name: u, uid }, Some(Group { name: g, gid })) } else { let uid = pw::get_uid_for_name(who) .ok_or(io::Error::new(io::ErrorKind::Other, "cannot get uid"))?; (User { name: who, uid }, None) } } else { return Err(Box::new(io::Error::new( io::ErrorKind::Other, "no user specified", ))); }; if let Some(files) = matches.get_many::("file") { for f in files { let action = Action { path: PathBuf::from(f), user: user.clone(), group: group.clone(), feedback, }; if let Some(r) = recurse { if action.path.is_dir() { action.recurse(recurse)?; } else if action.path.is_symlink() { if r.traversal != Traversal::NoLinks { action.recurse(recurse)?; } } } action.apply()?; } } Ok(()) } fn path(&self) -> Option { Some(crate::Path::Bin) } } fn args() -> [Arg; 8] { [ args::changes(), args::verbose(), args::recursive(), Arg::new("cli-traverse") .help("if a command line argument is a symbolic link to a directory, traverse it") .short('H') .action(ArgAction::SetTrue), Arg::new("full-traverse") .help("traverse every symbolic link encountered in a directory") .short('L') .action(ArgAction::SetTrue), Arg::new("no-traverse") .help("do not traverse any symbolic links (default)") .short('P') .action(ArgAction::SetTrue), Arg::new("same-filesystem") .help("do not cross filesystem boundaries (requires recursive)") .short('s') .requires("recursive") .long("same-filesystem") .action(ArgAction::SetTrue), Arg::new("file") .value_name("FILE") .value_hint(ValueHint::AnyPath) .num_args(1..) .required(true), ] } #[derive(Clone, Copy, Debug, PartialEq)] enum Traversal { CliLinks, FullLinks, NoLinks, } impl Traversal { fn from_matches(matches: &ArgMatches) -> Self { if matches.get_flag("full-traverse") { Traversal::FullLinks } else if matches.get_flag("cli-traverse") { Self::CliLinks } else { Self::NoLinks } } } #[derive(Clone, Copy, Debug)] struct Recurse { traversal: Traversal, same_filesystem: bool, } impl Recurse { fn from_matches(matches: &ArgMatches) -> Option { if matches.get_flag("recursive") { Some(Self { traversal: Traversal::from_matches(matches), same_filesystem: matches.get_flag("same-filesystem"), }) } else { None } } } #[derive(Clone, Debug)] struct User<'a> { name: &'a str, uid: u32, } #[derive(Clone, Debug)] struct Group<'a> { name: &'a str, gid: u32, } #[derive(Debug)] struct Action<'a> { path: PathBuf, user: User<'a>, group: Option>, feedback: Option, } impl Action<'_> { fn apply(&self) -> Result<(), Box> { let fd = File::open(&self.path)?; let meta = fd.metadata()?; let uid = meta.uid(); let gid = meta.gid(); unsafe { if libc::fchown( fd.as_raw_fd(), self.user.uid, self.group.as_ref().map(|x| x.gid).unwrap_or(gid), ) != 0 { return Err(io::Error::last_os_error().into()); } } drop(fd); if let Some(feedback) = self.feedback { match feedback { Feedback::Full => { if self.user.uid != uid || self.group.as_ref().map(|x| x.gid) != Some(gid) { self.display_changes(uid, gid)?; } else { self.display_retained(); } } Feedback::Changes => { if self.user.uid != uid || self.group.as_ref().map(|x| x.gid) != Some(gid) { self.display_changes(uid, gid)?; } } } } Ok(()) } fn display_changes(&self, uid: u32, gid: u32) -> Result<(), std::str::Utf8Error> { let username = pw::get_username_for_uid(uid)?; let groupname = pw::get_groupname_for_gid(gid)?; if let Some(g) = &self.group { println!( "{} changed from {username}:{groupname} to {}:{}", &self.path.display(), &self.user.name, &g.name ); } else { println!( "{} changed from {username} to {}", &self.path.display(), &self.user.name ); } Ok(()) } fn display_retained(&self) { if let Some(g) = &self.group { println!( "{} retained as {}:{}", &self.path.display(), &self.user.name, &g.name ); } else { println!("{} retained as {}", &self.path.display(), &self.user.name); } } fn into_child(&self, entry: DirEntry) -> Result> { let path = entry.path().to_path_buf(); Ok(Self { path, user: self.user.clone(), group: self.group.clone(), feedback: self.feedback, }) } fn recurse(&self, recurse: Option) -> Result<(), Box> { let walker = WalkDir::new(&self.path) .max_open(1) .same_file_system(recurse.map_or(false, |x| x.same_filesystem)) .follow_links(recurse.map_or(false, |x| match x.traversal { Traversal::FullLinks => true, _ => false, })); for entry in walker { let entry = entry?; let action = self.into_child(entry)?; action.apply()?; } Ok(()) } }