rustic_rs/commands/
ls.rs

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