use super::Cmd; use crate::{fs::FileType, pw}; use clap::{Arg, ArgAction, ArgGroup, Command, ValueHint}; use std::{ error::Error, fs::{self, File}, io, os::{fd::AsRawFd, unix::prelude::MetadataExt}, path::PathBuf, }; use walkdir::{DirEntry, WalkDir}; #[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")) .args([ Arg::new("user") .value_name("OWNER[:GROUP]") .value_hint(ValueHint::Username) .num_args(1) .required(true), Arg::new("changes") .help("report only when a change is made") .short('c') .long("changes") .conflicts_with("verbose") .action(ArgAction::SetTrue), Arg::new("verbose") .help("output a diagnostic for every file processed") .short('v') .long("verbose") .action(ArgAction::SetTrue), Arg::new("recursive") .help("operate on files and directories recursively") .short('R') .long("recursive") .requires("links") .action(ArgAction::SetTrue), 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), ]) .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 = if matches.get_flag("recursive") { if matches.get_flag("full-traverse") { Some(Recurse { traversal: Traversal::FullLinks, same_filesystem: matches.get_flag("same-filesystem"), }) } else if matches.get_flag("cli-traverse") { Some(Recurse { traversal: Traversal::CliLinks, same_filesystem: matches.get_flag("same-filesystem"), }) } else { Some(Recurse { traversal: Traversal::NoLinks, same_filesystem: matches.get_flag("same-filesystem"), }) } } else { None }; let feedback = if matches.get_flag("verbose") { Some(Feedback::Full) } else if matches.get_flag("changes") { Some(Feedback::Changes) } else { None }; 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.to_string(), uid, }, Some(Group { name: g.to_string(), gid, }), ) } else { let uid = pw::get_uid_for_name(who) .ok_or(io::Error::new(io::ErrorKind::Other, "cannot get uid"))?; ( User { name: who.to_string(), 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(), recurse, feedback, }; action.apply()?; } } Ok(()) } fn path(&self) -> Option { Some(crate::Path::Bin) } } #[derive(Clone, Copy, Debug, PartialEq)] enum Traversal { CliLinks, FullLinks, NoLinks, } impl Traversal { fn increment(&self) -> Self { match self { Self::CliLinks => Self::NoLinks, _ => *self, } } } #[derive(Clone, Copy, Debug)] struct Recurse { traversal: Traversal, same_filesystem: bool, } impl Recurse { fn increment(&self) -> Self { Self { traversal: self.traversal.increment(), same_filesystem: self.same_filesystem, } } } #[derive(Clone, Copy, Debug)] enum Feedback { Full, Changes, } #[derive(Clone, Debug)] struct User { name: String, uid: u32, } #[derive(Clone, Debug)] struct Group { name: String, gid: u32, } #[derive(Debug)] struct Action { path: PathBuf, user: User, group: Option, recurse: 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()); } } let ft = FileType::from(meta); match ft { FileType::File => {} FileType::Symlink => { let tgt = fs::read_link(&self.path)?; if tgt.is_dir() { if let Some(r) = self.recurse { if r.traversal != Traversal::NoLinks { self.recurse()?; } } } } FileType::Dir => { if let Some(r) = self.recurse { if r.traversal != Traversal::NoLinks { self.recurse()?; } } } } 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) { if let Some(g) = &self.group { println!( "{} changed to {}:{}", &self.path.display(), &self.user.name, &g.name ); } else { println!("{} changed to {}", &self.path.display(), &self.user.name); } } else { 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); } } } Feedback::Changes => { if self.user.uid != uid || self.group.as_ref().map(|x| x.gid) != Some(gid) { if let Some(g) = &self.group { println!( "{} changed to {}:{}", self.path.display(), &self.user.name, &g.name ); } else { println!("{}, changed to {}", self.path.display(), &self.user.name); } } } } } Ok(()) } fn into_child(&self, entry: DirEntry) -> Result> { let path = entry.path().to_path_buf(); let recurse = if let Some(r) = self.recurse { Some(r.increment()) } else { None }; Ok(Self { path, user: self.user.clone(), group: self.group.clone(), recurse, feedback: self.feedback, }) } fn recurse(&self) -> Result<(), Box> { let walker = WalkDir::new(&self.path) .same_file_system(self.recurse.map_or(false, |x| !x.same_filesystem)) .follow_links(self.recurse.map_or(false, |x| match x.traversal { Traversal::NoLinks | Traversal::CliLinks => false, _ => true, })); for entry in walker { let entry = entry?; let action = self.into_child(entry)?; action.apply()?; } Ok(()) } }