rustic_rs/commands/tui/
summary.rs

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        // Current dir
115    ) -> 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}