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, package_failed, package_skipped, package_success,
16                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 enum RollbackPackageStatus {
59    Succeeded,
60    Failed { error: String },
61    Skipped { reason: String },
62}
63
64pub struct RollbackPackageOutcome {
65    pub name: String,
66    pub status: RollbackPackageStatus,
67}
68
69pub struct RollbackRestoreOutcome {
70    pub restored: u32,
71    pub failed: u32,
72    pub packages: Vec<RollbackPackageOutcome>,
73}
74
75pub struct RollbackPruneOutcome {
76    pub pruned: u32,
77    pub missing: u32,
78    pub packages: Vec<RollbackPackageOutcome>,
79}
80
81impl RollbackOperation {
82    pub fn new() -> Result<Self> {
83        let paths = UpstreamPaths::new()?;
84        let package_storage = PackageStorage::new(&paths.config.packages_file)?;
85        let metadata_storage = MetadataStorage::new(&paths.config.metadata_file)?;
86        let rollback_file = RollbackManager::rollback_file_path(&paths);
87        let rollback_storage = RollbackStorage::new(&rollback_file)?;
88
89        Ok(Self {
90            paths,
91            package_storage,
92            metadata_storage,
93            rollback_storage,
94        })
95    }
96
97    fn manager(&mut self) -> RollbackManager<'_> {
98        RollbackManager::new(
99            &self.paths,
100            &mut self.package_storage,
101            &mut self.metadata_storage,
102            &mut self.rollback_storage,
103        )
104    }
105
106    pub fn restore_preview(&mut self, names: &[String]) -> Result<RollbackRestorePreview> {
107        if names.is_empty() {
108            return Err(anyhow!(
109                "At least one package name is required unless --prune is provided"
110            ));
111        }
112
113        let manager = self.manager();
114        let preview = restore_preview(names, &manager);
115        let targets = names
116            .iter()
117            .filter_map(|name| {
118                let record = manager.rollback_record(name)?;
119                Some(RollbackRestoreTarget {
120                    name: name.clone(),
121                    install_path: record
122                        .package_snapshot
123                        .install_path
124                        .as_ref()
125                        .map(|path| path.display().to_string())
126                        .unwrap_or_else(|| "<missing>".to_string()),
127                    source: record.source.clone(),
128                })
129            })
130            .collect();
131
132        Ok(RollbackRestorePreview { preview, targets })
133    }
134
135    pub fn restorable_names(&mut self, names: &[String]) -> Vec<String> {
136        let manager = self.manager();
137        names
138            .iter()
139            .filter(|name| manager.rollback_record(name).is_some())
140            .cloned()
141            .collect()
142    }
143
144    pub fn restore<H>(
145        &mut self,
146        names: &[String],
147        message_callback: &mut Option<H>,
148    ) -> Result<RollbackRestoreOutcome>
149    where
150        H: FnMut(&str, &str),
151    {
152        let restorable_names = self.restorable_names(names);
153        let transaction = TransactionLog::start(
154            &self.paths,
155            TransactionKind::Rollback,
156            planned_packages(restorable_names.clone()),
157            None,
158        )?;
159
160        let mut restored = 0_u32;
161        let mut failed = 0_u32;
162        let mut packages = Vec::new();
163        let mut transaction_packages = Vec::new();
164        {
165            let mut manager = self.manager();
166            for name in &restorable_names {
167                let package_name = name.clone();
168                let mut msg = Some(|line: &str| {
169                    if let Some(callback) = message_callback.as_mut() {
170                        callback(&package_name, line);
171                    }
172                });
173
174                match manager.restore_package(name, &mut msg) {
175                    Ok(_) => {
176                        packages.push(RollbackPackageOutcome {
177                            name: name.clone(),
178                            status: RollbackPackageStatus::Succeeded,
179                        });
180                        transaction_packages.push(package_success(name.clone()));
181                        restored += 1;
182                    }
183                    Err(err) => {
184                        let summary = output::error_summary(&err);
185                        packages.push(RollbackPackageOutcome {
186                            name: name.clone(),
187                            status: RollbackPackageStatus::Failed {
188                                error: summary.clone(),
189                            },
190                        });
191                        transaction_packages.push(package_failed(name.clone(), summary));
192                        failed += 1;
193                    }
194                }
195            }
196        }
197
198        if failed > 0 {
199            transaction.fail(
200                transaction_packages,
201                format!("{failed} rollback restore(s) failed"),
202            )?;
203        } else {
204            transaction.complete(transaction_packages)?;
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 transaction = if target_names.is_empty() {
238            None
239        } else {
240            Some(TransactionLog::start(
241                &self.paths,
242                TransactionKind::Rollback,
243                planned_packages(target_names.to_vec()),
244                None,
245            )?)
246        };
247
248        let mut pruned = 0_u32;
249        let mut missing = 0_u32;
250        let mut packages = Vec::new();
251        let mut transaction_packages = Vec::new();
252        let total = target_names.len();
253        {
254            let mut manager = self.manager();
255            for (idx, name) in target_names.iter().enumerate() {
256                if let Some(callback) = message_callback.as_mut() {
257                    callback(name, idx + 1, total);
258                }
259
260                match manager.prune_package(name) {
261                    Ok(true) => {
262                        pruned += 1;
263                        packages.push(RollbackPackageOutcome {
264                            name: name.clone(),
265                            status: RollbackPackageStatus::Succeeded,
266                        });
267                        transaction_packages.push(package_success(name.clone()));
268                    }
269                    Ok(false) => {
270                        missing += 1;
271                        let reason = "no rollback data found".to_string();
272                        packages.push(RollbackPackageOutcome {
273                            name: name.clone(),
274                            status: RollbackPackageStatus::Skipped {
275                                reason: reason.clone(),
276                            },
277                        });
278                        transaction_packages.push(package_skipped(name.clone(), reason));
279                    }
280                    Err(err) => {
281                        let summary = output::error_summary(&err);
282                        packages.push(RollbackPackageOutcome {
283                            name: name.clone(),
284                            status: RollbackPackageStatus::Failed {
285                                error: summary.clone(),
286                            },
287                        });
288                        transaction_packages.push(package_failed(name.clone(), summary.clone()));
289                        if let Some(transaction) = transaction.take() {
290                            transaction.fail(transaction_packages, summary)?;
291                        }
292                        return Err(err);
293                    }
294                }
295            }
296        }
297
298        if let Some(transaction) = transaction {
299            transaction.complete(transaction_packages)?;
300        }
301
302        Ok(RollbackPruneOutcome {
303            pruned,
304            missing,
305            packages,
306        })
307    }
308}
309
310fn restore_preview(names: &[String], manager: &RollbackManager<'_>) -> RollbackPreview {
311    let rows = names
312        .iter()
313        .filter_map(|name| {
314            let record = manager.rollback_record(name)?;
315            let pkg = &record.package_snapshot;
316            Some(RollbackPreviewRow {
317                package: format!("{}/{}", pkg.provider, pkg.name),
318                version: pkg.version.to_string(),
319                net_change: manager
320                    .estimate_restore_impact(name)
321                    .map(|impact| impact.net)
322                    .unwrap_or(SignedByteEstimate::exact(0)),
323            })
324        })
325        .collect::<Vec<_>>();
326    let missing_names = missing_names(names, &rows);
327    let impact = names
328        .iter()
329        .filter_map(|name| manager.estimate_restore_impact(name))
330        .fold(DiskImpact::empty(), |total, impact| total + impact);
331
332    RollbackPreview {
333        rows,
334        impact,
335        missing_names,
336    }
337}
338
339fn prune_preview(names: &[String], manager: &RollbackManager<'_>) -> RollbackPreview {
340    let rows = names
341        .iter()
342        .filter_map(|name| {
343            let record = manager.rollback_record(name)?;
344            let pkg = &record.package_snapshot;
345            Some(RollbackPreviewRow {
346                package: format!("{}/{}", pkg.provider, pkg.name),
347                version: pkg.version.to_string(),
348                net_change: manager
349                    .estimate_prune_impact(name)
350                    .map(|impact| impact.net)
351                    .unwrap_or(SignedByteEstimate::exact(0)),
352            })
353        })
354        .collect::<Vec<_>>();
355    let missing_names = missing_names(names, &rows);
356    let impact = names
357        .iter()
358        .filter_map(|name| manager.estimate_prune_impact(name))
359        .fold(DiskImpact::empty(), |total, impact| total + impact);
360
361    RollbackPreview {
362        rows,
363        impact,
364        missing_names,
365    }
366}
367
368fn missing_names(names: &[String], rows: &[RollbackPreviewRow]) -> Vec<String> {
369    names
370        .iter()
371        .filter(|name| {
372            !rows
373                .iter()
374                .any(|row| row.package.ends_with(&format!("/{name}")))
375        })
376        .cloned()
377        .collect()
378}
379
380impl From<&RollbackPreviewRow> for output::TransactionRow {
381    fn from(row: &RollbackPreviewRow) -> Self {
382        output::TransactionRow::single_version(
383            row.package.clone(),
384            row.version.clone(),
385            row.net_change,
386            ByteEstimate::exact(0),
387        )
388    }
389}