shitbox/src/cmd/chown/mod.rs

335 lines
11 KiB
Rust
Raw Normal View History

use super::Cmd;
use crate::{fs::FileType, pw};
use clap::{Arg, ArgAction, ArgGroup, Command, ValueHint};
2023-01-18 21:45:18 -05:00
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<dyn std::error::Error>> {
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,
2023-01-18 21:45:18 -05:00
same_filesystem: matches.get_flag("same-filesystem"),
})
} else if matches.get_flag("cli-traverse") {
Some(Recurse {
traversal: Traversal::CliLinks,
2023-01-18 21:45:18 -05:00
same_filesystem: matches.get_flag("same-filesystem"),
})
} else {
Some(Recurse {
traversal: Traversal::NoLinks,
2023-01-18 21:45:18 -05:00
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::<String>("user") {
if let Some((u, g)) = who.split_once(':') {
2023-01-18 21:45:18 -05:00
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 {
2023-01-18 21:45:18 -05:00
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::<String>("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<crate::Path> {
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<Group>,
recurse: Option<Recurse>,
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 {
2023-01-18 21:45:18 -05:00
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 {
2023-01-18 21:45:18 -05:00
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()?;
}
}
}
2023-01-18 21:45:18 -05:00
}
FileType::Dir => {
if let Some(r) = self.recurse {
if r.traversal != Traversal::NoLinks {
self.recurse()?;
}
}
2023-01-18 21:45:18 -05:00
}
}
if let Some(feedback) = self.feedback {
match feedback {
Feedback::Full => {
2023-01-18 21:45:18 -05:00
if self.user.uid != uid || self.group.as_ref().map(|x| x.gid) != Some(gid) {
if let Some(g) = &self.group {
2023-01-18 21:45:18 -05:00
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 {
2023-01-18 21:45:18 -05:00
println!(
"{} retained as {}:{}",
&self.path.display(),
&self.user.name,
&g.name
);
} else {
println!("{} retained as {}", &self.path.display(), &self.user.name);
}
}
}
Feedback::Changes => {
2023-01-18 21:45:18 -05:00
if self.user.uid != uid || self.group.as_ref().map(|x| x.gid) != Some(gid) {
if let Some(g) = &self.group {
2023-01-18 21:45:18 -05:00
println!(
"{} changed to {}:{}",
self.path.display(),
&self.user.name,
&g.name
);
} else {
println!("{}, changed to {}", self.path.display(), &self.user.name);
}
}
2023-01-18 21:45:18 -05:00
}
}
}
Ok(())
}
fn into_child(&self, entry: DirEntry) -> Result<Self, Box<dyn Error>> {
let path = entry.path().to_path_buf();
2023-01-18 21:45:18 -05:00
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<dyn Error>> {
let walker = WalkDir::new(&self.path)
.same_file_system(self.recurse.map_or(false, |x| !x.same_filesystem))
2023-01-18 21:45:18 -05:00
.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(())
}
}