1use std::collections::{BTreeMap, BTreeSet};
2
3use anyhow::Result;
4use derive_more::Add;
5use itertools::EitherOrBoth;
6use ratatui::text::Text;
7use rustic_core::{
8 DataId, Progress, TreeId,
9 repofile::{Metadata, Node, Tree},
10};
11
12use crate::{
13 commands::{ls::Summary, tui::diff::DiffNode},
14 helpers::bytes_size_to_string,
15 repository::IndexedRepo,
16};
17
18#[derive(Default)]
19pub struct SummaryMap(BTreeMap<TreeId, TreeSummary>);
20
21impl SummaryMap {
22 pub fn get(&self, id: &TreeId) -> Option<&TreeSummary> {
23 self.0.get(id)
24 }
25
26 pub fn compute(&mut self, repo: &IndexedRepo, id: TreeId, p: &Progress) -> Result<()> {
27 let _ = TreeSummary::from_tree(repo, id, &mut self.0, p)?;
28 Ok(())
29 }
30
31 pub fn node_summary(&self, node: &Node) -> Summary {
32 if let Some(id) = node.subtree
33 && let Some(summary) = self.0.get(&id)
34 {
35 summary.summary
36 } else {
37 Summary::from_node(node)
38 }
39 }
40
41 pub fn compute_statistics<'a>(
42 &self,
43 nodes: impl IntoIterator<Item = &'a Node>,
44 repo: &IndexedRepo,
45 ) -> Result<Statistics> {
46 let builder = nodes
47 .into_iter()
48 .fold(StatisticsBuilder::default(), |builder, node| {
49 builder.append_from_node(node, self)
50 });
51 builder.build(repo)
52 }
53
54 pub fn compute_diff_statistics(
55 &self,
56 node: &DiffNode,
57 repo: &IndexedRepo,
58 ) -> Result<DiffStatistics> {
59 let stats = match node.map(|n| StatisticsBuilder::default().append_from_node(n, self)) {
60 EitherOrBoth::Both(left, right) => {
61 let stats_left = Statistics {
62 summary: left.summary,
63 sizes: Sizes::from_blobs(left.blobs.difference(&right.blobs), repo)?,
64 };
65 let stats_right = Statistics {
66 summary: right.summary,
67 sizes: Sizes::from_blobs(right.blobs.difference(&left.blobs), repo)?,
68 };
69 let both_sizes = Sizes::from_blobs(left.blobs.intersection(&right.blobs), repo)?;
70 return Ok(DiffStatistics {
71 stats: EitherOrBoth::Both(stats_left, stats_right),
72 both_sizes,
73 });
74 }
75 EitherOrBoth::Left(b) => EitherOrBoth::Left(b.build(repo)?),
76 EitherOrBoth::Right(b) => EitherOrBoth::Right(b.build(repo)?),
77 };
78 Ok(DiffStatistics {
79 stats,
80 ..Default::default()
81 })
82 }
83}
84
85#[derive(Default, Clone)]
86pub struct TreeSummary {
87 pub id_without_meta: TreeId,
88 pub summary: Summary,
89 blobs: BTreeSet<DataId>,
90 subtrees: Vec<TreeId>,
91}
92
93impl TreeSummary {
94 fn update(&mut self, other: Self) {
95 self.summary += other.summary;
96 }
97
98 fn update_from_node(&mut self, node: &Node) {
99 for id in node.content.iter().flatten() {
100 _ = self.blobs.insert(*id);
101 }
102 self.summary.update(node);
103 }
104
105 pub fn from_tree(
106 repo: &IndexedRepo,
107 id: TreeId,
108 summary_map: &mut BTreeMap<TreeId, Self>,
109 p: &Progress,
110 ) -> Result<Self> {
111 if let Some(summary) = summary_map.get(&id) {
112 return Ok(summary.clone());
113 }
114
115 let mut summary = Self::default();
116
117 let tree = repo.get_tree(&id)?;
118 let mut tree_without_meta = Tree::default();
119 p.inc(1);
120 for node in &tree.nodes {
121 let mut node_without_meta =
122 Node::new_node(&node.name(), node.node_type.clone(), Metadata::default());
123 node_without_meta.content = node.content.clone();
124 summary.update_from_node(node);
125 if let Some(id) = node.subtree {
126 let subtree_summary = Self::from_tree(repo, id, summary_map, p)?;
127 node_without_meta.subtree = Some(subtree_summary.id_without_meta);
128 summary.update(subtree_summary);
129 summary.subtrees.push(id);
130 }
131 tree_without_meta.nodes.push(node_without_meta);
132 }
133 let (_, id_without_meta) = tree_without_meta.serialize()?;
134 summary.id_without_meta = id_without_meta;
135
136 _ = summary_map.insert(id, summary.clone());
137 Ok(summary)
138 }
139}
140
141#[derive(Default, Clone)]
142pub struct StatisticsBuilder<'a> {
143 blobs: BTreeSet<&'a DataId>,
144 summary: Summary,
145}
146
147impl<'a> StatisticsBuilder<'a> {
148 fn append_blobs_from_tree_id(&mut self, tree_id: TreeId, summary_map: &'a SummaryMap) {
149 if let Some(summary) = summary_map.get(&tree_id) {
150 self.blobs.extend(&summary.blobs);
151 for id in &summary.subtrees {
152 self.append_blobs_from_tree_id(*id, summary_map);
153 }
154 }
155 }
156 pub fn append_from_tree(&mut self, tree_id: TreeId, summary_map: &'a SummaryMap) {
157 if let Some(summary) = summary_map.get(&tree_id) {
158 self.summary += summary.summary;
159 }
160 self.append_blobs_from_tree_id(tree_id, summary_map);
161 self.summary.dirs += 1;
162 }
163 pub fn append_from_node(mut self, node: &'a Node, summary_map: &'a SummaryMap) -> Self {
164 if let Some(tree_id) = &node.subtree {
165 self.append_from_tree(*tree_id, summary_map);
166 } else {
167 self.blobs.extend(node.content.iter().flatten());
168 self.summary += summary_map.node_summary(node);
169 }
170 self
171 }
172 pub fn build(self, repo: &'a IndexedRepo) -> Result<Statistics> {
173 let sizes = Sizes::from_blobs(&self.blobs, repo)?;
174 Ok(Statistics {
175 summary: self.summary,
176 sizes,
177 })
178 }
179}
180
181#[derive(Default)]
182pub struct Statistics {
183 pub summary: Summary,
184 pub sizes: Sizes,
185}
186
187impl Statistics {
188 pub fn table<'a>(&self, header: String) -> Vec<Vec<Text<'a>>> {
189 let row_bytes =
190 |title, n: u64| vec![Text::from(title), Text::from(bytes_size_to_string(n))];
191 let row_count = |title, n: usize| vec![Text::from(title), Text::from(n.to_string())];
192
193 let mut rows = Vec::new();
194 rows.push(vec![Text::from(""), Text::from(header)]);
195 rows.push(row_bytes("total size", self.summary.size));
196 rows.push(row_count("total files", self.summary.files));
197 rows.push(row_count("total dirs", self.summary.dirs));
198 rows.push(vec![Text::from(String::new()); 3]);
199 rows.push(row_count("total blobs", self.sizes.blobs));
200 rows.push(row_bytes(
201 "total size after deduplication",
202 self.sizes.dedup_size,
203 ));
204 rows.push(row_bytes("total repoSize", self.sizes.repo_size));
205 rows.push(vec![
206 Text::from("compression ratio"),
207 Text::from(format!("{:.2}", self.sizes.compression_ratio())),
208 ]);
209 rows
210 }
211}
212
213#[derive(Default, Add, Clone, Copy)]
214pub struct Sizes {
215 pub blobs: usize,
216 pub repo_size: u64,
217 pub dedup_size: u64,
218}
219
220impl Sizes {
221 pub fn from_blobs<'a>(
222 blobs: impl IntoIterator<Item = &'a &'a DataId>,
223 repo: &'a IndexedRepo,
224 ) -> Result<Self> {
225 blobs
226 .into_iter()
227 .map(|id| repo.get_index_entry(*id))
228 .try_fold(Self::default(), |sum, ie| -> Result<_> {
229 let ie = ie?;
230 Ok(Self {
231 blobs: sum.blobs + 1,
232 repo_size: sum.repo_size + u64::from(ie.location.length),
233 dedup_size: sum.dedup_size + u64::from(ie.location.data_length()),
234 })
235 })
236 }
237
238 pub fn compression_ratio(&self) -> f64 {
239 self.dedup_size as f64 / self.repo_size as f64
240 }
241}
242
243pub struct DiffStatistics {
244 pub stats: EitherOrBoth<Statistics>,
245 pub both_sizes: Sizes,
246}
247
248impl DiffStatistics {
249 pub fn map<'a, F, T>(&'a self, f: F) -> EitherOrBoth<T>
250 where
251 F: Fn(&'a Statistics) -> T,
252 {
253 self.stats.as_ref().map_any(&f, &f)
254 }
255
256 pub fn sizes(&self) -> DiffSizes {
257 DiffSizes(self.map(|d| d.sizes))
258 }
259 pub fn total_sizes(&self) -> DiffSizes {
260 DiffSizes(self.map(|d| d.sizes + self.both_sizes))
261 }
262 pub fn both_sizes(&self) -> DiffSizes {
263 DiffSizes(self.map(|_| self.both_sizes))
264 }
265 pub fn summary(&self) -> DiffSummary {
266 DiffSummary(self.map(|d| d.summary))
267 }
268 pub fn table<'a>(&self, header_left: String, header_right: String) -> Vec<Vec<Text<'a>>> {
269 fn row_map<'a, T>(
270 title: &'static str,
271 n: EitherOrBoth<T>,
272 map: fn(T) -> String,
273 ) -> Vec<Text<'a>> {
274 let (left, right) = n.left_and_right();
275 vec![
276 Text::from(title),
277 Text::from(left.map_or_else(String::new, map)),
278 Text::from(right.map_or_else(String::new, map)),
279 ]
280 }
281
282 let row_bytes = |title, n: EitherOrBoth<u64>| row_map(title, n, bytes_size_to_string);
283 let row_count = |title, n: EitherOrBoth<usize>| row_map(title, n, |n| n.to_string());
284
285 let mut rows = Vec::new();
286 rows.push(vec![
287 Text::from(""),
288 Text::from(header_left),
289 Text::from(header_right),
290 ]);
291 rows.push(row_bytes("total size", self.summary().size()));
292 rows.push(row_count("total files", self.summary().files()));
293 rows.push(row_count("total dirs", self.summary().dirs()));
294 rows.push(vec![Text::from(String::new()); 3]);
295 rows.push(row_count("exclusive blobs", self.sizes().blobs()));
296 rows.push(row_count("shared blobs", self.both_sizes().blobs()));
297 rows.push(row_count("total blobs", self.total_sizes().blobs()));
298 rows.push(vec![Text::from(String::new()); 3]);
299 rows.push(row_bytes(
300 "exclusive size after deduplication",
301 self.sizes().dedup_size(),
302 ));
303 rows.push(row_bytes(
304 "shared size after deduplication",
305 self.both_sizes().dedup_size(),
306 ));
307 rows.push(row_bytes(
308 "total size after deduplication",
309 self.total_sizes().dedup_size(),
310 ));
311 rows.push(vec![Text::from(String::new()); 3]);
312 rows.push(row_bytes("exclusive repoSize", self.sizes().repo_size()));
313 rows.push(row_bytes("shared repoSize", self.both_sizes().repo_size()));
314 rows.push(row_bytes("total repoSize", self.total_sizes().repo_size()));
315 rows.push(vec![Text::from(String::new()); 3]);
316 rows.push(row_map(
317 "compression ratio",
318 self.total_sizes().compression_ratio(),
319 |r| format!("{r:.2}"),
320 ));
321 rows
322 }
323}
324
325impl Default for DiffStatistics {
326 fn default() -> Self {
327 Self {
328 stats: EitherOrBoth::Both(Statistics::default(), Statistics::default()),
329 both_sizes: Sizes::default(),
330 }
331 }
332}
333
334pub struct DiffSizes(EitherOrBoth<Sizes>);
335impl DiffSizes {
336 pub fn blobs(&self) -> EitherOrBoth<usize> {
337 let map = |s: &Sizes| s.blobs;
338 self.0.as_ref().map_any(map, map)
339 }
340 pub fn repo_size(&self) -> EitherOrBoth<u64> {
341 let map = |s: &Sizes| s.repo_size;
342 self.0.as_ref().map_any(map, map)
343 }
344 pub fn dedup_size(&self) -> EitherOrBoth<u64> {
345 let map = |s: &Sizes| s.dedup_size;
346 self.0.as_ref().map_any(map, map)
347 }
348 pub fn compression_ratio(&self) -> EitherOrBoth<f64> {
349 let map = |s: &Sizes| s.compression_ratio();
350 self.0.as_ref().map_any(map, map)
351 }
352}
353pub struct DiffSummary(EitherOrBoth<Summary>);
354impl DiffSummary {
355 pub fn size(&self) -> EitherOrBoth<u64> {
356 let map = |s: &Summary| s.size;
357 self.0.as_ref().map_any(map, map)
358 }
359 pub fn files(&self) -> EitherOrBoth<usize> {
360 let map = |s: &Summary| s.files;
361 self.0.as_ref().map_any(map, map)
362 }
363 pub fn dirs(&self) -> EitherOrBoth<usize> {
364 let map = |s: &Summary| s.dirs;
365 self.0.as_ref().map_any(map, map)
366 }
367}