use anyhow::{Context, Result, bail};
use std::path::{Path, PathBuf};
use tokio::fs;
use tracing::{debug, info, warn};
pub struct BackupManager {
original_path: PathBuf,
backup_path: PathBuf,
}
impl BackupManager {
pub fn new(executable_path: PathBuf) -> Self {
let mut backup_path = executable_path.clone();
backup_path.set_file_name(format!(
"{}.backup",
executable_path.file_name().unwrap_or_default().to_string_lossy()
));
Self {
original_path: executable_path,
backup_path,
}
}
pub async fn create_backup(&self) -> Result<()> {
if !self.original_path.exists() {
bail!("Original file does not exist: {:?}", self.original_path);
}
if self.backup_path.exists() {
debug!("Removing old backup at {:?}", self.backup_path);
fs::remove_file(&self.backup_path).await.context("Failed to remove old backup")?;
}
info!("Creating backup at {:?}", self.backup_path);
fs::copy(&self.original_path, &self.backup_path)
.await
.context("Failed to create backup")?;
#[cfg(unix)]
{
let metadata = fs::metadata(&self.original_path)
.await
.context("Failed to read original file metadata")?;
let permissions = metadata.permissions();
fs::set_permissions(&self.backup_path, permissions)
.await
.context("Failed to set backup permissions")?;
}
info!("Backup created successfully");
Ok(())
}
pub async fn restore_backup(&self) -> Result<()> {
if !self.backup_path.exists() {
bail!("No backup found at {:?}", self.backup_path);
}
warn!("Restoring from backup at {:?}", self.backup_path);
let mut attempts = 0;
const MAX_ATTEMPTS: u32 = 3;
while attempts < MAX_ATTEMPTS {
match self.attempt_restore().await {
Ok(()) => {
info!("Successfully restored from backup");
return Ok(());
}
Err(e) if attempts < MAX_ATTEMPTS - 1 => {
warn!("Restore attempt {} failed: {}. Retrying...", attempts + 1, e);
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
attempts += 1;
}
Err(e) => return Err(e),
}
}
bail!("Failed to restore backup after {MAX_ATTEMPTS} attempts")
}
async fn attempt_restore(&self) -> Result<()> {
if self.original_path.exists() {
fs::remove_file(&self.original_path)
.await
.context("Failed to remove corrupted binary")?;
}
fs::copy(&self.backup_path, &self.original_path)
.await
.context("Failed to restore backup")?;
#[cfg(unix)]
{
let metadata =
fs::metadata(&self.backup_path).await.context("Failed to read backup metadata")?;
let permissions = metadata.permissions();
fs::set_permissions(&self.original_path, permissions)
.await
.context("Failed to restore permissions")?;
}
Ok(())
}
pub async fn cleanup_backup(&self) -> Result<()> {
if self.backup_path.exists() {
debug!("Cleaning up backup at {:?}", self.backup_path);
fs::remove_file(&self.backup_path).await.context("Failed to remove backup")?;
}
Ok(())
}
pub fn backup_exists(&self) -> bool {
self.backup_path.exists()
}
pub fn backup_path(&self) -> &Path {
&self.backup_path
}
}