summaryrefslogtreecommitdiffstats
path: root/src/launcher
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2025-01-15 22:36:17 -0600
committerLibravatar bigfoot547 <[email protected]>2025-01-15 22:37:06 -0600
commite5d13bf03a3b7e8444ae367689852fcd6633e221 (patch)
tree9361fdfb51764d9c16092000552671afc25aaeb6 /src/launcher
parentwip: asset index (diff)
assets done
Diffstat (limited to 'src/launcher')
-rw-r--r--src/launcher/assets.rs162
-rw-r--r--src/launcher/download.rs13
2 files changed, 159 insertions, 16 deletions
diff --git a/src/launcher/assets.rs b/src/launcher/assets.rs
index 020885f..7d883d8 100644
--- a/src/launcher/assets.rs
+++ b/src/launcher/assets.rs
@@ -1,13 +1,17 @@
use std::error::Error;
-use std::fmt::{Display, Formatter, Write};
+use std::ffi::OsStr;
+use std::fmt::{Display, Formatter};
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::path::Component::Normal;
+use futures::{stream, TryFutureExt, TryStreamExt};
use log::{debug, info, warn};
+use sha1_smol::Sha1;
use tokio::{fs, io};
-use crate::assets::AssetIndex;
+use crate::assets::{Asset, AssetIndex};
+use crate::launcher::download::{MultiDownloader, VerifiedDownload};
use crate::util;
-use crate::util::FileVerifyError;
+use crate::util::{FileVerifyError, IntegrityError};
use crate::version::DownloadInfo;
const INDEX_PATH: &'static str = "indexes";
@@ -21,7 +25,14 @@ pub struct AssetRepository {
#[derive(Debug)]
pub enum AssetError {
InvalidId(Option<String>),
- IO { what: &'static str, error: io::Error }
+ IO { what: &'static str, error: io::Error },
+ IndexParse(serde_json::Error),
+ Offline,
+ MissingURL,
+ DownloadIndex(reqwest::Error),
+ Integrity(IntegrityError),
+ AssetObjectDownload,
+ AssetVerifyError(FileVerifyError)
}
impl Display for AssetError {
@@ -29,7 +40,14 @@ impl Display for AssetError {
match self {
AssetError::InvalidId(None) => f.write_str("missing asset index id"),
AssetError::InvalidId(Some(id)) => write!(f, "invalid asset index id: {}", id),
- AssetError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error)
+ AssetError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error),
+ AssetError::IndexParse(error) => write!(f, "error parsing asset index: {}", error),
+ AssetError::Offline => f.write_str("cannot download asset index while offline"),
+ AssetError::MissingURL => f.write_str("missing asset index URL"),
+ AssetError::DownloadIndex(e) => write!(f, "error downloading asset index: {}", e),
+ AssetError::Integrity(e) => write!(f, "asset index integrity error: {}", e),
+ AssetError::AssetObjectDownload => f.write_str("asset object download failed"),
+ AssetError::AssetVerifyError(e) => write!(f, "error verifying asset object: {e}")
}
}
}
@@ -38,6 +56,10 @@ impl Error for AssetError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
AssetError::IO { error, .. } => Some(error),
+ AssetError::IndexParse(error) => Some(error),
+ AssetError::DownloadIndex(error) => Some(error),
+ AssetError::Integrity(error) => Some(error),
+ AssetError::AssetVerifyError(error) => Some(error),
_ => None
}
}
@@ -68,15 +90,17 @@ impl AssetRepository {
}
fn get_index_path(&self, id: &str) -> Result<PathBuf, AssetError> {
- let indexes_path: &Path = (&self.home, INDEX_PATH).as_ref();
+ let mut indexes_path: PathBuf = [self.home.as_ref(), OsStr::new(INDEX_PATH)].iter().collect();
let Some(Normal(path)) = Path::new(id).components().last() else {
- return Err(AssetError::InvalidId(id.into()));
+ return Err(AssetError::InvalidId(Some(id.into())));
};
- let path = path.to_str().ok_or(AssetError::InvalidId(Some(path.to_string_lossy())))?;
+ let path = path.to_str().ok_or(AssetError::InvalidId(Some(path.to_string_lossy().into())))?;
// FIXME: change this once "add_extension" is stabilized
- Ok((indexes_path, format!("{}.json", path)).into())
+ indexes_path.push(format!("{}.json", path));
+
+ Ok(indexes_path)
}
pub async fn load_index(&self, index: &DownloadInfo, id: Option<&str>) -> Result<AssetIndex, AssetError> {
@@ -87,28 +111,136 @@ impl AssetRepository {
info!("Loading asset index {}", id);
let path = self.get_index_path(id)?;
- debug!("Asset index {} is located at {}", id, path);
+ debug!("Asset index {} is located at {}", id, path.display());
match util::verify_file(&path, index.size, index.sha1).await {
- Ok(_) => todo!(), // load local index
+ Ok(_) => {
+ debug!("Asset index {} verified on disk. Loading it.", id);
+ let idx_data = fs::read_to_string(&path).await.map_err(|e| AssetError::IO {
+ what: "reading asset index",
+ error: e
+ })?;
+
+ return Ok(serde_json::from_str(&idx_data).map_err(|e| AssetError::IndexParse(e))?);
+ },
Err(FileVerifyError::Open(_, e)) => match e.kind() {
- ErrorKind::NotFound => (), // download
+ ErrorKind::NotFound => {
+ debug!("Asset index {} not found on disk. Must download it.", id);
+ },
_ => return Err(("opening asset index", e).into())
},
Err(FileVerifyError::Integrity(_, e)) => {
info!("Asset index {} has mismatched integrity: {}, must download it.", id, e);
let _ = fs::remove_file(&path).await.map_err(|e| warn!("Error deleting modified index {}: {} (ignored)", id, e));
- // download
},
Err(FileVerifyError::Read(_, e)) => return Err(("reading asset index", e).into())
}
if !self.online {
- warn!("Must redownload asset index {}, but the launcher is in offline mode. Please try again in online mode.", id);
- return todo!();
+ warn!("Must download asset index {}, but the launcher is in offline mode. Please try again in online mode.", id);
+ return Err(AssetError::Offline);
+ }
+
+ let Some(url) = index.url.as_ref() else {
+ return Err(AssetError::MissingURL);
+ };
+
+ debug!("Downloading asset index {} from {}", id, url);
+
+ if let Some(parent) = path.parent() {
+ fs::create_dir_all(parent).await.map_err(|e| AssetError::IO {
+ what: "creating asset index folder",
+ error: e
+ })?;
+ }
+
+ let idx_text = reqwest::get(url).await
+ .map_err(|e| AssetError::DownloadIndex(e))?
+ .text().await
+ .map_err(|e| AssetError::DownloadIndex(e))?;
+
+ if index.size.is_some_and(|s| s != idx_text.len()) {
+ return Err(AssetError::Integrity(IntegrityError::SizeMismatch {
+ expect: index.size.unwrap(),
+ actual: idx_text.len()
+ }));
+ }
+
+ if let Some(expect) = index.sha1 {
+ let actual = Sha1::from(&idx_text).digest();
+ return Err(AssetError::Integrity(IntegrityError::Sha1Mismatch { expect, actual }));
}
+ debug!("Saving downloaded asset index to {}", path.display());
+ fs::write(&path, &idx_text).await.map_err(|e| AssetError::IO {
+ what: "writing asset index",
+ error: e
+ })?;
+
+ Ok(serde_json::from_str(&idx_text).map_err(|e| AssetError::IndexParse(e))?)
+ }
+
+ fn get_object_url(obj: &Asset) -> String {
+ format!("{}{:02x}/{}", super::constants::URL_RESOURCE_BASE, obj.hash.bytes()[0], obj.hash)
+ }
+
+ async fn download_assets(&self, index: &AssetIndex) -> Result<(), AssetError> {
todo!()
}
+
+ async fn ensure_dir(path: impl AsRef<Path>) -> Result<(), io::Error> {
+ match fs::create_dir(path).await {
+ Ok(_) => Ok(()),
+ Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()),
+ Err(e) => Err(e)
+ }
+ }
+
+ pub async fn ensure_assets(&self, index: &AssetIndex) -> Result<(), AssetError> {
+ let mut downloads = Vec::new();
+ let objects_path = [self.home.as_ref(), OsStr::new("objects")].iter().collect::<PathBuf>();
+
+ Self::ensure_dir(&objects_path).await.map_err(|e| AssetError::IO {
+ what: "creating objects directory",
+ error: e
+ })?;
+
+ for object in index.objects.iter() {
+ let hex_digest = object.hash.to_string();
+ let path = [objects_path.as_ref(), OsStr::new(&hex_digest[..2]), OsStr::new(&hex_digest)].iter().collect::<PathBuf>();
+
+ Self::ensure_dir(path.parent().unwrap()).await.map_err(|error| AssetError::IO { error, what: "creating directory for object" })?;
+
+ downloads.push(VerifiedDownload::new(&Self::get_object_url(object), &path, Some(object.size), Some(object.hash)));
+ }
+
+ if self.online {
+ info!("Downloading {} asset objects...", downloads.len());
+ let mut multi = MultiDownloader::new(downloads);
+ multi.perform().await
+ .inspect_err(|e| warn!("asset download failed: {e}"))
+ .try_fold((), |_, _| async {Ok(())})
+ .await
+ .map_err(|_| AssetError::AssetObjectDownload)?;
+ } else {
+ info!("Verifying {} asset objects...", downloads.len());
+ super::download::verify_files(downloads).await.map_err(|e| AssetError::AssetVerifyError(e))?;
+ }
+
+ Ok(())
+ }
}
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_it() {
+ let digest_str = "ad1115931887a73cd596300f2c93f84adf39521d";
+ assert_eq!(AssetRepository::get_object_url(&Asset {
+ name: String::from("test"),
+ hash: digest_str.parse().unwrap(),
+ size: 0usize
+ }), "https://resources.download.minecraft.net/ad/ad1115931887a73cd596300f2c93f84adf39521d");
+ }
+}
diff --git a/src/launcher/download.rs b/src/launcher/download.rs
index 7d9be73..d8d05f7 100644
--- a/src/launcher/download.rs
+++ b/src/launcher/download.rs
@@ -2,7 +2,7 @@ use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
-use futures::{stream, StreamExt, TryStream};
+use futures::{stream, StreamExt, TryStream, TryStreamExt};
use log::{debug, warn};
use reqwest::{Client, IntoUrl, Method, RequestBuilder};
use sha1_smol::{Digest, Sha1};
@@ -281,3 +281,14 @@ impl Download for VerifiedDownload {
Ok(())
}
}
+
+pub async fn verify_files(files: Vec<VerifiedDownload>) -> Result<(), FileVerifyError> {
+ stream::iter(files)
+ .map(|dl| Ok(async move {
+ debug!("Verifying library {}", dl.get_path().display());
+ util::verify_file(dl.get_path(), dl.get_expect_size(), dl.get_expect_sha1()).await
+ }))
+ .try_buffer_unordered(8)
+ .try_fold((), |_, _| async {Ok(())})
+ .await
+}