use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::error::{DotAgentError, Result};
const PROFILES_DIR: &str = "profiles";
const IGNORED_FILES: &[&str] = &[".git", ".DS_Store", ".gitignore", ".gitkeep"];
const IGNORED_EXTENSIONS: &[&str] = &[];
pub struct Profile {
pub name: String,
pub path: PathBuf,
}
impl Profile {
pub fn new(name: String, path: PathBuf) -> Self {
Self { name, path }
}
pub fn list_files(&self) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
for entry in WalkDir::new(&self.path).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_file() && !should_ignore(path) {
if let Ok(relative) = path.strip_prefix(&self.path) {
files.push(relative.to_path_buf());
}
}
}
files.sort();
Ok(files)
}
pub fn contents_summary(&self) -> String {
let mut summary = Vec::new();
if let Ok(entries) = fs::read_dir(&self.path) {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_dir() {
let name = path.file_name().unwrap().to_string_lossy().to_string();
let count = WalkDir::new(&path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file() && !should_ignore(e.path()))
.count();
if count > 0 {
summary.push(format!("{} ({})", name, count));
}
} else if path.is_file() {
let name = path.file_name().unwrap().to_string_lossy().to_string();
if !should_ignore(&path) {
summary.push(name);
}
}
}
}
if summary.is_empty() {
"(empty)".to_string()
} else {
summary.join(", ")
}
}
}
pub struct ProfileManager {
base_dir: PathBuf,
}
impl ProfileManager {
pub fn new(base_dir: PathBuf) -> Self {
Self { base_dir }
}
pub fn profiles_dir(&self) -> PathBuf {
self.base_dir.join(PROFILES_DIR)
}
pub fn list_profiles(&self) -> Result<Vec<Profile>> {
let profiles_dir = self.profiles_dir();
if !profiles_dir.exists() {
return Ok(Vec::new());
}
let mut profiles = Vec::new();
for entry in fs::read_dir(&profiles_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let name = path.file_name().unwrap().to_string_lossy().to_string();
profiles.push(Profile::new(name, path));
}
}
profiles.sort_by(|a, b| a.name.cmp(&b.name));
Ok(profiles)
}
pub fn get_profile(&self, name: &str) -> Result<Profile> {
let path = self.profiles_dir().join(name);
if !path.exists() {
return Err(DotAgentError::ProfileNotFound {
name: name.to_string(),
});
}
Ok(Profile::new(name.to_string(), path))
}
pub fn create_profile(&self, name: &str) -> Result<Profile> {
validate_profile_name(name)?;
let path = self.profiles_dir().join(name);
if path.exists() {
return Err(DotAgentError::ProfileAlreadyExists {
name: name.to_string(),
});
}
fs::create_dir_all(&path)?;
Ok(Profile::new(name.to_string(), path))
}
pub fn remove_profile(&self, name: &str) -> Result<()> {
let profile = self.get_profile(name)?;
fs::remove_dir_all(&profile.path)?;
Ok(())
}
pub fn copy_profile(&self, source_name: &str, dest_name: &str, force: bool) -> Result<Profile> {
let source = self.get_profile(source_name)?;
validate_profile_name(dest_name)?;
let dest_path = self.profiles_dir().join(dest_name);
if dest_path.exists() {
if !force {
return Err(DotAgentError::ProfileAlreadyExists {
name: dest_name.to_string(),
});
}
fs::remove_dir_all(&dest_path)?;
}
copy_dir_recursive(&source.path, &dest_path)?;
Ok(Profile::new(dest_name.to_string(), dest_path))
}
pub fn import_profile(&self, source: &Path, name: &str, force: bool) -> Result<Profile> {
validate_profile_name(name)?;
if !source.exists() {
return Err(DotAgentError::TargetNotFound {
path: source.to_path_buf(),
});
}
let dest = self.profiles_dir().join(name);
if dest.exists() {
if !force {
return Err(DotAgentError::ProfileAlreadyExists {
name: name.to_string(),
});
}
fs::remove_dir_all(&dest)?;
}
fs::create_dir_all(self.profiles_dir())?;
copy_dir_recursive(source, &dest)?;
Ok(Profile::new(name.to_string(), dest))
}
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) {
let src_path = entry.path();
let relative = src_path.strip_prefix(src).unwrap();
let dst_path = dst.join(relative);
if src_path.is_dir() {
fs::create_dir_all(&dst_path)?;
} else if src_path.is_file() && !should_ignore(src_path) {
if let Some(parent) = dst_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(src_path, &dst_path)?;
}
}
Ok(())
}
fn validate_profile_name(name: &str) -> Result<()> {
if name.is_empty() || name.len() > 64 {
return Err(DotAgentError::InvalidProfileName {
name: name.to_string(),
});
}
let first_char = name.chars().next().unwrap();
if !first_char.is_ascii_alphabetic() {
return Err(DotAgentError::InvalidProfileName {
name: name.to_string(),
});
}
for c in name.chars() {
if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
return Err(DotAgentError::InvalidProfileName {
name: name.to_string(),
});
}
}
Ok(())
}
fn should_ignore(path: &Path) -> bool {
if let Some(name) = path.file_name() {
let name = name.to_string_lossy();
if IGNORED_FILES.contains(&name.as_ref()) {
return true;
}
}
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy();
if IGNORED_EXTENSIONS.contains(&ext.as_ref()) {
return true;
}
}
false
}