zap_rs/
downloader.rs

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        // Try to extract filename from URL or use default
16        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        // Stream download with progress updates
86        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        // Make executable
108        #[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}