#![allow(clippy::ptr_arg, clippy::inherent_to_string)]
pub mod background_service;
pub mod backup;
pub mod checker;
pub mod config;
pub mod downloader;
pub mod manager;
pub mod platform;
pub mod safety;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "windows")]
pub mod windows;
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tracing::info;
use uuid::Uuid;
pub use background_service::{BackgroundUpdateService, ServiceStatistics, ServiceStatus};
pub use backup::{BackupManager, BackupMetadata, BackupStorageStats, BackupType};
pub use checker::UpdateChecker;
pub use config::UpdateSource;
pub use config::UpgradeConfig;
pub use downloader::{ProgressCallback, UpdateDownloader};
pub use manager::UpgradeManager;
pub use safety::{CompatibilityReport, ResourceReport, SafetyChecker};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct ApplicationVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub pre_release: Option<String>,
pub build_metadata: Option<String>,
pub build_date: Option<DateTime<Utc>>,
pub git_commit: Option<String>,
}
impl ApplicationVersion {
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
pre_release: None,
build_metadata: None,
build_date: None,
git_commit: None,
}
}
pub fn current() -> Self {
Self {
major: 0,
minor: 3,
patch: 1,
pre_release: None,
build_metadata: None,
build_date: Some(Utc::now()),
git_commit: option_env!("GIT_COMMIT").map(String::from),
}
}
pub fn to_string(&self) -> String {
let mut version = format!("{}.{}.{}", self.major, self.minor, self.patch);
if let Some(pre) = &self.pre_release {
version.push_str(&format!("-{}", pre));
}
if let Some(build) = &self.build_metadata {
version.push_str(&format!("+{}", build));
}
version
}
pub fn is_newer_than(&self, other: &Self) -> bool {
self > other
}
pub fn is_compatible_with(&self, other: &Self) -> bool {
self.major == other.major
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateInfo {
pub version: ApplicationVersion,
pub release_date: DateTime<Utc>,
pub changelog: String,
pub download_urls: HashMap<String, String>, pub checksums: HashMap<String, String>, pub signatures: HashMap<String, String>, pub size_bytes: HashMap<String, u64>, pub is_critical: bool,
pub is_security_update: bool,
pub minimum_version: Option<ApplicationVersion>,
pub deprecation_warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UpgradeStatus {
UpToDate,
Available(UpdateInfo),
Checking,
Downloading {
progress: f32,
bytes_downloaded: u64,
total_bytes: u64,
speed_bytes_per_sec: u64,
},
Installing {
stage: InstallationStage,
progress: f32,
},
Completed {
old_version: ApplicationVersion,
new_version: ApplicationVersion,
restart_required: bool,
},
Failed {
error: String,
recovery_available: bool,
},
RollingBack {
target_version: ApplicationVersion,
progress: f32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum InstallationStage {
PreparingBackup,
CreatingBackup,
VerifyingUpdate,
StoppingServices,
InstallingFiles,
UpdatingConfiguration,
StartingServices,
VerifyingInstallation,
CleaningUp,
}
impl InstallationStage {
pub fn description(&self) -> &'static str {
match self {
Self::PreparingBackup => "Preparing backup",
Self::CreatingBackup => "Creating backup",
Self::VerifyingUpdate => "Verifying update package",
Self::StoppingServices => "Stopping services",
Self::InstallingFiles => "Installing files",
Self::UpdatingConfiguration => "Updating configuration",
Self::StartingServices => "Starting services",
Self::VerifyingInstallation => "Verifying installation",
Self::CleaningUp => "Cleaning up",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpgradeEvent {
pub id: Uuid,
pub timestamp: DateTime<Utc>,
pub event_type: UpgradeEventType,
pub version: Option<ApplicationVersion>,
pub message: String,
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UpgradeEventType {
UpdateCheckStarted,
UpdateCheckCompleted,
UpdateCheckFailed,
UpdateAvailable,
DownloadStarted,
DownloadProgress,
DownloadCompleted,
DownloadFailed,
InstallationStarted,
InstallationProgress,
InstallationCompleted,
InstallationFailed,
RollbackStarted,
RollbackCompleted,
RollbackFailed,
ConfigurationUpdated,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum UpdateChannel {
#[default]
Stable,
Beta,
Nightly,
Custom(String),
}
impl UpdateChannel {
pub fn as_str(&self) -> &str {
match self {
Self::Stable => "stable",
Self::Beta => "beta",
Self::Nightly => "nightly",
Self::Custom(name) => name,
}
}
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"stable" => Self::Stable,
"beta" => Self::Beta,
"nightly" => Self::Nightly,
custom => Self::Custom(custom.to_string()),
}
}
}
#[async_trait::async_trait]
pub trait PlatformUpgradeHandler: Send + Sync {
fn supports_seamless_upgrade(&self) -> bool;
async fn prepare_for_upgrade(&self) -> Result<()>;
async fn install_update(&self, package_path: &PathBuf) -> Result<()>;
async fn restart_application(&self) -> Result<()>;
async fn verify_installation(&self) -> Result<bool>;
async fn cleanup_after_upgrade(&self) -> Result<()>;
fn requires_elevated_privileges(&self) -> bool;
fn get_installation_directory(&self) -> PathBuf;
fn get_backup_directory(&self) -> PathBuf;
}
#[derive(Debug, thiserror::Error)]
pub enum UpgradeError {
#[error("Network error during update check: {0}")]
NetworkError(String),
#[error("Invalid update package: {0}")]
InvalidPackage(String),
#[error("Verification failed: {0}")]
VerificationFailed(String),
#[error("Insufficient disk space: required {required} MB, available {available} MB")]
InsufficientDiskSpace { required: u64, available: u64 },
#[error("Platform not supported: {0}")]
PlatformNotSupported(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Backup failed: {0}")]
BackupFailed(String),
#[error("Installation failed: {0}")]
InstallationFailed(String),
#[error("Rollback failed: {0}")]
RollbackFailed(String),
#[error("Configuration error: {0}")]
ConfigurationError(String),
#[error("Upgrade cancelled by user")]
Cancelled,
#[error("Internal error: {0}")]
Internal(String),
}
impl From<anyhow::Error> for UpgradeError {
fn from(error: anyhow::Error) -> Self {
UpgradeError::Internal(error.to_string())
}
}
pub type UpgradeResult<T> = std::result::Result<T, UpgradeError>;
pub async fn init_upgrade_system(config: &crate::config::Config) -> Result<UpgradeManager> {
info!("Initializing upgrade system");
let upgrade_config = UpgradeConfig::from_config(config)?;
let manager = UpgradeManager::new(upgrade_config).await?;
info!("Upgrade system initialized successfully");
Ok(manager)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_application_version_comparison() {
let v1 = ApplicationVersion::new(1, 0, 0);
let v2 = ApplicationVersion::new(1, 0, 1);
let v3 = ApplicationVersion::new(2, 0, 0);
assert!(v2.is_newer_than(&v1));
assert!(v3.is_newer_than(&v2));
assert!(!v1.is_newer_than(&v2));
assert!(v1.is_compatible_with(&v2));
assert!(!v1.is_compatible_with(&v3));
}
#[test]
fn test_version_string_formatting() {
let mut version = ApplicationVersion::new(1, 2, 3);
assert_eq!(version.to_string(), "1.2.3");
version.pre_release = Some("beta.1".to_string());
assert_eq!(version.to_string(), "1.2.3-beta.1");
version.build_metadata = Some("20231201".to_string());
assert_eq!(version.to_string(), "1.2.3-beta.1+20231201");
}
#[test]
fn test_update_channel_conversion() {
assert_eq!(UpdateChannel::Stable.as_str(), "stable");
assert_eq!(UpdateChannel::from_str("beta"), UpdateChannel::Beta);
assert_eq!(
UpdateChannel::from_str("custom"),
UpdateChannel::Custom("custom".to_string())
);
}
}