Skip to main content

rustic_rs/commands/
ls.rs

1//! `ls` subcommand
2
3use 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    // constants from man page inode(7)
26    pub(super) const S_IRUSR: u32 = 0o400; //   owner has read permission
27    pub(super) const S_IWUSR: u32 = 0o200; //   owner has write permission
28    pub(super) const S_IXUSR: u32 = 0o100; //   owner has execute permission
29
30    pub(super) const S_IRGRP: u32 = 0o040; //   group has read permission
31    pub(super) const S_IWGRP: u32 = 0o020; //   group has write permission
32    pub(super) const S_IXGRP: u32 = 0o010; //   group has execute permission
33
34    pub(super) const S_IROTH: u32 = 0o004; //   others have read permission
35    pub(super) const S_IWOTH: u32 = 0o002; //   others have write permission
36    pub(super) const S_IXOTH: u32 = 0o001; //   others have execute permission
37}
38use constants::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
39
40/// `ls` subcommand
41#[derive(clap::Parser, Command, Debug)]
42pub(crate) struct LsCmd {
43    /// Snapshot:path to list (uses local path if no snapshot is given)
44    ///
45    /// The snapshot can be an id: "01a2b3c4" or "latest" or "latest~N" (N >= 0)
46    #[clap(value_name = "SNAPSHOT[:PATH]|PATH")]
47    snap: String,
48
49    /// show summary
50    #[clap(long, short = 's', conflicts_with = "json")]
51    summary: bool,
52
53    /// show long listing
54    #[clap(long, short = 'l', conflicts_with = "json")]
55    long: bool,
56
57    /// show listing in json
58    #[clap(long, conflicts_with_all = ["summary", "long"])]
59    json: bool,
60
61    /// show uid/gid instead of user/group
62    #[clap(long, long("numeric-uid-gid"))]
63    numeric_id: bool,
64
65    /// recursively list the dir
66    #[clap(long)]
67    pub recursive: bool,
68
69    #[cfg(feature = "tui")]
70    /// Run in interactive UI mode
71    #[clap(long, short)]
72    interactive: bool,
73
74    #[clap(flatten, next_help_heading = "Exclude options")]
75    /// exclude options
76    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/// Summary of a ls command
102///
103/// This struct is used to print a summary of the ls command.
104//
105// TODO: The same is defined in core - consolidate!
106#[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    /// Update the summary with the node
121    ///
122    /// # Arguments
123    ///
124    /// * `node` - the node to update the summary with
125    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                // create app and run it
200                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        // recursive is standard if we specify a snapshot without dirs. In other cases, use the parameter `recursive`
207        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
277/// Print node in format similar to unix `ls`
278///
279/// # Arguments
280///
281/// * `node` - the node to print
282/// * `path` - the path of the node
283pub 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
308/// Convert permissions into readable format
309fn 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
316/// Create a triplet of permissions
317///
318/// # Arguments
319///
320/// * `mode` - the mode to convert
321/// * `read` - the read bit
322/// * `write` - the write bit
323/// * `execute` - the execute bit
324///
325/// # Returns
326///
327/// The triplet of permissions as a string
328fn 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}