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 pub download_url: Url,
28 pub output: PathBuf,
32 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 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}