winbrew_cli\services\bootstrap/
cleanup.rs1use anyhow::Result;
21use std::fs;
22use std::path::{Path, PathBuf};
23use tracing::warn;
24
25use crate::core::fs::cleanup_path;
26use crate::core::temp_workspace::{is_temp_root_for, temp_root_base};
27use crate::database;
28use crate::models::domains::installed::{InstalledPackage, PackageStatus};
29
30pub fn cleanup_stale_installations() -> Result<()> {
36 let conn = database::get_conn()?;
37 let stale_packages = database::list_installing_packages(&conn)?;
38
39 for package in stale_packages {
40 cleanup_stale_installation(&conn, &package);
41 }
42
43 Ok(())
44}
45
46fn cleanup_stale_installation(conn: &crate::database::DbConnection, package: &InstalledPackage) {
47 if let Err(err) = database::update_status(conn, &package.name, PackageStatus::Failed) {
48 warn!(package = %package.name, error = %err, "failed to mark stale install as failed");
49 }
50
51 let install_dir = PathBuf::from(&package.install_dir);
52 cleanup_install_dir(&install_dir, &package.name);
53 cleanup_temp_roots(&package.name, &package.version);
54}
55
56fn cleanup_install_dir(install_dir: &Path, package_name: &str) {
57 if let Err(err) = cleanup_path(install_dir) {
58 warn!(package = package_name, path = %install_dir.display(), error = %err, "failed to clean stale install directory");
59 }
60}
61
62fn cleanup_temp_roots(name: &str, version: &str) {
63 let temp_root_base = temp_root_base();
64
65 if !temp_root_base.exists() {
66 return;
67 }
68
69 let entries = match fs::read_dir(&temp_root_base) {
70 Ok(entries) => entries,
71 Err(err) => {
72 warn!(package = name, error = %err, "failed to enumerate temp directory for stale install cleanup");
73 return;
74 }
75 };
76
77 for entry in entries.flatten() {
78 let path = entry.path();
79 if is_temp_root_for(name, version, &path)
80 && let Err(err) = cleanup_path(&path)
81 {
82 warn!(package = name, path = %path.display(), error = %err, "failed to clean stale temp root");
83 }
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::cleanup_stale_installations;
90 use crate::core::temp_workspace;
91 use crate::database;
92 use crate::models::domains::install::InstallerType;
93 use crate::models::domains::installed::{InstalledPackage, PackageStatus};
94 use std::fs;
95 use tempfile::tempdir;
96
97 fn sample_package(
98 name: &str,
99 version: &str,
100 install_dir: &std::path::Path,
101 ) -> InstalledPackage {
102 InstalledPackage {
103 name: name.to_string(),
104 version: version.to_string(),
105 kind: InstallerType::Portable,
106 deployment_kind: InstallerType::Portable.deployment_kind(),
107 engine_kind: InstallerType::Portable.into(),
108 engine_metadata: None,
109 install_dir: install_dir.to_string_lossy().into_owned(),
110 dependencies: Vec::new(),
111 status: PackageStatus::Installing,
112 installed_at: "2026-04-07T00:00:00Z".to_string(),
113 }
114 }
115
116 #[test]
117 fn cleanup_stale_installations_marks_installing_packages_failed_and_cleans_artifacts() {
118 let temp_root = tempdir().expect("temp root");
119 let root = temp_root.path();
120
121 let config = crate::database::Config::load_at(root).expect("config should load");
122 database::init(&config.resolved_paths()).expect("database should initialize");
123
124 let conn = database::get_conn().expect("db connection");
125 let install_dir = root.join("packages").join("Contoso.Stale");
126 fs::create_dir_all(&install_dir).expect("install dir");
127
128 let package = sample_package("Contoso.Stale", "1.0.0", &install_dir);
129 database::insert_package(&conn, &package).expect("insert package");
130
131 let temp_root_path = temp_workspace::temp_root_base().join(format!(
132 "{}test",
133 temp_workspace::temp_root_prefix(&package.name, &package.version)
134 ));
135 fs::create_dir_all(&temp_root_path).expect("stale temp root");
136
137 cleanup_stale_installations().expect("cleanup should succeed");
138
139 let stored = database::get_package(&conn, &package.name)
140 .expect("query package")
141 .expect("package should still exist");
142 assert_eq!(stored.status, PackageStatus::Failed);
143 assert!(!install_dir.exists());
144 assert!(!temp_root_path.exists());
145 }
146}