uranium_rs/modpack_maker/
maker.rs

1use 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
19/// Good -> Means Uranium found the mod
20/// Raw  -> Means the mod need to be added raw
21enum 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
35/// This struct is responsible for the creation
36/// of the modpacks given a minecraft path.
37pub 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    /// Starts the mod maker process.
70    ///
71    /// This method initializes the mod maker, reads the mods, and prepares
72    /// internal data structures for processing.
73    ///
74    /// # Errors
75    ///
76    /// This method can return an error of type `UraniumError` in the following
77    /// cases:
78    ///
79    /// - If there is an error while reading the mods.
80    ///
81    /// # Returns
82    ///
83    /// This method returns `Ok(())` if the mod maker was successfully started
84    /// and prepared for processing.
85    ///
86    /// # Example
87    ///
88    /// ```no_run
89    /// use uranium_rs::modpack_maker::ModpackMaker;
90    /// use uranium_rs::error::UraniumError;
91    ///
92    /// let mut mod_maker = ModpackMaker::new("path/to/your/modpack", "my_modpack");
93    ///
94    /// match mod_maker.start() {
95    ///     Ok(()) => println!("Mod maker started successfully!"),
96    ///     Err(err) => eprintln!("Error starting mod maker: {:?}", err),
97    /// }
98    /// ```
99    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    /// Finishes the mod maker process.
106    ///
107    /// This asynchronous method continues processing chunks until the mod maker
108    /// has completed its work.
109    ///
110    /// # Errors
111    ///
112    /// This method can return an error of type `UraniumError` if any error
113    /// occurs during the mod making process.
114    ///
115    /// # Returns
116    ///
117    /// This method returns `Ok(())` if the mod maker has successfully completed
118    /// its work.
119    ///
120    /// # Example
121    ///
122    /// ```no_run
123    /// # async {
124    ///     use uranium_rs::modpack_maker::ModpackMaker;
125    ///     use uranium_rs::error::UraniumError;
126    ///
127    ///     let mut mod_maker = ModpackMaker::new("your/modpack/path", "my_modpack");
128    ///
129    ///     match mod_maker.finish().await {
130    ///         Ok(()) => println!("Mod maker finished successfully!"),
131    ///         Err(err) => eprintln!("Error finishing mod maker: {:?}", err),
132    ///     }
133    /// # };
134    /// ```
135    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    /// Returns how many mods are in the minecraft
146    /// directory
147    #[must_use]
148    pub fn len(&self) -> usize {
149        self.hash_filenames.len()
150    }
151
152    /// Returns true if there are no mods in the minecraft directory
153    #[must_use]
154    pub fn is_empty(&self) -> bool {
155        self.len() == 0
156    }
157
158    /// Returns how many chunks the struct will download
159    ///
160    /// The formula is: `self.len()` / `self.threads`
161    #[must_use]
162    pub fn chunks(&self) -> usize {
163        self.len() / self.threads
164    }
165
166    /// This method will make progress until `Ok(State::Finish)` is returned
167    /// or throw an Err.
168    ///
169    /// It will return the current State of the process.
170    ///
171    /// # Errors
172    /// In case any of the steps fails this method will return
173    /// `Err(UraniumError)` with the cause.
174    ///
175    /// Can return any of the following variants:
176    /// - `UraniumError::CantReadModsDir` <br>
177    /// - `UraniumError::CantCompress` <br>
178    /// - `UraniumError::CantRemoveJSON`
179    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        // Get rinth_responses
246        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    /// # Errors
289    /// If the path dir cant be read then `Err(MakeError::CantReadModsDir)` will
290    /// be returned.
291    ///
292    /// # Panic
293    /// This function will panic when path is not a dir.
294    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        // Push all the (has, file_name) to the vector
315        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}