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}