mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-16 15:32:20 +08:00
348 lines
12 KiB
Rust
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(¤t_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");
|
|
}
|
|
}
|