use crate::{
services::packaging::PackageRemover,
services::packaging::RollbackManager,
services::packaging::disk_impact::DiskImpact,
services::storage::rollback_storage::RollbackSource,
services::storage::{metadata_storage::MetadataStorage, package_storage::PackageStorage},
utils::static_paths::UpstreamPaths,
};
use anyhow::{Context, Result, anyhow};
use console::style;
macro_rules! message {
($cb:expr, $($arg:tt)*) => {{
if let Some(cb) = $cb.as_mut() {
cb(&format!($($arg)*));
}
}};
}
pub struct RemoveOperation<'a> {
remover: PackageRemover<'a>,
package_storage: &'a mut PackageStorage,
metadata_storage: &'a mut MetadataStorage,
paths: &'a UpstreamPaths,
}
impl<'a> RemoveOperation<'a> {
pub fn new(
package_storage: &'a mut PackageStorage,
metadata_storage: &'a mut MetadataStorage,
paths: &'a UpstreamPaths,
) -> Self {
let remover = PackageRemover::new(paths);
Self {
remover,
package_storage,
metadata_storage,
paths,
}
}
pub fn remove_bulk<H, G>(
&mut self,
package_names: &Vec<String>,
purge_option: &bool,
message_callback: &mut Option<H>,
overall_progress_callback: &mut Option<G>,
) -> Result<(u32, u32)>
where
H: FnMut(&str),
G: FnMut(u32, u32),
{
let total = package_names.len() as u32;
let mut completed = 0;
let mut failures = 0;
for package_name in package_names {
message!(message_callback, "Removing '{}' ...", package_name);
match self
.remove_single(package_name, purge_option, message_callback)
.context(format!("Failed to remove package '{}'", package_name))
{
Ok(_) => message!(message_callback, "{}", style("Package removed").green()),
Err(e) => {
message!(message_callback, "{} {}", style("Removal failed:").red(), e);
failures += 1;
}
}
completed += 1;
if let Some(cb) = overall_progress_callback.as_mut() {
cb(completed, total);
}
}
if failures > 0 {
message!(
message_callback,
"{} package(s) failed to be removed",
failures
);
}
let removed = total - failures;
Ok((removed, failures))
}
pub fn preview_bulk<H>(
&mut self,
package_names: &Vec<String>,
purge_option: &bool,
message_callback: &mut Option<H>,
) -> Result<(u32, u32)>
where
H: FnMut(&str),
{
let mut planned = 0;
let mut failures = 0;
for package_name in package_names {
match self.preview_single(package_name, purge_option, message_callback) {
Ok(_) => planned += 1,
Err(err) => {
message!(
message_callback,
"{:<7} {:<28} {}",
"[fail]",
package_name,
err
);
failures += 1;
}
}
}
Ok((planned, failures))
}
pub fn estimate_bulk_impact(
&self,
package_names: &[String],
purge_option: bool,
) -> (DiskImpact, u32, u32) {
let mut impact = DiskImpact::empty();
let mut planned = 0_u32;
let mut failures = 0_u32;
for package_name in package_names {
let Some(package) = self.package_storage.get_package_by_name(package_name) else {
failures += 1;
continue;
};
impact = impact + self.remover.estimate_remove_impact(package, purge_option);
planned += 1;
}
(impact, planned, failures)
}
pub fn preview_single<H>(
&mut self,
package_name: &str,
purge_option: &bool,
message_callback: &mut Option<H>,
) -> Result<()>
where
H: FnMut(&str),
{
let package = self
.package_storage
.get_package_by_name(package_name)
.ok_or_else(|| anyhow!("Package '{}' is not installed", package_name))?
.clone();
let install_path = package
.install_path
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "<missing>".to_string());
let exec_path = package
.exec_path
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "<none>".to_string());
message!(
message_callback,
"{:<7} {:<28} would remove runtime files at {}",
"[plan]",
package.name,
install_path
);
message!(
message_callback,
" {:<28} would remove symlink/metadata (exec: {})",
package.name,
exec_path
);
if *purge_option {
message!(
message_callback,
" {:<28} would purge app-owned config/cache/data",
package.name
);
}
Ok(())
}
pub fn remove_single<H>(
&mut self,
package_name: &str,
purge_option: &bool,
message_callback: &mut Option<H>,
) -> Result<()>
where
H: FnMut(&str),
{
self.remove_single_with_source(
package_name,
purge_option,
RollbackSource::Remove,
message_callback,
)
}
pub fn remove_single_with_source<H>(
&mut self,
package_name: &str,
purge_option: &bool,
rollback_source: RollbackSource,
message_callback: &mut Option<H>,
) -> Result<()>
where
H: FnMut(&str),
{
let package = self
.package_storage
.get_package_by_name(package_name)
.ok_or_else(|| anyhow!("Package '{}' is not installed", package_name))?
.clone();
let mut rollback_captured = false;
if !*purge_option {
let rollback_file = RollbackManager::rollback_file_path(self.paths);
let mut rollback_storage =
crate::services::storage::rollback_storage::RollbackStorage::new(&rollback_file)?;
let mut rollback_manager = RollbackManager::new(
self.paths,
self.package_storage,
self.metadata_storage,
&mut rollback_storage,
);
if let Err(err) =
rollback_manager.capture_from_installed(&package, rollback_source, message_callback)
{
message!(
message_callback,
"Warning: failed to capture rollback for '{}': {}",
package_name,
err
);
} else {
rollback_captured = true;
}
}
if rollback_captured {
self.remover
.remove_runtime_and_desktop_artifacts(&package, message_callback)
.context(format!(
"Failed to perform removal operations for '{}'",
package_name
))?;
} else {
self.remover
.remove_package_files(&package, message_callback)
.context(format!(
"Failed to perform removal operations for '{}'",
package_name
))?;
}
self.package_storage
.remove_package_by_name(package_name)
.context(format!(
"Failed to remove '{}' from package storage",
package_name
))?;
self.metadata_storage
.remove_package(package_name)
.context(format!(
"Failed to remove '{}' from sidecar metadata",
package_name
))?;
if *purge_option {
self.remover
.purge_configs(package_name, message_callback)
.context(format!(
"Failed to purge configuration files for '{}'",
package_name
))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::RemoveOperation;
use crate::services::storage::{
metadata_storage::MetadataStorage, package_storage::PackageStorage,
};
use crate::utils::test_support;
use std::path::Path;
use std::{fs, io};
fn temp_root(name: &str) -> std::path::PathBuf {
test_support::temp_root("upstream-remove-op-test", name)
}
fn test_paths(root: &Path) -> crate::utils::static_paths::UpstreamPaths {
test_support::upstream_paths(root)
}
fn cleanup(path: &Path) -> io::Result<()> {
fs::remove_dir_all(path)
}
#[test]
fn remove_single_returns_error_for_missing_package() {
let root = temp_root("missing");
let paths = test_paths(&root);
fs::create_dir_all(paths.config.packages_file.parent().expect("parent"))
.expect("create metadata dir");
let mut storage = PackageStorage::new(&paths.config.packages_file).expect("storage");
let mut metadata_storage =
MetadataStorage::new(&paths.config.metadata_file).expect("metadata");
let mut op = RemoveOperation::new(&mut storage, &mut metadata_storage, &paths);
let mut msg: Option<fn(&str)> = None;
let err = op
.remove_single("missing", &false, &mut msg)
.expect_err("missing package");
assert!(err.to_string().contains("is not installed"));
cleanup(&root).expect("cleanup");
}
#[test]
fn remove_bulk_reports_failures_for_missing_packages() {
let root = temp_root("bulk");
let paths = test_paths(&root);
fs::create_dir_all(paths.config.packages_file.parent().expect("parent"))
.expect("create metadata dir");
let mut storage = PackageStorage::new(&paths.config.packages_file).expect("storage");
let mut metadata_storage =
MetadataStorage::new(&paths.config.metadata_file).expect("metadata");
let mut op = RemoveOperation::new(&mut storage, &mut metadata_storage, &paths);
let mut msg: Option<fn(&str)> = None;
let mut progress_calls = Vec::new();
let mut progress = Some(|done: u32, total: u32| {
progress_calls.push((done, total));
});
let names = vec!["a".to_string(), "b".to_string()];
let (removed, failed) = op
.remove_bulk(&names, &false, &mut msg, &mut progress)
.expect("bulk remove");
assert_eq!((removed, failed), (0, 2));
assert_eq!(progress_calls.last().copied(), Some((2, 2)));
cleanup(&root).expect("cleanup");
}
#[test]
fn preview_single_returns_error_for_missing_package() {
let root = temp_root("preview-missing");
let paths = test_paths(&root);
fs::create_dir_all(paths.config.packages_file.parent().expect("parent"))
.expect("create metadata dir");
let mut storage = PackageStorage::new(&paths.config.packages_file).expect("storage");
let mut metadata_storage =
MetadataStorage::new(&paths.config.metadata_file).expect("metadata");
let mut op = RemoveOperation::new(&mut storage, &mut metadata_storage, &paths);
let mut msg: Option<fn(&str)> = None;
let err = op
.preview_single("missing", &false, &mut msg)
.expect_err("missing package");
assert!(err.to_string().contains("is not installed"));
cleanup(&root).expect("cleanup");
}
#[test]
fn preview_bulk_reports_missing_without_mutating_storage() {
let root = temp_root("preview-bulk");
let paths = test_paths(&root);
fs::create_dir_all(paths.config.packages_file.parent().expect("parent"))
.expect("create metadata dir");
let mut storage = PackageStorage::new(&paths.config.packages_file).expect("storage");
let mut metadata_storage =
MetadataStorage::new(&paths.config.metadata_file).expect("metadata");
let mut op = RemoveOperation::new(&mut storage, &mut metadata_storage, &paths);
let mut msg: Option<fn(&str)> = None;
let names = vec!["a".to_string(), "b".to_string()];
let (planned, failed) = op
.preview_bulk(&names, &false, &mut msg)
.expect("preview bulk");
assert_eq!((planned, failed), (0, 2));
let persisted = PackageStorage::new(&paths.config.packages_file).expect("storage reload");
assert!(persisted.get_all_packages().is_empty());
cleanup(&root).expect("cleanup");
}
}