1use crate::{
4 Application, RUSTIC_APP,
5 helpers::{bold_cell, bytes_size_to_string, table, table_right_from},
6 repository::{CliOpenRepo, get_global_grouped_snapshots},
7 status_err,
8};
9
10use abscissa_core::{Command, Runnable, Shutdown};
11use anyhow::Result;
12use comfy_table::Cell;
13use derive_more::From;
14use humantime::format_duration;
15use itertools::Itertools;
16use serde::Serialize;
17
18use rustic_core::{
19 Progress, ProgressBars, SnapshotGroup,
20 repofile::{DeleteOption, SnapshotFile},
21};
22
23#[cfg(feature = "tui")]
24use crate::commands::tui;
25
26#[derive(clap::Parser, Command, Debug)]
28pub(crate) struct SnapshotCmd {
29 #[clap(value_name = "ID")]
31 ids: Vec<String>,
32
33 #[arg(long)]
35 long: bool,
36
37 #[clap(long, conflicts_with = "long")]
39 json: bool,
40
41 #[clap(long, conflicts_with_all = &["long", "json"])]
43 all: bool,
44
45 #[cfg(feature = "tui")]
46 #[clap(long, short)]
48 pub interactive: bool,
49}
50
51impl Runnable for SnapshotCmd {
52 fn run(&self) {
53 if let Err(err) = RUSTIC_APP
54 .config()
55 .repository
56 .run_open(|repo| self.inner_run(repo))
57 {
58 status_err!("{}", err);
59 RUSTIC_APP.shutdown(Shutdown::Crash);
60 };
61 }
62}
63
64impl SnapshotCmd {
65 fn inner_run(&self, repo: CliOpenRepo) -> Result<()> {
66 #[cfg(feature = "tui")]
67 if self.interactive {
68 return tui::run(|progress| {
69 let config = RUSTIC_APP.config();
70 config
71 .repository
72 .run_indexed_with_progress(progress.clone(), |repo| {
73 let p = progress.progress_spinner("starting rustic in interactive mode...");
74 p.finish();
75 let snapshots = tui::Snapshots::new(
77 &repo,
78 config.snapshot_filter.clone(),
79 config.global.group_by.unwrap_or_default(),
80 )?;
81 tui::run_app(progress.terminal, snapshots)
82 })
83 });
84 }
85
86 let groups = get_global_grouped_snapshots(&repo, &self.ids)?;
87
88 if self.json {
89 #[derive(Serialize, From)]
90 struct SnapshotsGroup {
91 group_key: SnapshotGroup,
92 snapshots: Vec<SnapshotFile>,
93 }
94 let groups: Vec<SnapshotsGroup> = groups.into_iter().map(|g| g.into()).collect();
95 let mut stdout = std::io::stdout();
96 if groups.len() == 1 && groups[0].group_key.is_empty() {
97 serde_json::to_writer_pretty(&mut stdout, &groups[0].snapshots)?;
99 } else {
100 serde_json::to_writer_pretty(&mut stdout, &groups)?;
101 }
102 return Ok(());
103 }
104
105 let mut total_count = 0;
106 for (group_key, snapshots) in groups {
107 if !group_key.is_empty() {
108 println!("\nsnapshots for {group_key}");
109 }
110 let count = snapshots.len();
111
112 if self.long {
113 for snap in snapshots {
114 let mut table = table();
115
116 let add_entry = |title: &str, value: String| {
117 _ = table.add_row([bold_cell(title), Cell::new(value)]);
118 };
119 fill_table(&snap, add_entry);
120
121 println!("{table}");
122 println!();
123 }
124 } else {
125 let mut table = table_right_from(
126 6,
127 [
128 "ID", "Time", "Host", "Label", "Tags", "Paths", "Files", "Dirs", "Size",
129 ],
130 );
131
132 if self.all {
133 _ = table.add_rows(snapshots.into_iter().map(|sn| snap_to_table(&sn, 0)));
135 } else {
136 _ = table.add_rows(
138 snapshots
139 .into_iter()
140 .chunk_by(|sn| sn.tree)
141 .into_iter()
142 .map(|(_, mut g)| snap_to_table(&g.next().unwrap(), g.count())),
143 );
144 }
145 println!("{table}");
146 }
147 println!("{count} snapshot(s)");
148 total_count += count;
149 }
150 println!();
151 println!("total: {total_count} snapshot(s)");
152
153 Ok(())
154 }
155}
156
157pub fn snap_to_table(sn: &SnapshotFile, count: usize) -> [String; 9] {
158 let tags = sn.tags.formatln();
159 let paths = sn.paths.formatln();
160 let time = sn.time.format("%Y-%m-%d %H:%M:%S");
161 let (files, dirs, size) = sn.summary.as_ref().map_or_else(
162 || ("?".to_string(), "?".to_string(), "?".to_string()),
163 |s| {
164 (
165 s.total_files_processed.to_string(),
166 s.total_dirs_processed.to_string(),
167 bytes_size_to_string(s.total_bytes_processed),
168 )
169 },
170 );
171 let id = match count {
172 0 => format!("{}", sn.id),
173 count => format!("{} (+{})", sn.id, count),
174 };
175 [
176 id,
177 time.to_string(),
178 sn.hostname.clone(),
179 sn.label.clone(),
180 tags,
181 paths,
182 files,
183 dirs,
184 size,
185 ]
186}
187
188pub fn fill_table(snap: &SnapshotFile, mut add_entry: impl FnMut(&str, String)) {
189 add_entry("Snapshot", snap.id.to_hex().to_string());
190 if let Some(original) = snap.original {
192 if original != snap.id {
193 add_entry("Original ID", original.to_hex().to_string());
194 }
195 }
196 add_entry("Time", snap.time.format("%Y-%m-%d %H:%M:%S").to_string());
197 add_entry("Generated by", snap.program_version.clone());
198 add_entry("Host", snap.hostname.clone());
199 add_entry("Label", snap.label.clone());
200 add_entry("Tags", snap.tags.formatln());
201 let delete = match snap.delete {
202 DeleteOption::NotSet => "not set".to_string(),
203 DeleteOption::Never => "never".to_string(),
204 DeleteOption::After(t) => format!("after {}", t.format("%Y-%m-%d %H:%M:%S")),
205 };
206 add_entry("Delete", delete);
207 add_entry("Paths", snap.paths.formatln());
208 let parent = snap.parent.map_or_else(
209 || "no parent snapshot".to_string(),
210 |p| p.to_hex().to_string(),
211 );
212 add_entry("Parent", parent);
213 if let Some(ref summary) = snap.summary {
214 add_entry("", String::new());
215 add_entry("Command", summary.command.clone());
216
217 let source = format!(
218 "files: {} / dirs: {} / size: {}",
219 summary.total_files_processed,
220 summary.total_dirs_processed,
221 bytes_size_to_string(summary.total_bytes_processed)
222 );
223 add_entry("Source", source);
224 add_entry("", String::new());
225
226 let files = format!(
227 "new: {:>10} / changed: {:>10} / unchanged: {:>10}",
228 summary.files_new, summary.files_changed, summary.files_unmodified,
229 );
230 add_entry("Files", files);
231
232 let trees = format!(
233 "new: {:>10} / changed: {:>10} / unchanged: {:>10}",
234 summary.dirs_new, summary.dirs_changed, summary.dirs_unmodified,
235 );
236 add_entry("Dirs", trees);
237 add_entry("", String::new());
238
239 let written = format!(
240 "data: {:>10} blobs / raw: {:>10} / packed: {:>10}\n\
241 tree: {:>10} blobs / raw: {:>10} / packed: {:>10}\n\
242 total: {:>10} blobs / raw: {:>10} / packed: {:>10}",
243 summary.data_blobs,
244 bytes_size_to_string(summary.data_added_files),
245 bytes_size_to_string(summary.data_added_files_packed),
246 summary.tree_blobs,
247 bytes_size_to_string(summary.data_added_trees),
248 bytes_size_to_string(summary.data_added_trees_packed),
249 summary.tree_blobs + summary.data_blobs,
250 bytes_size_to_string(summary.data_added),
251 bytes_size_to_string(summary.data_added_packed),
252 );
253 add_entry("Added to repo", written);
254
255 let duration = format!(
256 "backup start: {} / backup end: {} / backup duration: {}\n\
257 total duration: {}",
258 summary.backup_start.format("%Y-%m-%d %H:%M:%S"),
259 summary.backup_end.format("%Y-%m-%d %H:%M:%S"),
260 format_duration(std::time::Duration::from_secs_f64(summary.backup_duration)),
261 format_duration(std::time::Duration::from_secs_f64(summary.total_duration))
262 );
263 add_entry("Duration", duration);
264 }
265 if let Some(ref description) = snap.description {
266 add_entry("Description", description.clone());
267 }
268}