Modpack support (#60)

* Modpack support

* Finish feature

* Tauri errors fix (#61)

* async impl

* working

* fmt and redundancy

* moved ? to if let Ok block

* Finish modpacks support

* remove generated file

* fix compile err

* fix lint

* Fix code review comments + forge support

---------

Co-authored-by: Wyatt Verchere <wverchere@gmail.com>
This commit is contained in:
Geometrically
2023-04-05 19:04:09 -07:00
committed by GitHub
parent 6965487b56
commit b9a3a6dc11
28 changed files with 1006 additions and 534 deletions

View File

@@ -1,5 +1,6 @@
//! API for interacting with Theseus
pub mod auth;
pub mod pack;
pub mod process;
pub mod profile;
pub mod profile_create;
@@ -17,7 +18,7 @@ pub mod prelude {
pub use crate::{
auth::{self, Credentials},
data::*,
process,
pack, process,
profile::{self, Profile},
profile_create, settings, State,
};

338
theseus/src/api/pack.rs Normal file
View File

@@ -0,0 +1,338 @@
use crate::config::{MODRINTH_API_URL, REQWEST_CLIENT};
use crate::data::ModLoader;
use crate::state::{ModrinthProject, ModrinthVersion, SideType};
use crate::util::fetch::{fetch, fetch_mirrors, write, write_cached_icon};
use crate::State;
use async_zip::tokio::read::seek::ZipFileReader;
use futures::TryStreamExt;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Cursor;
use std::path::{Component, PathBuf};
use tokio::fs;
#[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
struct PackFormat {
pub game: String,
pub format_version: i32,
pub version_id: String,
pub name: String,
pub summary: Option<String>,
pub files: Vec<PackFile>,
pub dependencies: HashMap<PackDependency, String>,
}
#[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
struct PackFile {
pub path: String,
pub hashes: HashMap<PackFileHash, String>,
pub env: Option<HashMap<EnvType, SideType>>,
pub downloads: Vec<String>,
pub file_size: u32,
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "camelCase", from = "String")]
enum PackFileHash {
Sha1,
Sha512,
Unknown(String),
}
impl From<String> for PackFileHash {
fn from(s: String) -> Self {
return match s.as_str() {
"sha1" => PackFileHash::Sha1,
"sha512" => PackFileHash::Sha512,
_ => PackFileHash::Unknown(s),
};
}
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "camelCase")]
enum EnvType {
Client,
Server,
}
#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
enum PackDependency {
Forge,
FabricLoader,
QuiltLoader,
Minecraft,
}
pub async fn install_pack_from_version_id(
version_id: String,
) -> crate::Result<PathBuf> {
let version: ModrinthVersion = REQWEST_CLIENT
.get(format!("{}version/{}", MODRINTH_API_URL, version_id))
.send()
.await?
.json()
.await?;
let (url, hash) =
if let Some(file) = version.files.iter().find(|x| x.primary) {
Some((file.url.clone(), file.hashes.get("sha1")))
} else {
version
.files
.first()
.map(|file| (file.url.clone(), file.hashes.get("sha1")))
}
.ok_or_else(|| {
crate::ErrorKind::InputError(
"Specified version has no files".to_string(),
)
})?;
let file = async {
let state = &State::get().await?;
let semaphore = state.io_semaphore.acquire().await?;
fetch(&url, hash.map(|x| &**x), &semaphore).await
}
.await?;
let project: ModrinthProject = REQWEST_CLIENT
.get(format!(
"{}project/{}",
MODRINTH_API_URL, version.project_id
))
.send()
.await?
.json()
.await?;
let icon = if let Some(icon_url) = project.icon_url {
let state = State::get().await?;
let semaphore = state.io_semaphore.acquire().await?;
let icon_bytes = fetch(&icon_url, None, &semaphore).await?;
let filename = icon_url.rsplit('/').next();
if let Some(filename) = filename {
Some(
write_cached_icon(
filename,
&state.directories.caches_dir(),
icon_bytes,
&semaphore,
)
.await?,
)
} else {
None
}
} else {
None
};
install_pack(file, icon, Some(version.project_id)).await
}
pub async fn install_pack_from_file(path: PathBuf) -> crate::Result<PathBuf> {
let file = fs::read(path).await?;
install_pack(bytes::Bytes::from(file), None, None).await
}
async fn install_pack(
file: bytes::Bytes,
icon: Option<PathBuf>,
project_id: Option<String>,
) -> crate::Result<PathBuf> {
let state = &State::get().await?;
let reader = Cursor::new(&file);
// Create zip reader around file
let mut zip_reader = ZipFileReader::new(reader).await.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to read input modpack zip".to_string(),
))
})?;
// Extract index of modrinth.index.json
let zip_index_option = zip_reader
.file()
.entries()
.iter()
.position(|f| f.entry().filename() == "modrinth.index.json");
if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
let entry = zip_reader
.file()
.entries()
.get(zip_index)
.unwrap()
.entry()
.clone();
let mut reader = zip_reader.entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest, &entry).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?;
if &*pack.game != "minecraft" {
return Err(crate::ErrorKind::InputError(
"Pack does not support Minecraft".to_string(),
)
.into());
}
let mut game_version = None;
let mut mod_loader = None;
let mut loader_version = None;
for (key, value) in pack.dependencies {
match key {
PackDependency::Forge => {
mod_loader = Some(ModLoader::Forge);
loader_version = Some(value);
}
PackDependency::FabricLoader => {
mod_loader = Some(ModLoader::Fabric);
loader_version = Some(value);
}
PackDependency::QuiltLoader => {
mod_loader = Some(ModLoader::Quilt);
loader_version = Some(value);
}
PackDependency::Minecraft => game_version = Some(value),
}
}
let game_version = if let Some(game_version) = game_version {
game_version
} else {
return Err(crate::ErrorKind::InputError(
"Pack did not specify Minecraft version".to_string(),
)
.into());
};
let profile = crate::api::profile_create::profile_create(
pack.name,
game_version.clone(),
mod_loader.unwrap_or(ModLoader::Vanilla),
loader_version,
icon,
project_id,
)
.await?;
use futures::StreamExt;
futures::stream::iter(pack.files.into_iter())
.map(Ok::<PackFile, crate::Error>)
.try_for_each_concurrent(None, |project| {
let profile = profile.clone();
async move {
// TODO: Future update: prompt user for optional files in a modpack
if let Some(env) = project.env {
if env
.get(&EnvType::Client)
.map(|x| x == &SideType::Unsupported)
.unwrap_or(false)
{
return Ok(());
}
}
let permit = state.io_semaphore.acquire().await?;
let file = fetch_mirrors(
&project
.downloads
.iter()
.map(|x| &**x)
.collect::<Vec<&str>>(),
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
&permit,
)
.await?;
let path =
std::path::Path::new(&project.path).components().next();
if let Some(path) = path {
match path {
Component::CurDir | Component::Normal(_) => {
let path = profile.join(project.path);
write(&path, &file, &permit).await?;
}
_ => {}
};
}
Ok(())
}
})
.await?;
let extract_overrides = |overrides: String| async {
let reader = Cursor::new(&file);
let mut overrides_zip =
ZipFileReader::new(reader).await.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed extract overrides Zip".to_string(),
))
})?;
let profile = profile.clone();
async move {
for index in 0..overrides_zip.file().entries().len() {
let file = overrides_zip
.file()
.entries()
.get(index)
.unwrap()
.entry()
.clone();
let file_path = PathBuf::from(file.filename());
if file.filename().starts_with(&overrides)
&& !file.filename().ends_with('/')
{
// Reads the file into the 'content' variable
let mut content = Vec::new();
let mut reader = overrides_zip.entry(index).await?;
reader.read_to_end_checked(&mut content, &file).await?;
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
}
if new_path.file_name().is_some() {
let permit = state.io_semaphore.acquire().await?;
write(&profile.join(new_path), &content, &permit)
.await?;
}
}
}
Ok::<(), crate::Error>(())
}
.await
};
extract_overrides("overrides".to_string()).await?;
extract_overrides("client_overrides".to_string()).await?;
super::profile::sync(&profile).await?;
Ok(profile)
} else {
Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
)))
}
}

