Skip to main content

uranium_rs/modpack_maker/
maker.rs

1use std::{
2    collections::HashMap,
3    fs::read_dir,
4    path::{Path, PathBuf},
5};
6
7use log::error;
8use mine_data_structs::rinth::{RinthModpack, RinthVersion};
9
10use crate::variables::constants::OVERRIDES_FOLDER;
11use crate::{
12    error::Result, error::UraniumError, hashes::rinth_hash,
13    variables::constants::RINTH_JSON,
14};
15
16use rrhodium::{SearchBuilder, SearchType};
17
18#[derive(Clone, Copy)]
19pub enum State {
20    Starting,
21    Searching,
22    Checking,
23    Writing,
24    Finish,
25}
26
27impl From<&InnerState> for State {
28    fn from(value: &InnerState) -> Self {
29        use InnerState as IS;
30        match value {
31            IS::Reading => Self::Starting,
32            IS::Searching { mods: _ } => Self::Searching,
33            IS::Writing { data: _ } => Self::Writing,
34            _ => Self::Finish,
35        }
36    }
37}
38
39#[derive(Debug)]
40struct HashPath {
41    pub hash: String,
42    pub path: PathBuf,
43}
44
45#[derive(Debug)]
46enum SearchResult {
47    Found(Box<RinthVersion>),
48    NotFound(HashPath),
49}
50
51enum InnerState {
52    Reading,
53    Searching {
54        mods: Box<dyn Iterator<Item = HashPath>>,
55    },
56    Writing {
57        data: HashMap<String, SearchResult>,
58    },
59    Finish,
60}
61
62pub struct ModpackMaker {
63    /// Where the mods are
64    path: PathBuf,
65    /// Where to save the modpack
66    modpack_path: PathBuf,
67    state: InnerState,
68    client: reqwest::Client,
69}
70
71impl ModpackMaker {
72    pub fn new<I: AsRef<Path>, J: AsRef<Path>>(path: I, modpack_name: J) -> Self {
73        Self {
74            path: path.as_ref().to_path_buf(),
75            state: InnerState::Reading,
76            client: reqwest::ClientBuilder::new()
77                .user_agent("uranium-rs/modpack maker contact: sergious234@gmail.com")
78                .build()
79                .unwrap(),
80            modpack_path: modpack_name
81                .as_ref()
82                .to_path_buf(),
83        }
84    }
85
86    pub async fn finish(mut self) -> Result<()> {
87        loop {
88            match self.progress().await {
89                Ok(State::Finish) => return Ok(()),
90                Ok(_) => {}
91                Err(e) => return Err(e),
92            }
93        }
94    }
95
96    pub async fn progress(&mut self) -> Result<State> {
97        use InnerState as IS;
98
99        match &mut self.state {
100            IS::Reading => {
101                self.state = IS::Searching {
102                    mods: Box::new(self.read_mods()?),
103                };
104            }
105            IS::Searching { mods } => {
106                let url = SearchBuilder::new()
107                    .search_type(SearchType::VersionFiles)
108                    .build_url();
109
110                let mods_data = mods.collect::<Vec<_>>();
111                let mut results: HashMap<String, SearchResult> = self
112                    .client
113                    .post(url)
114                    .json(&serde_json::json!({
115                        "hashes": mods_data.iter().map(|m| &m.hash).collect::<Vec<_>>(),
116                        "algorithm": "sha1",
117                    }))
118                    .send()
119                    .await?
120                    .json::<HashMap<String, RinthVersion>>()
121                    .await?
122                    .into_iter()
123                    .map(|(k, v)| (k, SearchResult::Found(Box::new(v))))
124                    .collect();
125
126                for m in mods_data {
127                    results
128                        .entry(m.hash.clone())
129                        .or_insert(SearchResult::NotFound(m));
130                }
131                log::info!("{results:#?}");
132                self.state = IS::Writing { data: results };
133            }
134            IS::Writing { data: ref_data } => {
135                // Little trick here so the borrow checker is happy.
136                let data = std::mem::take(ref_data);
137                self.write_modpack(data)?;
138                self.state = IS::Finish;
139            }
140            IS::Finish => {}
141        };
142        Ok((&self.state).into())
143    }
144
145    /// # Errors
146    /// If the path dir cant be read then `Err(MakeError::CantReadModsDir)` will
147    /// be returned.
148    ///
149    /// # Panic
150    /// This function will panic when path is not a dir.
151    fn read_mods(&mut self) -> Result<impl Iterator<Item = HashPath> + 'static> {
152        if !self.path.is_dir() {
153            return Err(UraniumError::CantReadModsDir);
154        }
155
156        let mods_path = self.path.join("mods/");
157
158        let mods = match read_dir(&mods_path) {
159            Ok(e) => e
160                .into_iter()
161                .map(|f| f.unwrap().path()),
162            Err(e) => {
163                error!("Error reading the directory: {}", e);
164                return Err(UraniumError::IOError(e));
165            }
166        };
167
168        let hashes_names = mods.map(|path| {
169            HashPath {
170                hash: rinth_hash(&path),
171                path: path.to_owned(),
172            }
173        });
174
175        Ok(hashes_names)
176    }
177
178    fn write_modpack(&self, data: HashMap<String, SearchResult>) -> Result<()> {
179        let mut zip = zip::ZipWriter::new(std::fs::File::create(&self.modpack_path)?);
180
181        let options = zip::write::SimpleFileOptions::default()
182            .compression_method(zip::CompressionMethod::Deflated)
183            .unix_permissions(0o755);
184
185        for entry in walkdir::WalkDir::new(self.path.join("config"))
186            .into_iter()
187            .flatten()
188        {
189            let path = entry.path();
190            let name = path
191                .strip_prefix(&self.path)
192                .unwrap();
193
194            if path.is_file() {
195                Self::add_file_to_zip(&mut zip, path, options)?;
196            } else {
197                zip.add_directory(name.to_string_lossy(), options)?;
198            }
199        }
200
201        let mut rinth_pack =
202            RinthModpack::new_with("1.0".to_string(), self.modpack_path.clone(), vec![]);
203
204        zip.add_directory(OVERRIDES_FOLDER, options)?;
205        for value in data.into_values() {
206            match value {
207                SearchResult::Found(rv) => rinth_pack.add_mod((*rv).into()),
208                SearchResult::NotFound(m) => {
209                    let path = PathBuf::new()
210                        .join(OVERRIDES_FOLDER)
211                        .join(m.path.file_name().unwrap());
212                    Self::add_file_to_zip_path(&mut zip, path, &m.path, options)?;
213                }
214            }
215        }
216
217        zip.start_file(RINTH_JSON, options)?;
218        serde_json::to_writer(&mut zip, &rinth_pack).expect("Pack can't be serialized");
219
220        zip.finish()?;
221        log::info!("Modpack written successfully!");
222        Ok(())
223    }
224
225    fn add_file_to_zip<P: AsRef<std::path::Path>>(
226        zip: &mut zip::ZipWriter<std::fs::File>,
227        path: P,
228        options: zip::write::SimpleFileOptions,
229    ) -> Result<()> {
230        let mut f = std::fs::File::open(&path)?;
231        zip.start_file(
232            path.as_ref()
233                .to_string_lossy(),
234            options,
235        )?;
236        std::io::copy(&mut f, zip)?;
237        Ok(())
238    }
239
240    fn add_file_to_zip_path<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
241        zip: &mut zip::ZipWriter<std::fs::File>,
242        name: P,
243        file_path: Q,
244        options: zip::write::SimpleFileOptions,
245    ) -> Result<()> {
246        let mut f = std::fs::File::open(file_path)?;
247        zip.start_file(
248            name.as_ref()
249                .to_string_lossy(),
250            options,
251        )?;
252        std::io::copy(&mut f, zip)?;
253        Ok(())
254    }
255}