1use std::error::Error;
2use std::fmt;
3use std::fs;
4use std::io;
5use std::path::Path;
6use std::process::Command;
7
8#[derive(Debug)]
10pub enum DownloadError {
11 CreateDirectoryFailed(io::Error),
12 CurlExecutionFailed(io::Error),
13 DownloadFailed(String),
14 FileMetadataError(io::Error),
15 FileTooSmall(i64, i64),
16 RemoveFileFailed(io::Error),
17 ExtractionFailed(String),
18 CommandExecutionFailed(io::Error),
19 InvalidUrl(String),
20 NotAFile(String),
21 PlatformNotSupported(String),
22 InstallationFailed(String),
23}
24
25impl fmt::Display for DownloadError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 DownloadError::CreateDirectoryFailed(e) => {
30 write!(f, "Error creating directories: {}", e)
31 }
32 DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e),
33 DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url),
34 DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e),
35 DownloadError::FileTooSmall(size, min) => write!(
36 f,
37 "Error: Downloaded file is too small ({}KB < {}KB)",
38 size, min
39 ),
40 DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e),
41 DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e),
42 DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e),
43 DownloadError::InvalidUrl(url) => write!(f, "Invalid URL: {}", url),
44 DownloadError::NotAFile(path) => write!(f, "Not a file: {}", path),
45 DownloadError::PlatformNotSupported(msg) => write!(f, "{}", msg),
46 DownloadError::InstallationFailed(msg) => write!(f, "{}", msg),
47 }
48 }
49}
50
51impl Error for DownloadError {
53 fn source(&self) -> Option<&(dyn Error + 'static)> {
54 match self {
55 DownloadError::CreateDirectoryFailed(e) => Some(e),
56 DownloadError::CurlExecutionFailed(e) => Some(e),
57 DownloadError::FileMetadataError(e) => Some(e),
58 DownloadError::RemoveFileFailed(e) => Some(e),
59 DownloadError::CommandExecutionFailed(e) => Some(e),
60 _ => None,
61 }
62 }
63}
64
65pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
103 let dest_path = Path::new(dest);
105 fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?;
106
107 let filename = match url.split('/').last() {
109 Some(name) => name,
110 None => {
111 return Err(DownloadError::InvalidUrl(
112 "cannot extract filename".to_string(),
113 ))
114 }
115 };
116
117 let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename);
119
120 let temp_path = format!("{}.download", file_path);
122
123 println!("Downloading {} to {}", url, file_path);
125 let output = Command::new("curl")
126 .args(&[
127 "--progress-bar",
128 "--location",
129 "--fail",
130 "--output",
131 &temp_path,
132 url,
133 ])
134 .status()
135 .map_err(DownloadError::CurlExecutionFailed)?;
136
137 if !output.success() {
138 return Err(DownloadError::DownloadFailed(url.to_string()));
139 }
140
141 match fs::metadata(&temp_path) {
143 Ok(metadata) => {
144 let size_bytes = metadata.len();
145 let size_kb = size_bytes / 1024;
146 let size_mb = size_kb / 1024;
147 if size_mb > 1 {
148 println!(
149 "Download complete! File size: {:.2} MB",
150 size_bytes as f64 / (1024.0 * 1024.0)
151 );
152 } else {
153 println!(
154 "Download complete! File size: {:.2} KB",
155 size_bytes as f64 / 1024.0
156 );
157 }
158 }
159 Err(_) => println!("Download complete!"),
160 }
161
162 if min_size_kb > 0 {
164 let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
165 let size_kb = metadata.len() as i64 / 1024;
166 if size_kb < min_size_kb {
167 fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
168 return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
169 }
170 }
171
172 let lower_url = url.to_lowercase();
174 let is_archive = lower_url.ends_with(".tar.gz")
175 || lower_url.ends_with(".tgz")
176 || lower_url.ends_with(".tar")
177 || lower_url.ends_with(".zip");
178
179 if is_archive {
180 println!("Extracting {} to {}", temp_path, dest);
182 let output = if lower_url.ends_with(".zip") {
183 Command::new("unzip")
184 .args(&["-o", &temp_path, "-d", dest]) .status()
186 } else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") {
187 Command::new("tar")
188 .args(&["-xzvf", &temp_path, "-C", dest]) .status()
190 } else {
191 Command::new("tar")
192 .args(&["-xvf", &temp_path, "-C", dest]) .status()
194 };
195
196 match output {
197 Ok(status) => {
198 if !status.success() {
199 return Err(DownloadError::ExtractionFailed(
200 "Error extracting archive".to_string(),
201 ));
202 }
203 }
204 Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
205 }
206
207 match fs::read_dir(dest) {
209 Ok(entries) => {
210 let count = entries.count();
211 println!("Extraction complete! Extracted {} files/directories", count);
212 }
213 Err(_) => println!("Extraction complete!"),
214 }
215
216 fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
218
219 Ok(dest.to_string())
220 } else {
221 fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
223
224 Ok(file_path)
225 }
226}
227
228pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
259 let dest_path = Path::new(dest);
261 if let Some(parent) = dest_path.parent() {
262 fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?;
263 }
264
265 let temp_path = format!("{}.download", dest);
267
268 println!("Downloading {} to {}", url, dest);
270 let output = Command::new("curl")
271 .args(&[
272 "--progress-bar",
273 "--location",
274 "--fail",
275 "--output",
276 &temp_path,
277 url,
278 ])
279 .status()
280 .map_err(DownloadError::CurlExecutionFailed)?;
281
282 if !output.success() {
283 return Err(DownloadError::DownloadFailed(url.to_string()));
284 }
285
286 match fs::metadata(&temp_path) {
288 Ok(metadata) => {
289 let size_bytes = metadata.len();
290 let size_kb = size_bytes / 1024;
291 let size_mb = size_kb / 1024;
292 if size_mb > 1 {
293 println!(
294 "Download complete! File size: {:.2} MB",
295 size_bytes as f64 / (1024.0 * 1024.0)
296 );
297 } else {
298 println!(
299 "Download complete! File size: {:.2} KB",
300 size_bytes as f64 / 1024.0
301 );
302 }
303 }
304 Err(_) => println!("Download complete!"),
305 }
306
307 if min_size_kb > 0 {
309 let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
310 let size_kb = metadata.len() as i64 / 1024;
311 if size_kb < min_size_kb {
312 fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
313 return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
314 }
315 }
316
317 fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
319
320 Ok(dest.to_string())
321}
322
323pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
348 let path_obj = Path::new(path);
349
350 if !path_obj.exists() {
352 return Err(DownloadError::NotAFile(format!(
353 "Path does not exist: {}",
354 path
355 )));
356 }
357
358 if !path_obj.is_file() {
359 return Err(DownloadError::NotAFile(format!(
360 "Path is not a file: {}",
361 path
362 )));
363 }
364
365 let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?;
367 let mut permissions = metadata.permissions();
368
369 #[cfg(unix)]
371 {
372 use std::os::unix::fs::PermissionsExt;
373 let mode = permissions.mode();
374 let new_mode = mode | 0o111;
376 permissions.set_mode(new_mode);
377 }
378
379 #[cfg(not(unix))]
380 {
381 return Ok(format!(
384 "Made {} executable (note: non-Unix platform, may not be fully supported)",
385 path
386 ));
387 }
388
389 fs::set_permissions(path, permissions).map_err(|e| {
391 DownloadError::CommandExecutionFailed(io::Error::new(
392 io::ErrorKind::Other,
393 format!("Failed to set executable permissions: {}", e),
394 ))
395 })?;
396
397 Ok(format!("Made {} executable", path))
398}
399
400pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadError> {
431 let filename = match url.split('/').last() {
433 Some(name) => name,
434 None => {
435 return Err(DownloadError::InvalidUrl(
436 "cannot extract filename".to_string(),
437 ))
438 }
439 };
440
441 let dest_path = format!("/tmp/{}", filename);
443
444 let lower_url = url.to_lowercase();
446 let is_archive = lower_url.ends_with(".tar.gz")
447 || lower_url.ends_with(".tgz")
448 || lower_url.ends_with(".tar")
449 || lower_url.ends_with(".zip");
450
451 let download_result = if is_archive {
452 download(url, "/tmp", min_size_kb)?
454 } else {
455 download_file(url, &dest_path, min_size_kb)?
457 };
458
459 let path = Path::new(&dest_path);
461 if !path.is_file() {
462 return Ok(download_result); }
464
465 if dest_path.to_lowercase().ends_with(".deb") {
467 let platform_check = Command::new("sh")
469 .arg("-c")
470 .arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version")
471 .status();
472
473 match platform_check {
474 Ok(status) => {
475 if !status.success() {
476 return Err(DownloadError::PlatformNotSupported(
477 "Cannot install .deb package: not on a Debian-based system".to_string(),
478 ));
479 }
480 }
481 Err(_) => {
482 return Err(DownloadError::PlatformNotSupported(
483 "Failed to check system compatibility for .deb installation".to_string(),
484 ))
485 }
486 }
487
488 println!("Installing package: {}", dest_path);
490 let install_result = Command::new("sudo")
491 .args(&["dpkg", "--install", &dest_path])
492 .status();
493
494 match install_result {
495 Ok(status) => {
496 if !status.success() {
497 println!("Attempting to resolve dependencies...");
499 let fix_deps = Command::new("sudo")
500 .args(&["apt-get", "install", "-f", "-y"])
501 .status();
502
503 if let Ok(fix_status) = fix_deps {
504 if !fix_status.success() {
505 return Err(DownloadError::InstallationFailed(
506 "Failed to resolve package dependencies".to_string(),
507 ));
508 }
509 } else {
510 return Err(DownloadError::InstallationFailed(
511 "Failed to resolve package dependencies".to_string(),
512 ));
513 }
514 }
515 println!("Package installation completed successfully");
516 }
517 Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
518 }
519 }
520
521 Ok(download_result)
522}