Skip to main content

client_core/
upgrade_strategy.rs

1use std::fmt::Display;
2use std::path::PathBuf;
3
4use crate::{
5    api_types::{EnhancedServiceManifest, PatchPackageInfo},
6    architecture::Architecture,
7    constants::docker::get_compose_file_path,
8    constants::docker::get_docker_work_dir,
9    version::Version,
10};
11use anyhow::Result;
12use tracing::{debug, info};
13
14#[derive(Debug, Clone, PartialEq)]
15pub enum DownloadType {
16    /// 全量升级
17    Full,
18    /// 增量升级
19    Patch,
20}
21
22impl Display for DownloadType {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            DownloadType::Full => write!(f, "full"),
26            DownloadType::Patch => write!(f, "patch"),
27        }
28    }
29}
30
31/// 升级策略类型
32#[derive(Debug, Clone, PartialEq)]
33pub enum UpgradeStrategy {
34    /// 全量升级
35    FullUpgrade {
36        /// 下载URL
37        url: String,
38        /// 文件哈希
39        hash: String,
40        /// 签名
41        signature: String,
42        /// 目标版本
43        target_version: Version,
44        /// 下载类型
45        download_type: DownloadType,
46    },
47    /// 增量升级(补丁)
48    PatchUpgrade {
49        /// 补丁包信息
50        patch_info: PatchPackageInfo,
51        /// 目标版本
52        target_version: Version,
53        /// 下载类型
54        download_type: DownloadType,
55    },
56    /// 无需升级
57    NoUpgrade {
58        /// 目标版本
59        target_version: Version,
60    },
61}
62
63impl UpgradeStrategy {
64    ///获取此次升级,变更的文件,或者目录,使用相对工作目录的路径,工作目录是:./docker ,如果是全量升级,只备份: ./data 目录; 增量升级,还需要额外备份增量升级变更的文件或者目录
65    pub fn get_changed_files(&self) -> Vec<PathBuf> {
66        let change_files = match self {
67            UpgradeStrategy::FullUpgrade { .. } => vec!["data".to_string(), "upload".to_string()],
68            UpgradeStrategy::PatchUpgrade { patch_info, .. } => patch_info.get_changed_files(),
69            UpgradeStrategy::NoUpgrade { .. } => {
70                vec![]
71            }
72        };
73        change_files.into_iter().map(PathBuf::from).collect()
74    }
75}
76
77/// 决策因素分析
78#[derive(Debug, Clone)]
79pub struct DecisionFactors {
80    /// 版本兼容性分数(0.0-1.0)
81    pub version_compatibility: f64,
82    /// 网络条件分数(0.0-1.0)
83    pub network_condition: f64,
84    /// 磁盘空间分数(0.0-1.0)
85    pub disk_space: f64,
86    /// 风险评估分数(0.0-1.0,越低越好)
87    pub risk_assessment: f64,
88    /// 时间效率分数(0.0-1.0)
89    pub time_efficiency: f64,
90}
91
92/// 升级策略管理器
93#[derive(Debug)]
94pub struct UpgradeStrategyManager {
95    ///server端docker应用升级版本信息
96    manifest: EnhancedServiceManifest,
97    ///当前客户端版本
98    current_version: String,
99    ///是否强制全量升级
100    force_full: bool,
101    ///当前客户端架构
102    architecture: Architecture,
103}
104
105impl UpgradeStrategyManager {
106    /// 创建新的升级策略管理器
107    pub fn new(
108        current_version: String,
109        force_full: bool,
110        manifest: EnhancedServiceManifest,
111    ) -> Self {
112        Self {
113            manifest,
114            current_version,
115            force_full,
116            architecture: Architecture::detect(),
117        }
118    }
119
120    /// 确定升级策略(简化版本)
121    pub fn determine_strategy(&self) -> Result<UpgradeStrategy> {
122        info!("Starting upgrade strategy decision");
123        info!("   Current version: {}", self.current_version);
124        info!("   Server version: {}", self.manifest.version);
125        info!("   Target architecture: {}", self.architecture.as_str());
126        info!("   Force full upgrade: {}", self.force_full);
127
128        // 1. 解析当前版本
129        let current_ver = self.current_version.parse::<Version>()?;
130
131        // 2. 首先与基础服务器版本比较,确定是否需要升级
132        let server_ver = self.manifest.version.clone();
133        //比较当前版本和服务器版本,判断是全量,还是增量升级,还是不需要升级
134        let base_comparison = current_ver.compare_detailed(&server_ver);
135
136        info!("Current version details: {:?}", current_ver);
137        info!("Server version details: {:?}", server_ver);
138        info!("Base version comparison result: {:?}", base_comparison);
139
140        // 3. 强制全量升级
141        if self.force_full {
142            info!("Force executing full upgrade");
143            return self.select_full_upgrade_strategy();
144        }
145        //判断工作目录下,是否有docker目录,如果没有docker目录,则也使用全量升级
146        let work_dir = get_docker_work_dir();
147        let compose_file_path = get_compose_file_path();
148        if !work_dir.exists() || !compose_file_path.exists() {
149            info!(
150                "No docker directory or compose file found in working directory, selecting full upgrade strategy"
151            );
152            return self.select_full_upgrade_strategy();
153        }
154
155        // 4. 根据版本比较结果决策
156        match base_comparison {
157            crate::version::VersionComparison::Equal | crate::version::VersionComparison::Newer => {
158                info!("Current version is already latest, no upgrade needed");
159                Ok(UpgradeStrategy::NoUpgrade {
160                    target_version: self.manifest.version.clone(),
161                })
162            }
163            crate::version::VersionComparison::PatchUpgradeable => {
164                // 可以进行增量升级
165                if !self.has_patch_for_architecture() {
166                    info!(
167                        "No incremental upgrade package for current architecture, selecting full upgrade strategy"
168                    );
169                    self.select_full_upgrade_strategy()
170                } else {
171                    info!("Selecting incremental upgrade strategy");
172                    self.select_patch_upgrade_strategy()
173                }
174            }
175            crate::version::VersionComparison::FullUpgradeRequired => {
176                // 需要全量升级
177                info!("Selecting full upgrade strategy");
178                self.select_full_upgrade_strategy()
179            }
180        }
181    }
182
183    /// 选择全量升级策略
184    pub fn select_full_upgrade_strategy(&self) -> Result<UpgradeStrategy> {
185        debug!("Selecting full upgrade strategy");
186
187        if self.manifest.platforms.is_some() {
188            //使用分架构的全量包
189            let platform_info = self.get_platform_package()?;
190
191            debug!(
192                "Using architecture-specific full package: {}",
193                &platform_info.url
194            );
195            Ok(UpgradeStrategy::FullUpgrade {
196                url: platform_info.url.clone(),
197                hash: "external".to_string(), // 平台包通常没有预设哈希
198                signature: platform_info.signature.clone(),
199                target_version: self.manifest.version.clone(),
200                download_type: DownloadType::Full,
201            })
202        } else {
203            if let Some(package_info) = &self.manifest.packages {
204                let full_info = &package_info.full;
205                debug!("Using generic full package: {}", &full_info.url);
206                Ok(UpgradeStrategy::FullUpgrade {
207                    url: full_info.url.clone(),
208                    hash: full_info.hash.clone(),
209                    signature: full_info.signature.clone(),
210                    target_version: self.manifest.version.clone(),
211                    download_type: DownloadType::Full,
212                })
213            } else {
214                //未找到对应架构的全量升级包,这里主动报错
215                Err(anyhow::anyhow!(
216                    "Full upgrade package for corresponding architecture not found"
217                ))
218            }
219        }
220    }
221
222    /// 选择增量升级策略
223    pub fn select_patch_upgrade_strategy(&self) -> Result<UpgradeStrategy> {
224        debug!("Selecting incremental upgrade strategy");
225
226        let patch_info = self.get_patch_package()?;
227
228        debug!(
229            "Using architecture-specific patch package: {}",
230            &patch_info.url
231        );
232        Ok(UpgradeStrategy::PatchUpgrade {
233            patch_info: patch_info.clone(),
234            target_version: self.manifest.version.clone(),
235            download_type: DownloadType::Patch,
236        })
237    }
238
239    /// 获取指定架构的平台包信息
240    fn get_platform_package(&self) -> Result<crate::api_types::PlatformPackageInfo> {
241        if let Some(platforms) = self.manifest.platforms.as_ref() {
242            match self.architecture {
243                Architecture::X86_64 => platforms.x86_64.clone().ok_or_else(|| {
244                    anyhow::anyhow!("Full upgrade package for x86_64 architecture not found")
245                }),
246                Architecture::Aarch64 => platforms.aarch64.clone().ok_or_else(|| {
247                    anyhow::anyhow!("Full upgrade package for aarch64 architecture not found")
248                }),
249                Architecture::Unsupported(_) => Err(anyhow::anyhow!(
250                    "Full upgrade package for this architecture not found"
251                )),
252            }
253        } else {
254            //未找到对应架构的全量升级包,这里主动报错
255            Err(anyhow::anyhow!(
256                "Full upgrade package for the current architecture not found"
257            ))
258        }
259    }
260
261    /// 获取指定架构的补丁包信息
262    fn get_patch_package(&self) -> Result<&PatchPackageInfo> {
263        let patch_info = self
264            .manifest
265            .patch
266            .as_ref()
267            .ok_or_else(|| anyhow::anyhow!("Server does not support incremental upgrade"))?;
268        match self.architecture {
269            Architecture::X86_64 => patch_info.x86_64.as_ref().ok_or_else(|| {
270                anyhow::anyhow!("Patch package for x86_64 architecture not available")
271            }),
272            Architecture::Aarch64 => patch_info.aarch64.as_ref().ok_or_else(|| {
273                anyhow::anyhow!("Patch package for aarch64 architecture not available")
274            }),
275            Architecture::Unsupported(_) => Err(anyhow::anyhow!("Unsupported architecture")),
276        }
277    }
278
279    /// 检查指定架构是否有可用的补丁
280    fn has_patch_for_architecture(&self) -> bool {
281        self.manifest
282            .patch
283            .as_ref()
284            .map(|patch| match self.architecture {
285                Architecture::X86_64 => patch.x86_64.is_some(),
286                Architecture::Aarch64 => patch.aarch64.is_some(),
287                Architecture::Unsupported(_) => false,
288            })
289            .unwrap_or(false)
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use crate::api_types::*;
297    use std::fs;
298    use tempfile::TempDir;
299
300    // 创建测试用的增强服务清单
301    fn create_test_manifest() -> EnhancedServiceManifest {
302        EnhancedServiceManifest {
303            version: "0.0.13.2".parse::<Version>().unwrap(),
304            release_date: "2025-01-12T10:00:00Z".to_string(),
305            release_notes: "测试版本".to_string(),
306            packages: Some(ServicePackages {
307                full: PackageInfo {
308                    url: "https://example.com/docker.zip".to_string(),
309                    hash: "sha256:full_hash".to_string(),
310                    signature: "full_signature".to_string(),
311                    size: 100 * 1024 * 1024, // 100MB
312                },
313                patch: None,
314            }),
315            platforms: Some(PlatformPackages {
316                x86_64: Some(PlatformPackageInfo {
317                    signature: "x86_64_signature".to_string(),
318                    url: "https://example.com/x86_64/docker.zip".to_string(),
319                }),
320                aarch64: Some(PlatformPackageInfo {
321                    signature: "aarch64_signature".to_string(),
322                    url: "https://example.com/aarch64/docker.zip".to_string(),
323                }),
324            }),
325            patch: Some(PatchInfo {
326                x86_64: Some(PatchPackageInfo {
327                    url: "https://example.com/patches/x86_64-patch.tar.gz".to_string(),
328                    hash: Some("sha256:patch_hash_x86_64".to_string()),
329                    signature: Some("patch_signature_x86_64".to_string()),
330                    operations: PatchOperations {
331                        replace: Some(ReplaceOperations {
332                            files: vec!["app.jar".to_string(), "config.yml".to_string()],
333                            directories: vec!["front/".to_string()],
334                        }),
335                        delete: Some(ReplaceOperations {
336                            files: vec![
337                                "old-files/app.jar".to_string(),
338                                "old-files/config.yml".to_string(),
339                            ],
340                            directories: vec!["old-files/front/".to_string()],
341                        }),
342                    },
343                    notes: None,
344                }),
345                aarch64: Some(PatchPackageInfo {
346                    url: "https://example.com/patches/aarch64-patch.tar.gz".to_string(),
347                    hash: Some("sha256:patch_hash_aarch64".to_string()),
348                    signature: Some("patch_signature_aarch64".to_string()),
349                    operations: PatchOperations {
350                        replace: Some(ReplaceOperations {
351                            files: vec!["app.jar".to_string(), "config.yml".to_string()],
352                            directories: vec!["front/".to_string()],
353                        }),
354                        delete: Some(ReplaceOperations {
355                            files: vec![
356                                "old-files/app.jar".to_string(),
357                                "old-files/config.yml".to_string(),
358                            ],
359                            directories: vec!["old-files/front/".to_string()],
360                        }),
361                    },
362                    notes: None,
363                }),
364            }),
365        }
366    }
367
368    // 创建测试环境,包括必要的docker目录和文件
369    fn setup_test_environment() -> TempDir {
370        let temp_dir = TempDir::new().unwrap();
371
372        // 创建docker目录
373        let docker_dir = temp_dir.path().join("docker");
374        fs::create_dir(&docker_dir).unwrap();
375
376        // 创建docker-compose.yml文件
377        let compose_file = docker_dir.join("docker-compose.yml");
378        fs::write(
379            &compose_file,
380            "version: '3.8'\nservices:\n  test:\n    image: hello-world",
381        )
382        .unwrap();
383
384        // 设置当前工作目录为临时目录
385        std::env::set_current_dir(&temp_dir).unwrap();
386
387        temp_dir
388    }
389
390    #[test]
391    fn test_no_upgrade_needed() {
392        // 设置测试环境
393        let _temp_dir = setup_test_environment();
394
395        let manager =
396            UpgradeStrategyManager::new("0.0.13.2".to_string(), false, create_test_manifest());
397
398        // 当前版本与服务器版本相同
399        let strategy = manager.determine_strategy().unwrap();
400
401        assert!(matches!(strategy, UpgradeStrategy::NoUpgrade { .. }));
402    }
403
404    #[test]
405    fn test_current_version_newer() {
406        // 设置测试环境
407        let _temp_dir = setup_test_environment();
408
409        let manager =
410            UpgradeStrategyManager::new("0.0.13.4".to_string(), false, create_test_manifest());
411
412        // 当前版本比服务器版本新
413        let strategy = manager.determine_strategy().unwrap();
414
415        assert!(matches!(strategy, UpgradeStrategy::NoUpgrade { .. }));
416    }
417
418    #[test]
419    fn test_full_upgrade_different_base_version() {
420        // 设置测试环境
421        let _temp_dir = setup_test_environment();
422
423        let manager =
424            UpgradeStrategyManager::new("0.0.12".to_string(), false, create_test_manifest());
425
426        // 不同基础版本,需要全量升级
427        let strategy = manager.determine_strategy().unwrap();
428
429        match strategy {
430            UpgradeStrategy::FullUpgrade {
431                url,
432                target_version,
433                ..
434            } => {
435                assert_eq!(url, "https://example.com/aarch64/docker.zip");
436                assert_eq!(target_version, "0.0.13.2".parse::<Version>().unwrap());
437            }
438            _ => panic!("应该选择全量升级策略"),
439        }
440    }
441
442    #[test]
443    fn test_patch_upgrade_same_base_version() {
444        // 设置测试环境
445        let _temp_dir = setup_test_environment();
446
447        let manager =
448            UpgradeStrategyManager::new("0.0.13".to_string(), false, create_test_manifest());
449
450        // 相同基础版本,可以增量升级
451        let strategy = manager.determine_strategy().unwrap();
452
453        match strategy {
454            UpgradeStrategy::PatchUpgrade { target_version, .. } => {
455                assert_eq!(target_version, "0.0.13.2".parse::<Version>().unwrap());
456            }
457            _ => panic!("应该选择增量升级策略"),
458        }
459    }
460
461    #[test]
462    fn test_force_full_upgrade() {
463        // 设置测试环境
464        let _temp_dir = setup_test_environment();
465
466        let manager =
467            UpgradeStrategyManager::new("0.0.13.2".to_string(), true, create_test_manifest());
468
469        // 强制全量升级,即使可以增量升级
470        let strategy = manager.determine_strategy().unwrap();
471
472        assert!(matches!(strategy, UpgradeStrategy::FullUpgrade { .. }));
473    }
474}