open_launcher 1.3.0

Open Launcher is a package to install and launch modded and vanilla Minecraft instances totally automatically with Rust.
Documentation
use async_recursion::async_recursion;
use sha1::Digest;
use std::error::Error;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio_util::compat::TokioAsyncWriteCompatExt;

#[derive(Debug)]
pub struct InstallError(pub String);

impl std::fmt::Display for InstallError {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "{}", self.0)
	}
}

impl Error for InstallError {}

impl From<InstallError> for Box<dyn Error + Send> {
	fn from(error: InstallError) -> Self {
		Box::new(error) as Box<dyn Error + Send>
	}
}

#[async_recursion]
pub(crate) async fn try_download_file(
	url: &str,
	path: &std::path::Path,
	hash: &str,
	retries: u32,
) -> Result<(), Box<dyn Error + Send>> {
	let url = url.replace(std::path::MAIN_SEPARATOR_STR, "/");
	let url = url.as_str();

	let response = reqwest::get(url).await.unwrap();
	let data = response.bytes().await.unwrap();

	let mut file = fs::File::create(path).await.unwrap();
	file.write_all(&data).await.unwrap();

	if hash.len() != 40 {
		return Ok(());
	}

	let downloaded_hash = format!("{:x}", sha1::Sha1::digest(&fs::read(path).await.unwrap()));

	if downloaded_hash != hash {
		if retries > 0 {
			fs::remove_file(path).await.unwrap();
			try_download_file(url, path, hash, retries - 1).await?;
		} else {
			return Err(Box::from(InstallError(format!(
				"Failed to download file: {}",
				path.display()
			))));
		}
	}

	Ok(())
}

pub(crate) fn get_os() -> String {
	match std::env::consts::OS {
		"windows" => "windows".to_string(),
		"macos" => "osx".to_string(),
		"linux" => "linux".to_string(),
		_ => std::env::consts::OS.to_string(),
	}
}

pub(crate) async fn extract_file(
	zip_path: &std::path::Path,
	file_name: &str,
	extract_path: &std::path::Path,
) -> Result<(), Box<dyn Error + Send>> {
	if extract_path.exists() {
		return Ok(());
	}

	let archive = async_zip::tokio::read::fs::ZipFileReader::new(zip_path)
		.await
		.unwrap();

	for i in 0..archive.file().entries().len() {
		if archive
			.file()
			.entries()
			.get(i)
			.unwrap()
			.filename()
			.as_str()
			.unwrap() == file_name
		{
			if archive.file().entries().get(i).unwrap().dir().unwrap() {
				fs::create_dir_all(extract_path).await.unwrap();
			} else {
				let mut reader = archive.reader_without_entry(i).await.unwrap();
				if !extract_path.parent().unwrap().exists() {
					fs::create_dir_all(extract_path.parent().unwrap())
						.await
						.unwrap();
				}

				let writer = fs::OpenOptions::new()
					.write(true)
					.create_new(true)
					.open(&extract_path)
					.await
					.unwrap();

				futures_lite::io::copy(&mut reader, &mut writer.compat_write())
					.await
					.unwrap();

				return Ok(());
			}
		}
	}

	Ok(())
}

pub(crate) async fn extract_all(
	zip_path: &std::path::Path,
	extract_path: &std::path::Path,
) -> Result<(), Box<dyn Error + Send>> {
	let archive = async_zip::tokio::read::fs::ZipFileReader::new(zip_path)
		.await
		.unwrap();

	for i in 0..archive.file().entries().len() {
		let entry = archive.file().entries().get(i).unwrap();
		let path = extract_path.join(entry.filename().as_str().unwrap());

		if path.exists() {
			continue;
		}

		if entry.dir().unwrap() {
			fs::create_dir_all(path).await.unwrap();
		} else {
			let mut reader = archive.reader_without_entry(i).await.unwrap();
			if !path.parent().unwrap().exists() {
				fs::create_dir_all(path.parent().unwrap()).await.unwrap();
			}

			let writer = fs::OpenOptions::new()
				.write(true)
				.create_new(true)
				.open(&path)
				.await
				.unwrap();

			futures_lite::io::copy(&mut reader, &mut writer.compat_write())
				.await
				.unwrap();
		}
	}

	Ok(())
}