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!(
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 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 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 info!("Selecting full upgrade strategy");
178 self.select_full_upgrade_strategy()
179 }
180 }
181 }
182
183 pub fn select_full_upgrade_strategy(&self) -> Result<UpgradeStrategy> {
185 debug!("Selecting full upgrade strategy");
186
187 if self.manifest.platforms.is_some() {
188 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(), 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 Err(anyhow::anyhow!(
216 "Full upgrade package for corresponding architecture not found"
217 ))
218 }
219 }
220 }
221
222 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 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 Err(anyhow::anyhow!(
256 "Full upgrade package for the current architecture not found"
257 ))
258 }
259 }
260
261 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 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 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, },
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 fn setup_test_environment() -> TempDir {
370 let temp_dir = TempDir::new().unwrap();
371
372 let docker_dir = temp_dir.path().join("docker");
374 fs::create_dir(&docker_dir).unwrap();
375
376 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 std::env::set_current_dir(&temp_dir).unwrap();
386
387 temp_dir
388 }
389
390 #[test]
391 fn test_no_upgrade_needed() {
392 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 let strategy = manager.determine_strategy().unwrap();
400
401 assert!(matches!(strategy, UpgradeStrategy::NoUpgrade { .. }));
402 }
403
404 #[test]
405 fn test_current_version_newer() {
406 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 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 let _temp_dir = setup_test_environment();
422
423 let manager =
424 UpgradeStrategyManager::new("0.0.12".to_string(), false, create_test_manifest());
425
426 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 let _temp_dir = setup_test_environment();
446
447 let manager =
448 UpgradeStrategyManager::new("0.0.13".to_string(), false, create_test_manifest());
449
450 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 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 let strategy = manager.determine_strategy().unwrap();
471
472 assert!(matches!(strategy, UpgradeStrategy::FullUpgrade { .. }));
473 }
474}