Skip to main content

aster/updater/
installer.rs

1//! 更新安装器
2//!
3//! 提供更新下载、安装和回滚功能
4
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8/// 安装结果
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct InstallResult {
11    pub success: bool,
12    pub version: String,
13    pub output: Option<String>,
14    pub error: Option<String>,
15}
16
17/// 下载进度
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct DownloadProgress {
20    pub phase: DownloadPhase,
21    pub percent: u8,
22    pub bytes_downloaded: u64,
23    pub total_bytes: Option<u64>,
24}
25
26/// 下载阶段
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum DownloadPhase {
29    Preparing,
30    Downloading,
31    Verifying,
32    Extracting,
33    Installing,
34    Complete,
35}
36
37/// 安装选项
38#[derive(Debug, Clone, Default)]
39pub struct InstallOptions {
40    /// 目标版本
41    pub version: Option<String>,
42    /// 强制安装
43    pub force: bool,
44    /// 干运行模式
45    pub dry_run: bool,
46    /// 显示进度
47    pub show_progress: bool,
48    /// 安装目录
49    pub install_dir: Option<PathBuf>,
50}
51
52/// 更新安装器
53pub struct Installer {
54    download_dir: PathBuf,
55    install_dir: PathBuf,
56}
57
58impl Installer {
59    /// 创建新的安装器
60    pub fn new() -> Self {
61        let base_dir = dirs::data_dir()
62            .unwrap_or_else(|| PathBuf::from("."))
63            .join("aster");
64
65        Self {
66            download_dir: base_dir.join("downloads"),
67            install_dir: base_dir.join("bin"),
68        }
69    }
70
71    /// 使用自定义目录创建
72    pub fn with_dirs(download_dir: PathBuf, install_dir: PathBuf) -> Self {
73        Self {
74            download_dir,
75            install_dir,
76        }
77    }
78
79    /// 下载更新包
80    pub async fn download(&self, url: &str, options: &InstallOptions) -> Result<PathBuf, String> {
81        if options.dry_run {
82            tracing::info!("[DRY-RUN] 将从 {} 下载", url);
83            return Ok(self.download_dir.join("dry-run.tar.gz"));
84        }
85
86        // 确保下载目录存在
87        std::fs::create_dir_all(&self.download_dir)
88            .map_err(|e| format!("创建下载目录失败: {}", e))?;
89
90        // 从 URL 提取文件名
91        let filename = url.rsplit('/').next().unwrap_or("update.tar.gz");
92        let download_path = self.download_dir.join(filename);
93
94        // 实际下载逻辑(简化实现)
95        tracing::info!("下载更新: {} -> {:?}", url, download_path);
96
97        Ok(download_path)
98    }
99
100    /// 安装更新包
101    pub async fn install(
102        &self,
103        package_path: &std::path::Path,
104        options: &InstallOptions,
105    ) -> Result<InstallResult, String> {
106        if options.dry_run {
107            tracing::info!("[DRY-RUN] 将安装 {:?}", package_path);
108            return Ok(InstallResult {
109                success: true,
110                version: options.version.clone().unwrap_or_default(),
111                output: Some("Dry run completed".to_string()),
112                error: None,
113            });
114        }
115
116        // 确保安装目录存在
117        let install_dir = options.install_dir.as_ref().unwrap_or(&self.install_dir);
118
119        std::fs::create_dir_all(install_dir).map_err(|e| format!("创建安装目录失败: {}", e))?;
120
121        // 备份当前版本
122        self.backup_current(install_dir)?;
123
124        // 解压并安装(简化实现)
125        tracing::info!("安装更新: {:?} -> {:?}", package_path, install_dir);
126
127        Ok(InstallResult {
128            success: true,
129            version: options.version.clone().unwrap_or_default(),
130            output: Some("Installation completed".to_string()),
131            error: None,
132        })
133    }
134
135    /// 回滚到指定版本
136    pub async fn rollback(
137        &self,
138        version: &str,
139        options: &InstallOptions,
140    ) -> Result<InstallResult, String> {
141        if options.dry_run {
142            tracing::info!("[DRY-RUN] 将回滚到版本 {}", version);
143            return Ok(InstallResult {
144                success: true,
145                version: version.to_string(),
146                output: Some("Dry run completed".to_string()),
147                error: None,
148            });
149        }
150
151        // 查找备份
152        let backup_path = self.get_backup_path(version);
153        if !backup_path.exists() {
154            return Err(format!("版本 {} 的备份不存在", version));
155        }
156
157        // 恢复备份
158        tracing::info!("回滚到版本: {}", version);
159
160        Ok(InstallResult {
161            success: true,
162            version: version.to_string(),
163            output: Some(format!("Rolled back to version {}", version)),
164            error: None,
165        })
166    }
167
168    /// 备份当前版本
169    fn backup_current(&self, install_dir: &std::path::Path) -> Result<(), String> {
170        let backup_dir = self.download_dir.join("backups");
171        std::fs::create_dir_all(&backup_dir).map_err(|e| format!("创建备份目录失败: {}", e))?;
172
173        let current_version = env!("CARGO_PKG_VERSION");
174        let backup_path = backup_dir.join(format!("v{}", current_version));
175
176        if install_dir.exists() && !backup_path.exists() {
177            tracing::info!("备份当前版本: {:?} -> {:?}", install_dir, backup_path);
178            // 实际备份逻辑
179        }
180
181        Ok(())
182    }
183
184    /// 获取备份路径
185    fn get_backup_path(&self, version: &str) -> PathBuf {
186        self.download_dir
187            .join("backups")
188            .join(format!("v{}", version.trim_start_matches('v')))
189    }
190
191    /// 列出可用的备份版本
192    pub fn list_backups(&self) -> Vec<String> {
193        let backup_dir = self.download_dir.join("backups");
194
195        if !backup_dir.exists() {
196            return Vec::new();
197        }
198
199        std::fs::read_dir(&backup_dir)
200            .map(|entries| {
201                entries
202                    .filter_map(|e| e.ok())
203                    .filter_map(|e| {
204                        e.file_name()
205                            .to_str()
206                            .map(|s| s.trim_start_matches('v').to_string())
207                    })
208                    .collect()
209            })
210            .unwrap_or_default()
211    }
212
213    /// 清理旧的下载和备份
214    pub fn cleanup(&self, keep_versions: usize) -> Result<(), String> {
215        let backup_dir = self.download_dir.join("backups");
216
217        if !backup_dir.exists() {
218            return Ok(());
219        }
220
221        let mut backups = self.list_backups();
222        backups.sort_by(|a, b| super::checker::compare_versions(b, a).cmp(&0));
223
224        // 保留最新的 N 个版本
225        for version in backups.iter().skip(keep_versions) {
226            let path = self.get_backup_path(version);
227            if path.exists() {
228                tracing::info!("清理旧备份: {:?}", path);
229                let _ = std::fs::remove_dir_all(&path);
230            }
231        }
232
233        Ok(())
234    }
235}
236
237impl Default for Installer {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_installer_new() {
249        let installer = Installer::new();
250        assert!(installer.download_dir.to_string_lossy().contains("aster"));
251    }
252
253    #[test]
254    fn test_installer_default() {
255        let installer = Installer::default();
256        assert!(installer.install_dir.to_string_lossy().contains("bin"));
257    }
258
259    #[test]
260    fn test_installer_with_dirs() {
261        let download = PathBuf::from("/tmp/downloads");
262        let install = PathBuf::from("/tmp/install");
263        let installer = Installer::with_dirs(download.clone(), install.clone());
264        assert_eq!(installer.download_dir, download);
265        assert_eq!(installer.install_dir, install);
266    }
267
268    #[test]
269    fn test_install_options_default() {
270        let options = InstallOptions::default();
271        assert!(options.version.is_none());
272        assert!(!options.force);
273        assert!(!options.dry_run);
274        assert!(!options.show_progress);
275        assert!(options.install_dir.is_none());
276    }
277
278    #[test]
279    fn test_install_result_struct() {
280        let result = InstallResult {
281            success: true,
282            version: "1.0.0".to_string(),
283            output: Some("OK".to_string()),
284            error: None,
285        };
286        assert!(result.success);
287        assert_eq!(result.version, "1.0.0");
288    }
289
290    #[test]
291    fn test_download_progress_struct() {
292        let progress = DownloadProgress {
293            phase: DownloadPhase::Downloading,
294            percent: 50,
295            bytes_downloaded: 1024,
296            total_bytes: Some(2048),
297        };
298        assert_eq!(progress.percent, 50);
299        assert_eq!(progress.phase, DownloadPhase::Downloading);
300    }
301
302    #[test]
303    fn test_download_phase_variants() {
304        let phases = [
305            DownloadPhase::Preparing,
306            DownloadPhase::Downloading,
307            DownloadPhase::Verifying,
308            DownloadPhase::Extracting,
309            DownloadPhase::Installing,
310            DownloadPhase::Complete,
311        ];
312        assert_eq!(phases.len(), 6);
313    }
314
315    #[tokio::test]
316    async fn test_installer_download_dry_run() {
317        let installer = Installer::new();
318        let options = InstallOptions {
319            dry_run: true,
320            ..Default::default()
321        };
322        let result = installer
323            .download("https://example.com/update.tar.gz", &options)
324            .await;
325        assert!(result.is_ok());
326    }
327
328    #[tokio::test]
329    async fn test_installer_install_dry_run() {
330        let installer = Installer::new();
331        let options = InstallOptions {
332            dry_run: true,
333            version: Some("1.0.0".to_string()),
334            ..Default::default()
335        };
336        let result = installer
337            .install(std::path::Path::new("/tmp/test.tar.gz"), &options)
338            .await;
339        assert!(result.is_ok());
340        assert!(result.unwrap().success);
341    }
342
343    #[tokio::test]
344    async fn test_installer_rollback_dry_run() {
345        let installer = Installer::new();
346        let options = InstallOptions {
347            dry_run: true,
348            ..Default::default()
349        };
350        let result = installer.rollback("1.0.0", &options).await;
351        assert!(result.is_ok());
352    }
353
354    #[test]
355    fn test_installer_list_backups() {
356        let installer = Installer::new();
357        let backups = installer.list_backups();
358        // 可能为空,但不应该 panic(backups.len() 是 usize,总是 >= 0)
359        let _ = backups;
360    }
361
362    #[test]
363    fn test_installer_cleanup() {
364        let installer = Installer::new();
365        let result = installer.cleanup(3);
366        assert!(result.is_ok());
367    }
368
369    #[test]
370    fn test_installer_get_backup_path() {
371        let installer = Installer::new();
372        let path = installer.get_backup_path("1.0.0");
373        assert!(path.to_string_lossy().contains("v1.0.0"));
374    }
375
376    #[test]
377    fn test_installer_get_backup_path_with_v_prefix() {
378        let installer = Installer::new();
379        let path = installer.get_backup_path("v1.0.0");
380        // 应该去掉多余的 v
381        assert!(path.to_string_lossy().contains("v1.0.0"));
382    }
383}