client-core 0.1.0

Duck Client 核心库
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
use crate::architecture::Architecture;
use crate::constants::{backup, config, docker, updates, version};
use crate::version::Version; // 新增:导入Version类型
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use toml;

/// 应用配置结构
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AppConfig {
    pub versions: VersionConfig,
    pub docker: DockerConfig,
    pub backup: BackupConfig,
    pub cache: CacheConfig,
    pub updates: UpdatesConfig,
}

/// 版本配置结构(支持增量版本管理)
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VersionConfig {
    /// 基础Docker服务版本(向后兼容字段)
    pub docker_service: String,

    /// 补丁版本信息
    #[serde(default)]
    pub patch_version: String,

    /// 本地已应用的补丁级别
    #[serde(default)]
    pub local_patch_level: u32,

    /// 完整版本号(包含补丁级别)
    #[serde(default)]
    pub full_version_with_patches: String,

    /// 最后一次全量升级时间
    #[serde(default)]
    pub last_full_upgrade: Option<chrono::DateTime<chrono::Utc>>,

    /// 最后一次补丁升级时间
    #[serde(default)]
    pub last_patch_upgrade: Option<chrono::DateTime<chrono::Utc>>,

    /// 已应用的补丁历史
    #[serde(default)]
    pub applied_patches: Vec<AppliedPatch>,
}

/// 已应用的补丁记录
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppliedPatch {
    pub version: String,
    pub level: u32,
    pub applied_at: chrono::DateTime<chrono::Utc>,
}

// 为了向后兼容,保留Versions类型别名
pub type Versions = VersionConfig;

impl VersionConfig {
    /// 创建新的版本配置
    pub fn new() -> Self {
        let docker_service = version::version_info::DEFAULT_DOCKER_SERVICE_VERSION.to_string();
        let full_version = format!("{docker_service}.0");

        Self {
            docker_service: docker_service.clone(),
            patch_version: "0.0.0".to_string(),
            local_patch_level: 0,
            full_version_with_patches: full_version,
            last_full_upgrade: None,
            last_patch_upgrade: None,
            applied_patches: Vec::new(),
        }
    }

    /// 更新全量版本
    pub fn update_full_version(&mut self, new_version: String) {
        self.docker_service = new_version.clone();
        self.local_patch_level = 0; // 重置补丁级别
        self.full_version_with_patches = format!("{new_version}.0");
        self.last_full_upgrade = Some(chrono::Utc::now());
        self.applied_patches.clear(); // 清空补丁历史

        tracing::info!("Full version updated: {} -> {}", self.docker_service, new_version);
    }

    /// 应用补丁
    pub fn apply_patch(&mut self, patch_version: String) {
        self.patch_version = patch_version.clone();
        self.local_patch_level += 1;
        self.full_version_with_patches =
            format!("{}.{}", self.docker_service, self.local_patch_level);
        self.last_patch_upgrade = Some(chrono::Utc::now());

        // 记录补丁历史
        self.applied_patches.push(AppliedPatch {
            version: patch_version.clone(),
            level: self.local_patch_level,
            applied_at: chrono::Utc::now(),
        });

        tracing::info!(
            "Patch applied: {} (level: {})",
            patch_version,
            self.local_patch_level
        );
    }

    /// 获取当前完整版本
    pub fn get_current_version(&self) -> Result<Version> {
        if !self.full_version_with_patches.is_empty() {
            self.full_version_with_patches.parse::<Version>()
        } else {
            // 向后兼容:如果没有完整版本信息,基于docker_service构建
            format!("{}.0", self.docker_service).parse::<Version>()
        }
    }

    /// 检查是否需要版本配置迁移
    pub fn needs_migration(&self) -> bool {
        self.full_version_with_patches.is_empty()
            || (self.local_patch_level == 0 && !self.applied_patches.is_empty())
    }

    /// 执行版本配置迁移
    pub fn migrate(&mut self) -> Result<()> {
        if self.full_version_with_patches.is_empty() {
            // 基于docker_service构建完整版本号
            self.full_version_with_patches =
                format!("{}.{}", self.docker_service, self.local_patch_level);
            tracing::info!(
                "Configuration migration: building full version number {}",
                self.full_version_with_patches
            );
        }

        // 验证配置的一致性
        self.validate()?;

        Ok(())
    }

