289 lines
9.8 KiB
Rust
289 lines
9.8 KiB
Rust
|
use super::Cmd;
|
||
|
use crate::{fs::FileType, pw};
|
||
|
use clap::{Arg, ArgAction, ArgGroup, Command, ValueHint};
|
||
|
use std::{error::Error, fs::{File, self}, io, path::PathBuf, os::{unix::prelude::MetadataExt, fd::AsRawFd}};
|
||
|
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,
|
||
|
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::<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.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::<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 {
|
||
|
if libc::fchown(fd.as_raw_fd(), self.user.uid, self.group.clone().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.clone().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.clone().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<Self, Box<dyn Error>> {
|
||
|
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<dyn Error>> {
|
||
|
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(())
|
||
|
}
|
||
|
}
|