feat: implement basic modpack installation

This commit is contained in:
uku 2024-08-01 01:56:58 +02:00
parent 406eec9a1b
commit 4ba6dd9860
Signed by: uku
SSH key fingerprint: SHA256:4P0aN6M8ajKukNi6aPOaX0LacanGYtlfjmN+m/sHY/o
6 changed files with 328 additions and 24 deletions

178
Cargo.lock generated
View file

@ -41,6 +41,15 @@ dependencies = [
"libc",
]
[[package]]
name = "arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -198,6 +207,21 @@ dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -208,6 +232,17 @@ dependencies = [
"typenum",
]
[[package]]
name = "derive_arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -218,6 +253,38 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "directories"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "either"
version = "1.13.0"
@ -249,6 +316,16 @@ dependencies = [
"once_cell",
]
[[package]]
name = "flate2"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -652,6 +729,16 @@ version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.6.0",
"libc",
]
[[package]]
name = "lock_api"
version = "0.4.12"
@ -662,6 +749,12 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "lockfree-object-pool"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.4.22"
@ -764,6 +857,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "overload"
version = "0.1.1"
@ -946,6 +1045,17 @@ dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "redox_users"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]]
name = "regex"
version = "1.10.5"
@ -1180,6 +1290,15 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -1278,6 +1397,12 @@ dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "slab"
version = "0.4.9"
@ -1584,6 +1709,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"color-eyre",
"directories",
"hex",
"itertools",
"oauth2",
@ -1595,6 +1721,8 @@ dependencies = [
"tokio",
"tracing",
"tracing-subscriber",
"walkdir",
"zip",
]
[[package]]
@ -1648,6 +1776,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
@ -1770,6 +1908,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@ -1949,3 +2096,34 @@ name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]]
name = "zip"
version = "2.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40dd8c92efc296286ce1fbd16657c5dbefff44f1b4ca01cc5f517d8b7b3d3e2e"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"displaydoc",
"flate2",
"indexmap",
"memchr",
"thiserror",
"zopfli",
]
[[package]]
name = "zopfli"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
dependencies = [
"bumpalo",
"crc32fast",
"lockfree-object-pool",
"log",
"once_cell",
"simd-adler32",
]

View file

