Skip to main content

upstream_rs/application/operations/
remove_operation.rs

1use crate::{
2    output::{self, Status},
3    services::packaging::disk_impact::{DiskImpact, SignedByteEstimate, estimate_path_size},
4    services::packaging::{PackagePhase, PackageProgressEvent, PackageRemover, RollbackManager},
5    services::storage::rollback_storage::RollbackSource,
6    services::storage::{metadata_storage::MetadataStorage, package_storage::PackageStorage},
7    utils::static_paths::UpstreamPaths,
8};
9use anyhow::{Context, Result, anyhow};
10
11macro_rules! message {
12    ($cb:expr, $($arg:tt)*) => {{
13        if let Some(cb) = $cb.as_mut() {
14            cb(&format!($($arg)*));
15        }
16    }};
17}
18
19macro_rules! progress {
20    ($cb:expr, $name:expr, $event:expr) => {{
21        if let Some(cb) = $cb.as_mut() {
22            cb($name, $event);
23        }
24    }};
25}
26
27pub struct RemoveOperation<'a> {
28    remover: PackageRemover<'a>,
29    package_storage: &'a mut PackageStorage,
30    metadata_storage: &'a mut MetadataStorage,
31    paths: &'a UpstreamPaths,
32}
33
34impl<'a> RemoveOperation<'a> {
35    pub fn new(
36        package_storage: &'a mut PackageStorage,
37        metadata_storage: &'a mut MetadataStorage,
38        paths: &'a UpstreamPaths,
39    ) -> Self {
40        let remover = PackageRemover::new(paths);
41        Self {
42            remover,
43            package_storage,
44            metadata_storage,
45            paths,
46        }
47    }
48
49    pub fn remove_bulk<H, G>(
50        &mut self,
51        package_names: &Vec<String>,
52        purge_option: &bool,
53        force_option: &bool,
54        message_callback: &mut Option<H>,
55        overall_progress_callback: &mut Option<G>,
56    ) -> Result<(u32, u32)>
57    where
58        H: FnMut(&str),
59        G: FnMut(u32, u32),
60    {
61        let mut no_progress: Option<fn(&str, PackageProgressEvent)> = None;
62        self.remove_bulk_with_progress(
63            package_names,
64            purge_option,
65            force_option,
66            message_callback,
67            overall_progress_callback,
68            &mut no_progress,
69        )
70    }
71
72    pub fn remove_bulk_with_progress<H, G, P>(
73        &mut self,
74        package_names: &Vec<String>,
75        purge_option: &bool,
76        force_option: &bool,
77        message_callback: &mut Option<H>,
78        overall_progress_callback: &mut Option<G>,
79        progress_callback: &mut Option<P>,
80    ) -> Result<(u32, u32)>
81    where
82        H: FnMut(&str),
83        G: FnMut(u32, u32),
84        P: FnMut(&str, PackageProgressEvent),
85    {
86        let total = package_names.len() as u32;
87        let mut completed = 0;
88        let mut failures = 0;
89        let completion_subject_width =
90            output::status_subject_width(package_names.iter().map(String::as_str));
91
92        for package_name in package_names {
93            progress!(
94                progress_callback,
95                package_name,
96                PackageProgressEvent::Phase(PackagePhase::RemovingPackage)
97            );
98
99            match self
100                .remove_single_with_progress(
101                    package_name,
102                    purge_option,
103                    force_option,
104                    message_callback,
105                    progress_callback,
106                )
107                .context(format!("Failed to remove package '{}'", package_name))
108            {
109                Ok(_) => message!(
110                    message_callback,
111                    "{}",
112                    output::status_line_text_with_width(
113                        Status::Ok,
114                        package_name,
115                        "removed",
116                        completion_subject_width
117                    )
118                ),
119                Err(e) => {
120                    message!(
121                        message_callback,
122                        "{}",
123                        output::status_line_text_with_width(
124                            Status::Fail,
125                            package_name,
126                            output::error_summary(&e),
127                            completion_subject_width
128                        )
129                    );
130                    failures += 1;
131                }
132            }
133
134            completed += 1;
135            if let Some(cb) = overall_progress_callback.as_mut() {
136                cb(completed, total);
137            }
138        }
139
140        if failures > 0 {
141            message!(
142                message_callback,
143                "{} package(s) failed to be removed",
144                failures
145            );
146        }
147
148        let removed = total - failures;
149        Ok((removed, failures))
150    }
151
152    pub fn preview_bulk<H>(
153        &mut self,
154        package_names: &Vec<String>,
155        purge_option: &bool,
156        message_callback: &mut Option<H>,
157    ) -> Result<(u32, u32)>
158    where
159        H: FnMut(&str),
160    {
161        let mut planned = 0;
162        let mut failures = 0;
163        let completion_subject_width =
164            output::status_subject_width(package_names.iter().map(String::as_str));
165
166        for package_name in package_names {
167            match self.preview_single(package_name, purge_option, message_callback) {
168                Ok(_) => planned += 1,
169                Err(err) => {
170                    message!(
171                        message_callback,
172                        "{}",
173                        output::status_line_text_with_width(
174                            Status::Fail,
175                            package_name,
176                            output::error_summary(&err),
177                            completion_subject_width
178                        )
179                    );
180                    failures += 1;
181                }
182            }
183        }
184
185        Ok((planned, failures))
186    }
187
188    pub fn estimate_bulk_impact(
189        &self,
190        package_names: &[String],
191        purge_option: bool,
192    ) -> (DiskImpact, u32, u32) {
193        let mut impact = DiskImpact::empty();
194        let mut planned = 0_u32;
195        let mut failures = 0_u32;
196
197        for package_name in package_names {
198            let Some(package) = self.package_storage.get_package_by_name(package_name) else {
199                failures += 1;
200                continue;
201            };
202            impact = impact + self.remover.estimate_remove_impact(package, purge_option);
203            planned += 1;
204        }
205
206        (impact, planned, failures)
207    }
208
209    pub fn transaction_impact_rows(
210        &self,
211        package_names: &[String],
212        purge_option: bool,
213    ) -> Result<Vec<(String, String, DiskImpact)>> {
214        package_names
215            .iter()
216            .map(|package_name| {
217                let package = self
218                    .package_storage
219                    .get_package_by_name(package_name)
220                    .ok_or_else(|| anyhow!("Package '{}' is not installed", package_name))?;
221                Ok((
222                    format!("{}/{}", package.provider, package.name),
223                    package.version.to_string(),
224                    self.remover.estimate_remove_impact(package, purge_option),
225                ))
226            })
227            .collect()
228    }
229
230    pub fn estimate_rollback_impact(
231        &self,
232        package_names: &[String],
233        purge_option: bool,
234    ) -> SignedByteEstimate {
235        if purge_option {
236            return SignedByteEstimate::exact(0);
237        }
238
239        package_names
240            .iter()
241            .map(|package_name| {
242                let Some(package) = self.package_storage.get_package_by_name(package_name) else {
243                    return SignedByteEstimate::unknown();
244                };
245                let active_size = self.remover.estimate_active_size(package).unwrap_or(0);
246                let existing_rollback =
247                    estimate_path_size(&self.paths.install.rollback_dir.join(&package.name))
248                        .unwrap_or(0);
249                SignedByteEstimate::exact(
250                    i128::from(active_size).saturating_sub(i128::from(existing_rollback)),
251                )
252            })
253            .fold(SignedByteEstimate::exact(0), |total, impact| total + impact)
254    }
255
256    pub fn preview_single<H>(
257        &mut self,
258        package_name: &str,
259        purge_option: &bool,
260        message_callback: &mut Option<H>,
261    ) -> Result<()>
262    where
263        H: FnMut(&str),
264    {
265        let package = self
266            .package_storage
267            .get_package_by_name(package_name)
268            .ok_or_else(|| anyhow!("Package '{}' is not installed", package_name))?
269            .clone();
270
271        let install_path = package
272            .install_path
273            .as_ref()
274            .map(|path| path.display().to_string())
275            .unwrap_or_else(|| "<missing>".to_string());
276        let exec_path = package
277            .exec_path
278            .as_ref()
279            .map(|path| path.display().to_string())
280            .unwrap_or_else(|| "<none>".to_string());
281
282        message!(
283            message_callback,
284            "{:<7} {:<28} would remove runtime files at {}",
285            "[plan]",
286            package.name,
287            install_path
288        );
289        message!(
290            message_callback,
291            "        {:<28} would remove symlink/metadata (exec: {})",
292            package.name,
293            exec_path
294        );
295        if *purge_option {
296            message!(
297                message_callback,
298                "        {:<28} would purge app-owned config/cache/data",
299                package.name
300            );
301        }
302
303        Ok(())
304    }
305
306    pub fn remove_single<H>(
307        &mut self,
308        package_name: &str,
309        purge_option: &bool,
310        force_option: &bool,
311        message_callback: &mut Option<H>,
312    ) -> Result<()>
313    where
314        H: FnMut(&str),
315    {
316        let mut no_progress: Option<fn(&str, PackageProgressEvent)> = None;
317        self.remove_single_with_progress(
318            package_name,
319            purge_option,
320            force_option,
321            message_callback,
322            &mut no_progress,
323        )
324    }
325
326    pub fn remove_single_with_progress<H, P>(
327        &mut self,
328        package_name: &str,
329        purge_option: &bool,
330        force_option: &bool,
331        message_callback: &mut Option<H>,
332        progress_callback: &mut Option<P>,
333    ) -> Result<()>
334    where
335        H: FnMut(&str),
336        P: FnMut(&str, PackageProgressEvent),
337    {
338        self.remove_single_with_source(
339            package_name,
340            purge_option,
341            force_option,
342            RollbackSource::Remove,
343            message_callback,
344            progress_callback,
345        )
346    }
347
348    pub fn remove_single_with_source<H, P>(
349        &mut self,
350        package_name: &str,
351        purge_option: &bool,
352        force_option: &bool,
353        rollback_source: RollbackSource,
354        message_callback: &mut Option<H>,
355        progress_callback: &mut Option<P>,
356    ) -> Result<()>
357    where
358        H: FnMut(&str),
359        P: FnMut(&str, PackageProgressEvent),
360    {
361        let package = self
362            .package_storage
363            .get_package_by_name(package_name)
364            .ok_or_else(|| anyhow!("Package '{}' is not installed", package_name))?
365            .clone();
366
367        let mut rollback_captured = false;
368        if !*purge_option {
369            progress!(
370                progress_callback,
371                package_name,
372                PackageProgressEvent::Phase(PackagePhase::CreatingSnapshot)
373            );
374            let rollback_file = RollbackManager::rollback_file_path(self.paths);
375            let mut rollback_storage =
376                crate::services::storage::rollback_storage::RollbackStorage::new(&rollback_file)?;
377            let mut rollback_manager = RollbackManager::new(
378                self.paths,
379                self.package_storage,
380                self.metadata_storage,
381                &mut rollback_storage,
382            );
383            if let Err(err) =
384                rollback_manager.capture_from_installed(&package, rollback_source, message_callback)
385            {
386                progress!(
387                    progress_callback,
388                    package_name,
389                    PackageProgressEvent::Warning(format!(
390                        "Warning: failed to capture rollback: {err}"
391                    ))
392                );
393            } else {
394                rollback_captured = true;
395            }
396        }
397
398        let removal_result = if rollback_captured {
399            progress!(
400                progress_callback,
401                package_name,
402                PackageProgressEvent::Phase(PackagePhase::RemovingRuntimeLinks)
403            );
404            self.remover
405                .remove_runtime_and_desktop_artifacts(&package, message_callback)
406                .context(format!(
407                    "Failed to perform removal operations for '{}'",
408                    package_name
409                ))
410        } else {
411            progress!(
412                progress_callback,
413                package_name,
414                PackageProgressEvent::Phase(PackagePhase::RemovingPackage)
415            );
416            self.remover
417                .remove_package_files(&package, message_callback)
418                .context(format!(
419                    "Failed to perform removal operations for '{}'",
420                    package_name
421                ))
422        };
423
424        if let Err(err) = removal_result {
425            if !*force_option {
426                return Err(err);
427            }
428            message!(
429                message_callback,
430                "{}",
431                output::warning(format!(
432                    "Ignoring uninstall error for '{}': {}",
433                    package_name, err
434                ))
435            );
436        }
437
438        progress!(
439            progress_callback,
440            package_name,
441            PackageProgressEvent::Phase(PackagePhase::RemovingMetadata)
442        );
443        self.package_storage
444            .remove_package_by_name(package_name)
445            .context(format!(
446                "Failed to remove '{}' from package storage",
447                package_name
448            ))?;
449        self.metadata_storage
450            .remove_package(package_name)
451            .context(format!(
452                "Failed to remove '{}' from sidecar metadata",
453                package_name
454            ))?;
455
456        if *purge_option {
457            progress!(
458                progress_callback,
459                package_name,
460                PackageProgressEvent::Phase(PackagePhase::PurgingPackageData)
461            );
462            let purge_result = self
463                .remover
464                .purge_configs(package_name, message_callback)
465                .context(format!(
466                    "Failed to purge configuration files for '{}'",
467                    package_name
468                ));
469            if let Err(err) = purge_result {
470                if !*force_option {
471                    return Err(err);
472                }
473                message!(
474                    message_callback,
475                    "{}",
476                    output::warning(format!(
477                        "Ignoring purge error for '{}': {}",
478                        package_name, err
479                    ))
480                );
481            }
482        }
483
484        Ok(())
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::RemoveOperation;
491    use crate::services::storage::{
492        metadata_storage::MetadataStorage, package_storage::PackageStorage,
493    };
494    use crate::utils::test_support;
495    use std::path::Path;
496    use std::{fs, io};
497
498    fn temp_root(name: &str) -> std::path::PathBuf {
499        test_support::temp_root("upstream-remove-op-test", name)
500    }
501
502    fn test_paths(root: &Path) -> crate::utils::static_paths::UpstreamPaths {
503        test_support::upstream_paths(root)
504    }
505
506    fn cleanup(path: &Path) -> io::Result<()> {
507        fs::remove_dir_all(path)
508    }
509
510    #[test]
511    fn remove_single_returns_error_for_missing_package() {
512        let root = temp_root("missing");
513        let paths = test_paths(&root);
514        fs::create_dir_all(paths.config.packages_file.parent().expect("parent"))
515            .expect("create metadata dir");
516        let mut storage = PackageStorage::new(&paths.config.packages_file).expect("storage");
517        let mut metadata_storage =
518            MetadataStorage::new(&paths.config.metadata_file).expect("metadata");
519        let mut op = RemoveOperation::new(&mut storage, &mut metadata_storage, &paths);
520        let mut msg = Some(|_: &str| {});
521
522        let err = op
523            .remove_single("missing", &false, &false, &mut msg)
524            .expect_err("missing package");
525        assert!(err.to_string().contains("is not installed"));
526
527        cleanup(&root).expect("cleanup");
528    }
529
530    #[test]
531    fn remove_bulk_reports_failures_for_missing_packages() {
532        let root = temp_root("bulk");
533        let paths = test_paths(&root);
534        fs::create_dir_all(paths.config.packages_file.parent().expect("parent"))
535            .expect("create metadata dir");
536        let mut storage = PackageStorage::new(&paths.config.packages_file).expect("storage");
537        let mut metadata_storage =
538            MetadataStorage::new(&paths.config.metadata_file).expect("metadata");
539        let mut op = RemoveOperation::new(&mut storage, &mut metadata_storage, &paths);
540        let mut msg = Some(|_: &str| {});
541        let mut progress_calls = Vec::new();
542        let mut progress = Some(|done: u32, total: u32| {
543            progress_calls.push((done, total));
544        });
545        let names = vec!["a".to_string(), "b".to_string()];
546
547        let (removed, failed) = op
548            .remove_bulk(&names, &false, &false, &mut msg, &mut progress)
549            .expect("bulk remove");
550        assert_eq!((removed, failed), (0, 2));
551        assert_eq!(progress_calls.last().copied(), Some((2, 2)));
552
553        cleanup(&root).expect("cleanup");
554    }
555
556    #[test]
557    fn preview_single_returns_error_for_missing_package() {
558        let root = temp_root("preview-missing");
559        let paths = test_paths(&root);
560        fs::create_dir_all(paths.config.packages_file.parent().expect("parent"))
561            .expect("create metadata dir");
562        let mut storage = PackageStorage::new(&paths.config.packages_file).expect("storage");
563        let mut metadata_storage =
564            MetadataStorage::new(&paths.config.metadata_file).expect("metadata");
565        let mut op = RemoveOperation::new(&mut storage, &mut metadata_storage, &paths);
566        let mut msg = Some(|_: &str| {});
567
568        let err = op
569            .preview_single("missing", &false, &mut msg)
570            .expect_err("missing package");
571        assert!(err.to_string().contains("is not installed"));
572
573        cleanup(&root).expect("cleanup");
574    }
575
576    #[test]
577    fn preview_bulk_reports_missing_without_mutating_storage() {
578        let root = temp_root("preview-bulk");
579        let paths = test_paths(&root);
580        fs::create_dir_all(paths.config.packages_file.parent().expect("parent"))
581            .expect("create metadata dir");
582        let mut storage = PackageStorage::new(&paths.config.packages_file).expect("storage");
583        let mut metadata_storage =
584            MetadataStorage::new(&paths.config.metadata_file).expect("metadata");
585        let mut op = RemoveOperation::new(&mut storage, &mut metadata_storage, &paths);
586        let mut msg = Some(|_: &str| {});
587
588        let names = vec!["a".to_string(), "b".to_string()];
589        let (planned, failed) = op
590            .preview_bulk(&names, &false, &mut msg)
591            .expect("preview bulk");
592        assert_eq!((planned, failed), (0, 2));
593
594        let persisted = PackageStorage::new(&paths.config.packages_file).expect("storage reload");
595        assert!(persisted.get_all_packages().is_empty());
596
597        cleanup(&root).expect("cleanup");
598    }
599}