1use futures_util::StreamExt;
2use std::path::PathBuf;
3use tokio::{fs, io::AsyncWriteExt};
4
5use crate::{Error, Result, appimages_dir, make_progress_bar};
6
7#[derive(Debug, Default)]
8pub struct Downloader {}
9
10impl Downloader {
11 pub fn new() -> Self {
12 Self {}
13 }
14 pub fn prepare_path(&self, url: &str, executable: &str) -> Result<PathBuf> {
15 let filename = match url.split('/').next_back() {
17 Some(name) => {
18 if name.to_lowercase().ends_with(".appimage") {
19 name.to_string()
20 } else {
21 format!("{executable}.AppImage")
22 }
23 }
24 None => format!("{executable}.AppImage"),
25 };
26
27 Ok(appimages_dir()?.join(filename))
28 }
29 pub fn validate_response(&self, resp: &reqwest::Response) -> Result<()> {
30 if !resp.status().is_success() {
31 return Err(Error::Download {
32 url: resp.url().to_string(),
33 source: resp.error_for_status_ref().unwrap_err(),
34 });
35 }
36
37 if let Some(len) = resp.content_length()
38 && len < 1024
39 {
40 return Err(Error::InvalidAppImage);
41 }
42
43 let content_type = resp
44 .headers()
45 .get("content-type")
46 .and_then(|ct| ct.to_str().ok())
47 .unwrap_or("")
48 .to_lowercase();
49
50 let is_binary = matches!(
51 content_type.as_str(),
52 "application/octet-stream"
53 | "application/vnd.appimage"
54 | "application/x-executable"
55 | "application/x-elf"
56 | "binary/octet-stream"
57 | "application/binary",
58 );
59
60 if !is_binary {
61 return Err(Error::InvalidAppImage);
62 }
63
64 Ok(())
65 }
66 pub async fn download_with_progress(&self, url: &str, path: &PathBuf) -> Result<()> {
67 fs::create_dir_all(&appimages_dir()?).await?;
68
69 let temp_path = PathBuf::from(format!("{}.part", path.display()));
70
71 let resp = reqwest::get(&url.to_string())
72 .await
73 .map_err(|source| Error::Download {
74 url: url.to_string(),
75 source,
76 })?;
77
78 self.validate_response(&resp)?;
79
80 let total_size = resp.content_length().unwrap_or(0);
81
82 let bar = make_progress_bar(total_size)?;
83 let mut out = tokio::fs::File::create(&temp_path).await?;
84
85 let mut stream = resp.bytes_stream();
87 while let Some(chunk) = stream.next().await {
88 let chunk = match chunk {
89 Ok(chunk) => chunk,
90 Err(source) => {
91 fs::remove_file(temp_path).await?;
92 return Err(Error::Download {
93 url: url.to_string(),
94 source,
95 });
96 }
97 };
98 let len = chunk.len() as u64;
99 out.write_all(&chunk).await?;
100 bar.inc(len);
101 }
102
103 bar.finish_with_message("Download complete!");
104
105 fs::rename(temp_path, path).await?;
106
107 #[cfg(unix)]
109 {
110 use std::os::unix::fs::PermissionsExt;
111 let mut perms = fs::metadata(&path).await?.permissions();
112 perms.set_mode(0o755);
113 fs::set_permissions(&path, perms).await?;
114 }
115
116 Ok(())
117 }
118}