1use crate::{Result, VxEnvironment, VxError};
4use std::io::Write;
5use std::path::{Path, PathBuf};
6
7pub struct ToolDownloader {
9 env: VxEnvironment,
10}
11
12impl ToolDownloader {
13 pub fn new() -> Result<Self> {
15 let env = VxEnvironment::new()?;
16 Ok(Self { env })
17 }
18
19 pub async fn download_and_install(
21 &self,
22 tool_name: &str,
23 version: &str,
24 download_url: &str,
25 ) -> Result<PathBuf> {
26 let cache_dir = self.env.get_tool_cache_dir(tool_name);
28 std::fs::create_dir_all(&cache_dir)?;
29
30 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 let install_dir = self.env.get_version_install_dir(tool_name, version);
39 std::fs::create_dir_all(&install_dir)?;
40
41 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 #[cfg(unix)]
54 self.make_executable(&executable_path)?;
55
56 let _ = std::fs::remove_file(&download_path);
58
59 println!("✅ Installation completed: {}", executable_path.display());
60 Ok(executable_path)
61 }
62
63 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 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 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 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 self.install_binary(archive_path, install_dir, tool_name)
139 .await
140 }
141 }
142
143 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 self.find_executable_in_dir(install_dir, tool_name)
186 }
187
188 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 self.find_executable_in_dir(install_dir, tool_name)
209 }
210
211 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 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 #[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 #[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}