use anyhow::{Context, Result};
use chrono::Utc;
use fs2::FileExt;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use crate::agents::Backup;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct BackupInfo {
pub agent_name: String,
pub timestamp: String,
pub file_path: PathBuf,
pub size_bytes: u64,
}
pub struct BackupManager {
backup_dir: PathBuf,
max_per_agent: usize,
}
impl BackupManager {
pub fn new() -> Result<Self> {
let backup_dir = dirs::home_dir()
.context("无法获取用户主目录")?
.join(".agentswitch")
.join("backups");
fs::create_dir_all(&backup_dir).context("创建备份目录失败")?;
Ok(Self {
backup_dir,
max_per_agent: 10,
})
}
pub fn with_max_backups(mut self, max: usize) -> Self {
self.max_per_agent = max;
self
}
pub fn create_backup(
&self,
agent_name: &str,
config_path: &Path,
format: &str,
) -> Result<Backup> {
if !config_path.exists() {
anyhow::bail!("配置文件不存在: {:?}", config_path);
}
let file_size = fs::metadata(config_path)?.len();
self.check_disk_space(file_size * 2)?;
let timestamp = Utc::now();
let backup_filename = format!("backup-{}.{}", timestamp.format("%Y%m%d-%H%M%S"), format);
let backup_dir = self.backup_dir.join(agent_name);
fs::create_dir_all(&backup_dir).context("创建备份目录失败")?;
let backup_path = backup_dir.join(&backup_filename);
let _lock = self.acquire_lock()?;
fs::copy(config_path, &backup_path).context("创建备份文件失败")?;
#[cfg(unix)]
{
#[allow(unused_imports)]
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&backup_path)?.permissions();
perms.set_mode(0o600);
fs::set_permissions(&backup_path, perms).context("设置备份文件权限失败")?;
}
self.cleanup_old_backups(agent_name)?;
Ok(Backup {
agent_name: agent_name.to_string(),
original_config_path: config_path.to_path_buf(),
backup_path,
timestamp,
})
}
pub fn restore_backup(&self, backup: &Backup) -> Result<()> {
if !backup.backup_path.exists() {
anyhow::bail!("备份文件不存在: {:?}", backup.backup_path);
}
let _lock = self.acquire_lock()?;
let original_path = &backup.original_config_path;
if original_path.exists() {
let timestamp = Utc::now().format("%Y%m%d-%H%M%S");
let restore_backup_name = format!("restore-{}.bak", timestamp);
let restore_backup_path = self
.backup_dir
.join(&backup.agent_name)
.join(&restore_backup_name);
let _ = fs::copy(original_path, &restore_backup_path);
}
fs::copy(&backup.backup_path, original_path).context("恢复配置文件失败")?;
Ok(())
}
pub fn clean_old_backups_by_duration(&self, older_seconds: i64) -> Result<usize> {
let now = Utc::now();
let entries = fs::read_dir(&self.backup_dir).context("读取备份目录失败")?;
let mut cleaned_count = 0;
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Ok(metadata) = fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
let modified_time: chrono::DateTime<chrono::Utc> = modified.into();
let age = now.signed_duration_since(modified_time);
if age.num_seconds() > older_seconds {
fs::remove_file(&path)?;
cleaned_count += 1;
}
}
}
}
}
Ok(cleaned_count)
}
pub fn list_all_backups(&self) -> Result<Vec<BackupInfo>> {
let mut backups = Vec::new();
if !self.backup_dir.exists() {
return Ok(backups);
}
let entries = fs::read_dir(&self.backup_dir).context("读取备份目录失败")?;
for entry in entries.flatten() {
let agent_dir = entry.path();
if agent_dir.is_dir() {
let agent_name = agent_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
if let Ok(entries) = fs::read_dir(&agent_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file()
&& (path.extension().and_then(|s| s.to_str()) == Some("json")
|| path.extension().and_then(|s| s.to_str()) == Some("toml"))
{
#[allow(clippy::collapsible_if)]
if let Ok(metadata) = fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
let modified_time: chrono::DateTime<chrono::Utc> =
modified.into();
backups.push(BackupInfo {
agent_name: agent_name.to_string(),
timestamp: modified_time
.format("%Y-%m-%d %H:%M:%S")
.to_string(),
file_path: path.clone(),
size_bytes: metadata.len(),
});
}
}
}
}
}
}
}
backups.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
Ok(backups)
}
pub fn find_backup(&self, agent: &str, timestamp: &str) -> Result<Backup> {
let agent_dir = self.backup_dir.join(agent);
if !agent_dir.exists() {
anyhow::bail!("未找到 {} 的备份", agent);
}
let entries = fs::read_dir(&agent_dir).context("读取备份目录失败")?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if filename.contains(timestamp) {
let modified = fs::metadata(&path)?.modified()?;
let timestamp_dt: chrono::DateTime<chrono::Utc> = modified.into();
return Ok(Backup {
agent_name: agent.to_string(),
original_config_path: PathBuf::new(), backup_path: path,
timestamp: timestamp_dt,
});
}
}
}
anyhow::bail!("未找到时间戳为 {} 的备份", timestamp)
}
fn cleanup_old_backups(&self, agent_name: &str) -> Result<()> {
let backup_dir = self.backup_dir.join(agent_name);
if !backup_dir.exists() {
return Ok(());
}
let mut backups: Vec<_> = fs::read_dir(&backup_dir)
.context("读取备份目录失败")?
.filter_map(|entry| entry.ok())
.collect();
backups.sort_by(|a, b| {
let a_time = b.metadata().ok().and_then(|m| m.modified().ok());
let b_time = a.metadata().ok().and_then(|m| m.modified().ok());
b_time.cmp(&a_time)
});
if backups.len() > self.max_per_agent {
for old_backup in backups.into_iter().skip(self.max_per_agent) {
let path = old_backup.path();
if path.is_file() {
let _ = fs::remove_file(path);
}
}
}
Ok(())
}
fn check_disk_space(&self, required_bytes: u64) -> Result<()> {
#[cfg(unix)]
{
if let Ok(stat) = fs2::statvfs(&self.backup_dir) {
let available = stat.available_space();
if available < required_bytes {
anyhow::bail!(
"磁盘空间不足。需要 {} 字节,可用 {} 字节。\n\
建议运行 'asw backup clean' 清理旧备份或清理磁盘空间",
required_bytes,
available
);
}
}
}
Ok(())
}
fn acquire_lock(&self) -> Result<File> {
let lock_path = self.backup_dir.join(".lock");
let file = File::create(&lock_path).context("创建锁文件失败")?;
#[allow(clippy::let_unit_value)]
let _try_lock = file
.try_lock_exclusive()
.context("获取文件锁失败,可能有其他进程正在操作")?;
Ok(file)
}
}
impl Default for BackupManager {
fn default() -> Self {
Self::new().expect("创建 BackupManager 失败")
}
}