rustic_rs/commands/
ls.rs

1//! `ls` subcommand
2
3use 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    // constants from man page inode(7)
22    pub(super) const S_IRUSR: u32 = 0o400; //   owner has read permission
23    pub(super) const S_IWUSR: u32 = 0o200; //   owner has write permission
24    pub(super) const S_IXUSR: u32 = 0o100; //   owner has execute permission
25
26    pub(super) const S_IRGRP: u32 = 0o040; //   group has read permission
27    pub(super) const S_IWGRP: u32 = 0o020; //   group has write permission
28    pub(super) const S_IXGRP: u32 = 0o010; //   group has execute permission
29
30    pub(super) const S_IROTH: u32 = 0o004; //   others have read permission
31    pub(super) const S_IWOTH: u32 = 0o002; //   others have write permission
32    pub(super) const S_IXOTH: u32 = 0o001; //   others have execute permission
33}
34use constants::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
35
36/// `ls` subcommand
37#[derive(clap::Parser, Command, Debug)]
38pub(crate) struct LsCmd {
39    /// Snapshot/path to list
40    #[clap(value_name = "SNAPSHOT[:PATH]")]
41    snap: String,
42
43    /// show summary
44    #[clap(long, short = 's', conflicts_with = "json")]
45    summary: bool,
46
47    /// show long listing
48    #[clap(long, short = 'l', conflicts_with = "json")]
49    long: bool,
50
51    /// show listing in json
52    #[clap(long, conflicts_with_all = ["summary", "long"])]
53    json: bool,
54
55    /// show uid/gid instead of user/group
56    #[clap(long, long("numeric-uid-gid"))]
57    numeric_id: bool,
58
59    /// Listing options
60    #[clap(flatten)]
61    ls_opts: LsOptions,
62
63    #[cfg(feature = "tui")]
64    /// Run in interactive UI mode
65    #[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/// Summary of a ls command
83///
84/// This struct is used to print a summary of the ls command.
85#[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    /// Update the summary with the node
111    ///
112    /// # Arguments
113    ///
114    /// * `node` - the node to update the summary with
115    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                // create app and run it
174                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        // recursive if standard if we specify a snapshot without dirs. In other cases, use the parameter `recursive`
181        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
222/// Print node in format similar to unix `ls`
223///
224/// # Arguments
225///
226/// * `node` - the node to print
227/// * `path` - the path of the node
228pub 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
253/// Convert permissions into readable format
254fn 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
261/// Create a triplet of permissions
262///
263/// # Arguments
264///
265/// * `mode` - the mode to convert
266/// * `read` - the read bit
267/// * `write` - the write bit
268/// * `execute` - the execute bit
269///
270/// # Returns
271///
272/// The triplet of permissions as a string
273fn 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}