1use crate::{
4 downloader::Downloader,
5 formats::ArchiveExtractor,
6 progress::{ProgressContext, ProgressStyle},
7 Error, Result,
8};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13pub struct Installer {
15 downloader: Downloader,
16 extractor: ArchiveExtractor,
17}
18
19impl Installer {
20 pub async fn new() -> Result<Self> {
22 let downloader = Downloader::new()?;
23 let extractor = ArchiveExtractor::new();
24
25 Ok(Self {
26 downloader,
27 extractor,
28 })
29 }
30
31 pub async fn install(&self, config: &InstallConfig) -> Result<PathBuf> {
33 if !config.force && self.is_installed(config).await? {
35 return Err(Error::AlreadyInstalled {
36 tool_name: config.tool_name.clone(),
37 version: config.version.clone(),
38 });
39 }
40
41 let progress = ProgressContext::new(
43 crate::progress::create_progress_reporter(ProgressStyle::default(), true),
44 true,
45 );
46
47 match &config.install_method {
48 InstallMethod::Archive { format: _ } => {
49 self.install_from_archive(config, &progress).await
50 }
51 InstallMethod::Binary => self.install_binary(config, &progress).await,
52 InstallMethod::Script { url } => self.install_from_script(config, url, &progress).await,
53 InstallMethod::PackageManager { manager, package } => {
54 self.install_from_package_manager(config, manager, package, &progress)
55 .await
56 }
57 InstallMethod::Custom { method } => {
58 self.install_custom(config, method, &progress).await
59 }
60 }
61 }
62
63 pub async fn is_installed(&self, config: &InstallConfig) -> Result<bool> {
65 let install_dir = &config.install_dir;
66
67 if !install_dir.exists() {
69 return Ok(false);
70 }
71
72 let bin_dir = install_dir.join("bin");
74 if bin_dir.exists() {
75 let exe_name = if cfg!(windows) {
76 format!("{}.exe", config.tool_name)
77 } else {
78 config.tool_name.clone()
79 };
80
81 let exe_path = bin_dir.join(&exe_name);
82 Ok(exe_path.exists() && exe_path.is_file())
83 } else {
84 self.has_executables(install_dir)
86 }
87 }
88
89 pub async fn uninstall(&self, _tool_name: &str, install_dir: &Path) -> Result<()> {
91 if install_dir.exists() {
92 std::fs::remove_dir_all(install_dir)?;
93 }
94 Ok(())
95 }
96
97 async fn install_from_archive(
99 &self,
100 config: &InstallConfig,
101 progress: &ProgressContext,
102 ) -> Result<PathBuf> {
103 let download_url = config
104 .download_url
105 .as_ref()
106 .ok_or_else(|| Error::InvalidConfig {
107 message: "Download URL is required for archive installation".to_string(),
108 })?;
109
110 let temp_path = self
112 .downloader
113 .download_temp(download_url, progress)
114 .await?;
115
116 let extracted_files = self
118 .extractor
119 .extract(&temp_path, &config.install_dir, progress)
120 .await?;
121
122 let executable_path = self
124 .extractor
125 .find_best_executable(&extracted_files, &config.tool_name)?;
126
127 let _ = std::fs::remove_file(temp_path);
129
130 Ok(executable_path)
131 }
132
133 async fn install_binary(
135 &self,
136 config: &InstallConfig,
137 progress: &ProgressContext,
138 ) -> Result<PathBuf> {
139 let download_url = config
140 .download_url
141 .as_ref()
142 .ok_or_else(|| Error::InvalidConfig {
143 message: "Download URL is required for binary installation".to_string(),
144 })?;
145
146 let bin_dir = config.install_dir.join("bin");
148 std::fs::create_dir_all(&bin_dir)?;
149
150 let exe_name = if cfg!(windows) {
152 format!("{}.exe", config.tool_name)
153 } else {
154 config.tool_name.clone()
155 };
156
157 let exe_path = bin_dir.join(&exe_name);
158
159 self.downloader
161 .download(download_url, &exe_path, progress)
162 .await?;
163
164 #[cfg(unix)]
166 {
167 use std::os::unix::fs::PermissionsExt;
168 let metadata = std::fs::metadata(&exe_path)?;
169 let mut permissions = metadata.permissions();
170 permissions.set_mode(0o755);
171 std::fs::set_permissions(&exe_path, permissions)?;
172 }
173
174 Ok(exe_path)
175 }
176
177 async fn install_from_script(
179 &self,
180 _config: &InstallConfig,
181 _script_url: &str,
182 _progress: &ProgressContext,
183 ) -> Result<PathBuf> {
184 Err(Error::unsupported_format("script installation"))
186 }
187
188 async fn install_from_package_manager(
190 &self,
191 _config: &InstallConfig,
192 _manager: &str,
193 _package: &str,
194 _progress: &ProgressContext,
195 ) -> Result<PathBuf> {
196 Err(Error::unsupported_format("package manager installation"))
198 }
199
200 async fn install_custom(
202 &self,
203 _config: &InstallConfig,
204 _method: &str,
205 _progress: &ProgressContext,
206 ) -> Result<PathBuf> {
207 Err(Error::unsupported_format("custom installation"))
209 }
210
211 fn has_executables(&self, dir: &Path) -> Result<bool> {
213 if !dir.exists() {
214 return Ok(false);
215 }
216
217 for entry in walkdir::WalkDir::new(dir).max_depth(3) {
218 let entry = entry?;
219 let path = entry.path();
220
221 if path.is_file() {
222 #[cfg(unix)]
223 {
224 use std::os::unix::fs::PermissionsExt;
225 if let Ok(metadata) = std::fs::metadata(path) {
226 let permissions = metadata.permissions();
227 if permissions.mode() & 0o111 != 0 {
228 return Ok(true);
229 }
230 }
231 }
232
233 #[cfg(windows)]
234 {
235 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
236 if matches!(ext.to_lowercase().as_str(), "exe" | "bat" | "cmd" | "com") {
237 return Ok(true);
238 }
239 }
240 }
241 }
242 }
243
244 Ok(false)
245 }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct InstallConfig {
251 pub tool_name: String,
253
254 pub version: String,
256
257 pub install_method: InstallMethod,
259
260 pub download_url: Option<String>,
262
263 pub install_dir: PathBuf,
265
266 pub force: bool,
268
269 pub checksum: Option<String>,
271
272 pub metadata: HashMap<String, String>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub enum InstallMethod {
279 Archive { format: ArchiveFormat },
281
282 PackageManager { manager: String, package: String },
284
285 Script { url: String },
287
288 Binary,
290
291 Custom { method: String },
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
297pub enum ArchiveFormat {
298 Zip,
299 TarGz,
300 TarXz,
301 TarBz2,
302 SevenZip,
303}
304
305pub struct InstallConfigBuilder {
307 config: InstallConfig,
308}
309
310impl Default for InstallConfigBuilder {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316impl InstallConfigBuilder {
317 pub fn new() -> Self {
319 Self {
320 config: InstallConfig {
321 tool_name: String::new(),
322 version: String::new(),
323 install_method: InstallMethod::Binary,
324 download_url: None,
325 install_dir: PathBuf::new(),
326 force: false,
327 checksum: None,
328 metadata: HashMap::new(),
329 },
330 }
331 }
332
333 pub fn tool_name(mut self, name: impl Into<String>) -> Self {
335 self.config.tool_name = name.into();
336 self
337 }
338
339 pub fn version(mut self, version: impl Into<String>) -> Self {
341 self.config.version = version.into();
342 self
343 }
344
345 pub fn install_method(mut self, method: InstallMethod) -> Self {
347 self.config.install_method = method;
348 self
349 }
350
351 pub fn download_url(mut self, url: impl Into<String>) -> Self {
353 self.config.download_url = Some(url.into());
354 self
355 }
356
357 pub fn install_dir(mut self, dir: impl Into<PathBuf>) -> Self {
359 self.config.install_dir = dir.into();
360 self
361 }
362
363 pub fn force(mut self, force: bool) -> Self {
365 self.config.force = force;
366 self
367 }
368
369 pub fn checksum(mut self, checksum: impl Into<String>) -> Self {
371 self.config.checksum = Some(checksum.into());
372 self
373 }
374
375 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
377 self.config.metadata.insert(key.into(), value.into());
378 self
379 }
380
381 pub fn build(self) -> InstallConfig {
383 self.config
384 }
385}
386
387impl InstallConfig {
388 pub fn builder() -> InstallConfigBuilder {
390 InstallConfigBuilder::new()
391 }
392}