@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
color-eyre = "0.6.3"
directories = "5.0.1"
hex = "0.4.3"
itertools = "0.13.0"
oauth2 = "4.4.2"
@ -22,3 +23,5 @@ thiserror = "1.0.63"
tokio = { version = "1.38.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] }
walkdir = "2.5.0"
zip = { version = "2.1.6", default-features = false, features = ["deflate"] }

View file

@ -55,7 +55,7 @@ struct MinecraftLoginResponse {
access_token: String,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Default)]
pub struct MinecraftProfile {
pub id: String,
pub name: String,
@ -63,7 +63,7 @@ pub struct MinecraftProfile {
#[derive(Debug, Error)]
pub enum AuthenticationError {
#[error("reqwest error")]
#[error("reqwest error: {0}")]
Reqwest(#[from] reqwest::Error),
/// from `eyre::Report` because oauth2 error types are too complicated
#[error(transparent)]
@ -122,6 +122,7 @@ type Result<T> = std::result::Result<T, AuthenticationError>;
// TODO better error handling !!!!
pub async fn login() -> Result<MinecraftProfile> {
// temporary until i can access api.minecraftservices.com
return Ok(MinecraftProfile::default());
let access_token = request_msa_token().await?;
let (xbox_token, userhash) = auth_xbox_live(&access_token).await?;

View file

@ -2,6 +2,7 @@
mod auth;
mod modrinth;
mod storage;
use std::{fs::File, sync::LazyLock};
@ -39,6 +40,8 @@ async fn main() -> Result<()> {
tracing::info!("starting...");
tracing::debug!("starting debug");
storage::make_directories()?;
let profile = auth::login().await?;
tracing::info!("logged in as {} ({})", profile.name, profile.id);
@ -55,7 +58,7 @@ async fn main() -> Result<()> {
version.version_number
);
let path = version.download_file().await?;
let path = version.install_modpack().await?;
tracing::info!("downloaded file at {:?}", path);
}

View file

@ -1,10 +1,12 @@
use std::{env, fs::File, path::PathBuf};
use std::{collections::HashMap, path::PathBuf};
use chrono::{DateTime, Utc};
use color_eyre::eyre::{bail, Report, Result};
use color_eyre::eyre::{Report, Result};
use itertools::Itertools;
use serde::Deserialize;
use sha2::{Digest, Sha512};
use walkdir::WalkDir;
use crate::storage;
const PROJECT_ID: &str = "JR0bkFKa";
@ -21,7 +23,6 @@ pub struct ProjectVersion {
pub struct VersionFile {
url: String,
filename: String,
size: usize,
hashes: VersionFileHashes,
}
@ -30,33 +31,74 @@ pub struct VersionFileHashes {
sha512: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ModpackIndex {
files: Vec<ModpackFile>,
dependencies: HashMap<String, String>,
}
#[derive(Debug, Deserialize)]
struct ModpackFile {
path: PathBuf,
hashes: VersionFileHashes,
downloads: Vec<String>,
}
impl ProjectVersion {
pub async fn download_file(&self) -> Result<PathBuf> {
let version_file = &self.files[0];
let path = env::temp_dir().join(&version_file.filename);
let response = crate::HTTP_CLIENT.get(&version_file.url).send().await?;
let mut bytes = &*response.bytes().await?; // deref to get &[u8]
if bytes.len() != version_file.size {
bail!("invalid length!")
storage::download_to_cache(
&version_file.url,
&version_file.filename,
&version_file.hashes.sha512,
)
.await
}
let expected_hash = hex::decode(&version_file.hashes.sha512)?;
let computed_hash = {
let mut sha512 = Sha512::new();
sha512.update(bytes);
sha512.finalize()
pub async fn install_modpack(&self) -> Result<PathBuf> {
let mrpack_path = self.download_file().await?;
let decompressed = storage::decompress_zip(&mrpack_path)?;
let index_path = decompressed.join("modrinth.index.json");
let index = serde_json::from_slice::<ModpackIndex>(&std::fs::read(&index_path)?)?;
let version_dir = storage::get_version_directory(&self.game_versions[0])?;
for file in index.files {
let Some(filename) = file.path.file_name() else {
tracing::warn!("path {} does not have a filename", file.path.display());
continue;
};
if expected_hash[..] != computed_hash[..] {
bail!("hash mismatch!")
let download_path = storage::download_to_cache(
&file.downloads[0],
&filename.to_string_lossy(),
&file.hashes.sha512,
)
.await?;
let final_path = version_dir.join(file.path);
storage::move_file(download_path, &final_path)?;
tracing::info!("downloaded file {}", final_path.display());
}
let mut file = File::create(&path)?;
std::io::copy(&mut bytes, &mut file)?;
let overrides_path = decompressed.join("overrides");
for entry in WalkDir::new(&overrides_path) {
let entry = entry?;
Ok(path)
if entry.path().is_file() {
let stripped = entry.path().strip_prefix(&overrides_path)?;
let final_path = version_dir.join(stripped);
storage::move_file(entry.path(), &final_path)?;
tracing::info!("added override {}", final_path.display());
}
}
Ok(version_dir)
}
}

77
src/storage.rs Normal file
View file

@ -0,0 +1,77 @@
use std::{
fs::File,
path::{Path, PathBuf},
sync::LazyLock,
};
use color_eyre::eyre::{bail, eyre, OptionExt, Report, Result};
use directories::ProjectDirs;
use sha2::{Digest, Sha512};
use zip::ZipArchive;
static DIRS: LazyLock<ProjectDirs> = LazyLock::new(|| {
ProjectDirs::from("net", "uku3lig", "uklient")
.expect("could not instantiate project directories!")
});
pub fn make_directories() -> std::io::Result<()> {
std::fs::create_dir_all(DIRS.data_dir())?;
std::fs::create_dir_all(DIRS.cache_dir())?;
Ok(())
}
pub async fn download_to_cache(url: &str, filename: &str, sha512: &str) -> Result<PathBuf> {
let path = DIRS.cache_dir().join(filename);
let response = crate::HTTP_CLIENT.get(url).send().await?;
let mut bytes = &*response.bytes().await?; // deref to get &[u8]
let expected_hash = hex::decode(sha512)?;
let computed_hash = {
let mut sha512 = Sha512::new();
sha512.update(bytes);
sha512.finalize()
};
if expected_hash[..] != computed_hash[..] {
bail!(
"hash mismatch! expected: {}, got {}",
hex::encode(expected_hash),
hex::encode(computed_hash),
)
}
let mut file = File::create(&path)?;
std::io::copy(&mut bytes, &mut file)?;
Ok(path)
}
pub fn decompress_zip(path: &Path) -> Result<PathBuf> {
let dir = path.parent().ok_or_eyre("could not find parent")?;
let name = path.file_stem().ok_or_eyre("filename does not exist")?;
let extract_dir = dir.join(name);
let file = File::open(path)?;
let mut archive = ZipArchive::new(file)?;
archive.extract(&extract_dir)?;
Ok(extract_dir)
}
pub fn get_version_directory(version: &str) -> Result<PathBuf> {
let path = DIRS.data_dir().join("versions").join(version);
std::fs::create_dir_all(&path)?;
Ok(path)
}
pub fn move_file<S: AsRef<Path>, D: AsRef<Path>>(source: S, dest: D) -> Result<()> {
let parent = dest
.as_ref()
.parent()
.ok_or_else(|| eyre!("path {} does not have a parent", dest.as_ref().display()))?;
std::fs::create_dir_all(parent)?;
std::fs::rename(source, dest).map_err(Report::from)
}