Skip to main content

upstream_rs/application/operations/
rollback_op.rs

1use anyhow::{Result, anyhow};
2
3use crate::{
4    output,
5    services::packaging::{
6        RollbackManager,
7        disk_impact::{ByteEstimate, DiskImpact, SignedByteEstimate},
8    },
9    storage::{
10        database::PackageDatabase,
11        rollback::{RollbackSource, RollbackStorage},
12    },
13    utils::static_paths::UpstreamPaths,
14};
15
16pub struct RollbackOperation {
17    paths: UpstreamPaths,
18    package_database: PackageDatabase,
19    rollback_storage: RollbackStorage,
20}
21
22pub struct RollbackPreviewRow {
23    pub package: String,
24    pub version: String,
25    pub net_change: SignedByteEstimate,
26}
27
28pub struct RollbackRestoreTarget {
29    pub name: String,
30    pub install_path: String,
31    pub source: RollbackSource,
32}
33
34pub struct RollbackPreview {
35    pub rows: Vec<RollbackPreviewRow>,
36    pub impact: DiskImpact,
37    pub missing_names: Vec<String>,
38}
39
40pub struct RollbackRestorePreview {
41    pub preview: RollbackPreview,
42    pub targets: Vec<RollbackRestoreTarget>,
43}
44
45pub struct RollbackPrunePreview {
46    pub target_names: Vec<String>,
47    pub preview: RollbackPreview,
48}
49
50pub struct RollbackListRow {
51    pub name: String,
52    pub version: String,
53    pub source: RollbackSource,
54    pub install_path: String,
55}
56
57pub enum RollbackPackageStatus {
58    Succeeded,
59    Failed { error: String },
60    Skipped { reason: String },
61}
62
63pub struct RollbackPackageOutcome {
64    pub name: String,
65    pub status: RollbackPackageStatus,
66}
67
68pub struct RollbackRestoreOutcome {
69    pub restored: u32,
70    pub failed: u32,
71    pub packages: Vec<RollbackPackageOutcome>,
72}
73
74pub struct RollbackPruneOutcome {
75    pub pruned: u32,
76    pub missing: u32,
77    pub packages: Vec<RollbackPackageOutcome>,
78}
79
80impl RollbackOperation {
81    pub fn new() -> Result<Self> {
82        let paths = UpstreamPaths::new()?;
83        let package_database = PackageDatabase::open(&paths.config.packages_database_file)?;
84        let rollback_file = RollbackManager::rollback_file_path(&paths);
85        let rollback_storage = RollbackStorage::new(&rollback_file)?;
86
87        Ok(Self {
88            paths,
89            package_database,
90            rollback_storage,
91        })
92    }
93
94    fn manager(&mut self) -> RollbackManager<'_> {
95        RollbackManager::new(
96            &self.paths,
97            &mut self.package_database,
98            &mut self.rollback_storage,
99        )
100    }
101
102    pub fn restore_preview(&mut self, names: &[String]) -> Result<RollbackRestorePreview> {
103        if names.is_empty() {
104            return Err(anyhow!(
105                "At least one package name is required unless --prune is provided"
106            ));
107        }
108
109        let manager = self.manager();
110        let preview = restore_preview(names, &manager);
111        let targets = names
112            .iter()
113            .filter_map(|name| {
114                let record = manager.rollback_record(name)?;
115                Some(RollbackRestoreTarget {
116                    name: name.clone(),
117                    install_path: record
118                        .package_snapshot
119                        .install_path
120                        .as_ref()
121                        .map(|path| path.display().to_string())
122                        .unwrap_or_else(|| "<missing>".to_string()),
123                    source: record.source.clone(),
124                })
125            })
126            .collect();
127
128        Ok(RollbackRestorePreview { preview, targets })
129    }
130
131    pub fn list_rows(&mut self) -> Vec<RollbackListRow> {
132        let manager = self.manager();
133        manager
134            .rollback_packages()
135            .into_iter()
136            .filter_map(|name| {
137                let record = manager.rollback_record(&name)?;
138                let package = &record.package_snapshot;
139                Some(RollbackListRow {
140                    name,
141                    version: package.version.to_string(),
142                    source: record.source.clone(),
143                    install_path: package
144                        .install_path
145                        .as_ref()
146                        .map(|path| path.display().to_string())
147                        .unwrap_or_else(|| "-".to_string()),
148                })
149            })
150            .collect()
151    }
152
153    pub fn restorable_names(&mut self, names: &[String]) -> Vec<String> {
154        let manager = self.manager();
155        names
156            .iter()
157            .filter(|name| manager.rollback_record(name).is_some())
158            .cloned()
159            .collect()
160    }
161
162    pub fn restore<H>(
163        &mut self,
164        names: &[String],
165        message_callback: &mut Option<H>,
166    ) -> Result<RollbackRestoreOutcome>
167    where
168        H: FnMut(&str, &str),
169    {
170        let restorable_names = self.restorable_names(names);
171
172        let mut restored = 0_u32;
173        let mut failed = 0_u32;
174        let mut packages = Vec::new();
175        {
176            let mut manager = self.manager();
177            for name in &restorable_names {
178                let package_name = name.clone();
179                let mut msg = Some(|line: &str| {
180                    if let Some(callback) = message_callback.as_mut() {
181                        callback(&package_name, line);
182                    }
183                });
184
185                match manager.restore_package(name, &mut msg) {
186                    Ok(_) => {
187                        packages.push(RollbackPackageOutcome {
188                            name: name.clone(),
189                            status: RollbackPackageStatus::Succeeded,
190                        });
191                        restored += 1;
192                    }
193                    Err(err) => {
194                        let summary = output::error_summary(&err);
195                        packages.push(RollbackPackageOutcome {
196                            name: name.clone(),
197                            status: RollbackPackageStatus::Failed {
198                                error: summary.clone(),
199                            },
200                        });
201                        failed += 1;
202                    }
203                }
204            }
205        }
206
207        Ok(RollbackRestoreOutcome {
208            restored,
209            failed,
210            packages,
211        })
212    }
213
214    pub fn prune_preview(&mut self, names: Vec<String>) -> RollbackPrunePreview {
215        let manager = self.manager();
216        let target_names = if names.is_empty() {
217            manager.rollback_packages()
218        } else {
219            names
220        };
221        let preview = prune_preview(&target_names, &manager);
222
223        RollbackPrunePreview {
224            target_names,
225            preview,
226        }
227    }
228
229    pub fn prune<H>(
230        &mut self,
231        target_names: &[String],
232        message_callback: &mut Option<H>,
233    ) -> Result<RollbackPruneOutcome>
234    where
235        H: FnMut(&str, usize, usize),
236    {
237        let mut pruned = 0_u32;
238        let mut missing = 0_u32;
239        let mut packages = Vec::new();
240        let total = target_names.len();
241        {
242            let mut manager = self.manager();
243            for (idx, name) in target_names.iter().enumerate() {
244                if let Some(callback) = message_callback.as_mut() {
245                    callback(name, idx + 1, total);
246                }
247
248                match manager.prune_package(name) {
249                    Ok(true) => {
250                        pruned += 1;
251                        packages.push(RollbackPackageOutcome {
252                            name: name.clone(),
253                            status: RollbackPackageStatus::Succeeded,
254                        });
255                    }
256                    Ok(false) => {
257                        missing += 1;
258                        let reason = "no rollback data found".to_string();
259                        packages.push(RollbackPackageOutcome {
260                            name: name.clone(),
261                            status: RollbackPackageStatus::Skipped {
262                                reason: reason.clone(),
263                            },
264                        });
265                    }
266                    Err(err) => {
267                        let summary = output::error_summary(&err);
268                        packages.push(RollbackPackageOutcome {
269                            name: name.clone(),
270                            status: RollbackPackageStatus::Failed {
271                                error: summary.clone(),
272                            },
273                        });
274                        return Err(err);
275                    }
276                }
277            }
278        }
279
280        Ok(RollbackPruneOutcome {
281            pruned,
282            missing,
283            packages,
284        })
285    }
286}
287
288fn restore_preview(names: &[String], manager: &RollbackManager<'_>) -> RollbackPreview {
289    let rows = names
290        .iter()
291        .filter_map(|name| {
292            let record = manager.rollback_record(name)?;
293            let pkg = &record.package_snapshot;
294            Some(RollbackPreviewRow {
295                package: format!("{}/{}", pkg.provider, pkg.name),
296                version: pkg.version.to_string(),
297                net_change: manager
298                    .estimate_restore_impact(name)
299                    .map(|impact| impact.net)
300                    .unwrap_or(SignedByteEstimate::exact(0)),
301            })
302        })
303        .collect::<Vec<_>>();
304    let missing_names = missing_names(names, &rows);
305    let impact = names
306        .iter()
307        .filter_map(|name| manager.estimate_restore_impact(name))
308        .fold(DiskImpact::empty(), |total, impact| total + impact);
309
310    RollbackPreview {
311        rows,
312        impact,
313        missing_names,
314    }
315}
316
317fn prune_preview(names: &[String], manager: &RollbackManager<'_>) -> RollbackPreview {
318    let rows = names
319        .iter()
320        .filter_map(|name| {
321            let record = manager.rollback_record(name)?;
322            let pkg = &record.package_snapshot;
323            Some(RollbackPreviewRow {
324                package: format!("{}/{}", pkg.provider, pkg.name),
325                version: pkg.version.to_string(),
326                net_change: manager
327                    .estimate_prune_impact(name)
328                    .map(|impact| impact.net)
329                    .unwrap_or(SignedByteEstimate::exact(0)),
330            })
331        })
332        .collect::<Vec<_>>();
333    let missing_names = missing_names(names, &rows);
334    let impact = names
335        .iter()
336        .filter_map(|name| manager.estimate_prune_impact(name))
337        .fold(DiskImpact::empty(), |total, impact| total + impact);
338
339    RollbackPreview {
340        rows,
341        impact,
342        missing_names,
343    }
344}
345
346fn missing_names(names: &[String], rows: &[RollbackPreviewRow]) -> Vec<String> {
347    names
348        .iter()
349        .filter(|name| {
350            !rows
351                .iter()
352                .any(|row| row.package.ends_with(&format!("/{name}")))
353        })
354        .cloned()
355        .collect()
356}
357
358impl From<&RollbackPreviewRow> for output::TransactionRow {
359    fn from(row: &RollbackPreviewRow) -> Self {
360        output::TransactionRow::single_version(
361            row.package.clone(),
362            row.version.clone(),
363            row.net_change,
364            ByteEstimate::exact(0),
365        )
366    }
367}