feat: implement basic modpack installation
This commit is contained in:
parent
406eec9a1b
commit
4ba6dd9860
6 changed files with 328 additions and 24 deletions
178
Cargo.lock
generated
178
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
77
src/storage.rs
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue