libium/upgrade/
mod.rs

1pub mod check;
2pub mod mod_downloadable;
3pub mod modpack_downloadable;
4
5use crate::modpack::modrinth::structs::ModpackFile;
6use ferinth::structures::version::VersionFile;
7use furse::structures::file_structs::File;
8use octocrab::models::repos::Asset;
9use reqwest::{Client, Url};
10use std::{
11    fs::{create_dir_all, rename, OpenOptions},
12    io::{BufWriter, Write},
13    path::{Path, PathBuf},
14};
15
16#[derive(Debug, thiserror::Error)]
17#[error(transparent)]
18pub enum Error {
19    ReqwestError(#[from] reqwest::Error),
20    IOError(#[from] std::io::Error),
21}
22type Result<T> = std::result::Result<T, Error>;
23
24#[derive(Debug, Clone)]
25pub struct Downloadable {
26    /// A URL to download the file from
27    pub download_url: Url,
28    /// The path of the file relative to the output directory
29    ///
30    /// Is just the filename by default, can be configured with subdirectories for modpacks.
31    pub output: PathBuf,
32    /// The length of the file in bytes
33    pub length: usize,
34}
35
36#[derive(Debug, thiserror::Error)]
37#[error("The developer of this project has denied third party applications from downloading it")]
38pub struct DistributionDeniedError(pub i32, pub i32);
39
40impl TryFrom<File> for Downloadable {
41    type Error = DistributionDeniedError;
42    fn try_from(file: File) -> std::result::Result<Self, Self::Error> {
43        Ok(Self {
44            download_url: file
45                .download_url
46                .ok_or(DistributionDeniedError(file.mod_id, file.id))?,
47            output: file.file_name.into(),
48            length: file.file_length,
49        })
50    }
51}
52
53impl From<VersionFile> for Downloadable {
54    fn from(file: VersionFile) -> Self {
55        Self {
56            download_url: file.url,
57            output: file.filename.into(),
58            length: file.size,
59        }
60    }
61}
62impl From<ModpackFile> for Downloadable {
63    fn from(file: ModpackFile) -> Self {
64        Self {
65            download_url: file
66                .downloads
67                .first()
68                .expect("Download URLs not provided")
69                .clone(),
70            output: file.path,
71            length: file.file_size,
72        }
73    }
74}
75impl From<Asset> for Downloadable {
76    fn from(asset: Asset) -> Self {
77        Self {
78            download_url: asset.browser_download_url,
79            output: PathBuf::from("mods").join(asset.name),
80            length: asset.size as usize,
81        }
82    }
83}
84
85impl Downloadable {
86    /// Consumes `self` and downloads the file to the `output_dir`.
87    ///
88    /// The `update` closure is called with the chunk length whenever a chunk is downloaded and written.
89    ///
90    /// Returns the size of the file and the filename
91    pub async fn download(
92        self,
93        client: &Client,
94        output_dir: &Path,
95        update: impl Fn(usize) + Send,
96    ) -> Result<(usize, String)> {
97        let (filename, url, size) = (self.filename(), self.download_url, self.length);
98        let out_file_path = output_dir.join(&self.output);
99        let temp_file_path = out_file_path.with_extension("part");
100        if let Some(up_dir) = out_file_path.parent() {
101            create_dir_all(up_dir)?;
102        }
103
104        let mut temp_file = BufWriter::with_capacity(
105            size,
106            OpenOptions::new()
107                .append(true)
108                .create(true)
109                .open(&temp_file_path)?,
110        );
111
112        let mut response = client.get(url).send().await?;
113
114        while let Some(chunk) = response.chunk().await? {
115            temp_file.write_all(&chunk)?;
116            update(chunk.len());
117        }
118        temp_file.flush()?;
119        rename(temp_file_path, out_file_path)?;
120        Ok((size, filename))
121    }
122
123    pub fn filename(&self) -> String {
124        self.output
125            .file_name()
126            .unwrap()
127            .to_string_lossy()
128            .to_string()
129    }
130}