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}