Initial draft of profile metadata format & CLI (#17)

* Initial draft of profile metadata format

* Remove records, add Clippy to Nix, fix Clippy error

* Work on profile definition

* BREAKING: Make global settings consistent with profile settings

* Add builder methods & format

* Integrate launching with profiles

* Add profile loading

* Launching via profile, API tweaks, and yak shaving

* Incremental update, committing everything due to personal system maintainance

* Prepare for review cycle

* Remove reminents of experimental work

* CLI: allow people to override the non-empty directory check

* Fix mistake in previous commit

* Handle trailing whitespace and newlines in prompts

* Revamp prompt to use dialoguer and support defaults

* Make requested changes
This commit is contained in:
Danielle
2022-03-28 18:41:35 -07:00
committed by GitHub
parent 98aa66f9d8
commit d1070ca213
27 changed files with 1825 additions and 334 deletions

View File

@@ -1,15 +1,38 @@
use crate::launcher::LauncherError;
use crate::{
data::{DataError, Settings},
launcher::LauncherError,
};
use daedalus::get_path_from_artifact;
use daedalus::minecraft::{
fetch_assets_index, fetch_version_info, Asset, AssetsIndex, DownloadType, Library, Os, Version,
VersionInfo,
fetch_assets_index, fetch_version_info, Asset, AssetsIndex, DownloadType,
Library, Os, Version, VersionInfo,
};
use daedalus::modded::{
fetch_partial_version, merge_partial_version, LoaderVersion,
};
use daedalus::modded::{fetch_partial_version, merge_partial_version, LoaderVersion};
use futures::future;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::time::Duration;
use tokio::{
fs::File,
io::AsyncWriteExt,
sync::{OnceCell, Semaphore},
};
static DOWNLOADS_SEMAPHORE: OnceCell<Semaphore> = OnceCell::const_new();
pub async fn init() -> Result<(), DataError> {
DOWNLOADS_SEMAPHORE
.get_or_try_init(|| async {
let settings = Settings::get().await?;
Ok::<_, DataError>(Semaphore::new(
settings.max_concurrent_downloads,
))
})
.await?;
Ok(())
}
pub async fn download_version_info(
client_path: &Path,
@@ -22,8 +45,7 @@ pub async fn download_version_info(
};
let mut path = client_path.join(id);
path.push(id);
path.set_extension("json");
path.push(&format!("{id}.json"));
if path.exists() {
let contents = std::fs::read_to_string(path)?;
@@ -37,7 +59,7 @@ pub async fn download_version_info(
info.id = loader_version.id.clone();
}
let info_s = serde_json::to_string(&info)?;
save_file(&path, &bytes::Bytes::from(info_s))?;
save_file(&path, &bytes::Bytes::from(info_s)).await?;
Ok(info)
}
@@ -58,10 +80,14 @@ pub async fn download_client(
})?;
let mut path = client_path.join(version);
path.push(version);
path.set_extension("jar");
path.push(&format!("{version}.jar"));
save_and_download_file(&path, &client_download.url, Some(&client_download.sha1)).await?;
save_and_download_file(
&path,
&client_download.url,
Some(&client_download.sha1),
)
.await?;
Ok(())
}
@@ -69,7 +95,8 @@ pub async fn download_assets_index(
assets_path: &Path,
version: &VersionInfo,
) -> Result<AssetsIndex, LauncherError> {
let path = assets_path.join(format!("indexes/{}.json", &version.asset_index.id));
let path =
assets_path.join(format!("indexes/{}.json", &version.asset_index.id));
if path.exists() {
let content = std::fs::read_to_string(path)?;
@@ -77,7 +104,8 @@ pub async fn download_assets_index(
} else {
let index = fetch_assets_index(version).await?;
save_file(&path, &bytes::Bytes::from(serde_json::to_string(&index)?))?;
save_file(&path, &bytes::Bytes::from(serde_json::to_string(&index)?))
.await?;
Ok(index)
}
@@ -88,12 +116,9 @@ pub async fn download_assets(
legacy_path: Option<&Path>,
index: &AssetsIndex,
) -> Result<(), LauncherError> {
future::join_all(
index
.objects
.iter()
.map(|(name, asset)| download_asset(assets_path, legacy_path, name, asset)),
)
future::join_all(index.objects.iter().map(|(name, asset)| {
download_asset(assets_path, legacy_path, name, asset)
}))
.await
.into_iter()
.collect::<Result<Vec<()>, LauncherError>>()?;
@@ -114,14 +139,16 @@ async fn download_asset(
resource_path.push(sub_hash);
resource_path.push(hash);
let url = format!("https://resources.download.minecraft.net/{sub_hash}/{hash}");
let url =
format!("https://resources.download.minecraft.net/{sub_hash}/{hash}");
let resource = save_and_download_file(&resource_path, &url, Some(hash)).await?;
let resource =
save_and_download_file(&resource_path, &url, Some(hash)).await?;
if let Some(legacy_path) = legacy_path {
let resource_path =
legacy_path.join(name.replace('/', &std::path::MAIN_SEPARATOR.to_string()));
save_file(resource_path.as_path(), &resource)?;
let resource_path = legacy_path
.join(name.replace('/', &std::path::MAIN_SEPARATOR.to_string()));
save_file(resource_path.as_path(), &resource).await?;
}
Ok(())
@@ -132,11 +159,9 @@ pub async fn download_libraries(
natives_path: &Path,
libraries: &[Library],
) -> Result<(), LauncherError> {
future::join_all(
libraries
.iter()
.map(|library| download_library(libraries_path, natives_path, library)),
)
future::join_all(libraries.iter().map(|library| {
download_library(libraries_path, natives_path, library)
}))
.await
.into_iter()
.collect::<Result<Vec<()>, LauncherError>>()?;
@@ -173,7 +198,8 @@ async fn download_library_jar(
if let Some(downloads) = &library.downloads {
if let Some(library) = &downloads.artifact {
save_and_download_file(&path, &library.url, Some(&library.sha1)).await?;
save_and_download_file(&path, &library.url, Some(&library.sha1))
.await?;
}
} else {
let url = format!(
@@ -189,16 +215,21 @@ async fn download_library_jar(
Ok(())
}
async fn download_native(natives_path: &Path, library: &Library) -> Result<(), LauncherError> {
async fn download_native(
natives_path: &Path,
library: &Library,
) -> Result<(), LauncherError> {
use daedalus::minecraft::LibraryDownload;
use std::collections::HashMap;
// Try blocks in stable Rust when?
let optional_cascade = || -> Option<(&String, &HashMap<String, LibraryDownload>)> {
let os_key = library.natives.as_ref()?.get(&get_os())?;
let classifiers = library.downloads.as_ref()?.classifiers.as_ref()?;
Some((os_key, classifiers))
};
let optional_cascade =
|| -> Option<(&String, &HashMap<String, LibraryDownload>)> {
let os_key = library.natives.as_ref()?.get(&get_os())?;
let classifiers =
library.downloads.as_ref()?.classifiers.as_ref()?;
Some((os_key, classifiers))
};
if let Some((os_key, classifiers)) = optional_cascade() {
#[cfg(target_pointer_width = "64")]
@@ -227,19 +258,25 @@ async fn save_and_download_file(
Ok(bytes) => Ok(bytes::Bytes::from(bytes)),
Err(_) => {
let file = download_file(url, sha1).await?;
save_file(path, &file)?;
save_file(path, &file).await?;
Ok(file)
}
}
}
fn save_file(path: &Path, bytes: &bytes::Bytes) -> std::io::Result<()> {
async fn save_file(path: &Path, bytes: &bytes::Bytes) -> std::io::Result<()> {
let _save_permit = DOWNLOADS_SEMAPHORE
.get()
.expect("File operation semaphore not initialized!")
.acquire()
.await
.unwrap();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
tokio::fs::create_dir_all(parent).await?;
}
let mut file = File::create(path)?;
file.write_all(bytes)?;
let mut file = File::create(path).await?;
file.write_all(bytes).await?;
Ok(())
}
@@ -252,7 +289,17 @@ pub fn get_os() -> Os {
}
}
pub async fn download_file(url: &str, sha1: Option<&str>) -> Result<bytes::Bytes, LauncherError> {
pub async fn download_file(
url: &str,
sha1: Option<&str>,
) -> Result<bytes::Bytes, LauncherError> {
let _download_permit = DOWNLOADS_SEMAPHORE
.get()
.expect("File operation semaphore not initialized!")
.acquire()
.await
.unwrap();
let client = reqwest::Client::builder()
.tcp_keepalive(Some(Duration::from_secs(10)))
.build()
@@ -307,7 +354,9 @@ pub async fn download_file(url: &str, sha1: Option<&str>) -> Result<bytes::Bytes
/// Computes a checksum of the input bytes
async fn get_hash(bytes: bytes::Bytes) -> Result<String, LauncherError> {
let hash = tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest()).await?;
let hash =
tokio::task::spawn_blocking(|| sha1::Sha1::from(bytes).hexdigest())
.await?;
Ok(hash)
}