use crate::config::{get_biovault_home, Config};
use std::fs;
use std::path::Path;
use tracing::info;
const CURRENT_VERSION: &str = "0.1.28";
pub fn check_and_upgrade() -> anyhow::Result<()> {
let biovault_home = get_biovault_home()?;
let config_file = biovault_home.join("config.yaml");
if !config_file.exists() {
return Ok(());
}
let mut config = Config::from_file(&config_file)?;
let stored_version = config
.version
.clone()
.unwrap_or_else(|| "0.0.0".to_string());
if stored_version != CURRENT_VERSION {
info!(
"Upgrading BioVault from {} to {}",
stored_version, CURRENT_VERSION
);
upgrade_templates(&biovault_home)?;
if version_less_than(&stored_version, "0.1.27") {
fix_permission_yaml_indentation(&config)?;
}
config.version = Some(CURRENT_VERSION.to_string());
config.save(&config_file)?;
println!("✓ BioVault upgraded to version {}", CURRENT_VERSION);
}
Ok(())
}
fn upgrade_templates(biovault_home: &std::path::Path) -> anyhow::Result<()> {
let env_dir = biovault_home.join("env");
if !env_dir.exists() {
fs::create_dir_all(&env_dir)?;
}
let default_dir = env_dir.join("default");
if !default_dir.exists() {
fs::create_dir_all(&default_dir)?;
}
let template_nf_content = include_str!("../templates/default/template.nf");
let template_nf_path = default_dir.join("template.nf");
fs::write(&template_nf_path, template_nf_content)?;
info!("Updated default template.nf");
let nextflow_config_content = include_str!("../templates/default/nextflow.config");
let nextflow_config_path = default_dir.join("nextflow.config");
fs::write(&nextflow_config_path, nextflow_config_content)?;
info!("Updated default nextflow.config");
let snp_dir = env_dir.join("snp");
if !snp_dir.exists() {
fs::create_dir_all(&snp_dir)?;
}
let snp_template_nf_content = include_str!("../templates/snp/template.nf");
let snp_template_nf_path = snp_dir.join("template.nf");
fs::write(&snp_template_nf_path, snp_template_nf_content)?;
info!("Updated SNP template.nf");
let snp_nextflow_config_content = include_str!("../templates/snp/nextflow.config");
let snp_nextflow_config_path = snp_dir.join("nextflow.config");
fs::write(&snp_nextflow_config_path, snp_nextflow_config_content)?;
info!("Updated SNP nextflow.config");
let sheet_dir = env_dir.join("sheet");
if !sheet_dir.exists() {
fs::create_dir_all(&sheet_dir)?;
}
let sheet_template_nf_content = include_str!("../templates/sheet/template.nf");
let sheet_template_nf_path = sheet_dir.join("template.nf");
fs::write(&sheet_template_nf_path, sheet_template_nf_content)?;
info!("Updated sheet template.nf");
let sheet_nextflow_config_content = include_str!("../templates/sheet/nextflow.config");
let sheet_nextflow_config_path = sheet_dir.join("nextflow.config");
fs::write(&sheet_nextflow_config_path, sheet_nextflow_config_content)?;
info!("Updated sheet nextflow.config");
println!("✓ Templates updated:");
println!(" - Default templates: {}", default_dir.display());
println!(" - SNP templates: {}", snp_dir.display());
println!(" - Sheet templates: {}", sheet_dir.display());
Ok(())
}
fn version_less_than(v1: &str, v2: &str) -> bool {
let parse_version =
|v: &str| -> Vec<u32> { v.split('.').filter_map(|s| s.parse::<u32>().ok()).collect() };
let v1_parts = parse_version(v1);
let v2_parts = parse_version(v2);
for i in 0..3 {
let v1_part = v1_parts.get(i).unwrap_or(&0);
let v2_part = v2_parts.get(i).unwrap_or(&0);
if v1_part < v2_part {
return true;
} else if v1_part > v2_part {
return false;
}
}
false
}
fn fix_permission_yaml_indentation(config: &Config) -> anyhow::Result<()> {
println!("🔧 Fixing YAML permission file indentation...");
let data_dir = match config.get_syftbox_data_dir() {
Ok(dir) => dir,
Err(e) => {
info!("Skipping permission file fix (no SyftBox data dir): {}", e);
return Ok(());
}
};
let rpc_permission_file = data_dir
.join("datasites")
.join(&config.email)
.join("app_data")
.join("biovault")
.join("rpc")
.join("syft.pub.yaml");
if rpc_permission_file.exists() {
fix_yaml_file(&rpc_permission_file)?;
println!(
" ✓ Fixed RPC permission file: {}",
rpc_permission_file.display()
);
}
let app_permission_file = data_dir
.join("datasites")
.join(&config.email)
.join("app_data")
.join("biovault")
.join("syft.pub.yaml");
if app_permission_file.exists() {
fix_yaml_file(&app_permission_file)?;
println!(
" ✓ Fixed app permission file: {}",
app_permission_file.display()
);
}
Ok(())
}
fn fix_yaml_file(path: &Path) -> anyhow::Result<()> {
let s = path.to_string_lossy();
let is_rpc =
s.contains("/rpc/") || s.contains("\\rpc\\") || s.ends_with("/rpc") || s.ends_with("\\rpc");
let new_content = if is_rpc {
r#"rules:
- pattern: '**/*.request'
access:
admin: []
read:
- '*'
write:
- '*'
"#
} else {
r#"rules:
- pattern: '{{.UserEmail}}/*'
access:
admin: []
read:
- '*'
write: []
"#
};
fs::write(path, new_content)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config;
use tempfile::TempDir;
#[test]
fn version_compare_works() {
assert!(version_less_than("0.1.0", "0.1.27"));
assert!(!version_less_than("0.2.0", "0.1.27"));
assert!(!version_less_than("0.1.27", "0.1.27"));
assert!(version_less_than("0.1", "0.1.1"));
}
#[test]
fn upgrade_writes_templates_and_fixes_permissions() {
let tmp = TempDir::new().unwrap();
config::set_test_biovault_home(tmp.path().join(".bv"));
std::fs::create_dir_all(tmp.path().join(".bv")).unwrap();
let email = "user@example.com";
let cfg_path = tmp.path().join(".bv/config.yaml");
let syft_dir = tmp.path().join("syft");
std::fs::create_dir_all(&syft_dir).unwrap();
let syft_cfg = syft_dir.join("config.json");
let data_root = tmp.path().join("data");
std::fs::create_dir_all(&data_root).unwrap();
std::fs::write(
&syft_cfg,
serde_json::json!({"data_dir": data_root.to_string_lossy()}).to_string(),
)
.unwrap();
let cfg = Config {
email: email.into(),
syftbox_config: Some(syft_cfg.to_string_lossy().to_string()),
version: Some("0.1.0".into()),
binary_paths: None,
syftbox_credentials: None,
};
cfg.save(&cfg_path).unwrap();
let rpc_perm = data_root
.join("datasites")
.join(email)
.join("app_data/biovault/rpc/syft.pub.yaml");
let app_perm = data_root
.join("datasites")
.join(email)
.join("app_data/biovault/syft.pub.yaml");
std::fs::create_dir_all(rpc_perm.parent().unwrap()).unwrap();
std::fs::create_dir_all(app_perm.parent().unwrap()).unwrap();
std::fs::write(&rpc_perm, "bad").unwrap();
std::fs::write(&app_perm, "bad").unwrap();
check_and_upgrade().unwrap();
let updated = Config::from_file(&cfg_path).unwrap();
assert_eq!(updated.version.as_deref(), Some(CURRENT_VERSION));
let env_dir = tmp.path().join(".bv/env");
assert!(env_dir.join("default/template.nf").exists());
assert!(env_dir.join("default/nextflow.config").exists());
assert!(env_dir.join("snp/template.nf").exists());
assert!(env_dir.join("snp/nextflow.config").exists());
let rpc_fixed = std::fs::read_to_string(&rpc_perm).unwrap();
assert!(rpc_fixed.contains("**/*.request"));
let app_fixed = std::fs::read_to_string(&app_perm).unwrap();
assert!(app_fixed.contains("rules:"));
config::clear_test_biovault_home();
}
}