use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context as _, Result};
use walkdir::WalkDir;
use crate::utils::config::Config;
pub fn copy_profile_files(src: &Path, dst: &Path, files: &[&str]) -> Result<Vec<String>> {
let mut copied = Vec::new();
for pattern in files {
let src_path = src.join(pattern);
if src_path.is_file() {
let dst_path = dst.join(pattern);
if let Some(parent) = dst_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {}", parent.display()))?;
}
std::fs::copy(&src_path, &dst_path).with_context(|| {
format!(
"Failed to copy {} to {}",
src_path.display(),
dst_path.display()
)
})?;
if let Ok(metadata) = std::fs::metadata(&src_path) {
let _ = std::fs::set_permissions(&dst_path, metadata.permissions());
}
copied.push((*pattern).to_string());
} else if src_path.is_dir() {
copy_dir_recursive(&src_path, &dst.join(pattern))?;
copied.push(format!("{pattern}/ (directory)"));
}
}
Ok(copied)
}
pub fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
std::fs::create_dir_all(dst)
.with_context(|| format!("Failed to create directory: {}", dst.display()))?;
if let Ok(metadata) = std::fs::metadata(src) {
let _ = std::fs::set_permissions(dst, metadata.permissions());
}
for entry in WalkDir::new(src)
.into_iter()
.filter_map(std::result::Result::ok)
{
let path = entry.path();
let Ok(relative) = path.strip_prefix(src) else {
continue;
};
let destination = dst.join(relative);
if path.is_file() {
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(path, &destination)?;
if let Ok(metadata) = std::fs::metadata(path) {
let _ = std::fs::set_permissions(&destination, metadata.permissions());
}
} else if path.is_dir() && path != src {
std::fs::create_dir_all(&destination)?;
if let Ok(metadata) = std::fs::metadata(path) {
let _ = std::fs::set_permissions(&destination, metadata.permissions());
}
}
}
Ok(())
}
pub fn create_backup(codex_dir: &Path, backup_dir: &Path) -> Result<PathBuf> {
use chrono::Local;
let timestamp = Local::now().format("%Y%m%d_%H%M%S");
let backup_name = format!("codex_backup_{timestamp}");
let backup_path = backup_dir.join(&backup_name);
if !codex_dir.exists() {
anyhow::bail!("Codex directory does not exist: {}", codex_dir.display());
}
copy_dir_recursive(codex_dir, &backup_path)?;
Ok(backup_path)
}
pub fn write_bytes_preserve_permissions(path: &Path, data: &[u8]) -> Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create parent directory: {}", parent.display()))?;
let existing_permissions = std::fs::metadata(path).ok().map(|m| m.permissions());
let temp_path = unique_temp_path(parent, ".codexctl_write");
let mut file = std::fs::File::create(&temp_path)
.with_context(|| format!("Failed to create temp file: {}", temp_path.display()))?;
file.write_all(data)
.with_context(|| format!("Failed to write temp file: {}", temp_path.display()))?;
file.sync_all()
.with_context(|| format!("Failed to sync temp file: {}", temp_path.display()))?;
drop(file);
if let Some(perms) = existing_permissions {
std::fs::set_permissions(&temp_path, perms).with_context(|| {
format!(
"Failed to preserve permissions for temp file: {}",
temp_path.display()
)
})?;
}
#[cfg(windows)]
if path.exists() {
std::fs::remove_file(path)
.with_context(|| format!("Failed to remove existing file: {}", path.display()))?;
}
std::fs::rename(&temp_path, path).with_context(|| {
format!(
"Failed to atomically replace {} with {}",
path.display(),
temp_path.display()
)
})?;
Ok(())
}
fn unique_temp_path(parent: &Path, prefix: &str) -> PathBuf {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let rand_suffix: u64 = rand::random::<u64>();
parent.join(format!("{prefix}_{ts}_{rand_suffix:016x}"))
}
#[must_use]
#[allow(dead_code)]
pub fn check_codex_installed() -> bool {
which::which("codex").is_ok()
}
#[must_use]
pub fn get_critical_files() -> &'static [&'static str] {
Config::critical_files()
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
#[test]
fn test_copy_dir_recursive() {
let src_dir = TempDir::new().unwrap();
let dst_dir = TempDir::new().unwrap();
let sub_dir = src_dir.path().join("subdir");
std::fs::create_dir(&sub_dir).unwrap();
std::fs::write(src_dir.path().join("file1.txt"), "content1").unwrap();
std::fs::write(sub_dir.join("file2.txt"), "content2").unwrap();
copy_dir_recursive(src_dir.path(), dst_dir.path()).unwrap();
assert!(dst_dir.path().join("file1.txt").exists());
assert!(dst_dir.path().join("subdir/file2.txt").exists());
assert_eq!(
std::fs::read_to_string(dst_dir.path().join("file1.txt")).unwrap(),
"content1"
);
}
#[test]
fn test_copy_profile_files() {
let src_dir = TempDir::new().unwrap();
let dst_dir = TempDir::new().unwrap();
std::fs::write(src_dir.path().join("auth.json"), "{\"token\": \"test\"}").unwrap();
std::fs::write(src_dir.path().join("config.toml"), "model = \"gpt-4\"").unwrap();
let sessions_dir = src_dir.path().join("sessions");
std::fs::create_dir(&sessions_dir).unwrap();
std::fs::write(sessions_dir.join("session1.json"), "{}").unwrap();
let files = vec!["auth.json", "config.toml", "sessions/"];
let copied = copy_profile_files(src_dir.path(), dst_dir.path(), &files).unwrap();
assert!(copied.contains(&"auth.json".to_string()));
assert!(copied.contains(&"config.toml".to_string()));
assert!(copied.iter().any(|c| c.starts_with("sessions")));
assert!(dst_dir.path().join("auth.json").exists());
assert!(dst_dir.path().join("sessions/session1.json").exists());
}
#[test]
fn test_create_backup() {
let codex_dir = TempDir::new().unwrap();
let backup_dir = TempDir::new().unwrap();
std::fs::write(
codex_dir.path().join("auth.json"),
"{\"token\": \"secret\"}",
)
.unwrap();
let backup_path = create_backup(codex_dir.path(), backup_dir.path()).unwrap();
assert!(backup_path.exists());
assert!(backup_path.join("auth.json").exists());
assert_eq!(
std::fs::read_to_string(backup_path.join("auth.json")).unwrap(),
"{\"token\": \"secret\"}"
);
}
#[test]
fn test_create_backup_nonexistent() {
let backup_dir = TempDir::new().unwrap();
let nonexistent = std::path::Path::new("/nonexistent/path/that/does/not/exist");
let result = create_backup(nonexistent, backup_dir.path());
assert!(result.is_err());
}
#[test]
fn test_get_critical_files() {
let files = get_critical_files();
assert!(!files.is_empty());
assert!(files.contains(&"auth.json"));
assert!(files.contains(&"config.toml"));
}
#[test]
fn test_check_codex_installed() {
let _result = check_codex_installed();
}
#[test]
fn test_write_bytes_preserve_permissions_preserves_bytes() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("config.toml");
let original = b"line1\r\nline2\n\xff\xfebinary";
std::fs::write(&path, original).unwrap();
let updated = b"new\r\ncontent\n\x00\xff";
write_bytes_preserve_permissions(&path, updated).unwrap();
let actual = std::fs::read(&path).unwrap();
assert_eq!(actual, updated);
}
#[cfg(unix)]
#[test]
fn test_write_bytes_preserve_permissions_preserves_mode_unix() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let path = dir.path().join("auth.json");
std::fs::write(&path, b"{\"token\":\"x\"}").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
write_bytes_preserve_permissions(&path, b"{\"token\":\"y\"}").unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600);
}
}