vx_core/
downloader.rs

1//! Download and extraction utilities for VX tool manager
2
3use crate::{Result, VxEnvironment, VxError};
4use std::io::Write;
5use std::path::{Path, PathBuf};
6
7/// Tool downloader and installer
8pub struct ToolDownloader {
9    env: VxEnvironment,
10}
11
12impl ToolDownloader {
13    /// Create a new tool downloader
14    pub fn new() -> Result<Self> {
15        let env = VxEnvironment::new()?;
16        Ok(Self { env })
17    }
18
19    /// Download and install a tool from URL
20    pub async fn download_and_install(
21        &self,
22        tool_name: &str,
23        version: &str,
24        download_url: &str,
25    ) -> Result<PathBuf> {
26        // Create cache directory for downloads
27        let cache_dir = self.env.get_tool_cache_dir(tool_name);
28        std::fs::create_dir_all(&cache_dir)?;
29
30        // Download file
31        let filename = self.extract_filename_from_url(download_url);
32        let download_path = cache_dir.join(&filename);
33
34        println!("📥 Downloading {} from {}", filename, download_url);
35        self.download_file(download_url, &download_path).await?;
36
37        // Create installation directory
38        let install_dir = self.env.get_version_install_dir(tool_name, version);
39        std::fs::create_dir_all(&install_dir)?;
40
41        // Extract or copy file
42        let executable_path = if self.is_archive(&filename) {
43            println!("📦 Extracting archive...");
44            self.extract_archive(&download_path, &install_dir, tool_name)
45                .await?
46        } else {
47            println!("📋 Installing binary...");
48            self.install_binary(&download_path, &install_dir, tool_name)
49                .await?
50        };
51
52        // Make executable on Unix systems
53        #[cfg(unix)]
54        self.make_executable(&executable_path)?;
55
56        // Clean up download
57        let _ = std::fs::remove_file(&download_path);
58
59        println!("✅ Installation completed: {}", executable_path.display());
60        Ok(executable_path)
61    }
62
63    /// Download a file from URL
64    async fn download_file(&self, url: &str, output_path: &Path) -> Result<()> {
65        let client = crate::http::get_http_client();
66        let response = client
67            .get(url)
68            .send()
69            .await
70            .map_err(|e| VxError::DownloadFailed {
71                url: url.to_string(),
72                reason: e.to_string(),
73            })?;
74
75        if !response.status().is_success() {
76            return Err(VxError::DownloadFailed {
77                url: url.to_string(),
78                reason: format!("HTTP {}", response.status()),
79            });
80        }
81
82        let bytes = response
83            .bytes()
84            .await
85            .map_err(|e| VxError::DownloadFailed {
86                url: url.to_string(),
87                reason: e.to_string(),
88            })?;
89
90        let mut file = std::fs::File::create(output_path)?;
91        file.write_all(&bytes)?;
92        file.flush()?;
93
94        Ok(())
95    }
96
97    /// Extract filename from URL
98    fn extract_filename_from_url(&self, url: &str) -> String {
99        url.split('/')
100            .next_back()
101            .unwrap_or("download")
102            .split('?')
103            .next()
104            .unwrap_or("download")
105            .to_string()
106    }
107
108    /// Check if file is an archive
109    fn is_archive(&self, filename: &str) -> bool {
110        let lower = filename.to_lowercase();
111        lower.ends_with(".tar.gz")
112            || lower.ends_with(".tgz")
113            || lower.ends_with(".tar.xz")
114            || lower.ends_with(".tar.bz2")
115            || lower.ends_with(".zip")
116            || lower.ends_with(".7z")
117    }
118
119    /// Extract archive to installation directory
120    async fn extract_archive(
121        &self,
122        archive_path: &Path,
123        install_dir: &Path,
124        tool_name: &str,
125    ) -> Result<PathBuf> {
126        let filename = archive_path
127            .file_name()
128            .and_then(|n| n.to_str())
129            .unwrap_or("archive");
130
131        if filename.ends_with(".zip") {
132            self.extract_zip(archive_path, install_dir, tool_name).await
133        } else if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
134            self.extract_tar_gz(archive_path, install_dir, tool_name)
135                .await
136        } else {
137            // For other formats, try to use system tools or fall back to binary install
138            self.install_binary(archive_path, install_dir, tool_name)
139                .await
140        }
141    }
142
143    /// Extract ZIP archive
144    async fn extract_zip(
145        &self,
146        archive_path: &Path,
147        install_dir: &Path,
148        tool_name: &str,
149    ) -> Result<PathBuf> {
150        let file = std::fs::File::open(archive_path)?;
151        let mut archive = zip::ZipArchive::new(file).map_err(|e| VxError::InstallationFailed {
152            tool_name: tool_name.to_string(),
153            version: "unknown".to_string(),
154            message: format!("Failed to open ZIP archive: {}", e),
155        })?;
156
157        for i in 0..archive.len() {
158            let mut file = archive
159                .by_index(i)
160                .map_err(|e| VxError::InstallationFailed {
161                    tool_name: tool_name.to_string(),
162                    version: "unknown".to_string(),
163                    message: format!("Failed to read ZIP entry: {}", e),
164                })?;
165
166            let outpath = match file.enclosed_name() {
167                Some(path) => install_dir.join(path),
168                None => continue,
169            };
170
171            if file.name().ends_with('/') {
172                std::fs::create_dir_all(&outpath)?;
173            } else {
174                if let Some(p) = outpath.parent() {
175                    if !p.exists() {
176                        std::fs::create_dir_all(p)?;
177                    }
178                }
179                let mut outfile = std::fs::File::create(&outpath)?;
180                std::io::copy(&mut file, &mut outfile)?;
181            }
182        }
183
184        // Find the executable
185        self.find_executable_in_dir(install_dir, tool_name)
186    }
187
188    /// Extract tar.gz archive
189    async fn extract_tar_gz(
190        &self,
191        archive_path: &Path,
192        install_dir: &Path,
193        tool_name: &str,
194    ) -> Result<PathBuf> {
195        let file = std::fs::File::open(archive_path)?;
196        let decoder = flate2::read::GzDecoder::new(file);
197        let mut archive = tar::Archive::new(decoder);
198
199        archive
200            .unpack(install_dir)
201            .map_err(|e| VxError::InstallationFailed {
202                tool_name: tool_name.to_string(),
203                version: "unknown".to_string(),
204                message: format!("Failed to extract tar.gz: {}", e),
205            })?;
206
207        // Find the executable
208        self.find_executable_in_dir(install_dir, tool_name)
209    }
210
211    /// Install binary file
212    async fn install_binary(
213        &self,
214        binary_path: &Path,
215        install_dir: &Path,
216        tool_name: &str,
217    ) -> Result<PathBuf> {
218        let bin_dir = install_dir.join("bin");
219        std::fs::create_dir_all(&bin_dir)?;
220
221        let exe_name = if cfg!(windows) {
222            format!("{}.exe", tool_name)
223        } else {
224            tool_name.to_string()
225        };
226
227        let target_path = bin_dir.join(&exe_name);
228        std::fs::copy(binary_path, &target_path)?;
229
230        Ok(target_path)
231    }
232
233    /// Find executable in directory
234    fn find_executable_in_dir(&self, dir: &Path, tool_name: &str) -> Result<PathBuf> {
235        let env = &self.env;
236        env.find_executable_in_dir(dir, tool_name)
237    }
238
239    /// Make file executable on Unix systems
240    #[cfg(unix)]
241    fn make_executable(&self, path: &Path) -> Result<()> {
242        use std::os::unix::fs::PermissionsExt;
243
244        let metadata = std::fs::metadata(path)?;
245        let mut permissions = metadata.permissions();
246        permissions.set_mode(0o755);
247        std::fs::set_permissions(path, permissions)?;
248
249        Ok(())
250    }
251
252    /// Make file executable on Windows (no-op)
253    #[cfg(not(unix))]
254    #[allow(dead_code)]
255    fn make_executable(&self, _path: &Path) -> Result<()> {
256        Ok(())
257    }
258}
259
260impl Default for ToolDownloader {
261    fn default() -> Self {
262        Self::new().expect("Failed to create tool downloader")
263    }
264}