uranium_rs/modpack_maker/
maker.rs1use 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 path: PathBuf,
65 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 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 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}