Skip to main content

upstream_rs/application/operations/
remove_operation.rs

1use crate::services::packaging::PackageRemover;
2use crate::{
3    services::storage::package_storage::PackageStorage, utils::static_paths::UpstreamPaths,
4};
5use anyhow::{Context, Result, anyhow};
6use console::style;
7
8macro_rules! message {
9    ($cb:expr, $($arg:tt)*) => {{
10        if let Some(cb) = $cb.as_mut() {
11            cb(&format!($($arg)*));
12        }
13    }};
14}
15
16pub struct RemoveOperation<'a> {
17    remover: PackageRemover<'a>,
18    package_storage: &'a mut PackageStorage,
19}
20
21impl<'a> RemoveOperation<'a> {
22    pub fn new(package_storage: &'a mut PackageStorage, paths: &'a UpstreamPaths) -> Self {
23        let remover = PackageRemover::new(paths);
24        Self {
25            remover,
26            package_storage,
27        }
28    }
29
30    pub fn remove_bulk<H, G>(
31        &mut self,
32        package_names: &Vec<String>,
33        purge_option: &bool,
34        message_callback: &mut Option<H>,
35        overall_progress_callback: &mut Option<G>,
36    ) -> Result<(u32, u32)>
37    where
38        H: FnMut(&str),
39        G: FnMut(u32, u32),
40    {
41        let total = package_names.len() as u32;
42        let mut completed = 0;
43        let mut failures = 0;
44
45        for package_name in package_names {
46            message!(message_callback, "Removing '{}' ...", package_name);
47
48            match self
49                .remove_single(package_name, purge_option, message_callback)
50                .context(format!("Failed to remove package '{}'", package_name))
51            {
52                Ok(_) => message!(message_callback, "{}", style("Package removed").green()),
53                Err(e) => {
54                    message!(message_callback, "{} {}", style("Removal failed:").red(), e);
55                    failures += 1;
56                }
57            }
58
59            completed += 1;
60            if let Some(cb) = overall_progress_callback.as_mut() {
61                cb(completed, total);
62            }
63        }
64
65        if failures > 0 {
66            message!(
67                message_callback,
68                "{} package(s) failed to be removed",
69                failures
70            );
71        }
72
73        let removed = total - failures;
74        Ok((removed, failures))
75    }
76
77    pub fn remove_single<H>(
78        &mut self,
79        package_name: &str,
80        purge_option: &bool,
81        message_callback: &mut Option<H>,
82    ) -> Result<()>
83    where
84        H: FnMut(&str),
85    {
86        let package = self
87            .package_storage
88            .get_package_by_name(package_name)
89            .ok_or_else(|| anyhow!("Package '{}' is not installed", package_name))?;
90
91        self.remover
92            .remove_package_files(package, message_callback)
93            .context(format!(
94                "Failed to perform removal operations for '{}'",
95                package_name
96            ))?;
97
98        self.package_storage
99            .remove_package_by_name(package_name)
100            .context(format!(
101                "Failed to remove '{}' from package storage",
102                package_name
103            ))?;
104
105        if *purge_option {
106            self.remover
107                .purge_configs(package_name, message_callback)
108                .context(format!(
109                    "Failed to purge configuration files for '{}'",
110                    package_name
111                ))?;
112        }
113
114        Ok(())
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::RemoveOperation;
121    use crate::services::storage::package_storage::PackageStorage;
122    use crate::utils::static_paths::{
123        AppDirs, ConfigPaths, InstallPaths, IntegrationPaths, UpstreamPaths,
124    };
125    use std::path::{Path, PathBuf};
126    use std::time::{SystemTime, UNIX_EPOCH};
127    use std::{fs, io};
128
129    fn temp_root(name: &str) -> PathBuf {
130        let nanos = SystemTime::now()
131            .duration_since(UNIX_EPOCH)
132            .map(|d| d.as_nanos())
133            .unwrap_or(0);
134        std::env::temp_dir().join(format!("upstream-remove-op-test-{name}-{nanos}"))
135    }
136
137    fn test_paths(root: &Path) -> UpstreamPaths {
138        let dirs = AppDirs {
139            user_dir: root.to_path_buf(),
140            config_dir: root.join("config"),
141            data_dir: root.join("data"),
142            metadata_dir: root.join("data/metadata"),
143        };
144
145        UpstreamPaths {
146            config: ConfigPaths {
147                config_file: dirs.config_dir.join("config.toml"),
148                packages_file: dirs.metadata_dir.join("packages.json"),
149                paths_file: dirs.metadata_dir.join("paths.sh"),
150            },
151            install: InstallPaths {
152                appimages_dir: dirs.data_dir.join("appimages"),
153                binaries_dir: dirs.data_dir.join("binaries"),
154                archives_dir: dirs.data_dir.join("archives"),
155            },
156            integration: IntegrationPaths {
157                symlinks_dir: dirs.data_dir.join("symlinks"),
158                xdg_applications_dir: dirs.user_dir.join(".local/share/applications"),
159                icons_dir: dirs.data_dir.join("icons"),
160            },
161            dirs,
162        }
163    }
164
165    fn cleanup(path: &Path) -> io::Result<()> {
166        fs::remove_dir_all(path)
167    }
168
169    #[test]
170    fn remove_single_returns_error_for_missing_package() {
171        let root = temp_root("missing");
172        let paths = test_paths(&root);
173        fs::create_dir_all(paths.config.packages_file.parent().expect("parent"))
174            .expect("create metadata dir");
175        let mut storage = PackageStorage::new(&paths.config.packages_file).expect("storage");
176        let mut op = RemoveOperation::new(&mut storage, &paths);
177        let mut msg: Option<fn(&str)> = None;
178
179        let err = op
180            .remove_single("missing", &false, &mut msg)
181            .expect_err("missing package");
182        assert!(err.to_string().contains("is not installed"));
183
184        cleanup(&root).expect("cleanup");
185    }
186
187    #[test]
188    fn remove_bulk_reports_failures_for_missing_packages() {
189        let root = temp_root("bulk");
190        let paths = test_paths(&root);
191        fs::create_dir_all(paths.config.packages_file.parent().expect("parent"))
192            .expect("create metadata dir");
193        let mut storage = PackageStorage::new(&paths.config.packages_file).expect("storage");
194        let mut op = RemoveOperation::new(&mut storage, &paths);
195        let mut msg: Option<fn(&str)> = None;
196        let mut progress_calls = Vec::new();
197        let mut progress = Some(|done: u32, total: u32| {
198            progress_calls.push((done, total));
199        });
200        let names = vec!["a".to_string(), "b".to_string()];
201
202        let (removed, failed) = op
203            .remove_bulk(&names, &false, &mut msg, &mut progress)
204            .expect("bulk remove");
205        assert_eq!((removed, failed), (0, 2));
206        assert_eq!(progress_calls.last().copied(), Some((2, 2)));
207
208        cleanup(&root).expect("cleanup");
209    }
210}