1use std::{
4 ops::AddAssign,
5 path::{Path, PathBuf},
6};
7
8#[cfg(feature = "tui")]
9use crate::commands::tui;
10use crate::{
11 Application, RUSTIC_APP, commands::diff::arg_to_snap_path, repository::IndexedRepo, status_err,
12};
13
14use abscissa_core::{Command, Runnable, Shutdown};
15use anyhow::Result;
16
17use derive_more::Add;
18use rustic_core::{
19 Excludes, LocalSource, LocalSourceFilterOptions, LocalSourceSaveOptions, LsOptions,
20 ProgressType, ReadSource, ReadSourceEntry, RusticResult,
21 repofile::{Node, NodeType},
22};
23
24mod constants {
25 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; }
38use constants::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
39
40#[derive(clap::Parser, Command, Debug)]
42pub(crate) struct LsCmd {
43 #[clap(value_name = "SNAPSHOT[:PATH]|PATH")]
47 snap: String,
48
49 #[clap(long, short = 's', conflicts_with = "json")]
51 summary: bool,
52
53 #[clap(long, short = 'l', conflicts_with = "json")]
55 long: bool,
56
57 #[clap(long, conflicts_with_all = ["summary", "long"])]
59 json: bool,
60
61 #[clap(long, long("numeric-uid-gid"))]
63 numeric_id: bool,
64
65 #[clap(long)]
67 pub recursive: bool,
68
69 #[cfg(feature = "tui")]
70 #[clap(long, short)]
72 interactive: bool,
73
74 #[clap(flatten, next_help_heading = "Exclude options")]
75 pub excludes: Excludes,
77
78 #[clap(flatten, next_help_heading = "Exclude options for local source")]
79 ignore_opts: LocalSourceFilterOptions,
80}
81
82impl Runnable for LsCmd {
83 fn run(&self) {
84 let (snap_id, path) = arg_to_snap_path(&self.snap);
85
86 if let Err(err) = snap_id.map_or_else(
87 || self.inner_run_local(path),
88 |snap_id| {
89 RUSTIC_APP
90 .config()
91 .repository
92 .run_indexed(|repo| self.inner_run_snapshot(repo, snap_id, path))
93 },
94 ) {
95 status_err!("{}", err);
96 RUSTIC_APP.shutdown(Shutdown::Crash);
97 };
98 }
99}
100
101#[derive(Default, Clone, Copy, Add)]
107pub struct Summary {
108 pub files: usize,
109 pub size: u64,
110 pub dirs: usize,
111}
112
113impl AddAssign for Summary {
114 fn add_assign(&mut self, rhs: Self) {
115 *self = *self + rhs;
116 }
117}
118
119impl Summary {
120 pub fn update(&mut self, node: &Node) {
126 if node.is_dir() {
127 self.dirs += 1;
128 } else {
129 self.files += 1;
130 }
131
132 if node.is_file() {
133 self.size += node.meta.size;
134 }
135 }
136
137 pub fn from_node(node: &Node) -> Self {
138 let mut summary = Self::default();
139 summary.update(node);
140 summary
141 }
142}
143
144pub trait NodeLs {
145 fn mode_str(&self) -> String;
146 fn link_str(&self) -> String;
147}
148
149impl NodeLs for Node {
150 fn mode_str(&self) -> String {
151 format!(
152 "{:>1}{:>9}",
153 match self.node_type {
154 NodeType::Dir => 'd',
155 NodeType::Symlink { .. } => 'l',
156 NodeType::Chardev { .. } => 'c',
157 NodeType::Dev { .. } => 'b',
158 NodeType::Fifo => 'p',
159 NodeType::Socket => 's',
160 _ => '-',
161 },
162 self.meta
163 .mode
164 .map_or_else(|| "?????????".to_string(), parse_permissions)
165 )
166 }
167 fn link_str(&self) -> String {
168 if let NodeType::Symlink { .. } = &self.node_type {
169 ["->", &self.node_type.to_link().to_string_lossy()].join(" ")
170 } else {
171 String::new()
172 }
173 }
174}
175
176impl LsCmd {
177 fn inner_run_snapshot(
178 &self,
179 repo: IndexedRepo,
180 snap_id: &str,
181 path: Option<&str>,
182 ) -> Result<()> {
183 let config = RUSTIC_APP.config();
184
185 let path = path.unwrap_or("");
186 let snap = repo.get_snapshot_from_str(snap_id, |sn| config.snapshot_filter.matches(sn))?;
187
188 #[cfg(feature = "tui")]
189 if self.interactive {
190 use rustic_core::ProgressBars;
191 use tui::summary::SummaryMap;
192
193 return tui::run(|progress| {
194 let p = progress.progress(
195 ProgressType::Spinner,
196 "starting rustic in interactive mode...",
197 );
198 p.finish();
199 let ls = tui::Ls::new(&repo, snap, path, SummaryMap::default())?;
201 tui::run_app(progress.terminal, ls)
202 });
203 }
204 let node = repo.node_from_snapshot_and_path(&snap, path)?;
205
206 let ls_opts = LsOptions::default()
208 .excludes(self.excludes.clone())
209 .recursive(!self.snap.contains(':') || self.recursive);
210
211 self.display(repo.ls(&node, &ls_opts)?)?;
212 Ok(())
213 }
214
215 fn inner_run_local(&self, path: Option<&str>) -> Result<()> {
216 #[cfg(feature = "tui")]
217 if self.interactive {
218 anyhow::bail!("interactive ls with local path is not yet implemented!");
219 }
220 let path = path.unwrap_or(".");
221 let src = LocalSource::new(
222 LocalSourceSaveOptions::default(),
223 &self.excludes,
224 &self.ignore_opts,
225 &[&path],
226 )?
227 .entries()
228 .map(|item| -> RusticResult<_> {
229 let ReadSourceEntry { path, node, .. } = item?;
230 Ok((path, node))
231 });
232 self.display(src)?;
233 Ok(())
234 }
235
236 fn display(
237 &self,
238 tree_streamer: impl Iterator<Item = RusticResult<(PathBuf, Node)>>,
239 ) -> Result<()> {
240 let mut summary = Summary::default();
241
242 if self.json {
243 print!("[");
244 }
245
246 let mut first_item = true;
247 for item in tree_streamer {
248 let (path, node) = item?;
249 summary.update(&node);
250 if self.json {
251 if !first_item {
252 print!(",");
253 }
254 print!("{}", serde_json::to_string(&path)?);
255 } else if self.long {
256 print_node(&node, &path, self.numeric_id);
257 } else {
258 println!("{}", path.display());
259 }
260 first_item = false;
261 }
262
263 if self.json {
264 println!("]");
265 }
266
267 if self.summary {
268 println!(
269 "total: {} dirs, {} files, {} bytes",
270 summary.dirs, summary.files, summary.size
271 );
272 }
273 Ok(())
274 }
275}
276
277pub fn print_node(node: &Node, path: &Path, numeric_uid_gid: bool) {
284 println!(
285 "{:>10} {:>8} {:>8} {:>9} {:>17} {path:?} {}",
286 node.mode_str(),
287 if numeric_uid_gid {
288 node.meta.uid.map(|uid| uid.to_string())
289 } else {
290 node.meta.user.clone()
291 }
292 .unwrap_or_else(|| "?".to_string()),
293 if numeric_uid_gid {
294 node.meta.gid.map(|uid| uid.to_string())
295 } else {
296 node.meta.group.clone()
297 }
298 .unwrap_or_else(|| "?".to_string()),
299 node.meta.size,
300 node.meta.mtime.map_or_else(
301 || "?".to_string(),
302 |t| t.strftime("%_d %b %Y %H:%M").to_string()
303 ),
304 node.link_str(),
305 );
306}
307
308fn parse_permissions(mode: u32) -> String {
310 let user = triplet(mode, S_IRUSR, S_IWUSR, S_IXUSR);
311 let group = triplet(mode, S_IRGRP, S_IWGRP, S_IXGRP);
312 let other = triplet(mode, S_IROTH, S_IWOTH, S_IXOTH);
313 [user, group, other].join("")
314}
315
316fn triplet(mode: u32, read: u32, write: u32, execute: u32) -> String {
329 match (mode & read, mode & write, mode & execute) {
330 (0, 0, 0) => "---",
331 (_, 0, 0) => "r--",
332 (0, _, 0) => "-w-",
333 (0, 0, _) => "--x",
334 (_, 0, _) => "r-x",
335 (_, _, 0) => "rw-",
336 (0, _, _) => "-wx",
337 (_, _, _) => "rwx",
338 }
339 .to_string()
340}