1use std::io::Write;
2use std::{
3 fs::File,
4 path::{Path, PathBuf},
5 os::unix::fs::PermissionsExt
6};
7
8use log::{error, info};
9use mine_data_structs::minecraft::{
10 Library, MinecraftVersions, Profile, ProfilesJson, Resources, Root,
11};
12use reqwest;
13use tokio::io::AsyncWriteExt;
14
15use super::RuntimeDownloader;
16use super::gen_downloader::{DownloadState, DownloadableObject, FileDownloader, HashType};
17use crate::{
18 code_functions::N_THREADS,
19 error::{Result, UraniumError},
20 variables::constants::PROFILES_FILE,
21};
22
23const ASSETS_PATH: &str = "assets/";
24const OBJECTS_PATH: &str = "objects";
25const INSTANCES_LIST: &str = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
26
27pub async fn list_instances() -> Result<MinecraftVersions> {
43 let requester = reqwest::Client::new();
44
45 let instances = requester
46 .get(INSTANCES_LIST)
47 .send()
48 .await?
49 .json::<MinecraftVersions>()
50 .await?;
51
52 Ok(instances)
53}
54
55pub async fn get_last_snapshot() -> Result<String> {
67 let requester = reqwest::Client::new();
68 Ok(requester
69 .get(INSTANCES_LIST)
70 .send()
71 .await?
72 .json::<MinecraftVersions>()
73 .await?
74 .latest
75 .snapshot)
76}
77
78pub async fn get_last_release() -> Result<String> {
90 let requester = reqwest::Client::new();
91 Ok(requester
92 .get(INSTANCES_LIST)
93 .send()
94 .await?
95 .json::<MinecraftVersions>()
96 .await?
97 .latest
98 .release)
99}
100
101#[derive(Debug, Clone)]
109pub enum MinecraftDownloadState {
110 GettingSources,
111 DownloadingVersion,
112 DownloadingAssests,
113 DownloadingLibraries,
114 DownloadingRuntime,
115 CheckingFiles,
116 Completed,
117}
118
119pub struct MinecraftDownloader<T: FileDownloader + Send> {
157 requester: reqwest::Client,
158 dot_minecraft_path: PathBuf,
159 minecraft_instance: Root,
160 download_state: MinecraftDownloadState,
161 downloader: T,
162}
163
164impl<T: FileDownloader + Send + Sync> MinecraftDownloader<T> {
165 pub async fn init<I: AsRef<Path>>(
188 destination_path: I,
189 minecraft_version: &str,
190 ) -> Result<Self> {
191 let requester = reqwest::Client::new();
192 let instances = list_instances().await?;
193
194 let instance_url = instances
195 .get_instance_url(minecraft_version)
196 .ok_or(UraniumError::OtherWithReason(format!(
197 "Version {minecraft_version} doesn't exist"
198 )))?;
199
200 let minecraft_instance: Root = requester
201 .get(instance_url)
202 .send()
203 .await?
204 .json()
205 .await?;
206
207 let destination_path = destination_path
208 .as_ref()
209 .to_path_buf();
210
211 Ok(MinecraftDownloader::new(
212 destination_path,
213 minecraft_instance,
214 ))
215 }
216
217 fn new(destination_path: PathBuf, minecraft_instance: Root) -> Self {
219 MinecraftDownloader {
220 requester: reqwest::Client::new(),
221 dot_minecraft_path: destination_path,
222 minecraft_instance,
223 download_state: MinecraftDownloadState::GettingSources,
224 downloader: T::new(vec![]),
225 }
226 }
227
228 pub async fn start(&mut self) -> Result<MinecraftDownloadState> {
236 loop {
237 let state = self.progress().await;
238
239 match state {
240 Ok(MinecraftDownloadState::Completed) => break,
241 Err(e) => return Err(e),
242 _ => {}
243 }
244 }
245 Ok(MinecraftDownloadState::Completed)
246 }
247
248 pub async fn progress(&mut self) -> Result<MinecraftDownloadState> {
263 match self.download_state {
264 MinecraftDownloadState::GettingSources => {
265 let assets: Box<[DownloadableObject]> = self
266 .get_sources()
267 .await?
268 .collect();
269
270 if self
271 .create_assets_folders(assets.iter())
272 .is_err()
273 {
274 error!("Error creating assets folders");
275 return Err(UraniumError::CantCreateDir("assets"));
276 };
277
278 self.downloader
279 .add_objects(assets);
280 self.download_state = MinecraftDownloadState::DownloadingVersion;
281 }
282
283 MinecraftDownloadState::DownloadingVersion => {
284 self.create_version_folder()
285 .await?;
286 self.download_state = MinecraftDownloadState::DownloadingAssests;
287 }
288
289 MinecraftDownloadState::DownloadingAssests => {
290 let download_state = self
291 .downloader
292 .progress()
293 .await;
294
295 match download_state {
296 Ok(DownloadState::Completed) => {
297 let libs: Box<[_]> = self
298 .prepare_libraries()?
299 .collect();
300 self.downloader
301 .add_objects(libs);
302 self.download_state = MinecraftDownloadState::DownloadingLibraries;
303 }
304 Err(e) => {
305 error!("Error downloading assets: {e}");
306 return Err(e);
307 }
308 _ => {}
309 }
310 }
311
312 MinecraftDownloadState::DownloadingLibraries => {
313 let download_state = self
314 .downloader
315 .progress()
316 .await;
317
318 match download_state {
319 Ok(DownloadState::Completed) => {
320 self.download_state = MinecraftDownloadState::DownloadingRuntime;
321 }
322 Err(e) => {
323 error!("Error downloading libraries: {e}");
324 return Err(e);
325 }
326 _ => {}
327 }
328 }
329
330 MinecraftDownloadState::DownloadingRuntime => {
331 let runtime_res = RuntimeDownloader::new(
332 self.minecraft_instance
333 .java_version
334 .component
335 .to_string(),
336 )
337 .download()
338 .await;
339
340 if let Err(err) = runtime_res {
341 error!("Error downloading runtime: {}", err);
342 return Err(err);
343 }
344 self.download_state = MinecraftDownloadState::CheckingFiles;
345 }
346
347 MinecraftDownloadState::CheckingFiles => {
348 self.download_state = MinecraftDownloadState::Completed;
350 }
351
352 MinecraftDownloadState::Completed => {
353 info!("Minecraft download complete!");
354 }
355 };
356
357 Ok(self.download_state.clone())
358 }
359
360 async fn create_version_folder(&mut self) -> Result<()> {
373 let instance_folder = self
374 .dot_minecraft_path
375 .join("versions")
376 .join(&self.minecraft_instance.id);
377
378 info!("Instance folder: {instance_folder:?}");
379
380 if !instance_folder.exists() {
381 std::fs::create_dir_all(&instance_folder)?;
382 }
383
384 self.check_client(&instance_folder)
386 .await?;
387
388 self.check_instance(&instance_folder)?;
390 Ok(())
391 }
392
393 async fn check_client(&mut self, instance_folder: &Path) -> Result<()> {
395 let client_path = instance_folder.join(
396 self.minecraft_instance
397 .id
398 .clone()
399 + ".jar",
400 );
401 if !client_path.exists() {
402 info!("Downloading client!");
403 let (url, hash) = self
404 .minecraft_instance
405 .downloads
406 .get("client")
407 .map(|i| (&i.url, i.sha1.to_string()))
408 .ok_or(UraniumError::OtherWithReason(
409 "Client .jar not found in the minecraft instance".to_owned(),
410 ))?;
411 let obj = DownloadableObject::new(url, &client_path, Some(HashType::Sha1(hash)));
412 self.downloader
413 .add_object(obj);
414 self.downloader
415 .complete()
416 .await?;
417 std::fs::set_permissions(&client_path, std::fs::Permissions::from_mode(0o766))?
418 }
419 Ok(())
420 }
421
422 fn check_instance(&self, instance_folder: &Path) -> Result<()> {
423 let instance_path = instance_folder.join(
424 self.minecraft_instance
425 .id
426 .clone()
427 + ".json",
428 );
429 if !instance_path.exists() {
430 info!("Writing client json!");
431 let mut instance_file = File::create(instance_path)?;
432 instance_file.write_all(
433 serde_json::to_string(&self.minecraft_instance)
434 .unwrap()
435 .as_bytes(),
436 )?;
437 }
438 Ok(())
439 }
440
441 pub fn requests_left(&self) -> usize {
455 self.downloader
456 .requests_left()
457 }
458
459 pub fn lib_chunks(&self) -> usize {
462 let n = self
463 .minecraft_instance
464 .libraries
465 .len() as f64;
466 (n / N_THREADS() as f64).ceil() as usize
467 }
468
469 pub fn chunks(&self) -> usize {
473 let n = self.downloader.len() as f64;
474 (n / N_THREADS() as f64).ceil() as usize
475 }
476
477 async fn get_sources(&self) -> Result<impl Iterator<Item = DownloadableObject>> {
478 let resources: Resources = self
479 .requester
480 .get(
481 &self
482 .minecraft_instance
483 .asset_index
484 .url,
485 )
486 .send()
487 .await?
488 .json::<Resources>()
489 .await?;
490
491 std::fs::create_dir_all(
492 self.dot_minecraft_path
493 .join("assets/indexes"),
494 )
495 .map_err(|err| {
496 error!("Cant create assets/indexes: [{err}]");
497 UraniumError::CantCreateDir("assets/indexes")
498 })?;
499
500 std::fs::create_dir_all(
501 self.dot_minecraft_path
502 .join("assets/objects"),
503 )
504 .map_err(|err| {
505 error!("Cant create assets/objects: [{err}]");
506 UraniumError::CantCreateDir("assets/objects")
507 })?;
508
509 self.create_indexes(&resources)
510 .await?;
511
512 let base = PathBuf::from(ASSETS_PATH).join(OBJECTS_PATH);
513
514 let x = resources
515 .objects
516 .into_values()
517 .map(move |obj| {
518 let url = obj.get_link();
519 let path = base
520 .join(&obj.hash[..2])
521 .join(&obj.hash);
522 DownloadableObject::new(
523 &url,
524 &self
525 .dot_minecraft_path
526 .join(path),
527 Some(HashType::Sha1(obj.hash.to_owned())),
528 )
529 });
530 Ok(x)
531 }
532
533 async fn create_indexes(&self, resources: &Resources) -> Result<()> {
535 let indexes_path = self
536 .dot_minecraft_path
537 .join(ASSETS_PATH)
538 .join("indexes")
539 .join(
540 self.minecraft_instance
541 .get_index_name(),
542 );
543
544 let mut indexes = tokio::fs::File::create(indexes_path).await?;
545
546 indexes
547 .write_all(
548 serde_json::to_string(resources)
549 .map_err(|_| {
550 UraniumError::OtherWithReason(
551 "Resources is not serializable or has wrong data".to_string(),
552 )
553 })?
554 .as_bytes(),
555 )
556 .await?;
557
558 Ok(())
559 }
560
561 fn create_assets_folders<'a>(
563 &self,
564 names: impl Iterator<Item = &'a DownloadableObject>,
565 ) -> Result<()> {
566 for dir in names {
567 std::fs::create_dir_all(
568 self.dot_minecraft_path
569 .join(
570 dir.name()
571 .ok_or(UraniumError::other("No filename"))?,
572 )
573 .parent()
574 .ok_or(UraniumError::other("Error creating assests forlder"))?,
575 )?;
576 }
577
578 Ok(())
579 }
580
581 #[allow(dead_code)]
583 fn get_os_libraries(&self, libraries: &[Library]) -> impl Iterator<Item = DownloadableObject> {
587 let lib_path = self
588 .dot_minecraft_path
589 .join("libraries");
590
591 let current_os = match std::env::consts::OS {
592 "linux" => mine_data_structs::minecraft::Os::Linux,
593 "macos" => mine_data_structs::minecraft::Os::Other,
594 _ => mine_data_structs::minecraft::Os::Windows,
596 };
597 libraries
598 .iter()
599 .filter(move |lib| {
600 lib.get_os()
601 .is_none_or(|os| os == current_os)
602 })
603 .map(move |lib| {
604 DownloadableObject::new(
605 lib.get_url(),
606 &lib_path.join(lib.get_rel_path().unwrap()),
607 None,
608 )
609 })
610 }
611
612 fn prepare_libraries(&self) -> Result<impl Iterator<Item = DownloadableObject>> {
622 let lib_path = self
623 .dot_minecraft_path
624 .join("libraries");
625
626 Ok(self
627 .minecraft_instance
628 .libraries
629 .iter()
630 .map(move |l| {
631 DownloadableObject::new(
632 l.get_url(),
633 &lib_path.join(
634 l.get_rel_path()
635 .expect("Missing download field for library {l:?}"),
636 ),
637 l.get_hash()
638 .map(|h| HashType::Sha1(h.to_string())),
639 )
640 }))
641 }
642
643 pub fn add_instance<I: AsRef<Path>>(
658 &self,
659 minecraft_path: I,
660 instance_name: &str,
661 icon: Option<&str>,
662 ) -> Result<()> {
663 let profiles_path = minecraft_path
664 .as_ref()
665 .to_path_buf()
666 .join(PROFILES_FILE);
667
668 if !profiles_path.exists() {
669 error!("{profiles_path:?} doesn't exist!");
670 return Err(UraniumError::FileNotFound(
671 profiles_path
672 .display()
673 .to_string(),
674 ));
675 }
676
677 let mut profiles: ProfilesJson = match serde_json::from_reader(File::open(&profiles_path)?)
678 {
679 Ok(v) => v,
680 Err(e) => Err(UraniumError::OtherWithReason(e.to_string()))?,
681 };
682
683 let icon = icon.unwrap_or("Grass");
684
685 let new_profile = Profile::new(
686 icon,
687 &self.minecraft_instance.id,
688 instance_name,
689 "custom",
690 Some(&self.dot_minecraft_path),
691 );
692
693 profiles.insert(instance_name, new_profile);
694
695 info!("Writing new profile");
696
697 let Ok(content) = serde_json::to_string_pretty(&profiles) else {
698 return Err(UraniumError::WrongFileFormat);
699 };
700
701 if let Err(err) = std::fs::write(profiles_path, content) {
702 error!("Error writing the new profile");
703 return Err(err.into());
704 }
705
706 info!("Profile added!");
707 Ok(())
708 }
709}
710
711pub fn get_index_path(installation_path: &Path, index_name: &Path) -> PathBuf {
712 installation_path
713 .join(ASSETS_PATH)
714 .join("indexes")
715 .join(index_name)
716}
717
718pub fn get_lib_path(installation_path: &Path, lib_path: &Path) -> PathBuf {
719 installation_path
720 .join("libraries")
721 .join(
722 lib_path
723 )
724}
725
726#[cfg(test)]
727mod tests {
728 use log::warn;
729
730use super::*;
731 use crate::downloaders::Downloader;
732 use crate::error::Result;
733 use crate::init_logger;
734
735 #[tokio::test(flavor = "multi_thread")]
736 pub async fn download_minecraft() -> Result<()> {
737 let mut downloader =
738 MinecraftDownloader::<Downloader>::init("/home/sergio/.minecraft", "1.20.1").await?;
739
740 let mut stdout = tokio::io::stdout();
741 let _ = init_logger();
742 let r = loop {
743 let state = if let Ok(x) = downloader.progress().await {
744 x
745 } else {
746 break None;
747 };
748
749 if let MinecraftDownloadState::Completed = state {
750 let instance_res = downloader.add_instance("/home/sergio/.minecraft", "Vanilla 1.20.1", None);
751 if let Err(err) = instance_res {
752 warn!("{err}");
753 }
754 break Some(());
755 }
756 stdout
757 .write_all(format!("{:?} [{:?}]\n", state, downloader.requests_left()).as_bytes())
758 .await?;
759 tokio::io::stdout()
760 .flush()
761 .await?;
762 };
763
764 let exits = std::env::home_dir()
765 .unwrap()
766 .join(".minecraft/versions/1.20.1/1.20.1.jar")
767 .exists();
768
769 if r.is_some() {
770 assert!(exits);
771 }
772 Ok(())
773 }
774}