#![allow(dead_code)]
use crate::update::method::UpdateError;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RollbackInfo {
pub previous_version: String,
pub new_version: String,
pub backup_path: PathBuf,
pub updated_at: u64,
}
impl RollbackInfo {
fn path() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".jarvy").join("rollback-info.json"))
}
pub fn load() -> Option<Self> {
let path = Self::path()?;
let content = fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
pub fn save(&self) -> std::io::Result<()> {
let path = Self::path().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "No home directory")
})?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let content = serde_json::to_string_pretty(self)?;
fs::write(path, content)
}
pub fn clear() -> std::io::Result<()> {
if let Some(path) = Self::path() {
if path.exists() {
fs::remove_file(path)?;
}
}
Ok(())
}
}
pub struct RollbackManager;
impl RollbackManager {
pub fn record_update(
previous_version: &str,
new_version: &str,
backup_path: &Path,
) -> Result<(), UpdateError> {
let info = RollbackInfo {
previous_version: previous_version.to_string(),
new_version: new_version.to_string(),
backup_path: backup_path.to_path_buf(),
updated_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
};
info.save().map_err(|e| {
UpdateError::RollbackFailed(format!("Cannot save rollback info: {}", e))
})?;
Ok(())
}
pub fn rollback() -> Result<RollbackResult, UpdateError> {
let info = RollbackInfo::load().ok_or_else(|| {
UpdateError::RollbackFailed("No rollback information available".to_string())
})?;
if !info.backup_path.exists() {
return Err(UpdateError::RollbackFailed(format!(
"Backup file not found: {}",
info.backup_path.display()
)));
}
let current_exe = std::env::current_exe()
.map_err(|e| UpdateError::RollbackFailed(format!("Cannot find current exe: {}", e)))?;
Self::restore_backup(&info.backup_path, ¤t_exe)?;
let _ = RollbackInfo::clear();
Ok(RollbackResult {
restored_version: info.previous_version,
replaced_version: info.new_version,
})
}
pub fn can_rollback() -> bool {
RollbackInfo::load()
.map(|info| info.backup_path.exists())
.unwrap_or(false)
}
pub fn info() -> Option<RollbackInfo> {
RollbackInfo::load().filter(|info| info.backup_path.exists())
}
fn restore_backup(backup: &Path, target: &Path) -> Result<(), UpdateError> {
fs::copy(backup, target)
.map_err(|e| UpdateError::RollbackFailed(format!("Restore failed: {}", e)))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(target)
.map_err(|e| UpdateError::RollbackFailed(e.to_string()))?
.permissions();
perms.set_mode(0o755);
fs::set_permissions(target, perms)
.map_err(|e| UpdateError::RollbackFailed(e.to_string()))?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct RollbackResult {
pub restored_version: String,
pub replaced_version: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rollback_info_serialization() {
let info = RollbackInfo {
previous_version: "1.0.0".to_string(),
new_version: "1.1.0".to_string(),
backup_path: PathBuf::from("/tmp/backup"),
updated_at: 1234567890,
};
let json = serde_json::to_string(&info).unwrap();
let parsed: RollbackInfo = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.previous_version, "1.0.0");
assert_eq!(parsed.new_version, "1.1.0");
assert_eq!(parsed.updated_at, 1234567890);
}
#[test]
fn test_can_rollback_no_info() {
let _ = RollbackManager::can_rollback();
}
}