upstream_rs/application/operations/
remove_operation.rs1use 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}