use crate::error::{Error, Result};
use crate::profiles::{DEFAULT_PROFILE, PROFILES_DIR};
use crate::storage::StorageBackend;
use log::{debug, info, warn};
use std::path::Path;
type MigrationFn = std::sync::Arc<dyn Fn(&Path) -> Result<()> + Send + Sync>;
#[derive(Clone, Default)]
pub enum ProfileMigrator {
#[default]
Auto,
Custom(MigrationFn),
None,
}
impl std::fmt::Debug for ProfileMigrator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Auto => write!(f, "Auto"),
Self::None => write!(f, "None"),
Self::Custom(_) => write!(f, "Custom(<closure>)"),
}
}
}
pub fn migrate<S: StorageBackend>(
root_dir: &Path,
target_name: &str,
single_file_mode: bool,
storage: &S,
strategy: &ProfileMigrator,
) -> Result<()> {
let ext = storage.extension();
let manifest_filename = format!(".profiles.{ext}");
let manifest_path = root_dir.join(&manifest_filename);
if manifest_path.exists() {
if single_file_mode {
let legacy_file = root_dir.with_extension(ext);
if legacy_file.exists() && legacy_file.is_file() {
warn!(
"Detected partial profile migration for '{target_name}'. Legacy file exists alongside manifest. Retrying migration."
);
} else {
debug!("Profiles already initialized for '{target_name}'");
return Ok(());
}
} else {
debug!("Profiles already initialized for '{target_name}'");
return Ok(());
}
}
let needs_migration = if single_file_mode {
let ext = storage.extension();
let legacy_file = root_dir.with_extension(ext);
legacy_file.exists() && legacy_file.is_file()
} else {
root_dir.exists()
&& root_dir
.read_dir()
.map_err(|e| Error::DirectoryRead {
path: root_dir.to_path_buf(),
source: e,
})?
.count()
> 0
};
if !needs_migration {
return Ok(());
}
info!("Migrating '{target_name}' to profile structure...");
match strategy {
ProfileMigrator::None => {
warn!(
"Profiles enabled for '{target_name}' but flat structure detected and migration is disabled."
);
Ok(())
}
ProfileMigrator::Custom(func) => func(root_dir),
ProfileMigrator::Auto => {
run_auto_migration(root_dir, target_name, single_file_mode, storage)
}
}
}
fn run_auto_migration<S: StorageBackend>(
root_dir: &Path,
target_name: &str,
single_file_mode: bool,
storage: &S,
) -> Result<()> {
let default_profile_dir = root_dir.join(PROFILES_DIR).join(DEFAULT_PROFILE);
crate::utils::security::ensure_secure_dir(&default_profile_dir)?;
if single_file_mode {
let ext = storage.extension();
let legacy_file = root_dir.with_extension(ext);
let dest = default_profile_dir.join(format!("{target_name}.{ext}"));
debug!(
"Moving single file {} -> {}",
legacy_file.display(),
dest.display()
);
std::fs::rename(&legacy_file, &dest).map_err(|e| Error::FileWrite {
path: dest.clone(),
source: e,
})?;
} else {
if root_dir.is_dir() {
for entry in std::fs::read_dir(root_dir).map_err(|e| Error::DirectoryRead {
path: root_dir.to_path_buf(),
source: e,
})? {
let entry = entry.map_err(|e| Error::DirectoryRead {
path: root_dir.to_path_buf(),
source: e,
})?;
let path = entry.path();
let ext = storage.extension();
let manifest_filename = format!(".profiles.{ext}");
if path.ends_with(&manifest_filename) || path.ends_with(PROFILES_DIR) {
continue;
}
if let Some(name) = path.file_name() {
let dest = default_profile_dir.join(name);
debug!("Moving {:?} -> {:?}", path.display(), dest.display());
std::fs::rename(&path, &dest).map_err(|e| Error::FileWrite {
path: dest.clone(),
source: e,
})?;
}
}
}
}
let ext = storage.extension();
let manifest_filename = format!(".profiles.{ext}");
let manifest = crate::profiles::ProfileManifest::default();
let manifest_path = root_dir.join(&manifest_filename);
storage.write(&manifest_path, &manifest)?;
info!("Successfully migrated '{target_name}' to profiles");
Ok(())
}
pub fn rollback_migration<S: StorageBackend>(
root_dir: &Path,
target_name: &str,
single_file_mode: bool,
storage: &S,
) -> Result<()> {
let ext = storage.extension();
let manifest_filename = format!(".profiles.{ext}");
let manifest_path = root_dir.join(&manifest_filename);
if !manifest_path.exists() {
return Err(Error::Config(format!(
"Cannot rollback '{target_name}': not using profiles (no manifest found)"
)));
}
let manifest: crate::profiles::ProfileManifest = storage.read(&manifest_path)?;
let active_profile = &manifest.active;
info!(
"Rolling back '{target_name}' from profiles to flat structure (preserving '{active_profile}' profile)"
);
let active_profile_dir = root_dir.join(PROFILES_DIR).join(active_profile);
if !active_profile_dir.exists() {
return Err(Error::Config(format!(
"Active profile directory not found: {}",
active_profile_dir.display()
)));
}
if single_file_mode {
let source = active_profile_dir.join(format!("{target_name}.{ext}"));
let dest = root_dir.with_extension(ext);
if source.exists() {
debug!(
"Moving single file {} -> {}",
source.display(),
dest.display()
);
std::fs::rename(&source, &dest).map_err(|e| Error::FileWrite {
path: dest.clone(),
source: e,
})?;
}
} else {
if active_profile_dir.is_dir() {
for entry in
std::fs::read_dir(&active_profile_dir).map_err(|e| Error::DirectoryRead {
path: active_profile_dir.clone(),
source: e,
})?
{
let entry = entry.map_err(|e| Error::DirectoryRead {
path: active_profile_dir.clone(),
source: e,
})?;
let source = entry.path();
if let Some(name) = source.file_name() {
let dest = root_dir.join(name);
debug!("Moving {:?} -> {:?}", source.display(), dest.display());
std::fs::rename(&source, &dest).map_err(|e| Error::FileWrite {
path: dest.clone(),
source: e,
})?;
}
}
}
}
let profiles_dir = root_dir.join(PROFILES_DIR);
if profiles_dir.exists() {
std::fs::remove_dir_all(&profiles_dir).map_err(|e| Error::DirectoryRead {
path: profiles_dir.clone(),
source: e,
})?;
}
std::fs::remove_file(&manifest_path).map_err(|e| Error::FileDelete {
path: manifest_path.clone(),
source: e,
})?;
info!(
"Successfully rolled back '{target_name}' to flat structure (preserved '{active_profile}' profile data, other profiles deleted)"
);
Ok(())
}