z_mirror/fmm

git clone https://git.raiguard.me/z_mirror/fmm

raiguardFix download loop when the download fails9f375e8

rust-ice
5.3 KiB203 linesraw
1use std::cmp::min;
2use std::fs::{self, File};
3use std::io::{Read, Write};
4
5use anyhow::{anyhow, Result};
6use console::style;
7use indicatif::{ProgressBar, ProgressStyle};
8use reqwest::blocking::Client;
9use reqwest::StatusCode;
10use semver::Version;
11use serde::Deserialize;
12use sha1::{Digest, Sha1};
13use thiserror::Error;
14
15use crate::config::Config;
16use crate::directory::Directory;
17use crate::get_mod_version;
18use crate::types::{ModEntry, ModIdent};
19
20pub fn download_mod(
21    mod_ident: &ModIdent,
22    directory: &mut Directory,
23    config: &Config,
24    client: &Client,
25) -> Result<bool> {
26    Ok(match download_mod_internal(mod_ident, config, client) {
27        Ok(data) => {
28            directory.add(data);
29            true
30        }
31        Err(err) => {
32            eprintln!(
33                "{} Could not download {}: {}",
34                style("Error:").red().bold(),
35                mod_ident.name,
36                err
37            );
38            false
39        }
40    })
41}
42
43fn download_mod_internal(
44    mod_ident: &ModIdent,
45    config: &Config,
46    client: &Client,
47) -> Result<(String, ModEntry)> {
48    // Get authentication token and username
49    let portal_auth = config
50        .portal_auth
51        .as_ref()
52        .ok_or(DownloadModErr::CredentialsNotFound)?;
53
54    // println!("{} {}", style("Fetching").cyan().bold(), mod_ident.name);
55
56    // Download mod information
57    let mod_info: ModPortalResult = client
58        .get(format!(
59            "https://mods.factorio.com/api/mods/{}",
60            mod_ident.name
61        ))
62        .send()?
63        .json()
64        .map_err(|_| DownloadModErr::ModNotFound)?;
65
66    // Get the corresponding release
67    let release =
68        get_mod_version(&mod_info.releases, mod_ident).ok_or(DownloadModErr::ModNotFound)?;
69
70    // Download the mod
71    let mut res = match client
72        .get(format!("https://mods.factorio.com{}", release.download_url,))
73        .query(&[
74            ("username", &portal_auth.username),
75            ("token", &portal_auth.token),
76        ])
77        .send()?
78        .error_for_status()
79    {
80        Ok(res) => res,
81        Err(err) if err.status().unwrap() == StatusCode::FORBIDDEN => {
82            return Err(anyhow!(DownloadModErr::InvalidCredentials))
83        }
84        Err(err) => return Err(anyhow!(err)),
85    };
86
87    let total_size = res
88        .content_length()
89        .ok_or(DownloadModErr::NoContentLength)?;
90
91    let pb = ProgressBar::new(total_size);
92    pb.set_style(
93        ProgressStyle::default_bar()
94            .template("{msg} [{elapsed_precise:.blue}] [{bar:.green}] {bytes} / {total_bytes} ({bytes_per_sec}, {eta})")
95            .progress_chars("=> "),
96    );
97
98    pb.set_message(format!(
99        "{} {} v{}",
100        style("Downloading").cyan().bold(),
101        mod_ident.name,
102        release.version
103    ));
104
105    let mut path = config.mods_dir.clone();
106    path.push(format!(
107        "{}_DOWNLOAD",
108        release
109            .file_name
110            .strip_suffix(".zip")
111            .unwrap_or(&release.file_name)
112    ));
113    let mut file = File::create(&path)?;
114
115    let mut downloaded: u64 = 0;
116
117    let mut buf = vec![0; 8_096];
118    let mut hasher = Sha1::new();
119
120    while downloaded < total_size {
121        if let Some(bytes) = match res.read(&mut buf) {
122            Ok(bytes) => Some(bytes),
123            Err(err) if matches!(err.kind(), std::io::ErrorKind::Interrupted) => None,
124            Err(err) => return Err(anyhow!(err)),
125        } {
126            file.write_all(&buf[0..bytes])?;
127            hasher.update(&buf[0..bytes]);
128            // Update progress bar
129            downloaded = min(downloaded + (bytes as u64), total_size);
130            pb.set_position(downloaded);
131        }
132    }
133
134    // Verify download
135    if hasher.finalize()[..] != hex::decode(&release.sha1)? {
136        return Err(anyhow!(DownloadModErr::DamagedDownload));
137    }
138
139    // Rename file
140    let mut proper_path = config.mods_dir.clone();
141    proper_path.push(&release.file_name);
142    fs::rename(&path, &proper_path)?;
143
144    // Finish up
145    pb.finish_and_clear();
146    println!(
147        "{} {} v{}",
148        style("Downloaded").cyan().bold(),
149        mod_ident.name,
150        release.version
151    );
152
153    Ok((
154        mod_info.name,
155        ModEntry {
156            entry: fs::read_dir(&config.mods_dir)?
157                .filter_map(|entry| entry.ok())
158                .find(|entry| {
159                    entry
160                        .file_name()
161                        .to_str()
162                        .map(|file_name| file_name == release.file_name)
163                        .is_some()
164                })
165                .unwrap(),
166            version: release.version.clone(),
167        },
168    ))
169}
170
171#[derive(Debug, Error)]
172enum DownloadModErr {
173    #[error("Could not find mod portal credentials")]
174    CredentialsNotFound,
175    #[error("Damaged download")]
176    DamagedDownload,
177    #[error("Invalid mod portal credentials")]
178    InvalidCredentials,
179    #[error("Mod was not found on the portal")]
180    ModNotFound,
181    #[error("Could not get content length")]
182    NoContentLength,
183}
184
185#[derive(Debug, Deserialize)]
186struct ModPortalResult {
187    name: String,
188    releases: Vec<ModPortalRelease>,
189}
190
191#[derive(Debug, Deserialize)]
192struct ModPortalRelease {
193    download_url: String,
194    file_name: String,
195    sha1: String,
196    version: Version,
197}
198
199impl crate::HasVersion for ModPortalRelease {
200    fn get_version(&self) -> &Version {
201        &self.version
202    }
203}