    /// 验证版本配置的一致性
    pub fn validate(&self) -> Result<()> {
        // 验证基础版本号格式
        if self.docker_service.is_empty() {
            return Err(anyhow::anyhow!("docker_service cannot be empty"));
        }

        // 验证完整版本号格式
        if !self.full_version_with_patches.is_empty() {
            let _version = self
                .full_version_with_patches
                .parse::<Version>()
                .map_err(|e| anyhow::anyhow!(format!("Invalid full version number format: {e}")))?;
        }

        // 验证补丁级别与历史记录的一致性
        if self.applied_patches.len() != self.local_patch_level as usize {
            tracing::warn!(
                "Patch level inconsistent with history: level={}, history_count={}",
                self.local_patch_level,
                self.applied_patches.len()
            );
        }

        Ok(())
    }

    /// 获取补丁应用历史摘要
    pub fn get_patch_summary(&self) -> String {
        if self.applied_patches.is_empty() {
            format!("版本: {} (无补丁)", self.docker_service)
        } else {
            format!(
                "版本: {} (已应用{}个补丁,当前级别: {})",
                self.docker_service,
                self.applied_patches.len(),
                self.local_patch_level
            )
        }
    }

    /// 回滚最后一个补丁
    pub fn rollback_last_patch(&mut self) -> Result<Option<AppliedPatch>> {
        if let Some(last_patch) = self.applied_patches.pop() {
            if self.local_patch_level > 0 {
                self.local_patch_level -= 1;
            }

            self.full_version_with_patches =
                format!("{}.{}", self.docker_service, self.local_patch_level);

            // 更新patch_version为前一个补丁的版本(如果存在)
            if let Some(prev_patch) = self.applied_patches.last() {
                self.patch_version = prev_patch.version.clone();
            } else {
                self.patch_version = "0.0.0".to_string();
            }

            tracing::info!(
                "Rolled back patch: {} (level: {})",
                last_patch.version,
                last_patch.level
            );
            Ok(Some(last_patch))
        } else {
            Ok(None)
        }
    }
}

impl Default for VersionConfig {
    fn default() -> Self {
        Self::new()
    }
}

/// Docker相关配置
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DockerConfig {
    #[serde(default = "default_compose_file_path")]
    pub compose_file: String,
    #[serde(default = "default_env_file_path")]
    pub env_file: String,
}
// 默认值函数, 用于获取默认的环境文件路径
fn default_env_file_path() -> String {
    docker::get_env_file_path_str()
}

fn default_compose_file_path() -> String {
    docker::get_compose_file_path_str()
}

/// 备份相关配置
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BackupConfig {
    pub storage_dir: String,
}

/// 缓存相关配置
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CacheConfig {
    pub cache_dir: String,
    pub download_dir: String,
}

/// 更新相关配置
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UpdatesConfig {
    pub check_frequency: String,
}

impl Default for AppConfig {
    fn default() -> Self {
        Self {
            versions: VersionConfig::new(),
            docker: DockerConfig {
                compose_file: docker::get_compose_file_path_str(),
                env_file: docker::get_env_file_path_str(),
            },
            backup: BackupConfig {
                storage_dir: backup::get_default_storage_dir()
                    .to_string_lossy()
                    .to_string(),
            },
            cache: CacheConfig {
                cache_dir: config::get_default_cache_dir()
                    .to_string_lossy()
                    .to_string(),
                download_dir: config::get_default_download_dir()
                    .to_string_lossy()
                    .to_string(),
            },
            updates: UpdatesConfig {
                check_frequency: updates::DEFAULT_CHECK_FREQUENCY.to_string(),
            },
        }
    }
}

impl AppConfig {
    /// 获取 docker 应用版本配置
    pub fn get_docker_versions(&self) -> String {
        self.versions.docker_service.clone()
    }

    /// 写入 docker 应用版本配置
    pub fn write_docker_versions(&mut self, docker_service: String) {
        self.versions.docker_service = docker_service;
    }

    /// 智能查找并加载配置文件
    /// 按优先级查找:config.toml -> /app/config.toml
    pub fn find_and_load_config() -> Result<Self> {
        let config_files = ["config.toml", "/app/config.toml"];

        for config_file in &config_files {
            if Path::new(config_file).exists() {
                tracing::info!("Found configuration file: {}", config_file);
                return Self::load_from_file(config_file);
            }
        }

        // 如果没找到配置文件,创建默认配置
        tracing::warn!("Configuration file not found, creating default config: config.toml");
        let default_config = Self::default();
        default_config.save_to_file("config.toml")?;
        Ok(default_config)
    }

    /// 从指定文件加载配置
    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
        let content = fs::read_to_string(&path)?;
        let config: AppConfig = toml::from_str(&content)?;

