Skip to content

Commit f5a763c

Browse files
author
Greyforge Admin
committed
Respect CORTEX_HOME for uninstall backups
1 parent 7954d02 commit f5a763c

2 files changed

Lines changed: 123 additions & 20 deletions

File tree

src/cortex-cli/src/uninstall_cmd.rs

Lines changed: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use anyhow::{Context, Result, bail};
1414
use clap::Parser;
1515
use std::collections::HashMap;
1616
use std::fs;
17-
use std::path::{Path, PathBuf};
17+
use std::path::{Component, Path, PathBuf};
1818

1919
/// Uninstall CLI command.
2020
#[derive(Debug, Parser)]
@@ -177,6 +177,13 @@ impl UninstallCli {
177177

178178
// Dry run mode - stop here
179179
if self.dry_run {
180+
if self.backup {
181+
println!();
182+
println!(
183+
"Backup location: {}",
184+
get_backup_root()?.join("<timestamp>").display()
185+
);
186+
}
180187
println!("\n[DRY RUN] No files were deleted.");
181188
return Ok(());
182189
}
@@ -319,8 +326,9 @@ fn collect_removal_items() -> Result<Vec<RemovalItem>> {
319326
// 1. Binary locations
320327
items.extend(collect_binary_locations(&home_dir)?);
321328

322-
// 2. Cortex home directory (~/.cortex)
323-
items.extend(collect_cortex_home_items(&home_dir)?);
329+
// 2. Cortex home directory (~/.cortex or CORTEX_HOME)
330+
let cortex_home = cortex_common::get_cortex_home().unwrap_or_else(|| home_dir.join(".cortex"));
331+
items.extend(collect_cortex_home_items(&cortex_home)?);
324332

325333
// 3. Platform-specific locations
326334
#[cfg(target_os = "windows")]
@@ -386,10 +394,9 @@ fn collect_binary_locations(home_dir: &Path) -> Result<Vec<RemovalItem>> {
386394
Ok(items)
387395
}
388396

389-
/// Collect items from the ~/.cortex directory.
390-
fn collect_cortex_home_items(home_dir: &Path) -> Result<Vec<RemovalItem>> {
397+
/// Collect items from the Cortex home directory.
398+
fn collect_cortex_home_items(cortex_home: &Path) -> Result<Vec<RemovalItem>> {
391399
let mut items = Vec::new();
392-
let cortex_home = home_dir.join(".cortex");
393400

394401
if !cortex_home.exists() {
395402
return Ok(items);
@@ -508,7 +515,7 @@ fn collect_cortex_home_items(home_dir: &Path) -> Result<Vec<RemovalItem>> {
508515
== 0
509516
{
510517
items.push(RemovalItem {
511-
path: cortex_home.clone(),
518+
path: cortex_home.to_path_buf(),
512519
description: "Cortex home directory".to_string(),
513520
size: get_dir_size(&cortex_home),
514521
requires_sudo: false,
@@ -517,7 +524,7 @@ fn collect_cortex_home_items(home_dir: &Path) -> Result<Vec<RemovalItem>> {
517524
} else {
518525
// Add the parent directory itself at the end (to be removed after contents)
519526
items.push(RemovalItem {
520-
path: cortex_home,
527+
path: cortex_home.to_path_buf(),
521528
description: "Cortex home directory (if empty)".to_string(),
522529
size: 0,
523530
requires_sudo: false,
@@ -710,10 +717,9 @@ fn prompt_yes_no() -> Result<bool> {
710717

711718
/// Create a backup of items before removal.
712719
fn create_backup(items: &[RemovalItem]) -> Result<()> {
713-
let backup_dir = dirs::home_dir()
714-
.context("Could not determine home directory")?
715-
.join(".cortex-backup")
716-
.join(chrono::Local::now().format("%Y%m%d_%H%M%S").to_string());
720+
let backup_root = get_backup_root()?;
721+
let backup_dir = backup_root.join(chrono::Local::now().format("%Y%m%d_%H%M%S").to_string());
722+
let home_dir = dirs::home_dir().unwrap_or_default();
717723

718724
fs::create_dir_all(&backup_dir)?;
719725

@@ -723,19 +729,19 @@ fn create_backup(items: &[RemovalItem]) -> Result<()> {
723729
if !item.path.exists() {
724730
continue;
725731
}
732+
if item.path == backup_root || item.path.starts_with(&backup_root) {
733+
continue;
734+
}
726735

727-
let relative_path = item
728-
.path
729-
.strip_prefix(dirs::home_dir().unwrap_or_default())
730-
.unwrap_or(&item.path);
736+
let relative_path = backup_relative_path(&item.path, &home_dir);
731737
let backup_path = backup_dir.join(relative_path);
732738

733739
if let Some(parent) = backup_path.parent() {
734740
fs::create_dir_all(parent)?;
735741
}
736742

737743
if item.path.is_dir() {
738-
copy_dir_all(&item.path, &backup_path)?;
744+
copy_dir_all_excluding(&item.path, &backup_path, &backup_root)?;
739745
} else {
740746
fs::copy(&item.path, &backup_path)?;
741747
}
@@ -745,16 +751,42 @@ fn create_backup(items: &[RemovalItem]) -> Result<()> {
745751
Ok(())
746752
}
747753

748-
/// Copy a directory recursively.
749-
fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
754+
fn get_backup_root() -> Result<PathBuf> {
755+
Ok(cortex_common::get_cortex_home()
756+
.context("Could not determine Cortex home directory")?
757+
.join(".cortex-backup"))
758+
}
759+
760+
fn backup_relative_path(path: &Path, home_dir: &Path) -> PathBuf {
761+
if let Ok(relative_path) = path.strip_prefix(home_dir) {
762+
return relative_path.to_path_buf();
763+
}
764+
765+
path.components()
766+
.filter_map(|component| match component {
767+
Component::Normal(part) => Some(part),
768+
_ => None,
769+
})
770+
.collect()
771+
}
772+
773+
fn path_contains(path: &Path, child: &Path) -> bool {
774+
path == child || child.starts_with(path)
775+
}
776+
777+
/// Copy a directory recursively, skipping a path if it appears inside the source.
778+
fn copy_dir_all_excluding(src: &Path, dst: &Path, excluded: &Path) -> Result<()> {
750779
fs::create_dir_all(dst)?;
751780
for entry in fs::read_dir(src)? {
752781
let entry = entry?;
753782
let src_path = entry.path();
783+
if !excluded.as_os_str().is_empty() && path_contains(&src_path, excluded) {
784+
continue;
785+
}
754786
let dst_path = dst.join(entry.file_name());
755787

756788
if src_path.is_dir() {
757-
copy_dir_all(&src_path, &dst_path)?;
789+
copy_dir_all_excluding(&src_path, &dst_path, excluded)?;
758790
} else {
759791
fs::copy(&src_path, &dst_path)?;
760792
}
@@ -926,6 +958,7 @@ fn clean_rc_file(path: &Path, patterns: &[&str]) -> Result<()> {
926958
#[cfg(test)]
927959
mod tests {
928960
use super::*;
961+
use serial_test::serial;
929962

930963
#[test]
931964
fn test_format_size() {
@@ -976,4 +1009,44 @@ mod tests {
9761009
| InstallMethod::Unknown => {}
9771010
}
9781011
}
1012+
1013+
#[test]
1014+
#[serial]
1015+
fn test_backup_root_uses_cortex_home() {
1016+
let original = std::env::var_os("CORTEX_HOME");
1017+
let temp_dir = tempfile::tempdir().unwrap();
1018+
let cortex_home = temp_dir.path().join("custom-cortex-home");
1019+
1020+
// SAFETY: This serialized test restores CORTEX_HOME before returning.
1021+
unsafe {
1022+
std::env::set_var("CORTEX_HOME", &cortex_home);
1023+
}
1024+
1025+
assert_eq!(
1026+
get_backup_root().unwrap(),
1027+
cortex_home.join(".cortex-backup")
1028+
);
1029+
1030+
// SAFETY: This serialized test restores CORTEX_HOME to its original value.
1031+
unsafe {
1032+
match original {
1033+
Some(value) => std::env::set_var("CORTEX_HOME", value),
1034+
None => std::env::remove_var("CORTEX_HOME"),
1035+
}
1036+
}
1037+
}
1038+
1039+
#[test]
1040+
fn test_backup_relative_path_never_returns_absolute_path() {
1041+
let relative_path = backup_relative_path(
1042+
Path::new("/tmp/custom-cortex-home/config.toml"),
1043+
Path::new("/home/tester"),
1044+
);
1045+
1046+
assert!(!relative_path.is_absolute());
1047+
assert_eq!(
1048+
relative_path,
1049+
PathBuf::from("tmp/custom-cortex-home/config.toml")
1050+
);
1051+
}
9791052
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use std::fs;
2+
use std::process::Command;
3+
4+
use tempfile::tempdir;
5+
6+
#[test]
7+
fn uninstall_backup_dry_run_uses_cortex_home() {
8+
let home_dir = tempdir().unwrap();
9+
let cortex_home = home_dir.path().join("custom-cortex-home");
10+
fs::create_dir_all(&cortex_home).unwrap();
11+
fs::write(cortex_home.join("config.toml"), "model = \"test\"\n").unwrap();
12+
13+
let output = Command::new(env!("CARGO_BIN_EXE_Cortex"))
14+
.args(["uninstall", "--backup", "--dry-run"])
15+
.env("HOME", home_dir.path())
16+
.env("CORTEX_HOME", &cortex_home)
17+
.output()
18+
.unwrap();
19+
20+
let stdout = String::from_utf8_lossy(&output.stdout);
21+
let stderr = String::from_utf8_lossy(&output.stderr);
22+
assert!(
23+
output.status.success(),
24+
"uninstall dry-run failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
25+
);
26+
assert!(
27+
stdout.contains(&*cortex_home.join(".cortex-backup").to_string_lossy()),
28+
"stdout did not contain CORTEX_HOME backup path:\n{stdout}"
29+
);
30+
}

0 commit comments

Comments
 (0)