Skip to main content

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, 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}