use std::{
ops::AddAssign,
path::{Path, PathBuf},
};
#[cfg(feature = "tui")]
use crate::commands::tui;
use crate::{
Application, RUSTIC_APP, commands::diff::arg_to_snap_path, repository::IndexedRepo, status_err,
};
use abscissa_core::{Command, Runnable, Shutdown};
use anyhow::Result;
use derive_more::Add;
use rustic_core::{
Excludes, LocalSource, LocalSourceFilterOptions, LocalSourceSaveOptions, LsOptions,
ProgressType, ReadSource, ReadSourceEntry, RusticResult,
repofile::{Node, NodeType},
};
mod constants {
pub(super) const S_IRUSR: u32 = 0o400; pub(super) const S_IWUSR: u32 = 0o200; pub(super) const S_IXUSR: u32 = 0o100;
pub(super) const S_IRGRP: u32 = 0o040; pub(super) const S_IWGRP: u32 = 0o020; pub(super) const S_IXGRP: u32 = 0o010;
pub(super) const S_IROTH: u32 = 0o004; pub(super) const S_IWOTH: u32 = 0o002; pub(super) const S_IXOTH: u32 = 0o001; }
use constants::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
#[derive(clap::Parser, Command, Debug)]
pub(crate) struct LsCmd {
#[clap(value_name = "SNAPSHOT[:PATH]|PATH")]
snap: String,
#[clap(long, short = 's', conflicts_with = "json")]
summary: bool,
#[clap(long, short = 'l', conflicts_with = "json")]
long: bool,
#[clap(long, conflicts_with_all = ["summary", "long"])]
json: bool,
#[clap(long, long("numeric-uid-gid"))]
numeric_id: bool,
#[clap(long)]
pub recursive: bool,
#[cfg(feature = "tui")]
#[clap(long, short)]
interactive: bool,
#[clap(flatten, next_help_heading = "Exclude options")]
pub excludes: Excludes,
#[clap(flatten, next_help_heading = "Exclude options for local source")]
ignore_opts: LocalSourceFilterOptions,
}
impl Runnable for LsCmd {
fn run(&self) {
let (snap_id, path) = arg_to_snap_path(&self.snap);
if let Err(err) = snap_id.map_or_else(
|| self.inner_run_local(path),
|snap_id| {
RUSTIC_APP
.config()
.repository
.run_indexed(|repo| self.inner_run_snapshot(repo, snap_id, path))
},
) {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
#[derive(Default, Clone, Copy, Add)]
pub struct Summary {
pub files: usize,
pub size: u64,
pub dirs: usize,
}
impl AddAssign for Summary {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
impl Summary {
pub fn update(&mut self, node: &Node) {
if node.is_dir() {
self.dirs += 1;
} else {
self.files += 1;
}
if node.is_file() {
self.size += node.meta.size;
}
}
pub fn from_node(node: &Node) -> Self {
let mut summary = Self::default();
summary.update(node);
summary
}
}
pub trait NodeLs {
fn mode_str(&self) -> String;
fn link_str(&self) -> String;
}
impl NodeLs for Node {
fn mode_str(&self) -> String {
format!(
"{:>1}{:>9}",
match self.node_type {
NodeType::Dir => 'd',
NodeType::Symlink { .. } => 'l',
NodeType::Chardev { .. } => 'c',
NodeType::Dev { .. } => 'b',
NodeType::Fifo => 'p',
NodeType::Socket => 's',
_ => '-',
},
self.meta
.mode
.map_or_else(|| "?????????".to_string(), parse_permissions)
)
}
fn link_str(&self) -> String {
if let NodeType::Symlink { .. } = &self.node_type {
["->", &self.node_type.to_link().to_string_lossy()].join(" ")
} else {
String::new()
}
}
}
impl LsCmd {
fn inner_run_snapshot(
&self,
repo: IndexedRepo,
snap_id: &str,
path: Option<&str>,
) -> Result<()> {
let config = RUSTIC_APP.config();
let path = path.unwrap_or("");
let snap = repo.get_snapshot_from_str(snap_id, |sn| config.snapshot_filter.matches(sn))?;
#[cfg(feature = "tui")]
if self.interactive {
use rustic_core::ProgressBars;
use tui::summary::SummaryMap;
return tui::run(|progress| {
let p = progress.progress(
ProgressType::Spinner,
"starting rustic in interactive mode...",
);
p.finish();
let ls = tui::Ls::new(&repo, snap, path, SummaryMap::default())?;
tui::run_app(progress.terminal, ls)
});
}
let node = repo.node_from_snapshot_and_path(&snap, path)?;
let ls_opts = LsOptions::default()
.excludes(self.excludes.clone())
.recursive(!self.snap.contains(':') || self.recursive);
self.display(repo.ls(&node, &ls_opts)?)?;
Ok(())
}
fn inner_run_local(&self, path: Option<&str>) -> Result<()> {
#[cfg(feature = "tui")]
if self.interactive {
anyhow::bail!("interactive ls with local path is not yet implemented!");
}
let path = path.unwrap_or(".");
let src = LocalSource::new(
LocalSourceSaveOptions::default(),
&self.excludes,
&self.ignore_opts,
&[&path],
)?
.entries()
.map(|item| -> RusticResult<_> {
let ReadSourceEntry { path, node, .. } = item?;
Ok((path, node))
});
self.display(src)?;
Ok(())
}
fn display(
&self,
tree_streamer: impl Iterator<Item = RusticResult<(PathBuf, Node)>>,
) -> Result<()> {
let mut summary = Summary::default();
if self.json {
print!("[");
}
let mut first_item = true;
for item in tree_streamer {
let (path, node) = item?;
summary.update(&node);
if self.json {
if !first_item {
print!(",");
}
print!("{}", serde_json::to_string(&path)?);
} else if self.long {
print_node(&node, &path, self.numeric_id);
} else {
println!("{}", path.display());
}
first_item = false;
}
if self.json {
println!("]");
}
if self.summary {
println!(
"total: {} dirs, {} files, {} bytes",
summary.dirs, summary.files, summary.size
);
}
Ok(())
}
}
pub fn print_node(node: &Node, path: &Path, numeric_uid_gid: bool) {
println!(
"{:>10} {:>8} {:>8} {:>9} {:>17} {path:?} {}",
node.mode_str(),
if numeric_uid_gid {
node.meta.uid.map(|uid| uid.to_string())
} else {
node.meta.user.clone()
}
.unwrap_or_else(|| "?".to_string()),
if numeric_uid_gid {
node.meta.gid.map(|uid| uid.to_string())
} else {
node.meta.group.clone()
}
.unwrap_or_else(|| "?".to_string()),
node.meta.size,
node.meta.mtime.map_or_else(
|| "?".to_string(),
|t| t.strftime("%_d %b %Y %H:%M").to_string()
),
node.link_str(),
);
}
fn parse_permissions(mode: u32) -> String {
let user = triplet(mode, S_IRUSR, S_IWUSR, S_IXUSR);
let group = triplet(mode, S_IRGRP, S_IWGRP, S_IXGRP);
let other = triplet(mode, S_IROTH, S_IWOTH, S_IXOTH);
[user, group, other].join("")
}
fn triplet(mode: u32, read: u32, write: u32, execute: u32) -> String {
match (mode & read, mode & write, mode & execute) {
(0, 0, 0) => "---",
(_, 0, 0) => "r--",
(0, _, 0) => "-w-",
(0, 0, _) => "--x",
(_, 0, _) => "r-x",
(_, _, 0) => "rw-",
(0, _, _) => "-wx",
(_, _, _) => "rwx",
}
.to_string()
}