1use std::{ops::AddAssign, path::Path};
4
5#[cfg(feature = "tui")]
6use crate::commands::tui;
7use crate::{Application, RUSTIC_APP, repository::CliIndexedRepo, status_err};
8
9use abscissa_core::{Command, Runnable, Shutdown};
10use anyhow::Result;
11
12use derive_more::Add;
13use rustic_core::{
14 LsOptions,
15 repofile::{Node, NodeType},
16};
17
18mod constants {
19 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; }
32use constants::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
33
34#[derive(clap::Parser, Command, Debug)]
36pub(crate) struct LsCmd {
37 #[clap(value_name = "SNAPSHOT[:PATH]")]
39 snap: String,
40
41 #[clap(long, short = 's', conflicts_with = "json")]
43 summary: bool,
44
45 #[clap(long, short = 'l', conflicts_with = "json")]
47 long: bool,
48
49 #[clap(long, conflicts_with_all = ["summary", "long"])]
51 json: bool,
52
53 #[clap(long, long("numeric-uid-gid"))]
55 numeric_id: bool,
56
57 #[clap(flatten)]
59 ls_opts: LsOptions,
60
61 #[cfg(feature = "tui")]
62 #[clap(long, short)]
64 interactive: bool,
65}
66
67impl Runnable for LsCmd {
68 fn run(&self) {
69 if let Err(err) = RUSTIC_APP
70 .config()
71 .repository
72 .run_indexed(|repo| self.inner_run(repo))
73 {
74 status_err!("{}", err);
75 RUSTIC_APP.shutdown(Shutdown::Crash);
76 };
77 }
78}
79
80#[derive(Default, Clone, Copy, Add)]
84pub struct Summary {
85 pub files: usize,
86 pub size: u64,
87 pub dirs: usize,
88}
89
90impl AddAssign for Summary {
91 fn add_assign(&mut self, rhs: Self) {
92 *self = *self + rhs;
93 }
94}
95
96impl Summary {
97 pub fn update(&mut self, node: &Node) {
103 if node.is_dir() {
104 self.dirs += 1;
105 }
106 if node.is_file() {
107 self.files += 1;
108 self.size += node.meta.size;
109 }
110 }
111
112 pub fn from_node(node: &Node) -> Self {
113 let mut summary = Self::default();
114 summary.update(node);
115 summary
116 }
117}
118
119pub trait NodeLs {
120 fn mode_str(&self) -> String;
121 fn link_str(&self) -> String;
122}
123
124impl NodeLs for Node {
125 fn mode_str(&self) -> String {
126 format!(
127 "{:>1}{:>9}",
128 match self.node_type {
129 NodeType::Dir => 'd',
130 NodeType::Symlink { .. } => 'l',
131 NodeType::Chardev { .. } => 'c',
132 NodeType::Dev { .. } => 'b',
133 NodeType::Fifo => 'p',
134 NodeType::Socket => 's',
135 _ => '-',
136 },
137 self.meta
138 .mode
139 .map_or_else(|| "?????????".to_string(), parse_permissions)
140 )
141 }
142 fn link_str(&self) -> String {
143 if let NodeType::Symlink { .. } = &self.node_type {
144 ["->", &self.node_type.to_link().to_string_lossy()].join(" ")
145 } else {
146 String::new()
147 }
148 }
149}
150
151impl LsCmd {
152 fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> {
153 let config = RUSTIC_APP.config();
154
155 let (snap_id, path) = self.snap.split_once(':').unwrap_or((&self.snap, ""));
156 let snap = repo.get_snapshot_from_str(snap_id, |sn| config.snapshot_filter.matches(sn))?;
157
158 #[cfg(feature = "tui")]
159 if self.interactive {
160 use rustic_core::{Progress, ProgressBars};
161 use tui::summary::SummaryMap;
162
163 return tui::run(|progress| {
164 let p = progress.progress_spinner("starting rustic in interactive mode...");
165 p.finish();
166 let ls = tui::Ls::new(&repo, snap, path, SummaryMap::default())?;
168 tui::run_app(progress.terminal, ls)
169 });
170 }
171 let node = repo.node_from_snapshot_and_path(&snap, path)?;
172
173 let mut ls_opts = self.ls_opts.clone();
175 ls_opts.recursive = !self.snap.contains(':') || ls_opts.recursive;
176
177 let mut summary = Summary::default();
178
179 if self.json {
180 print!("[");
181 }
182
183 let mut first_item = true;
184 for item in repo.ls(&node, &ls_opts)? {
185 let (path, node) = item?;
186 summary.update(&node);
187 if self.json {
188 if !first_item {
189 print!(",");
190 }
191 print!("{}", serde_json::to_string(&path)?);
192 } else if self.long {
193 print_node(&node, &path, self.numeric_id);
194 } else {
195 println!("{}", path.display());
196 }
197 first_item = false;
198 }
199
200 if self.json {
201 println!("]");
202 }
203
204 if self.summary {
205 println!(
206 "total: {} dirs, {} files, {} bytes",
207 summary.dirs, summary.files, summary.size
208 );
209 }
210
211 Ok(())
212 }
213}
214
215pub fn print_node(node: &Node, path: &Path, numeric_uid_gid: bool) {
222 println!(
223 "{:>10} {:>8} {:>8} {:>9} {:>17} {path:?} {}",
224 node.mode_str(),
225 if numeric_uid_gid {
226 node.meta.uid.map(|uid| uid.to_string())
227 } else {
228 node.meta.user.clone()
229 }
230 .unwrap_or_else(|| "?".to_string()),
231 if numeric_uid_gid {
232 node.meta.gid.map(|uid| uid.to_string())
233 } else {
234 node.meta.group.clone()
235 }
236 .unwrap_or_else(|| "?".to_string()),
237 node.meta.size,
238 node.meta.mtime.map_or_else(
239 || "?".to_string(),
240 |t| t.format("%_d %b %Y %H:%M").to_string()
241 ),
242 node.link_str(),
243 );
244}
245
246fn parse_permissions(mode: u32) -> String {
248 let user = triplet(mode, S_IRUSR, S_IWUSR, S_IXUSR);
249 let group = triplet(mode, S_IRGRP, S_IWGRP, S_IXGRP);
250 let other = triplet(mode, S_IROTH, S_IWOTH, S_IXOTH);
251 [user, group, other].join("")
252}
253
254fn triplet(mode: u32, read: u32, write: u32, execute: u32) -> String {
267 match (mode & read, mode & write, mode & execute) {
268 (0, 0, 0) => "---",
269 (_, 0, 0) => "r--",
270 (0, _, 0) => "-w-",
271 (0, 0, _) => "--x",
272 (_, 0, _) => "r-x",
273 (_, _, 0) => "rw-",
274 (0, _, _) => "-wx",
275 (_, _, _) => "rwx",
276 }
277 .to_string()
278}