        Ok(config)
    }

    /// 保存配置到文件
    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
        let content = self.to_toml_with_comments();
        fs::write(&path, content)?;
        Ok(())
    }

    /// 生成带注释的TOML配置
    fn to_toml_with_comments(&self) -> String {
        const TEMPLATE: &str = include_str!("../templates/config.toml.template");

        // 将所有路径的反斜杠替换为正斜杠,确保TOML兼容性
        let compose_file = self.docker.compose_file.replace('\\', "/");
        let backup_storage_dir = self.backup.storage_dir.replace('\\', "/");
        let cache_dir = self.cache.cache_dir.replace('\\', "/");
        let download_dir = self.cache.download_dir.replace('\\', "/");

        TEMPLATE
            .replace("{docker_service_version}", &self.get_docker_versions())
            .replace("{compose_file}", &compose_file)
            .replace("{backup_storage_dir}", &backup_storage_dir)
            .replace("{cache_dir}", &cache_dir)
            .replace("{download_dir}", &download_dir)
            .replace("{check_frequency}", &self.updates.check_frequency)
    }

    /// 确保缓存目录存在
    pub fn ensure_cache_dirs(&self) -> Result<()> {
        fs::create_dir_all(&self.cache.cache_dir)?;
        fs::create_dir_all(&self.cache.download_dir)?;
        Ok(())
    }

    /// 获取下载目录路径
    pub fn get_download_dir(&self) -> PathBuf {
        PathBuf::from(&self.cache.download_dir)
    }

    /// 获取指定版本的全量下载目录路径
    pub fn get_version_download_dir(&self, version: &str, download_type: &str) -> PathBuf {
        PathBuf::from(&self.cache.download_dir)
            .join(version)
            .join(download_type)
    }

    /// 获取指定版本的全量下载文件路径
    ///
    /// 返回 Result,如果找不到归档文件则返回错误
    pub fn get_version_download_file_path(
        &self,
        version: &str,
        download_type: &str,
        filename: Option<&str>,
    ) -> Result<PathBuf> {
        let download_dir = self.get_version_download_dir(version, download_type);

        let path = match filename {
            Some(filename) => download_dir.join(filename),
            None => {
                // 自动查找目录中的归档文件
                match find_archive_file(&download_dir) {
                    Some(found) => download_dir.join(found),
                    None => {
                        // 未找到归档文件,返回错误
                        return Err(anyhow::anyhow!(
                            "Archive file not found, directory: {}, supported formats: .zip, .tar.gz",
                            download_dir.display()
                        ));
                    }
                }
            }
        };

        // 验证文件存在
        if !path.exists() {
            return Err(anyhow::anyhow!("Archive file does not exist: {}", path.display()));
        }

        Ok(path)
    }

    /// 确保指定版本的下载目录存在
    pub fn ensure_version_download_dir(
        &self,
        version: &str,
        download_type: &str,
    ) -> Result<PathBuf> {
        let dir = self.get_version_download_dir(version, download_type);
        fs::create_dir_all(&dir)?;
        Ok(dir)
    }

    /// 获取备份目录路径
    pub fn get_backup_dir(&self) -> PathBuf {
        PathBuf::from(&self.backup.storage_dir)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use tempfile::TempDir;

    #[test]
    fn test_version_config_new() {
        let config = VersionConfig::new();

        assert!(!config.docker_service.is_empty());
        assert_eq!(config.patch_version, "0.0.0");
        assert_eq!(config.local_patch_level, 0);
        assert!(config.full_version_with_patches.ends_with(".0"));
        assert!(config.applied_patches.is_empty());
        assert!(config.last_full_upgrade.is_none());
        assert!(config.last_patch_upgrade.is_none());
    }

    #[test]
    fn test_update_full_version() {
        let mut config = VersionConfig::new();
        config.apply_patch("0.0.1".to_string());
        assert_eq!(config.local_patch_level, 1);

        // 更新全量版本应该重置补丁信息
        config.update_full_version("0.0.14".to_string());

        assert_eq!(config.docker_service, "0.0.14");
        assert_eq!(config.local_patch_level, 0);
        assert_eq!(config.full_version_with_patches, "0.0.14.0");
        assert!(config.applied_patches.is_empty());
        assert!(config.last_full_upgrade.is_some());
    }

    #[test]
    fn test_apply_patch() {
        let mut config = VersionConfig::new();
        let initial_service_version = config.docker_service.clone();

        // 应用第一个补丁
        config.apply_patch("patch-0.0.1".to_string());

        assert_eq!(config.patch_version, "patch-0.0.1");
        assert_eq!(config.local_patch_level, 1);
        assert_eq!(
            config.full_version_with_patches,
            format!("{initial_service_version}.1")
        );
        assert_eq!(config.applied_patches.len(), 1);
        assert!(config.last_patch_upgrade.is_some());

        // 应用第二个补丁
        config.apply_patch("patch-0.0.2".to_string());

        assert_eq!(config.patch_version, "patch-0.0.2");
        assert_eq!(config.local_patch_level, 2);
        assert_eq!(
            config.full_version_with_patches,
            format!("{initial_service_version}.2")
        );
        assert_eq!(config.applied_patches.len(), 2);

        // 验证补丁历史记录
        assert_eq!(config.applied_patches[0].version, "patch-0.0.1");
        assert_eq!(config.applied_patches[0].level, 1);
        assert_eq!(config.applied_patches[1].version, "patch-0.0.2");
        assert_eq!(config.applied_patches[1].level, 2);
    }

    #[test]
    fn test_get_current_version() {
        let mut config = VersionConfig::new();

        // 测试基础版本
        let version = config.get_current_version().unwrap();
        assert_eq!(version.build, 0);

        // 应用补丁后测试
        config.apply_patch("patch-0.0.1".to_string());
        let version = config.get_current_version().unwrap();
        assert_eq!(version.build, 1);
    }

    #[test]
    fn test_backward_compatibility() {
        // 测试向后兼容性:旧配置只有docker_service字段
        let old_config = VersionConfig {
            docker_service: "0.0.13".to_string(),
            patch_version: String::new(),
            local_patch_level: 0,
            full_version_with_patches: String::new(),
            last_full_upgrade: None,
            last_patch_upgrade: None,
            applied_patches: Vec::new(),
        };

        assert!(old_config.needs_migration());

        let version = old_config.get_current_version().unwrap();
        assert_eq!(version.to_string(), "0.0.13.0");
    }

    #[test]
    fn test_migration() {
        let mut config = VersionConfig {
            docker_service: "0.0.13".to_string(),
            patch_version: String::new(),
            local_patch_level: 2,
            full_version_with_patches: String::new(),
            last_full_upgrade: None,
            last_patch_upgrade: None,
            applied_patches: Vec::new(),
        };

        assert!(config.needs_migration());

        config.migrate().unwrap();

        assert!(!config.needs_migration());
        assert_eq!(config.full_version_with_patches, "0.0.13.2");
    }

    #[test]
    fn test_validation() {
        let mut config = VersionConfig::new();

        // 有效配置应该通过验证
        assert!(config.validate().is_ok());

        // docker_service为空应该失败
        config.docker_service = String::new();
        assert!(config.validate().is_err());

        // 无效的版本号格式应该失败
        config.docker_service = "0.0.13".to_string();
        config.full_version_with_patches = "invalid.version".to_string();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_rollback_last_patch() {
        let mut config = VersionConfig::new();

        // 没有补丁时回滚应该返回None
        assert!(config.rollback_last_patch().unwrap().is_none());

        // 应用两个补丁
        config.apply_patch("patch-1".to_string());
        config.apply_patch("patch-2".to_string());
        assert_eq!(config.local_patch_level, 2);
        assert_eq!(config.applied_patches.len(), 2);

        // 回滚最后一个补丁
        let rolled_back = config.rollback_last_patch().unwrap();
        assert!(rolled_back.is_some());
        assert_eq!(rolled_back.unwrap().version, "patch-2");
        assert_eq!(config.local_patch_level, 1);
        assert_eq!(config.applied_patches.len(), 1);
        assert_eq!(config.patch_version, "patch-1");

        // 回滚剩余的补丁
        let rolled_back = config.rollback_last_patch().unwrap();
        assert!(rolled_back.is_some());
        assert_eq!(rolled_back.unwrap().version, "patch-1");
        assert_eq!(config.local_patch_level, 0);
        assert_eq!(config.applied_patches.len(), 0);
        assert_eq!(config.patch_version, "0.0.0");
    }

    #[test]
    fn test_patch_summary() {
        let mut config = VersionConfig::new();

        // 无补丁时的摘要
        let summary = config.get_patch_summary();
        assert!(summary.contains("无补丁"));

        // 有补丁时的摘要
        config.apply_patch("patch-1".to_string());
        config.apply_patch("patch-2".to_string());
        let summary = config.get_patch_summary();
        assert!(summary.contains("已应用2个补丁"));
        assert!(summary.contains("当前级别: 2"));
    }

    #[test]
    fn test_serde_compatibility() {
        // 测试序列化和反序列化
        let mut config = VersionConfig::new();
        config.apply_patch("test-patch".to_string());

        // 序列化
        let serialized = toml::to_string(&config).unwrap();

        // 反序列化
        let deserialized: VersionConfig = toml::from_str(&serialized).unwrap();

        assert_eq!(config.docker_service, deserialized.docker_service);
        assert_eq!(config.patch_version, deserialized.patch_version);
        assert_eq!(config.local_patch_level, deserialized.local_patch_level);
        assert_eq!(
            config.full_version_with_patches,
            deserialized.full_version_with_patches
        );
        assert_eq!(
            config.applied_patches.len(),
            deserialized.applied_patches.len()
        );
    }

    // Task 1.3 验收标准测试
    #[test]
    fn test_task_1_3_acceptance_criteria() {
        // 验收标准:扩展VersionConfig结构体
        let mut config = VersionConfig::new();
        assert!(!config.docker_service.is_empty());
        assert!(config.patch_version.is_empty() || config.patch_version == "0.0.0");

        // 验收标准:update_full_version方法
        config.update_full_version("0.0.14".to_string());
        assert_eq!(config.full_version_with_patches, "0.0.14.0");

        // 验收标准:apply_patch方法
        config.apply_patch("0.0.1".to_string());
        assert_eq!(config.full_version_with_patches, "0.0.14.1");

        // 验收标准:get_current_version方法
        let version = config.get_current_version().unwrap();
        assert_eq!(version.to_string(), "0.0.14.1");

        // 验收标准:配置迁移逻辑(向后兼容)
        let old_config = VersionConfig {
            docker_service: "0.0.13".to_string(),
            patch_version: String::new(),
            local_patch_level: 0,
            full_version_with_patches: String::new(),
            last_full_upgrade: None,
            last_patch_upgrade: None,
            applied_patches: Vec::new(),
        };
        assert!(old_config.needs_migration());

        println!("✅ Task 1.3: 配置文件结构扩展 - 验收标准测试通过");
        println!("   - ✅ VersionConfig结构体扩展完成");
        println!("   - ✅ update_full_version方法正常工作");
        println!("   - ✅ apply_patch方法正常工作");
        println!("   - ✅ get_current_version方法正常工作");
        println!("   - ✅ 配置迁移逻辑(向后兼容)正常工作");
    }

    // 测试 find_archive_file 函数
    #[test]
    fn test_find_archive_file() {
        let temp_dir = TempDir::new().unwrap();
        let dir = temp_dir.path();

        // 创建测试文件
        File::create(dir.join("docker-aarch64.zip")).unwrap();
        File::create(dir.join("docker-x86_64.tar.gz")).unwrap();
        File::create(dir.join("other.txt")).unwrap();
        File::create(dir.join("archive.zip")).unwrap(); // 不以 docker- 开头

        // 查找应该返回第一个匹配的文件
        let result = find_archive_file(dir);
        assert_eq!(result, Some("docker-aarch64.zip".to_string()));
    }

    #[test]
    fn test_find_archive_file_no_match() {
        let temp_dir = TempDir::new().unwrap();
        let dir = temp_dir.path();

        // 创建不以 docker- 开头的文件
        File::create(dir.join("archive.zip")).unwrap();
        File::create(dir.join("other.txt")).unwrap();

        // 查找应该返回 None
        let result = find_archive_file(dir);
        assert!(result.is_none());
    }

    #[test]
    fn test_find_archive_file_empty_dir() {
        let temp_dir = TempDir::new().unwrap();
        let dir = temp_dir.path();

        // 空目录
        let result = find_archive_file(dir);
        assert!(result.is_none());
    }

    #[test]
    fn test_find_archive_file_tar_gz() {
        let temp_dir = TempDir::new().unwrap();
        let dir = temp_dir.path();

        // 创建 .tar.gz 文件
        File::create(dir.join("docker-aarch64.tar.gz")).unwrap();

        let result = find_archive_file(dir);
        assert_eq!(result, Some("docker-aarch64.tar.gz".to_string()));
    }
}

/// 在指定目录中查找归档文件(.zip 或 .tar.gz)
///
/// 查找规则:
/// 1. 文件名以 `docker-` 开头
/// 2. 扩展名为 `.zip` 或 `.tar.gz`
/// 3. 返回第一个匹配的文件
fn find_archive_file(dir: &Path) -> Option<String> {
    let entries = fs::read_dir(dir).ok()?;

    for entry in entries.flatten() {
        let path = entry.path();
        if !path.is_file() {
            continue;
        }

        let file_name = path.file_name()?.to_str()?;

        // 检查文件名是否以 docker- 开头
        if !file_name.starts_with("docker-") {
            continue;
        }

        // 检查扩展名
        if file_name.ends_with(".zip") || file_name.ends_with(".tar.gz") {
            return Some(file_name.to_string());
        }
    }

    None
}