use crate::utils::config::Config;
use anyhow::Result;
use semver::Version;
use serde::{Deserialize, Serialize};
const CURRENT_SCHEMA_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, Serialize, Deserialize)]
struct MigrationMeta {
schema_version: String,
last_migration: chrono::DateTime<chrono::Utc>,
migrations_applied: Vec<String>,
}
impl Default for MigrationMeta {
fn default() -> Self {
Self {
schema_version: CURRENT_SCHEMA_VERSION.to_string(),
last_migration: chrono::Utc::now(),
migrations_applied: Vec::new(),
}
}
}
pub async fn auto_migrate(config: &Config) -> Result<()> {
let meta_path = config.profiles_dir().join(".migration_meta.json");
tokio::fs::create_dir_all(config.profiles_dir()).await.ok();
let mut meta = if meta_path.exists() {
tokio::fs::read_to_string(&meta_path)
.await
.ok()
.and_then(|content| serde_json::from_str(&content).ok())
.unwrap_or_default()
} else {
MigrationMeta::default()
};
let current = Version::parse(CURRENT_SCHEMA_VERSION)?;
let stored =
Version::parse(&meta.schema_version).unwrap_or_else(|_| Version::parse("0.0.0").unwrap());
if stored >= current && meta_path.exists() {
return Ok(());
}
tracing::info!("Migrating from {} to {}", stored, current);
if stored < Version::parse("0.4.0")? {
migrate_to_v0_4_0(config, &mut meta).await?;
}
meta.schema_version = CURRENT_SCHEMA_VERSION.to_string();
meta.last_migration = chrono::Utc::now();
let meta_json = serde_json::to_string_pretty(&meta)?;
tokio::fs::write(&meta_path, meta_json).await?;
tracing::info!("Migration complete");
Ok(())
}
async fn migrate_to_v0_4_0(config: &Config, meta: &mut MigrationMeta) -> Result<()> {
tracing::info!("Applying migration: v0.4.0");
let profiles_dir = config.profiles_dir();
if !profiles_dir.exists() {
return Ok(());
}
let mut entries = tokio::fs::read_dir(profiles_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
if name.starts_with('.') || name == "backups" {
continue;
}
let profile_json_path = path.join("profile.json");
if profile_json_path.exists()
&& let Ok(content) = tokio::fs::read_to_string(&profile_json_path).await
&& let Ok(mut profile_meta) = serde_json::from_str::<serde_json::Value>(&content)
&& profile_meta.get("encrypted").is_none()
{
profile_meta["encrypted"] = serde_json::json!(false);
if let Ok(updated) = serde_json::to_string_pretty(&profile_meta) {
let _ = tokio::fs::write(&profile_json_path, updated).await;
}
}
}
let current_marker = profiles_dir.join(".current_profile");
let previous_marker = profiles_dir.join(".previous_profile");
if !current_marker.exists() {
let _ = tokio::fs::write(¤t_marker, "").await;
}
if !previous_marker.exists() {
let _ = tokio::fs::write(&previous_marker, "").await;
}
meta.migrations_applied.push("v0.4.0".to_string());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_migration_meta_creation() {
let temp_dir = TempDir::new().unwrap();
let config = Config::new(Some(temp_dir.path().to_path_buf())).unwrap();
auto_migrate(&config).await.unwrap();
let meta_path = config.profiles_dir().join(".migration_meta.json");
assert!(meta_path.exists());
let content = tokio::fs::read_to_string(&meta_path).await.unwrap();
let meta: MigrationMeta = serde_json::from_str(&content).unwrap();
assert_eq!(meta.schema_version, CURRENT_SCHEMA_VERSION);
}
}