shitbox/corebox/commands/chown/mod.rs

271 lines
8.0 KiB
Rust

use super::{Cmd, Feedback};
use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command, ValueHint};
use shitbox::args;
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: &clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let recurse = Recurse::from_matches(matches);
let feedback = Feedback::from_matches(matches);
let (user, group) = if let Some(who) = matches.get_one::<String>("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::<String>("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.path.is_symlink()
&& r.traversal != Traversal::NoLinks
{
action.recurse(recurse)?;
}
}
action.apply()?;
}
}
Ok(())
}
fn path(&self) -> Option<shitbox::Path> {
Some(shitbox::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)]
#[allow(clippy::enum_variant_names)]
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<Self> {
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<Group<'a>>,
feedback: Option<Feedback>,
}
impl Action<'_> {
fn apply(&self) -> Result<(), Box<dyn Error>> {
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 as_child(&self, entry: DirEntry) -> Result<Self, Box<dyn Error>> {
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<Recurse>) -> Result<(), Box<dyn Error>> {
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| matches!(x.traversal, Traversal::FullLinks)));
for entry in walker {
let entry = entry?;
let action = self.as_child(entry)?;
action.apply()?;
}
Ok(())
}
}