Files
rustfs/rustfs/src/update.rs
T

348 lines
12 KiB
Rust

// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::version;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use thiserror::Error;
use tracing::{debug, error, info};
/// Update check related errors
#[derive(Error, Debug)]
pub enum UpdateCheckError {
#[error("HTTP request failed: {0}")]
HttpError(#[from] reqwest::Error),
#[error("Version parsing failed: {0}")]
VersionParseError(String),
#[error("Invalid version response: {0}")]
InvalidResponse(String),
}
/// Version information structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionInfo {
/// Version number
pub version: String,
/// Release date
pub release_date: Option<String>,
/// Release notes
pub release_notes: Option<String>,
/// Download URL
pub download_url: Option<String>,
}
/// Update check result
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateCheckResult {
/// Whether update is available
pub update_available: bool,
/// Current version
pub current_version: String,
/// Latest version information
pub latest_version: Option<VersionInfo>,
/// Check time
pub check_time: jiff::Zoned,
}
/// Version checker
pub struct VersionChecker {
/// HTTP client
client: reqwest::Client,
/// Version server URL
version_url: String,
/// Request timeout
timeout: Duration,
}
impl Default for VersionChecker {
fn default() -> Self {
Self::new()
}
}
impl VersionChecker {
/// Create a new version checker
pub fn new() -> Self {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.user_agent(format!("RustFS/{}", get_current_version()))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
Self {
client,
version_url: "https://version.rustfs.com/latest.json".to_string(),
timeout: Duration::from_secs(10),
}
}
/// Create version checker with custom configuration
#[allow(dead_code)]
pub fn with_config(url: String, timeout: Duration) -> Self {
let client = reqwest::Client::builder()
.timeout(timeout)
.user_agent(format!("RustFS/{}", get_current_version()))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
Self {
client,
version_url: url,
timeout,
}
}
/// Check for updates
pub async fn check_for_updates(&self) -> Result<UpdateCheckResult, UpdateCheckError> {
let current_version = get_current_version();
debug!("Checking for updates, current version: {}", current_version);
// Send HTTP GET request to get latest version information
let response = self.client.get(&self.version_url).timeout(self.timeout).send().await?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
error!("Version check request failed, status code: {}, response: {}", status, error_text);
return Err(UpdateCheckError::InvalidResponse(format!(
"HTTP status code: {status}, response: {error_text}"
)));
}
// Parse response
let response_bytes = response.bytes().await?;
let version_info: VersionInfo = match serde_json::from_slice(&response_bytes) {
Ok(v) => v,
Err(e) => {
let error_text = String::from_utf8_lossy(&response_bytes);
error!("Version check request failed, response: {}", e);
return Err(UpdateCheckError::InvalidResponse(format!(
"JSON parsing failed: {e}, response: {error_text}"
)));
}
};
debug!("Retrieved latest version information: {:?}", version_info);
// Compare versions using version.rs functions
let update_available = version::is_newer_version(&current_version, &version_info.version)
.map_err(|e| UpdateCheckError::VersionParseError(e.to_string()))?;
let result = UpdateCheckResult {
update_available,
current_version,
latest_version: Some(version_info),
check_time: jiff::Zoned::now(),
};
if result.update_available {
info!(
"New version available: {} -> {}",
result.current_version,
result.latest_version.as_ref().unwrap().version
);
} else {
info!("Current version is up to date: {}", result.current_version);
}
Ok(result)
}
}
/// Get current version number
pub fn get_current_version() -> String {
version::get_version()
}
/// Convenience function for async update checking
pub async fn check_updates() -> Result<UpdateCheckResult, UpdateCheckError> {
let checker = VersionChecker::new();
checker.check_for_updates().await
}
/// Update check with custom URL
#[allow(dead_code)]
pub async fn check_updates_with_url(url: String) -> Result<UpdateCheckResult, UpdateCheckError> {
let checker = VersionChecker::with_config(url, Duration::from_secs(10));
checker.check_for_updates().await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_get_current_version() {
let version = get_current_version();
assert!(!version.is_empty());
debug!("Current version: {version}");
}
#[test]
fn test_update_check_result() {
// Test creating UpdateCheckResult with update available
let version_info = VersionInfo {
version: "1.2.0".to_string(),
release_date: Some("2024-01-15T10:00:00Z".to_string()),
release_notes: Some("Bug fixes and new features".to_string()),
download_url: Some("https://github.com/rustfs/rustfs/releases/tag/v1.2.0".to_string()),
};
let check_time = jiff::Zoned::now();
let result = UpdateCheckResult {
update_available: true,
current_version: "1.1.0".to_string(),
latest_version: Some(version_info),
check_time: check_time.clone(),
};
debug!("Update check result: {:?}", serde_json::to_string(&result).unwrap());
// Test fields
assert!(result.update_available);
assert_eq!(result.current_version, "1.1.0");
assert!(result.latest_version.is_some());
assert_eq!(result.check_time, check_time);
// Test latest version info
if let Some(latest) = &result.latest_version {
assert_eq!(latest.version, "1.2.0");
assert_eq!(latest.release_date, Some("2024-01-15T10:00:00Z".to_string()));
assert_eq!(latest.release_notes, Some("Bug fixes and new features".to_string()));
assert_eq!(
latest.download_url,
Some("https://github.com/rustfs/rustfs/releases/tag/v1.2.0".to_string())
);
}
// Test Clone functionality
let cloned_result = result.clone();
assert_eq!(cloned_result.update_available, result.update_available);
assert_eq!(cloned_result.current_version, result.current_version);
assert_eq!(cloned_result.check_time, result.check_time);
// Test Debug functionality (should not panic)
let debug_output = format!("{result:?}");
assert!(debug_output.contains("UpdateCheckResult"));
assert!(debug_output.contains("1.1.0"));
assert!(debug_output.contains("1.2.0"));
// Test creating UpdateCheckResult with no update available
let no_update_result = UpdateCheckResult {
update_available: false,
current_version: "1.2.0".to_string(),
latest_version: Some(VersionInfo {
version: "1.2.0".to_string(),
release_date: Some("2024-01-15T10:00:00Z".to_string()),
release_notes: None,
download_url: None,
}),
check_time: jiff::Zoned::now(),
};
assert!(!no_update_result.update_available);
assert_eq!(no_update_result.current_version, "1.2.0");
// Test creating UpdateCheckResult with None latest_version (error case)
let error_result = UpdateCheckResult {
update_available: false,
current_version: "1.1.0".to_string(),
latest_version: None,
check_time: jiff::Zoned::now(),
};
assert!(!error_result.update_available);
assert!(error_result.latest_version.is_none());
debug!("UpdateCheckResult tests passed successfully");
}
#[test]
fn test_version_info() {
// Test VersionInfo structure
let version_info = VersionInfo {
version: "2.0.0".to_string(),
release_date: Some("2024-02-01T12:00:00Z".to_string()),
release_notes: Some("Major release with breaking changes".to_string()),
download_url: Some("https://github.com/rustfs/rustfs/releases/tag/v2.0.0".to_string()),
};
// Test fields
assert_eq!(version_info.version, "2.0.0");
assert_eq!(version_info.release_date, Some("2024-02-01T12:00:00Z".to_string()));
assert_eq!(version_info.release_notes, Some("Major release with breaking changes".to_string()));
assert_eq!(
version_info.download_url,
Some("https://github.com/rustfs/rustfs/releases/tag/v2.0.0".to_string())
);
// Test Clone functionality
let cloned_info = version_info.clone();
assert_eq!(cloned_info.version, version_info.version);
assert_eq!(cloned_info.release_date, version_info.release_date);
assert_eq!(cloned_info.release_notes, version_info.release_notes);
assert_eq!(cloned_info.download_url, version_info.download_url);
// Test Debug functionality
let debug_output = format!("{version_info:?}");
assert!(debug_output.contains("VersionInfo"));
assert!(debug_output.contains("2.0.0"));
// Test minimal VersionInfo with only version
let minimal_info = VersionInfo {
version: "1.0.0".to_string(),
release_date: None,
release_notes: None,
download_url: None,
};
assert_eq!(minimal_info.version, "1.0.0");
assert!(minimal_info.release_date.is_none());
assert!(minimal_info.release_notes.is_none());
assert!(minimal_info.download_url.is_none());
// Test JSON serialization/deserialization
let json_string = serde_json::to_string(&version_info).unwrap();
debug!("Serialized version info: {json_string}");
assert!(json_string.contains("2.0.0"));
assert!(json_string.contains("Major release"));
let deserialized: VersionInfo = serde_json::from_str(&json_string).unwrap();
assert_eq!(deserialized.version, version_info.version);
assert_eq!(deserialized.release_notes, version_info.release_notes);
debug!("VersionInfo tests passed successfully");
}
#[test]
fn test_version_functions_integration() {
// Test that version functions from version.rs work correctly
assert_eq!(version::clean_version("refs/tags/1.0.0-alpha.17"), "1.0.0-alpha.17");
assert_eq!(version::clean_version("v1.0.0"), "1.0.0");
// Test version comparison
assert!(version::is_newer_version("1.0.0", "1.0.1").unwrap());
assert!(!version::is_newer_version("1.0.1", "1.0.0").unwrap());
// Test version parsing using parse_version
assert_eq!(version::parse_version("1.0.0").unwrap(), (1, 0, 0, None));
assert_eq!(version::parse_version("2.1.3-alpha.1").unwrap(), (2, 1, 3, Some("alpha.1".to_string())));
debug!("Version functions integration tests passed successfully");
}
}