summaryrefslogtreecommitdiffstats
path: root/src/launcher/assets.rs
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2025-01-31 02:32:19 -0600
committerLibravatar bigfoot547 <[email protected]>2025-01-31 02:32:19 -0600
commitcdeee17c2be5b8b9a333b977b3e2d373b94dfe0a (patch)
tree58ec48b5bfa9afe03ebbd9716f1f90841af914e9 /src/launcher/assets.rs
parentRemove some unused imports but not all of them (diff)
do clippy stuff and change line endings
Diffstat (limited to 'src/launcher/assets.rs')
-rw-r--r--src/launcher/assets.rs644
1 files changed, 322 insertions, 322 deletions
diff --git a/src/launcher/assets.rs b/src/launcher/assets.rs
index dacd01d..7c5dcf3 100644
--- a/src/launcher/assets.rs
+++ b/src/launcher/assets.rs
@@ -1,322 +1,322 @@
-use std::error::Error;
-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, TryStreamExt};
-use log::{debug, info, warn};
-use reqwest::Client;
-use sha1_smol::Sha1;
-use tokio::{fs, io};
-use tokio::fs::File;
-use crate::assets::{Asset, AssetIndex};
-use crate::launcher::download::{MultiDownloader, VerifiedDownload};
-use crate::util;
-use crate::util::{FileVerifyError, IntegrityError};
-use crate::version::DownloadInfo;
-
-const INDEX_PATH: &'static str = "indexes";
-const OBJECT_PATH: &'static str = "objects";
-
-pub struct AssetRepository {
- online: bool,
- home: PathBuf
-}
-
-#[derive(Debug)]
-pub enum AssetError {
- InvalidId(Option<String>),
- IO { what: &'static str, error: io::Error },
- IndexParse(serde_json::Error),
- Offline,
- MissingURL,
- DownloadIndex(reqwest::Error),
- Integrity(IntegrityError),
- AssetObjectDownload,
- AssetVerifyError(FileVerifyError),
- AssetNameError(&'static str)
-}
-
-impl Display for AssetError {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- 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::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}"),
- AssetError::AssetNameError(e) => write!(f, "invalid asset name: {e}")
- }
- }
-}
-
-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
- }
- }
-}
-
-impl From<(&'static str, io::Error)> for AssetError {
- fn from((what, error): (&'static str, io::Error)) -> Self {
- AssetError::IO { what, error }
- }
-}
-
-impl AssetRepository {
- pub async fn new(online: bool, home: impl AsRef<Path>) -> Result<AssetRepository, io::Error> {
- let home = home.as_ref().to_owned();
-
- match fs::create_dir_all(&home).await {
- Ok(_) => (),
- Err(e) => match e.kind() {
- ErrorKind::AlreadyExists => (),
- _ => return Err(e)
- }
- };
-
- Ok(AssetRepository {
- online,
- home
- })
- }
-
- pub fn get_home(&self) -> &Path {
- self.home.as_path()
- }
-
- fn get_index_path(&self, id: &str) -> Result<PathBuf, AssetError> {
- 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(Some(id.into())));
- };
-
- let path = path.to_str().ok_or(AssetError::InvalidId(Some(path.to_string_lossy().into())))?;
-
- // FIXME: change this once "add_extension" is stabilized
- indexes_path.push(format!("{}.json", path));
-
- Ok(indexes_path)
- }
-
- pub async fn load_index(&self, index: &DownloadInfo, id: Option<&str>) -> Result<AssetIndex, AssetError> {
- let Some(id) = id else {
- return Err(AssetError::InvalidId(None));
- };
-
- info!("Loading asset index {}", id);
-
- let path = self.get_index_path(id)?;
- debug!("Asset index {} is located at {}", id, path.display());
-
- match util::verify_file(&path, index.size, index.sha1).await {
- 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 => {
- 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));
- },
- Err(FileVerifyError::Read(_, e)) => return Err(("reading asset index", e).into())
- }
-
- if !self.online {
- 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();
-
- if actual != expect {
- 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)
- }
-
- pub fn get_object_path(&self, obj: &Asset) -> PathBuf {
- let hex_digest = obj.hash.to_string();
- [self.home.as_ref(), OsStr::new(OBJECT_PATH), OsStr::new(&hex_digest[..2]), OsStr::new(&hex_digest)].iter().collect()
- }
-
- 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(OBJECT_PATH)].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.values() {
- let path = self.get_object_path(object);
-
- 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 client = Client::new();
- MultiDownloader::with_concurrent(downloads.iter_mut(), 32).perform(&client).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.iter_mut()).await.map_err(|e| AssetError::AssetVerifyError(e))?;
- }
-
- Ok(())
- }
-
- pub async fn reconstruct_assets(&self, index: &AssetIndex, instance_path: &Path, index_id: Option<&str>) -> Result<Option<PathBuf>, AssetError> {
- let target_path: PathBuf;
- let Some(index_id) = index_id else {
- return Err(AssetError::InvalidId(None));
- };
-
- if index.virtual_assets {
- target_path = [self.home.as_ref(), OsStr::new("virtual"), OsStr::new(index_id)].iter().collect();
- } else if index.map_to_resources {
- target_path = [instance_path, Path::new("resources")].iter().collect();
- } else {
- info!("This asset index does not request a virtual assets folder. Nothing to be done.");
- return Ok(None);
- }
-
- info!("Reconstructing virtual assets for {}", index_id);
-
- fs::create_dir_all(&target_path).await.map_err(|e| AssetError::from(("creating virtual assets directory", e)))?;
-
- stream::iter(index.objects.values()
- .map(|object| {
- let obj_path = util::check_path(object.name.as_str()).map_err(AssetError::AssetNameError)?;
- let obj_path = target_path.join(obj_path);
-
- Ok((object, obj_path))
- }))
- .try_filter_map(|(object, obj_path)| async move {
- match util::verify_file(&obj_path, Some(object.size), Some(object.hash)).await {
- Ok(_) => {
- debug!("Not copying asset {}, integrity matches.", object.name);
- Ok(None)
- }
- Err(FileVerifyError::Open(_, e)) if e.kind() == ErrorKind::NotFound => {
- debug!("Copying asset {}, file does not exist.", object.name);
- Ok(Some((object, obj_path)))
- },
- Err(FileVerifyError::Integrity(_, e)) => {
- debug!("Copying asset {}: {}", object.name, e);
- Ok(Some((object, obj_path)))
- },
- Err(e) => {
- debug!("Error while reconstructing assets: {e}");
- Err(AssetError::AssetVerifyError(e))
- }
- }
- })
- .try_for_each_concurrent(32, |(object, obj_path)| async move {
- if let Some(parent) = obj_path.parent() {
- fs::create_dir_all(parent).await
- .inspect_err(|e| debug!("Error creating directory for asset object {}: {e}", object.name))
- .map_err(|e| AssetError::from(("creating asset object directory", e)))?;
- }
-
- let mut fromfile = File::open(self.get_object_path(object)).await
- .map_err(|e| AssetError::from(("opening source object", e)))?;
- let mut tofile = File::create(&obj_path).await
- .map_err(|e| AssetError::from(("creating target object", e)))?;
-
- io::copy(&mut fromfile, &mut tofile).await.map_err(|e| AssetError::from(("copying asset object", e)))?;
- debug!("Copied object {} to {}.", object.name, obj_path.display());
- Ok(())
- }).await.map(|_| Some(target_path))
- }
-}
-
-#[cfg(test)]
-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");
- }
-}
+use std::error::Error;
+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, TryStreamExt};
+use log::{debug, info, warn};
+use reqwest::Client;
+use sha1_smol::Sha1;
+use tokio::{fs, io};
+use tokio::fs::File;
+use crate::assets::{Asset, AssetIndex};
+use crate::launcher::download::{MultiDownloader, VerifiedDownload};
+use crate::util;
+use crate::util::{FileVerifyError, IntegrityError};
+use crate::version::DownloadInfo;
+
+const INDEX_PATH: &str = "indexes";
+const OBJECT_PATH: &str = "objects";
+
+pub struct AssetRepository {
+ online: bool,
+ home: PathBuf
+}
+
+#[derive(Debug)]
+pub enum AssetError {
+ InvalidId(Option<String>),
+ IO { what: &'static str, error: io::Error },
+ IndexParse(serde_json::Error),
+ Offline,
+ MissingURL,
+ DownloadIndex(reqwest::Error),
+ Integrity(IntegrityError),
+ AssetObjectDownload,
+ AssetVerifyError(FileVerifyError),
+ AssetNameError(&'static str)
+}
+
+impl Display for AssetError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ 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::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}"),
+ AssetError::AssetNameError(e) => write!(f, "invalid asset name: {e}")
+ }
+ }
+}
+
+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
+ }
+ }
+}
+
+impl From<(&'static str, io::Error)> for AssetError {
+ fn from((what, error): (&'static str, io::Error)) -> Self {
+ AssetError::IO { what, error }
+ }
+}
+
+impl AssetRepository {
+ pub async fn new(online: bool, home: impl AsRef<Path>) -> Result<AssetRepository, io::Error> {
+ let home = home.as_ref().to_owned();
+
+ match fs::create_dir_all(&home).await {
+ Ok(_) => (),
+ Err(e) => match e.kind() {
+ ErrorKind::AlreadyExists => (),
+ _ => return Err(e)
+ }
+ };
+
+ Ok(AssetRepository {
+ online,
+ home
+ })
+ }
+
+ pub fn get_home(&self) -> &Path {
+ self.home.as_path()
+ }
+
+ fn get_index_path(&self, id: &str) -> Result<PathBuf, AssetError> {
+ 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(Some(id.into())));
+ };
+
+ let path = path.to_str().ok_or(AssetError::InvalidId(Some(path.to_string_lossy().into())))?;
+
+ // FIXME: change this once "add_extension" is stabilized
+ indexes_path.push(format!("{}.json", path));
+
+ Ok(indexes_path)
+ }
+
+ pub async fn load_index(&self, index: &DownloadInfo, id: Option<&str>) -> Result<AssetIndex, AssetError> {
+ let Some(id) = id else {
+ return Err(AssetError::InvalidId(None));
+ };
+
+ info!("Loading asset index {}", id);
+
+ let path = self.get_index_path(id)?;
+ debug!("Asset index {} is located at {}", id, path.display());
+
+ match util::verify_file(&path, index.size, index.sha1).await {
+ 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 serde_json::from_str(&idx_data).map_err(AssetError::IndexParse);
+ },
+ Err(FileVerifyError::Open(_, e)) => match e.kind() {
+ 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));
+ },
+ Err(FileVerifyError::Read(_, e)) => return Err(("reading asset index", e).into())
+ }
+
+ if !self.online {
+ 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(AssetError::DownloadIndex)?
+ .text().await
+ .map_err(AssetError::DownloadIndex)?;
+
+ 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();
+
+ if actual != expect {
+ 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
+ })?;
+
+ serde_json::from_str(&idx_text).map_err(AssetError::IndexParse)
+ }
+
+ fn get_object_url(obj: &Asset) -> String {
+ format!("{}{:02x}/{}", super::constants::URL_RESOURCE_BASE, obj.hash.bytes()[0], obj.hash)
+ }
+
+ pub fn get_object_path(&self, obj: &Asset) -> PathBuf {
+ let hex_digest = obj.hash.to_string();
+ [self.home.as_ref(), OsStr::new(OBJECT_PATH), OsStr::new(&hex_digest[..2]), OsStr::new(&hex_digest)].iter().collect()
+ }
+
+ 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(OBJECT_PATH)].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.values() {
+ let path = self.get_object_path(object);
+
+ 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 client = Client::new();
+ MultiDownloader::with_concurrent(downloads.iter_mut(), 32).perform(&client).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.iter_mut()).await.map_err(AssetError::AssetVerifyError)?;
+ }
+
+ Ok(())
+ }
+
+ pub async fn reconstruct_assets(&self, index: &AssetIndex, instance_path: &Path, index_id: Option<&str>) -> Result<Option<PathBuf>, AssetError> {
+ let target_path: PathBuf;
+ let Some(index_id) = index_id else {
+ return Err(AssetError::InvalidId(None));
+ };
+
+ if index.virtual_assets {
+ target_path = [self.home.as_ref(), OsStr::new("virtual"), OsStr::new(index_id)].iter().collect();
+ } else if index.map_to_resources {
+ target_path = [instance_path, Path::new("resources")].iter().collect();
+ } else {
+ info!("This asset index does not request a virtual assets folder. Nothing to be done.");
+ return Ok(None);
+ }
+
+ info!("Reconstructing virtual assets for {}", index_id);
+
+ fs::create_dir_all(&target_path).await.map_err(|e| AssetError::from(("creating virtual assets directory", e)))?;
+
+ stream::iter(index.objects.values()
+ .map(|object| {
+ let obj_path = util::check_path(object.name.as_str()).map_err(AssetError::AssetNameError)?;
+ let obj_path = target_path.join(obj_path);
+
+ Ok((object, obj_path))
+ }))
+ .try_filter_map(|(object, obj_path)| async move {
+ match util::verify_file(&obj_path, Some(object.size), Some(object.hash)).await {
+ Ok(_) => {
+ debug!("Not copying asset {}, integrity matches.", object.name);
+ Ok(None)
+ }
+ Err(FileVerifyError::Open(_, e)) if e.kind() == ErrorKind::NotFound => {
+ debug!("Copying asset {}, file does not exist.", object.name);
+ Ok(Some((object, obj_path)))
+ },
+ Err(FileVerifyError::Integrity(_, e)) => {
+ debug!("Copying asset {}: {}", object.name, e);
+ Ok(Some((object, obj_path)))
+ },
+ Err(e) => {
+ debug!("Error while reconstructing assets: {e}");
+ Err(AssetError::AssetVerifyError(e))
+ }
+ }
+ })
+ .try_for_each_concurrent(32, |(object, obj_path)| async move {
+ if let Some(parent) = obj_path.parent() {
+ fs::create_dir_all(parent).await
+ .inspect_err(|e| debug!("Error creating directory for asset object {}: {e}", object.name))
+ .map_err(|e| AssetError::from(("creating asset object directory", e)))?;
+ }
+
+ let mut fromfile = File::open(self.get_object_path(object)).await
+ .map_err(|e| AssetError::from(("opening source object", e)))?;
+ let mut tofile = File::create(&obj_path).await
+ .map_err(|e| AssetError::from(("creating target object", e)))?;
+
+ io::copy(&mut fromfile, &mut tofile).await.map_err(|e| AssetError::from(("copying asset object", e)))?;
+ debug!("Copied object {} to {}.", object.name, obj_path.display());
+ Ok(())
+ }).await.map(|_| Some(target_path))
+ }
+}
+
+#[cfg(test)]
+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");
+ }
+}