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