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 Full,
18 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#[derive(Debug, Clone, PartialEq)]
33pub enum UpgradeStrategy {
34 FullUpgrade {
36 url: String,
38 hash: String,
40 signature: String,
42 target_version: Version,
44 download_type: DownloadType,
46 },
47 PatchUpgrade {
49 patch_info: PatchPackageInfo,
51 target_version: Version,
53 download_type: DownloadType,
55 },
56 NoUpgrade {
58 target_version: Version,
60 },
61}
62
63impl UpgradeStrategy {
64 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#[derive(Debug, Clone)]
79pub struct DecisionFactors {
80 pub version_compatibility: f64,
82 pub network_condition: f64,
84 pub disk_space: f64,
86 pub risk_assessment: f64,
88 pub time_efficiency: f64,
90}
91
92#[derive(Debug)]
94pub struct UpgradeStrategyManager {
95 manifest: EnhancedServiceManifest,
97 current_version: String,
99 force_full: bool,
101 architecture: Architecture,
103}
104
105impl UpgradeStrategyManager {
106 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 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 let current_ver = self.current_version.parse::<Version>()?;
130
131 let server_ver = self.manifest.version.clone();
133 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 if self.force_full {
142 info!("Force executing full upgrade");
143 return self.select_full_upgrade_strategy();
144 }
145 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!("No docker directory or compose file found in working directory, selecting full upgrade strategy");
150 return self.select_full_upgrade_strategy();
151 }
152
153 match base_comparison {
155 crate::version::VersionComparison::Equal | crate::version::VersionComparison::Newer => {
156 info!("Current version is already latest, no upgrade needed");
157 Ok(UpgradeStrategy::NoUpgrade {
158 target_version: self.manifest.version.clone(),
159 })
160 }
161 crate::version::VersionComparison::PatchUpgradeable => {
162 if !self.has_patch_for_architecture() {
164 info!("No incremental upgrade package for current architecture, selecting full upgrade strategy");
165 self.select_full_upgrade_strategy()
166 } else {
167 info!("Selecting incremental upgrade strategy");
168 self.select_patch_upgrade_strategy()
169 }
170 }
171 crate::version::VersionComparison::FullUpgradeRequired => {
172 info!("Selecting full upgrade strategy");
174 self.select_full_upgrade_strategy()
175 }
176 }
177 }
178
179 pub fn select_full_upgrade_strategy(&self) -> Result<UpgradeStrategy> {
181 debug!("Selecting full upgrade strategy");
182
183 if let Some(_) = &self.manifest.platforms {
184 let platform_info = self.get_platform_package()?;
186
187 debug!("Using architecture-specific full package: {}", &platform_info.url);
188 Ok(UpgradeStrategy::FullUpgrade {
189 url: platform_info.url.clone(),
190 hash: "external".to_string(), signature: platform_info.signature.clone(),
192 target_version: self.manifest.version.clone(),
193 download_type: DownloadType::Full,
194 })
195 } else {
196 if let Some(package_info) = &self.manifest.packages {
197 let full_info = &package_info.full;
198 debug!("Using generic full package: {}", &full_info.url);
199 Ok(UpgradeStrategy::FullUpgrade {
200 url: full_info.url.clone(),
201 hash: full_info.hash.clone(),
202 signature: full_info.signature.clone(),
203 target_version: self.manifest.version.clone(),
204 download_type: DownloadType::Full,
205 })
206 } else {
207 Err(anyhow::anyhow!("Full upgrade package for corresponding architecture not found"))
209 }
210 }
211 }
212
213 pub fn select_patch_upgrade_strategy(&self) -> Result<UpgradeStrategy> {
215 debug!("Selecting incremental upgrade strategy");
216
217 let patch_info = self.get_patch_package()?;
218
219 debug!("Using architecture-specific patch package: {}", &patch_info.url);
220 Ok(UpgradeStrategy::PatchUpgrade {
221 patch_info: patch_info.clone(),
222 target_version: self.manifest.version.clone(),
223 download_type: DownloadType::Patch,
224 })
225 }
226
227 fn get_platform_package<'a>(&self) -> Result<crate::api_types::PlatformPackageInfo> {
229 if let Some(platforms) = self.manifest.platforms.as_ref() {
230 match self.architecture {
231 Architecture::X86_64 => platforms
232 .x86_64
233 .clone()
234 .ok_or_else(|| anyhow::anyhow!("Full upgrade package for x86_64 architecture not found")),
235 Architecture::Aarch64 => platforms
236 .aarch64
237 .clone()
238 .ok_or_else(|| anyhow::anyhow!("Full upgrade package for aarch64 architecture not found")),
239 Architecture::Unsupported(_) => Err(anyhow::anyhow!("Full upgrade package for this architecture not found")),
240 }
241 } else {
242 Err(anyhow::anyhow!(
244 "Full upgrade package for the current architecture not found"
245 ))
246 }
247 }
248
249 fn get_patch_package(&self) -> Result<&PatchPackageInfo> {
251 let patch_info = self
252 .manifest
253 .patch
254 .as_ref()
255 .ok_or_else(|| anyhow::anyhow!("Server does not support incremental upgrade"))?;
256 match self.architecture {
257 Architecture::X86_64 => patch_info
258 .x86_64
259 .as_ref()
260 .ok_or_else(|| anyhow::anyhow!("Patch package for x86_64 architecture not available")),
261 Architecture::Aarch64 => patch_info
262 .aarch64
263 .as_ref()
264 .ok_or_else(|| anyhow::anyhow!("Patch package for aarch64 architecture not available")),
265 Architecture::Unsupported(_) => Err(anyhow::anyhow!("Unsupported architecture")),
266 }
267 }
268
269 fn has_patch_for_architecture(&self) -> bool {
271 self.manifest
272 .patch
273 .as_ref()
274 .map(|patch| match self.architecture {
275 Architecture::X86_64 => patch.x86_64.is_some(),
276 Architecture::Aarch64 => patch.aarch64.is_some(),
277 Architecture::Unsupported(_) => false,
278 })
279 .unwrap_or(false)
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::api_types::*;
287 use std::fs;
288 use tempfile::TempDir;
289
290 fn create_test_manifest() -> EnhancedServiceManifest {
292 EnhancedServiceManifest {
293 version: "0.0.13.2".parse::<Version>().unwrap(),
294 release_date: "2025-01-12T10:00:00Z".to_string(),
295 release_notes: "测试版本".to_string(),
296 packages: Some(ServicePackages {
297 full: PackageInfo {
298 url: "https://example.com/docker.zip".to_string(),
299 hash: "sha256:full_hash".to_string(),
300 signature: "full_signature".to_string(),
301 size: 100 * 1024 * 1024, },
303 patch: None,
304 }),
305 platforms: Some(PlatformPackages {
306 x86_64: Some(PlatformPackageInfo {
307 signature: "x86_64_signature".to_string(),
308 url: "https://example.com/x86_64/docker.zip".to_string(),
309 }),
310 aarch64: Some(PlatformPackageInfo {
311 signature: "aarch64_signature".to_string(),
312 url: "https://example.com/aarch64/docker.zip".to_string(),
313 }),
314 }),
315 patch: Some(PatchInfo {
316 x86_64: Some(PatchPackageInfo {
317 url: "https://example.com/patches/x86_64-patch.tar.gz".to_string(),
318 hash: Some("sha256:patch_hash_x86_64".to_string()),
319 signature: Some("patch_signature_x86_64".to_string()),
320 operations: PatchOperations {
321 replace: Some(ReplaceOperations {
322 files: vec!["app.jar".to_string(), "config.yml".to_string()],
323 directories: vec!["front/".to_string()],
324 }),
325 delete: Some(ReplaceOperations {
326 files: vec![
327 "old-files/app.jar".to_string(),
328 "old-files/config.yml".to_string(),
329 ],
330 directories: vec!["old-files/front/".to_string()],
331 }),
332 },
333 notes: None,
334 }),
335 aarch64: Some(PatchPackageInfo {
336 url: "https://example.com/patches/aarch64-patch.tar.gz".to_string(),
337 hash: Some("sha256:patch_hash_aarch64".to_string()),
338 signature: Some("patch_signature_aarch64".to_string()),
339 operations: PatchOperations {
340 replace: Some(ReplaceOperations {
341 files: vec!["app.jar".to_string(), "config.yml".to_string()],
342 directories: vec!["front/".to_string()],
343 }),
344 delete: Some(ReplaceOperations {
345 files: vec![
346 "old-files/app.jar".to_string(),
347 "old-files/config.yml".to_string(),
348 ],
349 directories: vec!["old-files/front/".to_string()],
350 }),
351 },
352 notes: None,
353 }),
354 }),
355 }
356 }
357
358 fn setup_test_environment() -> TempDir {
360 let temp_dir = TempDir::new().unwrap();
361
362 let docker_dir = temp_dir.path().join("docker");
364 fs::create_dir(&docker_dir).unwrap();
365
366 let compose_file = docker_dir.join("docker-compose.yml");
368 fs::write(
369 &compose_file,
370 "version: '3.8'\nservices:\n test:\n image: hello-world",
371 )
372 .unwrap();
373
374 std::env::set_current_dir(&temp_dir).unwrap();
376
377 temp_dir
378 }
379
380 #[test]
381 fn test_no_upgrade_needed() {
382 let _temp_dir = setup_test_environment();
384
385 let manager =
386 UpgradeStrategyManager::new("0.0.13.2".to_string(), false, create_test_manifest());
387
388 let strategy = manager.determine_strategy().unwrap();
390
391 assert!(matches!(strategy, UpgradeStrategy::NoUpgrade { .. }));
392 }
393
394 #[test]
395 fn test_current_version_newer() {
396 let _temp_dir = setup_test_environment();
398
399 let manager =
400 UpgradeStrategyManager::new("0.0.13.4".to_string(), false, create_test_manifest());
401
402 let strategy = manager.determine_strategy().unwrap();
404
405 assert!(matches!(strategy, UpgradeStrategy::NoUpgrade { .. }));
406 }
407
408 #[test]
409 fn test_full_upgrade_different_base_version() {
410 let _temp_dir = setup_test_environment();
412
413 let manager =
414 UpgradeStrategyManager::new("0.0.12".to_string(), false, create_test_manifest());
415
416 let strategy = manager.determine_strategy().unwrap();
418
419 match strategy {
420 UpgradeStrategy::FullUpgrade {
421 url,
422 target_version,
423 ..
424 } => {
425 assert_eq!(url, "https://example.com/aarch64/docker.zip");
426 assert_eq!(target_version, "0.0.13.2".parse::<Version>().unwrap());
427 }
428 _ => panic!("应该选择全量升级策略"),
429 }
430 }
431
432 #[test]
433 fn test_patch_upgrade_same_base_version() {
434 let _temp_dir = setup_test_environment();
436
437 let manager =
438 UpgradeStrategyManager::new("0.0.13".to_string(), false, create_test_manifest());
439
440 let strategy = manager.determine_strategy().unwrap();
442
443 match strategy {
444 UpgradeStrategy::PatchUpgrade { target_version, .. } => {
445 assert_eq!(target_version, "0.0.13.2".parse::<Version>().unwrap());
446 }
447 _ => panic!("应该选择增量升级策略"),
448 }
449 }
450
451 #[test]
452 fn test_force_full_upgrade() {
453 let _temp_dir = setup_test_environment();
455
456 let manager =
457 UpgradeStrategyManager::new("0.0.13.2".to_string(), true, create_test_manifest());
458
459 let strategy = manager.determine_strategy().unwrap();
461
462 assert!(matches!(strategy, UpgradeStrategy::FullUpgrade { .. }));
463 }
464}