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