mirror of
https://github.com/pandorafuture/wx-cli.git
synced 2026-08-29 04:00:55 +00:00
188 lines
6.2 KiB
Rust
188 lines
6.2 KiB
Rust
use std::path::Path;
|
|
|
|
use crate::sudo::chown_to_sudo_user;
|
|
|
|
const OLD_CONFIG_DIR_NAME: &str = "wechat-utils";
|
|
const SENTINEL_FILE: &str = ".migrated";
|
|
|
|
pub(crate) enum MigrationOutcome {
|
|
AlreadyMigrated,
|
|
NoLegacyConfig,
|
|
Migrated,
|
|
}
|
|
|
|
/// Migrate config files from legacy `~/.config/wechat-utils/` to the new
|
|
/// platform-correct config directory.
|
|
///
|
|
/// Strategy: **move then delete originals**. Files are first copied to the
|
|
/// new location, then originals are deleted. If copy fails, originals are
|
|
/// left untouched. If delete fails, both copies exist but new location wins.
|
|
///
|
|
/// Sentinel is only written when all operations succeed. If any step
|
|
/// fails, returns an error and the sentinel is NOT written, allowing retry.
|
|
pub(crate) fn ensure_config_migrated(
|
|
home: &Path,
|
|
new_config_root: &Path,
|
|
) -> Result<MigrationOutcome, std::io::Error> {
|
|
let sentinel = new_config_root.join(SENTINEL_FILE);
|
|
if sentinel.exists() {
|
|
return Ok(MigrationOutcome::AlreadyMigrated);
|
|
}
|
|
|
|
let old_config = home.join(".config").join(OLD_CONFIG_DIR_NAME);
|
|
if !old_config.exists() {
|
|
std::fs::create_dir_all(new_config_root)?;
|
|
chown_config_tree(home, new_config_root);
|
|
std::fs::File::create(&sentinel)?;
|
|
chown_to_sudo_user(&sentinel);
|
|
return Ok(MigrationOutcome::NoLegacyConfig);
|
|
}
|
|
|
|
std::fs::create_dir_all(new_config_root)?;
|
|
chown_config_tree(home, new_config_root);
|
|
|
|
for file in &["keys.toml", "settings.toml"] {
|
|
let src = old_config.join(file);
|
|
let dst = new_config_root.join(file);
|
|
if src.exists() && !dst.exists() {
|
|
std::fs::copy(&src, &dst)?;
|
|
chown_to_sudo_user(&dst);
|
|
// Delete original after successful copy
|
|
std::fs::remove_file(&src)?;
|
|
}
|
|
}
|
|
|
|
std::fs::File::create(&sentinel)?;
|
|
chown_to_sudo_user(&sentinel);
|
|
Ok(MigrationOutcome::Migrated)
|
|
}
|
|
|
|
/// Chown all path segments from the new config root up to (but not including)
|
|
/// the home directory. This ensures intermediate directories created under
|
|
/// sudo (e.g. ~/Library/Application Support/wx-cli/) are owned by the
|
|
/// real user.
|
|
fn chown_config_tree(home: &Path, config_root: &Path) {
|
|
let mut current = config_root.to_path_buf();
|
|
while current.starts_with(home) && current != home {
|
|
chown_to_sudo_user(¤t);
|
|
match current.parent() {
|
|
Some(parent) => current = parent.to_path_buf(),
|
|
None => break,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::fs;
|
|
|
|
#[test]
|
|
fn migration_no_legacy_config() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let home = tmp.path();
|
|
let new_config = home.join("new_config");
|
|
|
|
let result = ensure_config_migrated(home, &new_config).unwrap();
|
|
assert!(matches!(result, MigrationOutcome::NoLegacyConfig));
|
|
assert!(new_config.join(SENTINEL_FILE).exists());
|
|
}
|
|
|
|
#[test]
|
|
fn migration_already_migrated() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let home = tmp.path();
|
|
let new_config = home.join("new_config");
|
|
fs::create_dir_all(&new_config).unwrap();
|
|
fs::File::create(new_config.join(SENTINEL_FILE)).unwrap();
|
|
|
|
let result = ensure_config_migrated(home, &new_config).unwrap();
|
|
assert!(matches!(result, MigrationOutcome::AlreadyMigrated));
|
|
}
|
|
|
|
#[test]
|
|
fn migration_moves_files() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let home = tmp.path();
|
|
|
|
// Create legacy config
|
|
let old_config = home.join(".config").join(OLD_CONFIG_DIR_NAME);
|
|
fs::create_dir_all(&old_config).unwrap();
|
|
fs::write(old_config.join("keys.toml"), "test-keys").unwrap();
|
|
fs::write(old_config.join("settings.toml"), "test-settings").unwrap();
|
|
|
|
let new_config = home.join("new_config");
|
|
let result = ensure_config_migrated(home, &new_config).unwrap();
|
|
|
|
match result {
|
|
MigrationOutcome::Migrated => {}
|
|
_ => panic!("expected Migrated"),
|
|
}
|
|
|
|
// New files exist
|
|
assert_eq!(
|
|
fs::read_to_string(new_config.join("keys.toml")).unwrap(),
|
|
"test-keys"
|
|
);
|
|
assert_eq!(
|
|
fs::read_to_string(new_config.join("settings.toml")).unwrap(),
|
|
"test-settings"
|
|
);
|
|
|
|
// Old files deleted
|
|
assert!(!old_config.join("keys.toml").exists());
|
|
assert!(!old_config.join("settings.toml").exists());
|
|
|
|
// Sentinel exists
|
|
assert!(new_config.join(SENTINEL_FILE).exists());
|
|
}
|
|
|
|
#[test]
|
|
fn migration_skips_existing_dst() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let home = tmp.path();
|
|
|
|
// Create legacy config
|
|
let old_config = home.join(".config").join(OLD_CONFIG_DIR_NAME);
|
|
fs::create_dir_all(&old_config).unwrap();
|
|
fs::write(old_config.join("keys.toml"), "old-keys").unwrap();
|
|
|
|
// Create new config with existing file
|
|
let new_config = home.join("new_config");
|
|
fs::create_dir_all(&new_config).unwrap();
|
|
fs::write(new_config.join("keys.toml"), "new-keys").unwrap();
|
|
|
|
let result = ensure_config_migrated(home, &new_config).unwrap();
|
|
match result {
|
|
MigrationOutcome::Migrated => {}
|
|
_ => panic!("expected Migrated"),
|
|
}
|
|
|
|
// New file unchanged
|
|
assert_eq!(
|
|
fs::read_to_string(new_config.join("keys.toml")).unwrap(),
|
|
"new-keys"
|
|
);
|
|
// Old file still exists (wasn't migrated because dst exists)
|
|
assert!(old_config.join("keys.toml").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn migration_is_idempotent() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let home = tmp.path();
|
|
|
|
let old_config = home.join(".config").join(OLD_CONFIG_DIR_NAME);
|
|
fs::create_dir_all(&old_config).unwrap();
|
|
fs::write(old_config.join("keys.toml"), "test").unwrap();
|
|
|
|
let new_config = home.join("new_config");
|
|
|
|
// First run
|
|
ensure_config_migrated(home, &new_config).unwrap();
|
|
// Second run
|
|
let result = ensure_config_migrated(home, &new_config).unwrap();
|
|
assert!(matches!(result, MigrationOutcome::AlreadyMigrated));
|
|
}
|
|
}
|