1use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7
8#[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#[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#[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#[derive(Debug, Clone, Default)]
39pub struct InstallOptions {
40 pub version: Option<String>,
42 pub force: bool,
44 pub dry_run: bool,
46 pub show_progress: bool,
48 pub install_dir: Option<PathBuf>,
50}
51
52pub struct Installer {
54 download_dir: PathBuf,
55 install_dir: PathBuf,
56}
57
58impl Installer {
59 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 pub fn with_dirs(download_dir: PathBuf, install_dir: PathBuf) -> Self {
73 Self {
74 download_dir,
75 install_dir,
76 }
77 }
78
79 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 std::fs::create_dir_all(&self.download_dir)
88 .map_err(|e| format!("创建下载目录失败: {}", e))?;
89
90 let filename = url.rsplit('/').next().unwrap_or("update.tar.gz");
92 let download_path = self.download_dir.join(filename);
93
94 tracing::info!("下载更新: {} -> {:?}", url, download_path);
96
97 Ok(download_path)
98 }
99
100 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 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 self.backup_current(install_dir)?;
123
124 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 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 let backup_path = self.get_backup_path(version);
153 if !backup_path.exists() {
154 return Err(format!("版本 {} 的备份不存在", version));
155 }
156
157 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 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 }
180
181 Ok(())
182 }
183
184 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 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 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 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 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 assert!(path.to_string_lossy().contains("v1.0.0"));
382 }
383}