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 = Node::new_node(
122 node.name().as_os_str(),
123 node.node_type.clone(),
124 Metadata::default(),
125 );
126 node_without_meta.content = node.content.clone();
127 summary.update_from_node(node);
128 if let Some(id) = node.subtree {
129 let subtree_summary = Self::from_tree(repo, id, summary_map, p)?;
130 node_without_meta.subtree = Some(subtree_summary.id_without_meta);
131 summary.update(subtree_summary);
132 summary.subtrees.push(id);
133 }
134 tree_without_meta.nodes.push(node_without_meta);
135 }
136 let (_, id_without_meta) = tree_without_meta.serialize()?;
137 summary.id_without_meta = id_without_meta;
138
139 _ = summary_map.insert(id, summary.clone());
140 Ok(summary)
141 }
142}
143
144#[derive(Default, Clone)]
145pub struct StatisticsBuilder<'a> {
146 blobs: BTreeSet<&'a DataId>,
147 summary: Summary,
148}
149
150impl<'a> StatisticsBuilder<'a> {
151 fn append_blobs_from_tree_id(&mut self, tree_id: TreeId, summary_map: &'a SummaryMap) {
152 if let Some(summary) = summary_map.get(&tree_id) {
153 self.blobs.extend(&summary.blobs);
154 for id in &summary.subtrees {
155 self.append_blobs_from_tree_id(*id, summary_map);
156 }
157 }
158 }
159 pub fn append_from_tree(&mut self, tree_id: TreeId, summary_map: &'a SummaryMap) {
160 if let Some(summary) = summary_map.get(&tree_id) {
161 self.summary += summary.summary;
162 }
163 self.append_blobs_from_tree_id(tree_id, summary_map);
164 self.summary.dirs += 1;
165 }
166 pub fn append_from_node(mut self, node: &'a Node, summary_map: &'a SummaryMap) -> Self {
167 if let Some(tree_id) = &node.subtree {
168 self.append_from_tree(*tree_id, summary_map);
169 } else {
170 self.blobs.extend(node.content.iter().flatten());
171 self.summary += summary_map.node_summary(node);
172 }
173 self
174 }
175 pub fn build(self, repo: &'a IndexedRepo) -> Result<Statistics> {
176 let sizes = Sizes::from_blobs(&self.blobs, repo)?;
177 Ok(Statistics {
178 summary: self.summary,
179 sizes,
180 })
181 }
182}
183
184#[derive(Default)]
185pub struct Statistics {
186 pub summary: Summary,
187 pub sizes: Sizes,
188}
189
190impl Statistics {
191 pub fn table<'a>(&self, header: String) -> Vec<Vec<Text<'a>>> {
192 let row_bytes =
193 |title, n: u64| vec![Text::from(title), Text::from(bytes_size_to_string(n))];
194 let row_count = |title, n: usize| vec![Text::from(title), Text::from(n.to_string())];
195
196 let mut rows = Vec::new();
197 rows.push(vec![Text::from(""), Text::from(header)]);
198 rows.push(row_bytes("total size", self.summary.size));
199 rows.push(row_count("total files", self.summary.files));
200 rows.push(row_count("total dirs", self.summary.dirs));
201 rows.push(vec![Text::from(String::new()); 3]);
202 rows.push(row_count("total blobs", self.sizes.blobs));
203 rows.push(row_bytes(
204 "total size after deduplication",
205 self.sizes.dedup_size,
206 ));
207 rows.push(row_bytes("total repoSize", self.sizes.repo_size));
208 rows.push(vec![
209 Text::from("compression ratio"),
210 Text::from(format!("{:.2}", self.sizes.compression_ratio())),
211 ]);
212 rows
213 }
214}
215
216#[derive(Default, Add, Clone, Copy)]
217pub struct Sizes {
218 pub blobs: usize,
219 pub repo_size: u64,
220 pub dedup_size: u64,
221}
222
223impl Sizes {
224 pub fn from_blobs<'a>(
225 blobs: impl IntoIterator<Item = &'a &'a DataId>,
226 repo: &'a IndexedRepo,
227 ) -> Result<Self> {
228 blobs
229 .into_iter()
230 .map(|id| repo.get_index_entry(*id))
231 .try_fold(Self::default(), |sum, ie| -> Result<_> {
232 let ie = ie?;
233 Ok(Self {
234 blobs: sum.blobs + 1,
235 repo_size: sum.repo_size + u64::from(ie.location.length),
236 dedup_size: sum.dedup_size + u64::from(ie.location.data_length()),
237 })
238 })
239 }
240
241 pub fn compression_ratio(&self) -> f64 {
242 self.dedup_size as f64 / self.repo_size as f64
243 }
244}
245
246pub struct DiffStatistics {
247 pub stats: EitherOrBoth<Statistics>,
248 pub both_sizes: Sizes,
249}
250
251impl DiffStatistics {
252 pub fn map<'a, F, T>(&'a self, f: F) -> EitherOrBoth<T>
253 where
254 F: Fn(&'a Statistics) -> T,
255 {
256 self.stats.as_ref().map_any(&f, &f)
257 }
258
259 pub fn sizes(&self) -> DiffSizes {
260 DiffSizes(self.map(|d| d.sizes))
261 }
262 pub fn total_sizes(&self) -> DiffSizes {
263 DiffSizes(self.map(|d| d.sizes + self.both_sizes))
264 }
265 pub fn both_sizes(&self) -> DiffSizes {
266 DiffSizes(self.map(|_| self.both_sizes))
267 }
268 pub fn summary(&self) -> DiffSummary {
269 DiffSummary(self.map(|d| d.summary))
270 }
271 pub fn table<'a>(&self, header_left: String, header_right: String) -> Vec<Vec<Text<'a>>> {
272 fn row_map<'a, T>(
273 title: &'static str,
274 n: EitherOrBoth<T>,
275 map: fn(T) -> String,
276 ) -> Vec<Text<'a>> {
277 let (left, right) = n.left_and_right();
278 vec![
279 Text::from(title),
280 Text::from(left.map_or_else(String::new, map)),
281 Text::from(right.map_or_else(String::new, map)),
282 ]
283 }
284
285 let row_bytes = |title, n: EitherOrBoth<u64>| row_map(title, n, bytes_size_to_string);
286 let row_count = |title, n: EitherOrBoth<usize>| row_map(title, n, |n| n.to_string());
287
288 let mut rows = Vec::new();
289 rows.push(vec![
290 Text::from(""),
291 Text::from(header_left),
292 Text::from(header_right),
293 ]);
294 rows.push(row_bytes("total size", self.summary().size()));
295 rows.push(row_count("total files", self.summary().files()));
296 rows.push(row_count("total dirs", self.summary().dirs()));
297 rows.push(vec![Text::from(String::new()); 3]);
298 rows.push(row_count("exclusive blobs", self.sizes().blobs()));
299 rows.push(row_count("shared blobs", self.both_sizes().blobs()));
300 rows.push(row_count("total blobs", self.total_sizes().blobs()));
301 rows.push(vec![Text::from(String::new()); 3]);
302 rows.push(row_bytes(
303 "exclusive size after deduplication",
304 self.sizes().dedup_size(),
305 ));
306 rows.push(row_bytes(
307 "shared size after deduplication",
308 self.both_sizes().dedup_size(),
309 ));
310 rows.push(row_bytes(
311 "total size after deduplication",
312 self.total_sizes().dedup_size(),
313 ));
314 rows.push(vec![Text::from(String::new()); 3]);
315 rows.push(row_bytes("exclusive repoSize", self.sizes().repo_size()));
316 rows.push(row_bytes("shared repoSize", self.both_sizes().repo_size()));
317 rows.push(row_bytes("total repoSize", self.total_sizes().repo_size()));
318 rows.push(vec![Text::from(String::new()); 3]);
319 rows.push(row_map(
320 "compression ratio",
321 self.total_sizes().compression_ratio(),
322 |r| format!("{r:.2}"),
323 ));
324 rows
325 }
326}
327
328impl Default for DiffStatistics {
329 fn default() -> Self {
330 Self {
331 stats: EitherOrBoth::Both(Statistics::default(), Statistics::default()),
332 both_sizes: Sizes::default(),
333 }
334 }
335}
336
337pub struct DiffSizes(EitherOrBoth<Sizes>);
338impl DiffSizes {
339 pub fn blobs(&self) -> EitherOrBoth<usize> {
340 let map = |s: &Sizes| s.blobs;
341 self.0.as_ref().map_any(map, map)
342 }
343 pub fn repo_size(&self) -> EitherOrBoth<u64> {
344 let map = |s: &Sizes| s.repo_size;
345 self.0.as_ref().map_any(map, map)
346 }
347 pub fn dedup_size(&self) -> EitherOrBoth<u64> {
348 let map = |s: &Sizes| s.dedup_size;
349 self.0.as_ref().map_any(map, map)
350 }
351 pub fn compression_ratio(&self) -> EitherOrBoth<f64> {
352 let map = |s: &Sizes| s.compression_ratio();
353 self.0.as_ref().map_any(map, map)
354 }
355}
356pub struct DiffSummary(EitherOrBoth<Summary>);
357impl DiffSummary {
358 pub fn size(&self) -> EitherOrBoth<u64> {
359 let map = |s: &Summary| s.size;
360 self.0.as_ref().map_any(map, map)
361 }
362 pub fn files(&self) -> EitherOrBoth<usize> {
363 let map = |s: &Summary| s.files;
364 self.0.as_ref().map_any(map, map)
365 }
366 pub fn dirs(&self) -> EitherOrBoth<usize> {
367 let map = |s: &Summary| s.dirs;
368 self.0.as_ref().map_any(map, map)
369 }
370}