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