uranium_rs/modpack_maker/
maker.rs1use std::{
2 fs::read_dir,
3 path::{Path, PathBuf},
4};
5
6use futures::future::join_all;
7use log::{error, warn};
8use mine_data_structs::rinth::{RinthModpack, RinthVersion};
9use reqwest::Response;
10
11use crate::searcher::rinth::{SearchBuilder, SearchType};
12use crate::{
13 code_functions::N_THREADS, error::Result, error::UraniumError, hashes::rinth_hash,
14 variables::constants, variables::constants::RINTH_JSON, zipper::compress_pack,
15};
16
17type HashFilename = Vec<(String, String)>;
18
19enum ParseState {
22 Good(RinthVersion),
23 Raw(String),
24}
25
26#[derive(Clone, Copy)]
27pub enum State {
28 Starting,
29 Searching,
30 Checking,
31 Writing,
32 Finish,
33}
34
35pub struct ModpackMaker {
38 path: PathBuf,
39 current_state: State,
40 hash_filenames: HashFilename,
41 mods_states: Vec<ParseState>,
42 rinth_pack: RinthModpack,
43 raw_mods: Vec<PathBuf>,
44 client: reqwest::Client,
45 modpack_path: PathBuf,
46 threads: usize,
47}
48
49impl ModpackMaker {
50 pub fn new<I: AsRef<Path>, J: AsRef<Path>>(path: I, modpack_name: J) -> ModpackMaker {
51 ModpackMaker {
52 path: path.as_ref().to_path_buf(),
53 current_state: State::Starting,
54 hash_filenames: vec![],
55 mods_states: vec![],
56 rinth_pack: RinthModpack::new(),
57 raw_mods: vec![],
58 client: reqwest::ClientBuilder::new()
59 .user_agent("uranium-rs/mp-maker contact: sergious234@gmail.com")
60 .build()
61 .unwrap(),
62 modpack_path: modpack_name
63 .as_ref()
64 .to_path_buf(),
65 threads: N_THREADS(),
66 }
67 }
68
69 pub fn start(&mut self) -> Result<()> {
100 self.hash_filenames = self.read_mods()?;
101 self.mods_states = Vec::with_capacity(self.hash_filenames.len());
102 Ok(())
103 }
104
105 pub async fn finish(&mut self) -> Result<()> {
136 loop {
137 match self.chunk().await {
138 Ok(State::Finish) => return Ok(()),
139 Err(e) => return Err(e),
140 _ => {}
141 }
142 }
143 }
144
145 #[must_use]
148 pub fn len(&self) -> usize {
149 self.hash_filenames.len()
150 }
151
152 #[must_use]
154 pub fn is_empty(&self) -> bool {
155 self.len() == 0
156 }
157
158 #[must_use]
162 pub fn chunks(&self) -> usize {
163 self.len() / self.threads
164 }
165
166 pub async fn chunk(&mut self) -> Result<State> {
180 self.current_state = match self.current_state {
181 State::Starting => {
182 if self.hash_filenames.is_empty() {
183 self.hash_filenames = self.read_mods()?;
184 }
185 State::Searching
186 }
187 State::Searching => {
188 if self.hash_filenames.is_empty() {
189 State::Checking
190 } else {
191 self.search_mods().await;
192 State::Searching
193 }
194 }
195 State::Checking => {
196 for rinth_mod in &self.mods_states {
197 match rinth_mod {
198 ParseState::Good(m) => self
199 .rinth_pack
200 .add_mod(m.clone().into()),
201 ParseState::Raw(file_name) => self
202 .raw_mods
203 .push(PathBuf::from(file_name)),
204 }
205 }
206 State::Writing
207 }
208 State::Writing => {
209 self.rinth_pack
210 .write_mod_pack_with_name();
211
212 if let Err(e) = compress_pack(&self.modpack_path, &self.path, &self.raw_mods) {
213 error!("Error while compressing the modpack: {}", e);
214 return Err(UraniumError::CantCompress);
215 }
216
217 match std::fs::remove_file(RINTH_JSON) {
218 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
219 warn!("Couldn't remove {RINTH_JSON}")
220 }
221 Err(e) => Err(e)?,
222 Ok(_) => {}
223 }
224
225 State::Finish
226 }
227 State::Finish => State::Finish,
228 };
229
230 Ok(self.current_state)
231 }
232
233 async fn search_mods(&mut self) {
234 let end = if self.threads > self.hash_filenames.len() {
235 self.hash_filenames.len()
236 } else {
237 self.threads
238 };
239
240 let chunk: HashFilename = self
241 .hash_filenames
242 .drain(0..end)
243 .collect();
244
245 let mut rinth_responses = Vec::with_capacity(chunk.len());
247
248 let reqs = chunk
249 .iter()
250 .map(|f| {
251 tokio::task::spawn(
252 self.client
253 .get(
254 SearchBuilder::new()
255 .search_type(SearchType::VersionFile { hash: f.0.clone() })
256 .build_url(),
257 )
258 .send(),
259 )
260 })
261 .collect::<Vec<tokio::task::JoinHandle<std::result::Result<Response, reqwest::Error>>>>(
262 );
263
264 let responses = join_all(reqs)
265 .await
266 .into_iter()
267 .flatten()
268 .map(|x| x.map_err(|e| e.into()))
269 .collect::<Vec<Result<Response>>>();
270
271 rinth_responses.extend(responses);
272
273 let rinth_parses = parse_responses(rinth_responses).await;
274 for (file_name, rinth) in chunk
275 .into_iter()
276 .zip(rinth_parses.into_iter())
277 {
278 if let Ok(m) = rinth {
279 self.mods_states
280 .push(ParseState::Good(m));
281 } else {
282 self.mods_states
283 .push(ParseState::Raw(file_name.1));
284 }
285 }
286 }
287
288 fn read_mods(&mut self) -> Result<HashFilename> {
295 if !self.path.is_dir() {
296 return Err(UraniumError::CantReadModsDir);
297 }
298
299 let mods_path = self.path.join("mods/");
300
301 let mods = match read_dir(&mods_path) {
302 Ok(e) => e
303 .into_iter()
304 .map(|f| f.unwrap().path())
305 .collect::<Vec<PathBuf>>(),
306 Err(e) => {
307 error!("Error reading the directory: {}", e);
308 return Err(UraniumError::CantReadModsDir);
309 }
310 };
311
312 let mut hashes_names = Vec::with_capacity(mods.len());
313
314 for path in mods {
316 let mod_hash = rinth_hash(path.as_path());
317 let file_name = path
318 .file_name()
319 .unwrap()
320 .to_str()
321 .unwrap_or_default()
322 .to_owned();
323 hashes_names.push((mod_hash, file_name));
324 }
325
326 Ok(hashes_names)
327 }
328}
329
330async fn parse_responses(responses: Vec<Result<Response>>) -> Vec<Result<RinthVersion>> {
331 join_all(
332 responses
333 .into_iter()
334 .map(|request| {
335 request
336 .unwrap()
337 .json::<RinthVersion>()
338 }),
339 )
340 .await
341 .into_iter()
342 .map(|x| x.map_err(|e| e.into()))
343 .collect::<Vec<Result<RinthVersion>>>()
344}