use super::{Feedback, Group, Recurse, Traversal}; use shitbox::Cmd; use clap::{Arg, ArgGroup, Command, ValueHint}; use std::{ error::Error, fs::File, io, os::{fd::AsRawFd, unix::prelude::MetadataExt}, path::PathBuf, }; use walkdir::{DirEntry, WalkDir}; #[derive(Debug, Default)] pub struct Chgrp; impl Cmd for Chgrp { fn cli(&self) -> clap::Command { Command::new("chgrp") .about("change group ownership") .author("Nathan Fisher") .version(env!("CARGO_PKG_VERSION")) .arg( Arg::new("group") .value_name("GROUP") .value_hint(ValueHint::Username) .num_args(1) .required(true), ) .args(super::args()) .group( ArgGroup::new("links") .args(["cli-traverse", "full-traverse", "no-traverse"]) .requires("recursive") .multiple(false), ) } fn run(&self, matches: &clap::ArgMatches) -> Result<(), Box> { let recurse = Recurse::from_matches(matches); let feedback = Feedback::from_matches(matches); let group = if let Some(grp) = matches.get_one::("group") { let gid = pw::get_gid_for_groupname(grp) .ok_or(io::Error::new(io::ErrorKind::Other, "cannot get gid"))?; Group { name: grp, gid } } 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), 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(shitbox::Path::Bin) } } #[derive(Debug)] struct Action<'a> { path: PathBuf, group: Group<'a>, 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(), uid, self.group.gid) != 0 { return Err(io::Error::last_os_error().into()); } } drop(fd); if let Some(feedback) = self.feedback { match feedback { Feedback::Full => { if self.group.gid != gid { self.display_changes(gid)?; } else { self.display_retained(); } } Feedback::Changes => { if self.group.gid != gid { self.display_changes(gid)?; } } } } Ok(()) } fn display_changes(&self, gid: u32) -> Result<(), std::str::Utf8Error> { let groupname = pw::get_groupname_for_gid(gid)?; println!( "{} changed from {groupname} to {}", &self.path.display(), &self.group.name ); Ok(()) } fn display_retained(&self) { println!("{} retained as {}", &self.path.display(), &self.group.name); } fn into_child(&self, entry: DirEntry) -> Result> { let path = entry.path().to_path_buf(); Ok(Self { path, 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(()) } }