use crate::error::AppError; use chrono::Utc; use std::fs; use std::path::Path; const MAX_BACKUPS: usize = 10; /// 配置导入导出相关业务逻辑 pub struct ConfigService; impl ConfigService { /// 为当前 config.json 创建备份,返回备份 ID(若文件不存在则返回空字符串)。 pub fn create_backup(config_path: &Path) -> Result { if !config_path.exists() { return Ok(String::new()); } let timestamp = Utc::now().format("%Y%m%d_%H%M%S"); let backup_id = format!("backup_{timestamp}"); let backup_dir = config_path .parent() .ok_or_else(|| AppError::Config("Invalid config path".into()))? .join("backups"); fs::create_dir_all(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?; let backup_path = backup_dir.join(format!("{backup_id}.json")); let contents = fs::read(config_path).map_err(|e| AppError::io(config_path, e))?; fs::write(&backup_path, contents).map_err(|e| AppError::io(&backup_path, e))?; Self::cleanup_old_backups(&backup_dir, MAX_BACKUPS)?; Ok(backup_id) } fn cleanup_old_backups(backup_dir: &Path, retain: usize) -> Result<(), AppError> { if retain == 0 { return Ok(()); } let entries = match fs::read_dir(backup_dir) { Ok(iter) => iter .filter_map(|entry| entry.ok()) .filter(|entry| { entry .path() .extension() .map(|ext| ext == "json") .unwrap_or(false) }) .collect::>(), Err(_) => return Ok(()), }; if entries.len() <= retain { return Ok(()); } let remove_count = entries.len().saturating_sub(retain); let mut sorted = entries; sorted.sort_by(|a, b| { let a_time = a.metadata().and_then(|m| m.modified()).ok(); let b_time = b.metadata().and_then(|m| m.modified()).ok(); a_time.cmp(&b_time) }); for entry in sorted.into_iter().take(remove_count) { if let Err(err) = fs::remove_file(entry.path()) { log::warn!( "Failed to remove old backup {}: {}", entry.path().display(), err ); } } Ok(()) } }