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