Skip to main content

client_core/
config.rs

1use crate::constants::{backup, config, docker, updates, version};
2use crate::version::Version; // 新增:导入Version类型
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::{Path, PathBuf};
7use toml;
8
9/// 应用配置结构
10#[derive(Debug, Serialize, Deserialize, Clone)]
11pub struct AppConfig {
12    pub versions: VersionConfig,
13    pub docker: DockerConfig,
14    pub backup: BackupConfig,
15    pub cache: CacheConfig,
16    pub updates: UpdatesConfig,
17}
18
19/// 版本配置结构(支持增量版本管理)
20#[derive(Debug, Serialize, Deserialize, Clone)]
21pub struct VersionConfig {
22    /// 基础Docker服务版本(向后兼容字段)
23    pub docker_service: String,
24
25    /// 补丁版本信息
26    #[serde(default)]
27    pub patch_version: String,
28
29    /// 本地已应用的补丁级别
30    #[serde(default)]
31    pub local_patch_level: u32,
32
33    /// 完整版本号(包含补丁级别)
34    #[serde(default)]
35    pub full_version_with_patches: String,
36
37    /// 最后一次全量升级时间
38    #[serde(default)]
39    pub last_full_upgrade: Option<chrono::DateTime<chrono::Utc>>,
40
41    /// 最后一次补丁升级时间
42    #[serde(default)]
43    pub last_patch_upgrade: Option<chrono::DateTime<chrono::Utc>>,
44
45    /// 已应用的补丁历史
46    #[serde(default)]
47    pub applied_patches: Vec<AppliedPatch>,
48}
49
50/// 已应用的补丁记录
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AppliedPatch {
53    pub version: String,
54    pub level: u32,
55    pub applied_at: chrono::DateTime<chrono::Utc>,
56}
57
58// 为了向后兼容,保留Versions类型别名
59pub type Versions = VersionConfig;
60
61impl VersionConfig {
62    /// 创建新的版本配置
63    pub fn new() -> Self {
64        let docker_service = version::version_info::DEFAULT_DOCKER_SERVICE_VERSION.to_string();
65        let full_version = format!("{docker_service}.0");
66
67        Self {
68            docker_service: docker_service.clone(),
69            patch_version: "0.0.0".to_string(),
70            local_patch_level: 0,
71            full_version_with_patches: full_version,
72            last_full_upgrade: None,
73            last_patch_upgrade: None,
74            applied_patches: Vec::new(),
75        }
76    }
77
78    /// 更新全量版本
79    pub fn update_full_version(&mut self, new_version: String) {
80        self.docker_service = new_version.clone();
81        self.local_patch_level = 0; // 重置补丁级别
82        self.full_version_with_patches = format!("{new_version}.0");
83        self.last_full_upgrade = Some(chrono::Utc::now());
84        self.applied_patches.clear(); // 清空补丁历史
85
86        tracing::info!(
87            "Full version updated: {} -> {}",
88            self.docker_service,
89            new_version
90        );
91    }
92
93    /// 应用补丁
94    pub fn apply_patch(&mut self, patch_version: String) {
95        self.patch_version = patch_version.clone();
96        self.local_patch_level += 1;
97        self.full_version_with_patches =
98            format!("{}.{}", self.docker_service, self.local_patch_level);
99        self.last_patch_upgrade = Some(chrono::Utc::now());
100
101        // 记录补丁历史
102        self.applied_patches.push(AppliedPatch {
103            version: patch_version.clone(),
104            level: self.local_patch_level,
105            applied_at: chrono::Utc::now(),
106        });
107
108        tracing::info!(
109            "Patch applied: {} (level: {})",
110            patch_version,
111            self.local_patch_level
112        );
113    }
114
115    /// 获取当前完整版本
116    pub fn get_current_version(&self) -> Result<Version> {
117        if !self.full_version_with_patches.is_empty() {
118            self.full_version_with_patches.parse::<Version>()
119        } else {
120            // 向后兼容:如果没有完整版本信息,基于docker_service构建
121            format!("{}.0", self.docker_service).parse::<Version>()
122        }
123    }
124
125    /// 检查是否需要版本配置迁移
126    pub fn needs_migration(&self) -> bool {
127        self.full_version_with_patches.is_empty()
128            || (self.local_patch_level == 0 && !self.applied_patches.is_empty())
129    }
130
131    /// 执行版本配置迁移
132    pub fn migrate(&mut self) -> Result<()> {
133        if self.full_version_with_patches.is_empty() {
134            // 基于docker_service构建完整版本号
135            self.full_version_with_patches =
136                format!("{}.{}", self.docker_service, self.local_patch_level);
137            tracing::info!(
138                "Configuration migration: building full version number {}",
139                self.full_version_with_patches
140            );
141        }
142
143        // 验证配置的一致性
144        self.validate()?;
145
146        Ok(())
147    }
148
149    /// 验证版本配置的一致性
150    pub fn validate(&self) -> Result<()> {
151        // 验证基础版本号格式
152        if self.docker_service.is_empty() {
153            return Err(anyhow::anyhow!("docker_service cannot be empty"));
154        }
155
156        // 验证完整版本号格式
157        if !self.full_version_with_patches.is_empty() {
158            let _version = self
159                .full_version_with_patches
160                .parse::<Version>()
161                .map_err(|e| anyhow::anyhow!(format!("Invalid full version number format: {e}")))?;
162        }
163
164        // 验证补丁级别与历史记录的一致性
165        if self.applied_patches.len() != self.local_patch_level as usize {
166            tracing::warn!(
167                "Patch level inconsistent with history: level={}, history_count={}",
168                self.local_patch_level,
169                self.applied_patches.len()
170            );
171        }
172
173        Ok(())
174    }
175
176    /// 获取补丁应用历史摘要
177    pub fn get_patch_summary(&self) -> String {
178        if self.applied_patches.is_empty() {
179            format!("版本: {} (无补丁)", self.docker_service)
180        } else {
181            format!(
182                "版本: {} (已应用{}个补丁,当前级别: {})",
183                self.docker_service,
184                self.applied_patches.len(),
185                self.local_patch_level
186            )
187        }
188    }
189
190    /// 回滚最后一个补丁
191    pub fn rollback_last_patch(&mut self) -> Result<Option<AppliedPatch>> {
192        if let Some(last_patch) = self.applied_patches.pop() {
193            if self.local_patch_level > 0 {
194                self.local_patch_level -= 1;
195            }
196
197            self.full_version_with_patches =
198                format!("{}.{}", self.docker_service, self.local_patch_level);
199
200            // 更新patch_version为前一个补丁的版本(如果存在)
201            if let Some(prev_patch) = self.applied_patches.last() {
202                self.patch_version = prev_patch.version.clone();
203            } else {
204                self.patch_version = "0.0.0".to_string();
205            }
206
207            tracing::info!(
208                "Rolled back patch: {} (level: {})",
209                last_patch.version,
210                last_patch.level
211            );
212            Ok(Some(last_patch))
213        } else {
214            Ok(None)
215        }
216    }
217}
218
219impl Default for VersionConfig {
220    fn default() -> Self {
221        Self::new()
222    }
223}
224
225/// Docker相关配置
226#[derive(Debug, Serialize, Deserialize, Clone)]
227pub struct DockerConfig {
228    #[serde(default = "default_compose_file_path")]
229    pub compose_file: String,
230    #[serde(default = "default_env_file_path")]
231    pub env_file: String,
232}
233// 默认值函数, 用于获取默认的环境文件路径
234fn default_env_file_path() -> String {
235    docker::get_env_file_path_str()
236}
237
238fn default_compose_file_path() -> String {
239    docker::get_compose_file_path_str()
240}
241
242/// 备份相关配置
243#[derive(Debug, Serialize, Deserialize, Clone)]
244pub struct BackupConfig {
245    pub storage_dir: String,
246}
247
248/// 缓存相关配置
249#[derive(Debug, Serialize, Deserialize, Clone)]
250pub struct CacheConfig {
251    pub cache_dir: String,
252    pub download_dir: String,
253}
254
255/// 更新相关配置
256#[derive(Debug, Serialize, Deserialize, Clone)]
257pub struct UpdatesConfig {
258    pub check_frequency: String,
259}
260
261impl Default for AppConfig {
262    fn default() -> Self {
263        Self {
264            versions: VersionConfig::new(),
265            docker: DockerConfig {
266                compose_file: docker::get_compose_file_path_str(),
267                env_file: docker::get_env_file_path_str(),
268            },
269            backup: BackupConfig {
270                storage_dir: backup::get_default_storage_dir()
271                    .to_string_lossy()
272                    .to_string(),
273            },
274            cache: CacheConfig {
275                cache_dir: config::get_default_cache_dir()
276                    .to_string_lossy()
277                    .to_string(),
278                download_dir: config::get_default_download_dir()
279                    .to_string_lossy()
280                    .to_string(),
281            },
282            updates: UpdatesConfig {
283                check_frequency: updates::DEFAULT_CHECK_FREQUENCY.to_string(),
284            },
285        }
286    }
287}
288
289impl AppConfig {
290    /// 获取 docker 应用版本配置
291    pub fn get_docker_versions(&self) -> String {
292        self.versions.docker_service.clone()
293    }
294
295    /// 写入 docker 应用版本配置
296    pub fn write_docker_versions(&mut self, docker_service: String) {
297        self.versions.docker_service = docker_service;
298    }
299
300    /// 智能查找并加载配置文件
301    /// 按优先级查找:config.toml -> /app/config.toml
302    pub fn find_and_load_config() -> Result<Self> {
303        let config_files = ["config.toml", "/app/config.toml"];
304
305        for config_file in &config_files {
306            if Path::new(config_file).exists() {
307                tracing::info!("Found configuration file: {}", config_file);
308                return Self::load_from_file(config_file);
309            }
310        }
311
312        // 如果没找到配置文件,创建默认配置
313        tracing::warn!("Configuration file not found, creating default config: config.toml");
314        let default_config = Self::default();
315        default_config.save_to_file("config.toml")?;
316        Ok(default_config)
317    }
318
319    /// 从指定文件加载配置
320    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
321        let content = fs::read_to_string(&path)?;
322        let config: AppConfig = toml::from_str(&content)?;
323
324        Ok(config)
325    }
326
327    /// 保存配置到文件
328    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
329        let content = self.to_toml_with_comments();
330        fs::write(&path, content)?;
331        Ok(())
332    }
333
334    /// 生成带注释的TOML配置
335    fn to_toml_with_comments(&self) -> String {
336        const TEMPLATE: &str = include_str!("../templates/config.toml.template");
337
338        // 将所有路径的反斜杠替换为正斜杠,确保TOML兼容性
339        let compose_file = self.docker.compose_file.replace('\\', "/");
340        let backup_storage_dir = self.backup.storage_dir.replace('\\', "/");
341        let cache_dir = self.cache.cache_dir.replace('\\', "/");
342        let download_dir = self.cache.download_dir.replace('\\', "/");
343
344        TEMPLATE
345            .replace("{docker_service_version}", &self.get_docker_versions())
346            .replace("{compose_file}", &compose_file)
347            .replace("{backup_storage_dir}", &backup_storage_dir)
348            .replace("{cache_dir}", &cache_dir)
349            .replace("{download_dir}", &download_dir)
350            .replace("{check_frequency}", &self.updates.check_frequency)
351    }
352
353    /// 确保缓存目录存在
354    pub fn ensure_cache_dirs(&self) -> Result<()> {
355        fs::create_dir_all(&self.cache.cache_dir)?;
356        fs::create_dir_all(&self.cache.download_dir)?;
357        Ok(())
358    }
359
360    /// 获取下载目录路径
361    pub fn get_download_dir(&self) -> PathBuf {
362        PathBuf::from(&self.cache.download_dir)
363    }
364
365    /// 获取指定版本的全量下载目录路径
366    pub fn get_version_download_dir(&self, version: &str, download_type: &str) -> PathBuf {
367        PathBuf::from(&self.cache.download_dir)
368            .join(version)
369            .join(download_type)
370    }
371
372    /// 获取指定版本的全量下载文件路径
373    ///
374    /// 返回 Result,如果找不到归档文件则返回错误
375    pub fn get_version_download_file_path(
376        &self,
377        version: &str,
378        download_type: &str,
379        filename: Option<&str>,
380    ) -> Result<PathBuf> {
381        let download_dir = self.get_version_download_dir(version, download_type);
382
383        let path = match filename {
384            Some(filename) => download_dir.join(filename),
385            None => {
386                // 自动查找目录中的归档文件
387                match find_archive_file(&download_dir) {
388                    Some(found) => download_dir.join(found),
389                    None => {
390                        // 未找到归档文件,返回错误
391                        return Err(anyhow::anyhow!(
392                            "Archive file not found, directory: {}, supported formats: .zip, .tar.gz",
393                            download_dir.display()
394                        ));
395                    }
396                }
397            }
398        };
399
400        // 验证文件存在
401        if !path.exists() {
402            return Err(anyhow::anyhow!(
403                "Archive file does not exist: {}",
404                path.display()
405            ));
406        }
407
408        Ok(path)
409    }
410
411    /// 确保指定版本的下载目录存在
412    pub fn ensure_version_download_dir(
413        &self,
414        version: &str,
415        download_type: &str,
416    ) -> Result<PathBuf> {
417        let dir = self.get_version_download_dir(version, download_type);
418        fs::create_dir_all(&dir)?;
419        Ok(dir)
420    }
421
422    /// 获取备份目录路径
423    pub fn get_backup_dir(&self) -> PathBuf {
424        PathBuf::from(&self.backup.storage_dir)
425    }
426}
427
428/// 在指定目录中查找归档文件(.zip 或 .tar.gz)
429///
430/// 查找规则:
431/// 1. 文件名以 `docker-` 开头
432/// 2. 扩展名为 `.zip` 或 `.tar.gz`
433/// 3. 返回第一个匹配的文件
434fn find_archive_file(dir: &Path) -> Option<String> {
435    let entries = fs::read_dir(dir).ok()?;
436
437    for entry in entries.flatten() {
438        let path = entry.path();
439        if !path.is_file() {
440            continue;
441        }
442
443        let file_name = path.file_name()?.to_str()?;
444
445        // 检查文件名是否以 docker- 开头
446        if !file_name.starts_with("docker-") {
447            continue;
448        }
449
450        // 检查扩展名
451        if file_name.ends_with(".zip") || file_name.ends_with(".tar.gz") {
452            return Some(file_name.to_string());
453        }
454    }
455
456    None
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use std::fs::File;
463    use tempfile::TempDir;
464
465    #[test]
466    fn test_version_config_new() {
467        let config = VersionConfig::new();
468
469        assert!(!config.docker_service.is_empty());
470        assert_eq!(config.patch_version, "0.0.0");
471        assert_eq!(config.local_patch_level, 0);
472        assert!(config.full_version_with_patches.ends_with(".0"));
473        assert!(config.applied_patches.is_empty());
474        assert!(config.last_full_upgrade.is_none());
475        assert!(config.last_patch_upgrade.is_none());
476    }
477
478    #[test]
479    fn test_update_full_version() {
480        let mut config = VersionConfig::new();
481        config.apply_patch("0.0.1".to_string());
482        assert_eq!(config.local_patch_level, 1);
483
484        // 更新全量版本应该重置补丁信息
485        config.update_full_version("0.0.14".to_string());
486
487        assert_eq!(config.docker_service, "0.0.14");
488        assert_eq!(config.local_patch_level, 0);
489        assert_eq!(config.full_version_with_patches, "0.0.14.0");
490        assert!(config.applied_patches.is_empty());
491        assert!(config.last_full_upgrade.is_some());
492    }
493
494    #[test]
495    fn test_apply_patch() {
496        let mut config = VersionConfig::new();
497        let initial_service_version = config.docker_service.clone();
498
499        // 应用第一个补丁
500        config.apply_patch("patch-0.0.1".to_string());
501
502        assert_eq!(config.patch_version, "patch-0.0.1");
503        assert_eq!(config.local_patch_level, 1);
504        assert_eq!(
505            config.full_version_with_patches,
506            format!("{initial_service_version}.1")
507        );
508        assert_eq!(config.applied_patches.len(), 1);
509        assert!(config.last_patch_upgrade.is_some());
510
511        // 应用第二个补丁
512        config.apply_patch("patch-0.0.2".to_string());
513
514        assert_eq!(config.patch_version, "patch-0.0.2");
515        assert_eq!(config.local_patch_level, 2);
516        assert_eq!(
517            config.full_version_with_patches,
518            format!("{initial_service_version}.2")
519        );
520        assert_eq!(config.applied_patches.len(), 2);
521
522        // 验证补丁历史记录
523        assert_eq!(config.applied_patches[0].version, "patch-0.0.1");
524        assert_eq!(config.applied_patches[0].level, 1);
525        assert_eq!(config.applied_patches[1].version, "patch-0.0.2");
526        assert_eq!(config.applied_patches[1].level, 2);
527    }
528
529    #[test]
530    fn test_get_current_version() {
531        let mut config = VersionConfig::new();
532
533        // 测试基础版本
534        let version = config.get_current_version().unwrap();
535        assert_eq!(version.build, 0);
536
537        // 应用补丁后测试
538        config.apply_patch("patch-0.0.1".to_string());
539        let version = config.get_current_version().unwrap();
540        assert_eq!(version.build, 1);
541    }
542
543    #[test]
544    fn test_backward_compatibility() {
545        // 测试向后兼容性:旧配置只有docker_service字段
546        let old_config = VersionConfig {
547            docker_service: "0.0.13".to_string(),
548            patch_version: String::new(),
549            local_patch_level: 0,
550            full_version_with_patches: String::new(),
551            last_full_upgrade: None,
552            last_patch_upgrade: None,
553            applied_patches: Vec::new(),
554        };
555
556        assert!(old_config.needs_migration());
557
558        let version = old_config.get_current_version().unwrap();
559        assert_eq!(version.to_string(), "0.0.13.0");
560    }
561
562    #[test]
563    fn test_migration() {
564        let mut config = VersionConfig {
565            docker_service: "0.0.13".to_string(),
566            patch_version: String::new(),
567            local_patch_level: 2,
568            full_version_with_patches: String::new(),
569            last_full_upgrade: None,
570            last_patch_upgrade: None,
571            applied_patches: Vec::new(),
572        };
573
574        assert!(config.needs_migration());
575
576        config.migrate().unwrap();
577
578        assert!(!config.needs_migration());
579        assert_eq!(config.full_version_with_patches, "0.0.13.2");
580    }
581
582    #[test]
583    fn test_validation() {
584        let mut config = VersionConfig::new();
585
586        // 有效配置应该通过验证
587        assert!(config.validate().is_ok());
588
589        // docker_service为空应该失败
590        config.docker_service = String::new();
591        assert!(config.validate().is_err());
592
593        // 无效的版本号格式应该失败
594        config.docker_service = "0.0.13".to_string();
595        config.full_version_with_patches = "invalid.version".to_string();
596        assert!(config.validate().is_err());
597    }
598
599    #[test]
600    fn test_rollback_last_patch() {
601        let mut config = VersionConfig::new();
602
603        // 没有补丁时回滚应该返回None
604        assert!(config.rollback_last_patch().unwrap().is_none());
605
606        // 应用两个补丁
607        config.apply_patch("patch-1".to_string());
608        config.apply_patch("patch-2".to_string());
609        assert_eq!(config.local_patch_level, 2);
610        assert_eq!(config.applied_patches.len(), 2);
611
612        // 回滚最后一个补丁
613        let rolled_back = config.rollback_last_patch().unwrap();
614        assert!(rolled_back.is_some());
615        assert_eq!(rolled_back.unwrap().version, "patch-2");
616        assert_eq!(config.local_patch_level, 1);
617        assert_eq!(config.applied_patches.len(), 1);
618        assert_eq!(config.patch_version, "patch-1");
619
620        // 回滚剩余的补丁
621        let rolled_back = config.rollback_last_patch().unwrap();
622        assert!(rolled_back.is_some());
623        assert_eq!(rolled_back.unwrap().version, "patch-1");
624        assert_eq!(config.local_patch_level, 0);
625        assert_eq!(config.applied_patches.len(), 0);
626        assert_eq!(config.patch_version, "0.0.0");
627    }
628
629    #[test]
630    fn test_patch_summary() {
631        let mut config = VersionConfig::new();
632
633        // 无补丁时的摘要
634        let summary = config.get_patch_summary();
635        assert!(summary.contains("无补丁"));
636
637        // 有补丁时的摘要
638        config.apply_patch("patch-1".to_string());
639        config.apply_patch("patch-2".to_string());
640        let summary = config.get_patch_summary();
641        assert!(summary.contains("已应用2个补丁"));
642        assert!(summary.contains("当前级别: 2"));
643    }
644
645    #[test]
646    fn test_serde_compatibility() {
647        // 测试序列化和反序列化
648        let mut config = VersionConfig::new();
649        config.apply_patch("test-patch".to_string());
650
651        // 序列化
652        let serialized = toml::to_string(&config).unwrap();
653
654        // 反序列化
655        let deserialized: VersionConfig = toml::from_str(&serialized).unwrap();
656
657        assert_eq!(config.docker_service, deserialized.docker_service);
658        assert_eq!(config.patch_version, deserialized.patch_version);
659        assert_eq!(config.local_patch_level, deserialized.local_patch_level);
660        assert_eq!(
661            config.full_version_with_patches,
662            deserialized.full_version_with_patches
663        );
664        assert_eq!(
665            config.applied_patches.len(),
666            deserialized.applied_patches.len()
667        );
668    }
669
670    // Task 1.3 验收标准测试
671    #[test]
672    fn test_task_1_3_acceptance_criteria() {
673        // 验收标准:扩展VersionConfig结构体
674        let mut config = VersionConfig::new();
675        assert!(!config.docker_service.is_empty());
676        assert!(config.patch_version.is_empty() || config.patch_version == "0.0.0");
677
678        // 验收标准:update_full_version方法
679        config.update_full_version("0.0.14".to_string());
680        assert_eq!(config.full_version_with_patches, "0.0.14.0");
681
682        // 验收标准:apply_patch方法
683        config.apply_patch("0.0.1".to_string());
684        assert_eq!(config.full_version_with_patches, "0.0.14.1");
685
686        // 验收标准:get_current_version方法
687        let version = config.get_current_version().unwrap();
688        assert_eq!(version.to_string(), "0.0.14.1");
689
690        // 验收标准:配置迁移逻辑(向后兼容)
691        let old_config = VersionConfig {
692            docker_service: "0.0.13".to_string(),
693            patch_version: String::new(),
694            local_patch_level: 0,
695            full_version_with_patches: String::new(),
696            last_full_upgrade: None,
697            last_patch_upgrade: None,
698            applied_patches: Vec::new(),
699        };
700        assert!(old_config.needs_migration());
701
702        println!("✅ Task 1.3: 配置文件结构扩展 - 验收标准测试通过");
703        println!("   - ✅ VersionConfig结构体扩展完成");
704        println!("   - ✅ update_full_version方法正常工作");
705        println!("   - ✅ apply_patch方法正常工作");
706        println!("   - ✅ get_current_version方法正常工作");
707        println!("   - ✅ 配置迁移逻辑(向后兼容)正常工作");
708    }
709
710    // 测试 find_archive_file 函数
711    #[test]
712    fn test_find_archive_file() {
713        let temp_dir = TempDir::new().unwrap();
714        let dir = temp_dir.path();
715
716        // 创建测试文件
717        File::create(dir.join("docker-aarch64.zip")).unwrap();
718        File::create(dir.join("docker-x86_64.tar.gz")).unwrap();
719        File::create(dir.join("other.txt")).unwrap();
720        File::create(dir.join("archive.zip")).unwrap(); // 不以 docker- 开头
721
722        // 查找应该返回第一个匹配的文件
723        let result = find_archive_file(dir);
724        assert_eq!(result, Some("docker-aarch64.zip".to_string()));
725    }
726
727    #[test]
728    fn test_find_archive_file_no_match() {
729        let temp_dir = TempDir::new().unwrap();
730        let dir = temp_dir.path();
731
732        // 创建不以 docker- 开头的文件
733        File::create(dir.join("archive.zip")).unwrap();
734        File::create(dir.join("other.txt")).unwrap();
735
736        // 查找应该返回 None
737        let result = find_archive_file(dir);
738        assert!(result.is_none());
739    }
740
741    #[test]
742    fn test_find_archive_file_empty_dir() {
743        let temp_dir = TempDir::new().unwrap();
744        let dir = temp_dir.path();
745
746        // 空目录
747        let result = find_archive_file(dir);
748        assert!(result.is_none());
749    }
750
751    #[test]
752    fn test_find_archive_file_tar_gz() {
753        let temp_dir = TempDir::new().unwrap();
754        let dir = temp_dir.path();
755
756        // 创建 .tar.gz 文件
757        File::create(dir.join("docker-aarch64.tar.gz")).unwrap();
758
759        let result = find_archive_file(dir);
760        assert_eq!(result, Some("docker-aarch64.tar.gz".to_string()));
761    }
762}