View File

@@ -12,32 +12,12 @@ use std::{
};
use tokio::{process::Command, sync::RwLock};
/// Add a profile to the in-memory state
#[tracing::instrument]
pub async fn add(profile: Profile) -> crate::Result<()> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
profiles.insert(profile)?;
Ok(())
}
/// Add a path as a profile in-memory
#[tracing::instrument]
pub async fn add_path(path: &Path) -> crate::Result<()> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
profiles.insert_from(path).await?;
Ok(())
}
/// Remove a profile
#[tracing::instrument]
pub async fn remove(path: &Path) -> crate::Result<()> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
profiles.remove(path)?;
profiles.remove(path).await?;
Ok(())
}
@@ -48,29 +28,7 @@ pub async fn get(path: &Path) -> crate::Result<Option<Profile>> {
let state = State::get().await?;
let profiles = state.profiles.read().await;
profiles.0.get(path).map_or(Ok(None), |prof| match prof {
Some(prof) => Ok(Some(prof.clone())),
None => Err(crate::ErrorKind::UnloadedProfileError(
path.display().to_string(),
)
.as_error()),
})
}
/// Check if a profile is already managed by Theseus
#[tracing::instrument]
pub async fn is_managed(profile: &Path) -> crate::Result<bool> {
let state = State::get().await?;
let profiles = state.profiles.read().await;
Ok(profiles.0.contains_key(profile))
}
/// Check if a profile is loaded
#[tracing::instrument]
pub async fn is_loaded(profile: &Path) -> crate::Result<bool> {
let state = State::get().await?;
let profiles = state.profiles.read().await;
Ok(profiles.0.get(profile).and_then(Option::as_ref).is_some())
Ok(profiles.0.get(path).cloned())
}
/// Edit a profile using a given asynchronous closure
@@ -85,11 +43,7 @@ where
let mut profiles = state.profiles.write().await;
match profiles.0.get_mut(path) {
Some(&mut Some(ref mut profile)) => action(profile).await,
Some(&mut None) => Err(crate::ErrorKind::UnloadedProfileError(
path.display().to_string(),
)
.as_error()),
Some(ref mut profile) => action(profile).await,
None => Err(crate::ErrorKind::UnmanagedProfileError(
path.display().to_string(),
)
@@ -99,13 +53,45 @@ where
/// Get a copy of the profile set
#[tracing::instrument]
pub async fn list(
) -> crate::Result<std::collections::HashMap<PathBuf, Option<Profile>>> {
pub async fn list() -> crate::Result<std::collections::HashMap<PathBuf, Profile>>
{
let state = State::get().await?;
let profiles = state.profiles.read().await;
Ok(profiles.0.clone())
}
/// Query + sync profile's projects with the UI from the FS
#[tracing::instrument]
pub async fn sync(path: &Path) -> crate::Result<()> {
let state = State::get().await?;
if let Some(profile) = get(path).await? {
let paths = profile.get_profile_project_paths()?;
let projects = crate::state::infer_data_from_files(
paths,
state.directories.caches_dir(),
&state.io_semaphore,
)
.await?;
{
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(path) {
profile.projects = projects;
}
}
State::sync().await?;
Ok(())
} else {
Err(
crate::ErrorKind::UnmanagedProfileError(path.display().to_string())
.as_error(),
)
}
}
/// Run Minecraft using a profile
/// Returns Arc pointer to RwLock to Child
#[tracing::instrument(skip_all)]
@@ -113,7 +99,7 @@ pub async fn run(
path: &Path,
credentials: &crate::auth::Credentials,
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
let state = State::get().await.unwrap();
let state = State::get().await?;
let settings = state.settings.read().await;
let profile = get(path).await?.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
@@ -141,19 +127,21 @@ pub async fn run(
for hook in pre_launch_hooks.iter() {
// TODO: hook parameters
let mut cmd = hook.split(' ');
let result = Command::new(cmd.next().unwrap())
.args(&cmd.collect::<Vec<&str>>())
.current_dir(path)
.spawn()?
.wait()
.await?;
if let Some(command) = cmd.next() {
let result = Command::new(command)
.args(&cmd.collect::<Vec<&str>>())
.current_dir(path)
.spawn()?
.wait()
.await?;
if !result.success() {
return Err(crate::ErrorKind::LauncherError(format!(
"Non-zero exit code for pre-launch hook: {}",
result.code().unwrap_or(-1)
))
.as_error());
if !result.success() {
return Err(crate::ErrorKind::LauncherError(format!(
"Non-zero exit code for pre-launch hook: {}",
result.code().unwrap_or(-1)
))
.as_error());
}
}
}

View File

@@ -1,5 +1,5 @@
//! Theseus profile management interface
use crate::{prelude::ModLoader, profile};
use crate::prelude::ModLoader;
pub use crate::{
state::{JavaSettings, Profile},
State,
@@ -22,8 +22,9 @@ pub async fn profile_create_empty() -> crate::Result<PathBuf> {
String::from(DEFAULT_NAME), // the name/path of the profile
String::from("1.19.2"), // the game version of the profile
ModLoader::Vanilla, // the modloader to use
String::from("stable"), // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
None, // the icon for the profile
None, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
None, // the icon for the profile
None,
)
.await
}
@@ -32,17 +33,17 @@ pub async fn profile_create_empty() -> crate::Result<PathBuf> {
// Returns filepath at which it can be accessed in the State
#[tracing::instrument]
pub async fn profile_create(
name: String, // the name of the profile, and relative path
game_version: String, // the game version of the profile
modloader: ModLoader, // the modloader to use
loader_version: String, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
icon: Option<PathBuf>, // the icon for the profile
name: String, // the name of the profile, and relative path
game_version: String, // the game version of the profile
modloader: ModLoader, // the modloader to use
loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader. defaults to latest
icon: Option<PathBuf>, // the icon for the profile
linked_project_id: Option<String>, // the linked project ID (mainly for modpacks)- used for updating
) -> crate::Result<PathBuf> {
let state = State::get().await?;
let uuid = Uuid::new_v4();
let path = state.directories.profiles_dir().join(uuid.to_string());
if path.exists() {
if !path.is_dir() {
return Err(ProfileCreationError::NotFolder.into());
@@ -64,6 +65,7 @@ pub async fn profile_create(
} else {
fs::create_dir_all(&path).await?;
}
println!(
"Creating profile at path {}",
&canonicalize(&path)?.display()
@@ -71,12 +73,12 @@ pub async fn profile_create(
let loader = modloader;
let loader = if loader != ModLoader::Vanilla {
let version = loader_version;
let version = loader_version.unwrap_or_else(|| "latest".to_string());
let filter = |it: &LoaderVersion| match version.as_str() {
"latest" => true,
"stable" => it.stable,
id => it.id == *id,
id => it.id == *id || format!("{}-{}", game_version, id) == it.id,
};
let loader_data = match loader {
@@ -93,7 +95,12 @@ pub async fn profile_create(
let loaders = &loader_data
.game_versions
.iter()
.find(|it| it.id == game_version)
.find(|it| {
it.id.replace(
daedalus::modded::DUMMY_REPLACE_STRING,
&game_version,
) == game_version
})
.ok_or_else(|| {
ProfileCreationError::ModloaderUnsupported(
loader.to_string(),
@@ -130,13 +137,19 @@ pub async fn profile_create(
let path = canonicalize(&path)?;
let mut profile = Profile::new(name, game_version, path.clone()).await?;
if let Some(ref icon) = icon {
profile.with_icon(icon).await?;
profile.set_icon(icon).await?;
}
if let Some((loader_version, loader)) = loader {
profile.with_loader(loader, Some(loader_version));
profile.metadata.loader = loader;
profile.metadata.loader_version = Some(loader_version);
}
profile.metadata.linked_project_id = linked_project_id;
{
let mut profiles = state.profiles.write().await;
profiles.insert(profile)?;
}
profile::add(profile).await?;
State::sync().await?;
Ok(path)

View File

@@ -11,10 +11,19 @@ pub static BINCODE_CONFIG: Lazy<bincode::config::Configuration> =
});
pub static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
let mut headers = reqwest::header::HeaderMap::new();
let header = reqwest::header::HeaderValue::from_str(&format!(
"modrinth/daedalus/{} (support@modrinth.com)",
env!("CARGO_PKG_VERSION")
))
.unwrap();
headers.insert(reqwest::header::USER_AGENT, header);
reqwest::Client::builder()
.timeout(time::Duration::from_secs(15))
.tcp_keepalive(Some(time::Duration::from_secs(10)))
.default_headers(headers)
.build()
.unwrap()
.expect("Reqwest Client Building Failed")
});
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";

View File

@@ -67,10 +67,8 @@ pub enum ErrorKind {
#[error("Recv error: {0}")]
RecvError(#[from] tokio::sync::oneshot::error::RecvError),
#[error(
"Tried to access unloaded profile {0}, loading it probably failed"
)]
UnloadedProfileError(String),
#[error("Error acquiring semaphore: {0}")]
AcquireError(#[from] tokio::sync::AcquireError),
#[error("Profile {0} is not managed by Theseus!")]
UnmanagedProfileError(String),
@@ -78,6 +76,9 @@ pub enum ErrorKind {
#[error("Could not create profile: {0}")]
ProfileCreationError(#[from] profile_create::ProfileCreationError),
#[error("Zip error: {0}")]
ZipError(#[from] async_zip::error::ZipError),
#[error("Error: {0}")]
OtherError(String),
}

View File

@@ -36,7 +36,7 @@ pub fn get_class_paths(
return None;
}
Some(get_lib_path(libraries_path, &library.name))
Some(get_lib_path(libraries_path, &library.name, false))
})
.collect::<Result<Vec<_>, _>>()?;
@@ -62,17 +62,25 @@ pub fn get_class_paths_jar<T: AsRef<str>>(
) -> crate::Result<String> {
let cps = libraries
.iter()
.map(|library| get_lib_path(libraries_path, library.as_ref()))
.map(|library| get_lib_path(libraries_path, library.as_ref(), false))
.collect::<Result<Vec<_>, _>>()?;
Ok(cps.join(classpath_separator()))
}
pub fn get_lib_path(libraries_path: &Path, lib: &str) -> crate::Result<String> {
pub fn get_lib_path(
libraries_path: &Path,
lib: &str,
allow_not_exist: bool,
) -> crate::Result<String> {
let mut path = libraries_path.to_path_buf();
path.push(get_path_from_artifact(lib)?);
if !path.exists() && allow_not_exist {
return Ok(path.to_string_lossy().to_string());
}
let path = &canonicalize(&path).map_err(|_| {
crate::ErrorKind::LauncherError(format!(
"Library file at path {} does not exist",
@@ -343,13 +351,14 @@ pub fn get_processor_arguments<T: AsRef<str>>(
get_lib_path(
libraries_path,
&entry.client[1..entry.client.len() - 1],
true,
)?
} else {
entry.client.clone()
})
}
} else if argument.as_ref().starts_with('[') {
new_arguments.push(get_lib_path(libraries_path, trimmed_arg)?)
new_arguments.push(get_lib_path(libraries_path, trimmed_arg, true)?)
} else {
new_arguments.push(argument.as_ref().to_string())
}
@@ -361,7 +370,7 @@ pub fn get_processor_arguments<T: AsRef<str>>(
pub async fn get_processor_main_class(
path: String,
) -> crate::Result<Option<String>> {
tokio::task::spawn_blocking(move || {
let main_class = tokio::task::spawn_blocking(move || {
let zipfile = std::fs::File::open(&path)?;
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
crate::ErrorKind::LauncherError(format!(
@@ -394,6 +403,7 @@ pub async fn get_processor_main_class(
Ok::<Option<String>, crate::Error>(None)
})
.await
.unwrap()
.await??;
Ok(main_class)
}

View File

@@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
use url::Url;
lazy_static! {
static ref HYDRA_URL: Url =
Url::parse("https://hydra.modrinth.com").unwrap();
static ref HYDRA_URL: Url = Url::parse("https://hydra.modrinth.com")
.expect("Hydra URL parse failed");
}
// Socket messages

View File

@@ -39,11 +39,12 @@ pub async fn download_version_info(
version: &GameVersion,
loader: Option<&LoaderVersion>,
) -> crate::Result<GameVersionInfo> {
let version_id = loader.map_or(&version.id, |it| &it.id);
let version_id = loader
.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
log::debug!("Loading version info for Minecraft {version_id}");
let path = st
.directories
.version_dir(version_id)
.version_dir(&version_id)
.join(format!("{version_id}.json"));
let res = if path.exists() {
@@ -58,10 +59,10 @@ pub async fn download_version_info(
if let Some(loader) = loader {
let partial = d::modded::fetch_partial_version(&loader.url).await?;
info = d::modded::merge_partial_version(partial, info);
info.id = loader.id.clone();
}
info.id = version_id.clone();
let permit = st.io_semaphore.acquire().await.unwrap();
let permit = st.io_semaphore.acquire().await?;
write(&path, &serde_json::to_vec(&info)?, &permit).await?;
Ok(info)
}?;
@@ -92,7 +93,7 @@ pub async fn download_client(
.join(format!("{version}.jar"));
if !path.exists() {
let permit = st.io_semaphore.acquire().await.unwrap();
let permit = st.io_semaphore.acquire().await?;
let bytes =
fetch(&client_download.url, Some(&client_download.sha1), &permit)
.await?;
@@ -122,7 +123,7 @@ pub async fn download_assets_index(
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
} else {
let index = d::minecraft::fetch_assets_index(version).await?;
let permit = st.io_semaphore.acquire().await.unwrap();
let permit = st.io_semaphore.acquire().await?;
write(&path, &serde_json::to_vec(&index)?, &permit).await?;
log::info!("Fetched assets index");
Ok(index)
@@ -141,7 +142,7 @@ pub async fn download_assets(
log::debug!("Loading assets");
stream::iter(index.objects.iter())
.map(Ok::<(&String, &Asset), crate::Error>)
.try_for_each_concurrent(Some(st.settings.read().await.max_concurrent_downloads), |(name, asset)| async move {
.try_for_each_concurrent(None, |(name, asset)| async move {
let hash = &asset.hash;
let resource_path = st.directories.object_dir(hash);
let url = format!(
@@ -153,7 +154,7 @@ pub async fn download_assets(
tokio::try_join! {
async {
if !resource_path.exists() {
let permit = st.io_semaphore.acquire().await.unwrap();
let permit = st.io_semaphore.acquire().await?;
let resource = fetch_cell
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
.await?;
@@ -164,7 +165,7 @@ pub async fn download_assets(
},
async {
if with_legacy {
let permit = st.io_semaphore.acquire().await.unwrap();
let permit = st.io_semaphore.acquire().await?;
let resource = fetch_cell
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
.await?;
@@ -201,7 +202,7 @@ pub async fn download_libraries(
stream::iter(libraries.iter())
.map(Ok::<&Library, crate::Error>)
.try_for_each_concurrent(Some(st.settings.read().await.max_concurrent_downloads), |library| async move {
.try_for_each_concurrent(None, |library| async move {
if let Some(rules) = &library.rules {
if !rules.iter().all(super::parse_rule) {
return Ok(());
@@ -218,7 +219,7 @@ pub async fn download_libraries(
artifact: Some(ref artifact),
..
}) => {
let permit = st.io_semaphore.acquire().await.unwrap();
let permit = st.io_semaphore.acquire().await?;
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &permit)
.await?;
write(&path, &bytes, &permit).await?;
@@ -234,7 +235,7 @@ pub async fn download_libraries(
&artifact_path
].concat();
let permit = st.io_semaphore.acquire().await.unwrap();
let permit = st.io_semaphore.acquire().await?;
let bytes = fetch(&url, None, &permit).await?;
write(&path, &bytes, &permit).await?;
log::info!("Fetched library {}", &library.name);
@@ -262,12 +263,17 @@ pub async fn download_libraries(
);
if let Some(native) = classifiers.get(&parsed_key) {
let permit = st.io_semaphore.acquire().await.unwrap();
let permit = st.io_semaphore.acquire().await?;
let data = fetch(&native.url, Some(&native.sha1), &permit).await?;
let reader = std::io::Cursor::new(&data);
let mut archive = zip::ZipArchive::new(reader).unwrap();
archive.extract(&st.directories.version_natives_dir(version)).unwrap();
log::info!("Fetched native {}", &library.name);
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
match archive.extract(&st.directories.version_natives_dir(version)) {
Ok(_) => log::info!("Fetched native {}", &library.name),
Err(err) => log::error!("Failed extracting native {}. err: {}", &library.name, err)
}
} else {
log::error!("Failed extracting native {}", &library.name)
}
}
}

View File

@@ -72,9 +72,10 @@ pub async fn launch_minecraft(
"Invalid game version: {game_version}"
)))?;
let version_jar = loader_version
.as_ref()
.map_or(version.id.clone(), |it| it.id.clone());
let version_jar =
loader_version.as_ref().map_or(version.id.clone(), |it| {
format!("{}-{}", version.id.clone(), it.id.clone())
});
let mut version_info = download::download_version_info(
&state,
@@ -85,7 +86,7 @@ pub async fn launch_minecraft(
let client_path = state
.directories
.version_dir(&version.id)
.version_dir(&version_jar)
.join(format!("{version_jar}.jar"));
download::download_minecraft(&state, &version_info).await?;
@@ -133,6 +134,7 @@ pub async fn launch_minecraft(
args::get_processor_main_class(args::get_lib_path(
&state.directories.libraries_dir(),
&processor.jar,
false,
)?)
.await?
.ok_or_else(|| {
@@ -193,7 +195,7 @@ pub async fn launch_minecraft(
args::get_jvm_arguments(
args.get(&d::minecraft::ArgumentType::Jvm)
.map(|x| x.as_slice()),
&state.directories.version_natives_dir(&version.id),
&state.directories.version_natives_dir(&version_jar),
&state.directories.libraries_dir(),
&args::get_class_paths(
&state.directories.libraries_dir(),
@@ -205,7 +207,6 @@ pub async fn launch_minecraft(
Vec::from(java_args),
)?
.into_iter()
.map(|r| r.replace(' ', r"\ "))
.collect::<Vec<_>>(),
)
.arg(version_info.main_class.clone())
@@ -223,7 +224,6 @@ pub async fn launch_minecraft(
*resolution,
)?
.into_iter()
.map(|r| r.replace(' ', r"\ "))
.collect::<Vec<_>>(),
)
.current_dir(instance_path.clone())

View File

@@ -10,7 +10,7 @@ use daedalus::{
use futures::prelude::*;
use std::collections::LinkedList;
const METADATA_URL: &str = "https://meta.modrinth.com/gamedata";
const METADATA_URL: &str = "https://meta.modrinth.com";
const METADATA_DB_FIELD: &[u8] = b"metadata";
const RETRY_ATTEMPTS: i32 = 3;

View File

@@ -77,17 +77,17 @@ impl State {
let settings =
Settings::init(&directories.settings_file()).await?;
// Launcher data
let (metadata, profiles) = tokio::try_join! {
Metadata::init(&database),
Profiles::init(&database, &directories),
}?;
let users = Users::init(&database)?;
// Loose initializations
let io_semaphore =
Semaphore::new(settings.max_concurrent_downloads);
// Launcher data
let (metadata, profiles) = tokio::try_join! {
Metadata::init(&database),
Profiles::init(&database, &directories, &io_semaphore),
}?;
let users = Users::init(&database)?;
let children = Children::new();
let auth_flow = AuthTask::new();
@@ -133,8 +133,7 @@ impl State {
reader.sync(&state.directories.settings_file()).await?;
Ok::<_, crate::Error>(())
})
.await
.unwrap()
.await?
};
let sync_profiles = async {
@@ -148,15 +147,16 @@ impl State {
profiles.sync(&mut batch).await?;
Ok::<_, crate::Error>(())
})
.await
.unwrap()
.await?
};
tokio::try_join!(sync_settings, sync_profiles)?;
state
.database
.apply_batch(Arc::try_unwrap(batch).unwrap().into_inner())?;
state.database.apply_batch(
Arc::try_unwrap(batch)
.expect("Error saving state by acquiring Arc")
.into_inner(),
)?;
state.database.flush_async().await?;
Ok(())

View File

@@ -11,11 +11,12 @@ use std::{
path::{Path, PathBuf},
};
use tokio::fs;
use tokio::sync::Semaphore;
const PROFILE_JSON_PATH: &str = "profile.json";
const PROFILE_SUBTREE: &[u8] = b"profiles";
pub(crate) struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
pub(crate) struct Profiles(pub HashMap<PathBuf, Profile>);
// TODO: possibly add defaults to some of these values
pub const CURRENT_FORMAT_VERSION: u32 = 1;
@@ -30,7 +31,6 @@ pub struct Profile {
#[serde(skip)]
pub path: PathBuf,
pub metadata: ProfileMetadata,
pub projects: HashMap<PathBuf, Project>,
#[serde(skip_serializing_if = "Option::is_none")]
pub java: Option<JavaSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -39,6 +39,7 @@ pub struct Profile {
pub resolution: Option<WindowSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<Hooks>,
pub projects: HashMap<PathBuf, Project>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -52,6 +53,7 @@ pub struct ProfileMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub loader_version: Option<LoaderVersion>,
pub format_version: u32,
pub linked_project_id: Option<String>,
}
// TODO: Quilt?
@@ -64,6 +66,7 @@ pub enum ModLoader {
Vanilla,
Forge,
Fabric,
Quilt,
}
impl std::fmt::Display for ModLoader {
@@ -72,6 +75,7 @@ impl std::fmt::Display for ModLoader {
Self::Vanilla => "Vanilla",
Self::Forge => "Forge",
Self::Fabric => "Fabric",
Self::Quilt => "Quilt",
})
}
}
@@ -107,6 +111,7 @@ impl Profile {
loader: ModLoader::Vanilla,
loader_version: None,
format_version: CURRENT_FORMAT_VERSION,
linked_project_id: None,
},
projects: HashMap::new(),
java: None,
@@ -116,16 +121,8 @@ impl Profile {
})
}
// TODO: deduplicate these builder methods
// They are flat like this in order to allow builder-style usage
#[tracing::instrument]
pub fn with_name(&mut self, name: String) -> &mut Self {
self.metadata.name = name;
self
}
#[tracing::instrument]
pub async fn with_icon<'a>(
pub async fn set_icon<'a>(
&'a mut self,
icon: &'a Path,
) -> crate::Result<&'a mut Self> {
@@ -149,54 +146,24 @@ impl Profile {
}
}
#[tracing::instrument]
pub fn with_game_version(&mut self, version: String) -> &mut Self {
self.metadata.game_version = version;
self
}
pub fn get_profile_project_paths(&self) -> crate::Result<Vec<PathBuf>> {
let mut files = Vec::new();
let mut read_paths = |path: &str| {
let new_path = self.path.join(path);
if new_path.exists() {
for path in std::fs::read_dir(self.path.join(path))? {
files.push(path?.path());
}
}
Ok::<(), crate::Error>(())
};
#[tracing::instrument]
pub fn with_loader(
&mut self,
loader: ModLoader,
version: Option<LoaderVersion>,
) -> &mut Self {
self.metadata.loader = loader;
self.metadata.loader_version = version;
self
}
read_paths("mods")?;
read_paths("shaders")?;
read_paths("resourcepacks")?;
read_paths("datapacks")?;
#[tracing::instrument]
pub fn with_java_settings(
&mut self,
settings: Option<JavaSettings>,
) -> &mut Self {
self.java = settings;
self
}
#[tracing::instrument]
pub fn with_memory(
&mut self,
settings: Option<MemorySettings>,
) -> &mut Self {
self.memory = settings;
self
}
#[tracing::instrument]
pub fn with_resolution(
&mut self,
resolution: Option<WindowSize>,
) -> &mut Self {
self.resolution = resolution;
self
}
#[tracing::instrument]
pub fn with_hooks(&mut self, hooks: Option<Hooks>) -> &mut Self {
self.hooks = hooks;
self
Ok(files)
}
}
@@ -205,6 +172,7 @@ impl Profiles {
pub async fn init(
db: &sled::Db,
dirs: &DirectoryInfo,
io_sempahore: &Semaphore,
) -> crate::Result<Self> {
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
Ok(Default::default()),
@@ -229,38 +197,34 @@ impl Profiles {
};
(path, prof)
})
.collect::<HashMap<PathBuf, Option<Profile>>>()
.filter_map(|(key, opt_value)| async move {
opt_value.map(|value| (key, value))
})
.collect::<HashMap<PathBuf, Profile>>()
.await;
// project path, parent profile path
let mut files: HashMap<PathBuf, PathBuf> = HashMap::new();
{
for (profile_path, _profile_opt) in profiles.iter() {
let mut read_paths = |path: &str| {
let new_path = profile_path.join(path);
if new_path.exists() {
for path in std::fs::read_dir(profile_path.join(path))?
{
files.insert(path?.path(), profile_path.clone());
}
}
Ok::<(), crate::Error>(())
};
read_paths("mods")?;
read_paths("shaders")?;
read_paths("resourcepacks")?;
read_paths("datapacks")?;
for (profile_path, profile) in profiles.iter() {
let paths = profile.get_profile_project_paths()?;
for path in paths {
files.insert(path, profile_path.clone());
}
}
}
let inferred = super::projects::infer_data_from_files(
files.keys().cloned().collect(),
dirs.caches_dir(),
io_sempahore,
)
.await?;
for (key, value) in inferred {
if let Some(profile_path) = files.get(&key) {
if let Some(Some(profile)) = profiles.get_mut(profile_path) {
if let Some(profile) = profiles.get_mut(profile_path) {
profile.projects.insert(key, value);
}
}
@@ -278,7 +242,7 @@ impl Profiles {
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
)?
.into(),
Some(profile),
profile,
);
Ok(self)
}
@@ -292,9 +256,15 @@ impl Profiles {
}
#[tracing::instrument(skip(self))]
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
let path = PathBuf::from(&canonicalize(path)?.to_str().unwrap());
pub async fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
let path =
PathBuf::from(&canonicalize(path)?.to_string_lossy().to_string());
self.0.remove(&path);
if path.exists() {
fs::remove_dir_all(path).await?;
}
Ok(self)
}
@@ -308,8 +278,8 @@ impl Profiles {
.try_for_each_concurrent(None, |(path, profile)| async move {
let json = serde_json::to_vec_pretty(&profile)?;
let json_path =
Path::new(path.to_str().unwrap()).join(PROFILE_JSON_PATH);
let json_path = Path::new(&path.to_string_lossy().to_string())
.join(PROFILE_JSON_PATH);
fs::write(json_path, json).await?;
Ok::<_, crate::Error>(())

View File

@@ -1,17 +1,17 @@
//! Project management + inference
use crate::config::{MODRINTH_API_URL, REQWEST_CLIENT};
use crate::util::fetch::write_cached_icon;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::Digest;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use tokio::io::AsyncReadExt;
use zip::ZipArchive;
use tokio::sync::Semaphore;
// use zip::ZipArchive;
use async_zip::tokio::read::fs::ZipFileReader;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Project {
@@ -33,8 +33,8 @@ pub struct ModrinthProject {
pub published: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub client_side: String,
pub server_side: String,
pub client_side: SideType,
pub server_side: SideType,
pub downloads: u32,
pub followers: u32,
@@ -46,7 +46,75 @@ pub struct ModrinthProject {
pub versions: Vec<String>,
pub icon_url: String,
pub icon_url: Option<String>,
}
/// A specific version of a project
#[derive(Serialize, Deserialize)]
pub struct ModrinthVersion {
pub id: String,
pub project_id: String,
pub author_id: String,
pub featured: bool,
pub name: String,
pub version_number: String,
pub changelog: String,
pub changelog_url: Option<String>,
pub date_published: DateTime<Utc>,
pub downloads: u32,
pub version_type: String,
pub files: Vec<ModrinthVersionFile>,
pub dependencies: Vec<Dependency>,
pub game_versions: Vec<String>,
pub loaders: Vec<String>,
}
#[derive(Serialize, Deserialize)]
pub struct ModrinthVersionFile {
pub hashes: HashMap<String, String>,
pub url: String,
pub filename: String,
pub primary: bool,
pub size: u32,
pub file_type: Option<FileType>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct Dependency {
pub version_id: Option<String>,
pub project_id: Option<String>,
pub file_name: Option<String>,
pub dependency_type: DependencyType,
}
#[derive(Serialize, Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum DependencyType {
Required,
Optional,
Incompatible,
Embedded,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum SideType {
Required,
Optional,
Unsupported,
Unknown,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum FileType {
RequiredResourcePack,
OptionalResourcePack,
Unknown,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -63,9 +131,57 @@ pub enum ProjectMetadata {
Unknown,
}
async fn read_icon_from_file(
icon_path: Option<String>,
cache_dir: &Path,
path: &PathBuf,
io_semaphore: &Semaphore,
) -> crate::Result<Option<PathBuf>> {
if let Some(icon_path) = icon_path {
// we have to repoen the zip twice here :(
let zip_file_reader = ZipFileReader::new(path).await;
if let Ok(zip_file_reader) = zip_file_reader {
// Get index of icon file and open it
let zip_index_option = zip_file_reader
.file()
.entries()
.iter()
.position(|f| f.entry().filename() == icon_path);
if let Some(index) = zip_index_option {
let entry = zip_file_reader
.file()
.entries()
.get(index)
.unwrap()
.entry();
let mut bytes = Vec::new();
if zip_file_reader
.entry(zip_index_option.unwrap())
.await?
.read_to_end_checked(&mut bytes, entry)
.await
.is_ok()
{
let bytes = bytes::Bytes::from(bytes);
let permit = io_semaphore.acquire().await?;
let path = write_cached_icon(
&icon_path, cache_dir, bytes, &permit,
)
.await?;
return Ok(Some(path));
}
};
}
}
Ok(None)
}
pub async fn infer_data_from_files(
paths: Vec<PathBuf>,
cache_dir: PathBuf,
io_semaphore: &Semaphore,
) -> crate::Result<HashMap<PathBuf, Project>> {
let mut file_path_hashes = HashMap::new();
@@ -139,51 +255,28 @@ pub async fn infer_data_from_files(
}
for (hash, path) in further_analyze_projects {
let file = File::open(path.clone())?;
// TODO: get rid of below unwrap
let mut zip = ZipArchive::new(file).unwrap();
let read_icon_from_file =
|icon_path: Option<String>| -> crate::Result<Option<PathBuf>> {
if let Some(icon_path) = icon_path {
// we have to repoen the zip twice here :(
let zip_file = File::open(path.clone())?;
if let Ok(mut zip) = ZipArchive::new(zip_file) {
if let Ok(mut file) = zip.by_name(&icon_path) {
let mut bytes = Vec::new();
if file.read_to_end(&mut bytes).is_ok() {
let extension = Path::new(&icon_path)
.extension()
.and_then(OsStr::to_str);
let hash = sha1::Sha1::from(&bytes).hexdigest();
let path = cache_dir.join("icons").join(
if let Some(ext) = extension {
format!("{hash}.{ext}")
} else {
hash
},
);
if !path.exists() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = File::create(path.clone())?;
file.write_all(&bytes)?;
}
return Ok(Some(path));
}
};
}
}
Ok(None)
};
if let Ok(mut file) = zip.by_name("META-INF/mods.toml") {
let zip_file_reader = if let Ok(zip_file_reader) =
ZipFileReader::new(path.clone()).await
{
zip_file_reader
} else {
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: path.ends_with(".disabled"),
metadata: ProjectMetadata::Unknown,
},
);
continue;
};
let zip_index_option = zip_file_reader
.file()
.entries()
.iter()
.position(|f| f.entry().filename() == "META-INF/mods.toml");
if let Some(index) = zip_index_option {
let file = zip_file_reader.file().entries().get(index).unwrap();
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ForgeModInfo {
@@ -201,18 +294,30 @@ pub async fn infer_data_from_files(
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if zip_file_reader
.entry(index)
.await?
.read_to_string_checked(&mut file_str, file.entry())
.await
.is_ok()
{
if let Ok(pack) =
serde_json::from_str::<ForgeModInfo>(&file_str)
{
if let Some(pack) = pack.mods.first() {
let icon = read_icon_from_file(pack.logo_file.clone())?;
let icon = read_icon_from_file(
pack.logo_file.clone(),
&cache_dir,
&path,
io_semaphore,
)
.await?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
disabled: path.ends_with(".disabled"),
metadata: ProjectMetadata::Inferred {
title: Some(
pack.display_name
@@ -236,7 +341,13 @@ pub async fn infer_data_from_files(
}
}
if let Ok(mut file) = zip.by_name("mcmod.info") {
let zip_index_option = zip_file_reader
.file()
.entries()
.iter()
.position(|f| f.entry().filename() == "mcmod.info");
if let Some(index) = zip_index_option {
let file = zip_file_reader.file().entries().get(index).unwrap();
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ForgeMod {
@@ -249,15 +360,27 @@ pub async fn infer_data_from_files(
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if zip_file_reader
.entry(index)
.await?
.read_to_string_checked(&mut file_str, file.entry())
.await
.is_ok()
{
if let Ok(pack) = serde_json::from_str::<ForgeMod>(&file_str) {
let icon = read_icon_from_file(pack.logo_file)?;
let icon = read_icon_from_file(
pack.logo_file,
&cache_dir,
&path,
io_semaphore,
)
.await?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
disabled: path.ends_with(".disabled"),
metadata: ProjectMetadata::Inferred {
title: Some(if pack.name.is_empty() {
pack.modid
@@ -276,7 +399,13 @@ pub async fn infer_data_from_files(
}
}
if let Ok(mut file) = zip.by_name("fabric.mod.json") {
let zip_index_option = zip_file_reader
.file()
.entries()
.iter()
.position(|f| f.entry().filename() == "fabric.mod.json");
if let Some(index) = zip_index_option {
let file = zip_file_reader.file().entries().get(index).unwrap();
#[derive(Deserialize)]
#[serde(untagged)]
enum FabricAuthor {
@@ -295,15 +424,27 @@ pub async fn infer_data_from_files(
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if zip_file_reader
.entry(index)
.await?
.read_to_string_checked(&mut file_str, file.entry())
.await
.is_ok()
{
if let Ok(pack) = serde_json::from_str::<FabricMod>(&file_str) {
let icon = read_icon_from_file(pack.icon)?;
let icon = read_icon_from_file(
pack.icon,
&cache_dir,
&path,
io_semaphore,
)
.await?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
disabled: path.ends_with(".disabled"),
metadata: ProjectMetadata::Inferred {
title: Some(pack.name.unwrap_or(pack.id)),
description: pack.description,
@@ -325,7 +466,13 @@ pub async fn infer_data_from_files(
}
}
if let Ok(mut file) = zip.by_name("quilt.mod.json") {
let zip_index_option = zip_file_reader
.file()
.entries()
.iter()
.position(|f| f.entry().filename() == "quilt.mod.json");
if let Some(index) = zip_index_option {
let file = zip_file_reader.file().entries().get(index).unwrap();
#[derive(Deserialize)]
struct QuiltMetadata {
pub name: Option<String>,
@@ -341,17 +488,27 @@ pub async fn infer_data_from_files(
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if zip_file_reader
.entry(index)
.await?
.read_to_string_checked(&mut file_str, file.entry())
.await
.is_ok()
{
if let Ok(pack) = serde_json::from_str::<QuiltMod>(&file_str) {
let icon = read_icon_from_file(
pack.metadata.as_ref().and_then(|x| x.icon.clone()),
)?;
&cache_dir,
&path,
io_semaphore,
)
.await?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
disabled: path.ends_with(".disabled"),
metadata: ProjectMetadata::Inferred {
title: Some(
pack.metadata
@@ -383,23 +540,39 @@ pub async fn infer_data_from_files(
}
}
if let Ok(mut file) = zip.by_name("pack.mcmeta") {
let zip_index_option = zip_file_reader
.file()
.entries()
.iter()
.position(|f| f.entry().filename() == "pack.mcdata");
if let Some(index) = zip_index_option {
let file = zip_file_reader.file().entries().get(index).unwrap();
#[derive(Deserialize)]
struct Pack {
description: Option<String>,
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if zip_file_reader
.entry(index)
.await?
.read_to_string_checked(&mut file_str, file.entry())
.await
.is_ok()
{
if let Ok(pack) = serde_json::from_str::<Pack>(&file_str) {
let icon =
read_icon_from_file(Some("pack.png".to_string()))?;
let icon = read_icon_from_file(
Some("pack.png".to_string()),
&cache_dir,
&path,
io_semaphore,
)
.await?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
disabled: path.ends_with(".disabled"),
metadata: ProjectMetadata::Inferred {
title: None,
description: pack.description,
@@ -415,10 +588,10 @@ pub async fn infer_data_from_files(
}
return_projects.insert(
path,
path.clone(),
Project {
sha512: hash,
disabled: false,
disabled: path.ends_with(".disabled"),
metadata: ProjectMetadata::Unknown,
},
);

View File

@@ -1,76 +1,85 @@
//! Functions for fetching infromation from the Internet
use crate::config::REQWEST_CLIENT;
use futures::prelude::*;
use std::{collections::LinkedList, convert::TryInto, path::Path, sync::Arc};
use bytes::Bytes;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use tokio::{
fs::{self, File},
io::AsyncWriteExt,
sync::{Semaphore, SemaphorePermit},
sync::SemaphorePermit,
};
const FETCH_ATTEMPTS: usize = 3;
/// Downloads a file with retry and checksum functionality
#[tracing::instrument(skip(_permit))]
pub async fn fetch<'a>(
url: &str,
sha1: Option<&str>,
_permit: &SemaphorePermit<'a>,
) -> crate::Result<bytes::Bytes> {
let mut attempts = LinkedList::new();
for _ in 0..FETCH_ATTEMPTS {
attempts.push_back(
async {
let content = REQWEST_CLIENT.get(url).send().await?;
let bytes = content.bytes().await?;
for attempt in 1..=(FETCH_ATTEMPTS + 1) {
let result = REQWEST_CLIENT.get(url).send().await;
if let Some(hash) = sha1 {
let actual_hash = sha1_async(bytes.clone()).await;
if actual_hash != hash {
return Err(crate::ErrorKind::HashError(
actual_hash,
String::from(hash),
)
.into());
match result {
Ok(x) => {
let bytes = x.bytes().await;
if let Ok(bytes) = bytes {
if let Some(sha1) = sha1 {
let hash = sha1_async(bytes.clone()).await?;
if &*hash != sha1 {
if attempt <= 3 {
continue;
} else {
return Err(crate::ErrorKind::HashError(
sha1.to_string(),
hash,
)
.into());
}
}
}
}
Ok(bytes)
log::debug!("Done downloading URL {url}");
return Ok(bytes);
} else if attempt <= 3 {
continue;
} else if let Err(err) = bytes {
return Err(err.into());
}
}
.boxed(),
)
Err(_) if attempt <= 3 => continue,
Err(err) => return Err(err.into()),
}
}
log::debug!("Done downloading URL {url}");
future::select_ok(attempts).map_ok(|it| it.0).await
unreachable!()
}
// This is implemented, as it will be useful in porting modpacks
// For now, allow it to be dead code
#[allow(dead_code)]
#[tracing::instrument(skip(sem))]
pub async fn fetch_mirrors(
urls: &[&str],
/// Downloads a file from specified mirrors
#[tracing::instrument(skip(permit))]
pub async fn fetch_mirrors<'a>(
mirrors: &[&str],
sha1: Option<&str>,
permits: u32,
sem: &Semaphore,
permit: &SemaphorePermit<'a>,
) -> crate::Result<bytes::Bytes> {
let _permits = sem.acquire_many(permits).await.unwrap();
let sem = Arc::new(Semaphore::new(permits.try_into().unwrap()));
if mirrors.is_empty() {
return Err(crate::ErrorKind::InputError(
"No mirrors provided!".to_string(),
)
.into());
}
future::select_ok(urls.iter().map(|url| {
let sha1 = sha1.map(String::from);
let url = String::from(*url);
let sem = Arc::clone(&sem);
for (index, mirror) in mirrors.iter().enumerate() {
let result = fetch(mirror, sha1, permit).await;
tokio::spawn(async move {
let permit = sem.acquire().await.unwrap();
fetch(&url, sha1.as_deref(), &permit).await
})
.map(Result::unwrap)
.boxed()
}))
.await
.map(|it| it.0)
if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) {
return result;
}
}
unreachable!()
}
#[tracing::instrument(skip(bytes, _permit))]
@@ -89,8 +98,31 @@ pub async fn write<'a>(
Ok(())
}
async fn sha1_async(bytes: bytes::Bytes) -> String {
tokio::task::spawn_blocking(move || sha1::Sha1::from(bytes).hexdigest())
.await
.unwrap()
#[tracing::instrument(skip(bytes, permit))]
pub async fn write_cached_icon<'a>(
icon_path: &str,
cache_dir: &Path,
bytes: Bytes,
permit: &SemaphorePermit<'a>,
) -> crate::Result<PathBuf> {
let extension = Path::new(&icon_path).extension().and_then(OsStr::to_str);
let hash = sha1_async(bytes.clone()).await?;
let path = cache_dir.join("icons").join(if let Some(ext) = extension {
format!("{hash}.{ext}")
} else {
hash
});
write(&path, &bytes, permit).await?;
Ok(path)
}
async fn sha1_async(bytes: Bytes) -> crate::Result<String> {
let hash = tokio::task::spawn_blocking(move || {
sha1::Sha1::from(bytes).hexdigest()
})
.await?;
Ok(hash)
}

View File

@@ -209,7 +209,8 @@ pub fn check_java_at_filepath(path: PathBuf) -> Option<JavaVersion> {
// Extracting version numbers
lazy_static! {
static ref JAVA_VERSION_CAPTURE: Regex =
Regex::new(r#"version "([\d\._]+)""#).unwrap();
Regex::new(r#"version "([\d\._]+)""#)
.expect("Error creating java version capture regex");
}
// Extract version info from it