Skip to main content

upstream_rs/application/operations/
rollback_operation.rs

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