snm_core/
downloader.rs

1use crate::{
2    loader::{Bar, Spinner},
3    sysinfo::{platform_arch, platform_name},
4    types::{DownloadDir, ReleaseDir},
5};
6use console::style;
7use indicatif::HumanBytes;
8use std::{io::Read, path::PathBuf};
9use url::Url;
10
11use super::{version::DistVersion, SnmRes};
12
13#[derive(Debug)]
14struct Dist(String);
15
16impl Dist {
17    fn new(mirror: &Url, version: &DistVersion) -> Self {
18        #[cfg(unix)]
19        let extension = "tar.xz";
20
21        #[cfg(windows)]
22        let extension = "zip";
23
24        Dist(format!(
25            "{}/v{version}/node-v{version}-{}-{}.{}",
26            mirror,
27            platform_name(),
28            platform_arch(),
29            extension,
30            version = version
31        ))
32    }
33}
34
35impl AsRef<str> for Dist {
36    fn as_ref(&self) -> &str {
37        &self.0
38    }
39}
40
41pub struct Downloader<'a> {
42    version: &'a DistVersion,
43    dist: Dist,
44}
45
46impl<'a> Downloader<'a> {
47    pub fn new(mirror: &'a Url, version: &'a DistVersion) -> Self {
48        let dist = Dist::new(mirror, version);
49
50        Self { version, dist }
51    }
52
53    #[cfg(unix)]
54    fn extract_to<S: Read + Send>(
55        &self,
56        source: &mut S,
57        r_dir: &ReleaseDir,
58        d_dir: &DownloadDir,
59    ) -> SnmRes<()> {
60        let tmp_dir = tempfile::Builder::new().tempdir_in(d_dir.as_ref())?;
61
62        let xz_stream = xz2::read::XzDecoder::new(source);
63        let mut tar_stream = tar::Archive::new(xz_stream);
64        let entries = tar_stream.entries()?;
65
66        for entry in entries {
67            let mut entry = entry?;
68
69            // Stripping the first path segment which is usually the name of the file
70            let entry_path: PathBuf = entry.path()?.iter().skip(1).collect();
71
72            let tmp_dir = tmp_dir.as_ref().join(entry_path);
73
74            entry.unpack(tmp_dir)?;
75        }
76
77        let install_dir = r_dir.as_ref().join(self.version.to_string());
78
79        std::fs::rename(&tmp_dir, &install_dir)?;
80
81        Ok(())
82    }
83
84    #[cfg(windows)]
85    fn extract_to<S: Read + Send>(
86        &self,
87        source: &mut S,
88        r_dir: &ReleaseDir,
89        d_dir: &DownloadDir,
90    ) -> SnmRes<()> {
91        use std::{fs, io};
92
93        let mut tmp_file = tempfile::Builder::new().tempfile_in(d_dir.as_ref())?;
94
95        io::copy(source, &mut tmp_file)?;
96
97        let mut archive = zip::read::ZipArchive::new(&tmp_file)?;
98
99        let install_dir = r_dir.as_ref().join(self.version.to_string());
100
101        for i in 0..archive.len() {
102            let mut file = archive.by_index(i)?;
103
104            let outpath = {
105                let f = match file.enclosed_name() {
106                    // Stripping the first path segment which is usually the name of the file
107                    Some(path) => path.iter().skip(1).collect::<PathBuf>(),
108                    None => continue,
109                };
110
111                install_dir.join(f)
112            };
113
114            {
115                let comment = file.comment();
116                if !comment.is_empty() {
117                    println!("File {} comment: {}", i, comment);
118                }
119            }
120
121            if file.name().ends_with('/') {
122                fs::create_dir_all(&outpath)?;
123            } else {
124                if let Some(p) = outpath.parent() {
125                    if !p.exists() {
126                        fs::create_dir_all(&p)?;
127                    }
128                }
129
130                let mut outfile = fs::File::create(&outpath)?;
131
132                io::copy(&mut file, &mut outfile)?;
133            }
134
135            // Get and Set permissions
136            #[cfg(unix)]
137            {
138                use std::os::unix::fs::PermissionsExt;
139
140                if let Some(mode) = file.unix_mode() {
141                    fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?;
142                }
143            }
144        }
145
146        Ok(())
147    }
148
149    pub fn download(&self, r_dir: &ReleaseDir, d_dir: &DownloadDir) -> SnmRes<ReleaseDir> {
150        let version: String = self.version.to_string();
151
152        let dest = r_dir.join(&version);
153
154        if dest.as_ref().exists() {
155            anyhow::bail!("Version {} already exists locally", style(version).bold());
156        }
157
158        let resp = ureq::get(&self.dist.0).call()?;
159
160        let len = resp
161            .header("Content-Length")
162            .and_then(|x| x.parse::<u64>().ok());
163
164        let size = match len {
165            Some(l) => HumanBytes(l).to_string(),
166            None => "unknown".into(),
167        };
168
169        println!("Version   : {}", style(version).bold());
170        println!("Release   : {}", style(self.dist.as_ref()).bold());
171        println!("Size      : {}", style(size).bold());
172        println!();
173
174        if let Some(l) = len {
175            let bar = Bar::new(l);
176
177            self.extract_to(&mut bar.take_reader(resp.into_reader()), r_dir, d_dir)?;
178
179            bar.finish();
180        } else {
181            let spinner = Spinner::new("Installing...");
182
183            self.extract_to(&mut resp.into_reader(), r_dir, d_dir)?;
184
185            spinner.finish()
186        }
187
188        println!("Installed : {}", style(dest.as_ref().display()).bold());
189
190        Ok(dest)
191    }
192}