✨ feature(core): start package api

This commit is contained in:
Pauline 2024-07-15 03:35:57 +02:00
parent 72c2f42c16
commit 3522142a2c
No known key found for this signature in database
GPG key ID: 722BB181973296AE
15 changed files with 1693 additions and 102 deletions

View file

@ -5,6 +5,7 @@ contact_links:
url: https://polyfrost.org/discord
- name: Polyfrost Documentation
about: Useful documentation about Polyfrost, OneConfig, and other Polyfrost projects.
url: https://docs.modrinth.com
url: https://docs.polyfrost.org
- name: Polyfrost Docs^2
about: Useful documentation about how you can contribute to Polyfrost projects.
url: https://contributing.polyfrost.org

View file

@ -1,6 +1,8 @@
//! **OneLauncher Cluster**
//!
//! API for our managed Minecraft instances, Clusters.
//! API for creating our managed Minecraft instances, Clusters.
// TODO: (pauline) fully implement the cluster::self APIs.
use crate::proxy::send::send_cluster;
@ -201,7 +203,7 @@ pub async fn get_by_uuid(uuid: uuid::Uuid, clear: Option<bool>) -> crate::Result
#[tracing::instrument]
pub async fn get_full_path(path: &ClusterPath) -> crate::Result<PathBuf> {
let _ = get(path, Some(true)).await?.ok_or_else(|| {
anyhow::anyhow!("failed to get the full path of profile at path {}", path)
anyhow::anyhow!("failed to get the full path of cluster at path {}", path)
})?;
let full_path = io::canonicalize(path.full_path().await?)?;
@ -255,7 +257,7 @@ where
Ok(())
}
None => Err(anyhow::anyhow!("unmanaged profile edited at {}", path.to_string()).into()),
None => Err(anyhow::anyhow!("unmanaged cluster edited at {}", path.to_string()).into()),
}
}

View file

@ -1 +1,3 @@
//! Utilities for updating Clusters
// TODO: requires `store/package.rs` to be implemented.

View file

@ -1,20 +1,404 @@
//! Install modpacks from different sources.
use crate::data::{Loader, PackageData};
use crate::data::{Loader, ManagedPackage, ManagedVersion, PackageData};
use crate::prelude::ClusterPath;
use crate::proxy::send::{init_ingress, send_ingress};
use crate::proxy::IngressId;
use crate::store::{ClusterStage, PackageSide};
use crate::utils::http::{fetch, fetch_advanced, fetch_json, write_icon};
use crate::utils::io;
use crate::{IngressType, InnerPathLinux, State};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";
pub const CURSEFORGE_API_URL: &str = "https://api.cursefor";
#[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub 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")]
pub struct PackFile {
pub path: InnerPathLinux,
pub hashes: HashMap<PackFileHash, String>,
pub env: Option<HashMap<EnvType, PackageSide>>,
pub downloads: Vec<String>,
pub file_size: u32,
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
#[serde(rename_all = "camelCase", from = "String")]
pub 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")]
pub enum EnvType {
Client,
Server,
}
#[derive(Serialize, Deserialize, Clone, Copy, Hash, PartialEq, Eq, Debug)]
pub enum PackDependency {
#[serde(rename = "forge")]
Forge,
#[serde(rename = "neoforge")]
#[serde(alias = "neo-forge")]
NeoForge,
#[serde(rename = "fabric-loader")]
FabricLoader,
#[serde(rename = "quilt-loader")]
QuiltLoader,
#[serde(rename = "minecraft")]
Minecraft,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum CreatePackLocation {
/// Create a pack from a Modrinth modpack.
FromModrinth {
package_id: String,
version_id: String,
title: String,
icon_url: Option<String>,
},
/// Create a pack from a file (importing, importing an mrpack or zip-style modpack)
FromFile { path: PathBuf },
}
/// Modpack associated [`Cluster`] wrapper.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatePackCluster {
/// The name of the cluster, and relative path.
pub name: String,
/// The game version (MC) of the cluster.
pub mc_version: String,
/// The mod [`Loader`] to use (if applicable).
pub mod_loader: Loader,
/// The mod [`Loader`] version to use, set to `latest` or `stable` or the ID of the loader.
/// The default value is `latest` for the latest version of the mod loader.
pub loader_version: Option<String>,
/// The icon of the cluster.
pub icon: Option<PathBuf>,
/// The URL of an icon for a cluster. (ONLY USE THIS FOR A TEMPORARY CLUSTER)
pub icon_url: Option<String>,
/// The linked package data (mainly used for modpacks and updating files).
pub package_data: Option<PackageData>,
/// Whether or not the cluster should skip being installed.
pub skip: Option<bool>,
/// Whether or not the cluster should skip file watching.
pub skip_watch: Option<bool>,
}
impl Default for CreatePackCluster {
fn default() -> Self {
CreatePackCluster {
name: "Untitled".to_string(),
mc_version: "1.20.4".to_string(),
mod_loader: Loader::Vanilla,
loader_version: None,
icon: None,
icon_url: None,
package_data: None,
skip: Some(true),
skip_watch: Some(false),
}
}
}
#[derive(Clone)]
pub struct CreatePack {
pub file: bytes::Bytes,
pub description: CreatePackDescription,
}
#[derive(Clone, Debug)]
pub struct CreatePackDescription {
pub icon: Option<PathBuf>,
pub override_title: Option<String>,
pub package_id: Option<String>,
pub version_id: Option<String>,
pub existing_ingress: Option<IngressId>,
pub cluster_path: ClusterPath,
}
pub fn get_cluster_from_pack(location: CreatePackLocation) -> CreatePackCluster {
match location {
CreatePackLocation::FromModrinth {
package_id,
version_id,
title,
icon_url,
} => CreatePackCluster {
name: title,
icon_url,
package_data: Some(PackageData {
package_id: Some(package_id),
version_id: Some(version_id),
locked: Some(true),
}),
..Default::default()
},
CreatePackLocation::FromFile { path } => {
let file_name = path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string();
CreatePackCluster {
name: file_name,
..Default::default()
}
}
}
}
#[tracing::instrument]
#[onelauncher_debug::debugger]
pub async fn generate_pack_from_version_id(
package_id: String,
version_id: String,
title: String,
icon_url: Option<String>,
cluster_path: ClusterPath,
existing_ingress: Option<IngressId>,
) -> crate::Result<CreatePack> {
let state = State::get().await?;
let ingress = if let Some(ing) = existing_ingress {
send_ingress(&ing, 0.0, Some("downloading pack file")).await?;
ing
} else {
init_ingress(
IngressType::DownloadPack {
cluster_path: cluster_path.full_path().await?,
package_name: title,
icon: icon_url,
package_version: version_id.clone(),
},
100.0,
"downloading pack file",
)
.await?
};
send_ingress(&ingress, 0.0, Some("fetching version")).await?;
let version: ManagedVersion = fetch_json(
Method::GET,
&format!("{}version/{}", MODRINTH_API_URL, version_id),
None,
None,
&state.fetch_semaphore,
)
.await?;
send_ingress(&ingress, 10.0, None).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::AnyhowError(anyhow::anyhow!("specified version has no files"))
})?;
let file = fetch_advanced(
Method::GET,
&url,
hash.map(|x| &**x),
None,
None,
Some((&ingress, 70.0)),
&state.fetch_semaphore,
)
.await?;
send_ingress(&ingress, 0.0, Some("fetching project metadata")).await?;
let pkg: ManagedPackage = fetch_json(
Method::GET,
&format!("{}project/{}", MODRINTH_API_URL, version.package_id),
None,
None,
&state.fetch_semaphore,
)
.await?;
send_ingress(&ingress, 10.0, Some("Retrieving icon")).await?;
let icon = if let Some(icon_url) = pkg.icon_url {
let state = State::get().await?;
let icon_bytes = fetch(&icon_url, None, &state.fetch_semaphore).await?;
let filename = icon_url.rsplit('/').next();
if let Some(filename) = filename {
Some(
write_icon(
filename,
&state.directories.caches_dir().await,
icon_bytes,
&state.io_semaphore,
)
.await?,
)
} else {
None
}
} else {
None
};
send_ingress(&ingress, 10.0, None).await?;
Ok(CreatePack {
file,
description: CreatePackDescription {
icon,
override_title: None,
package_id: Some(package_id),
version_id: Some(version_id),
existing_ingress: Some(ingress),
cluster_path,
},
})
}
#[tracing::instrument]
#[onelauncher_debug::debugger]
pub async fn generate_pack_from_file(
path: PathBuf,
cluster_path: ClusterPath,
) -> crate::Result<CreatePack> {
let file = io::read(&path).await?;
Ok(CreatePack {
file: bytes::Bytes::from(file),
description: CreatePackDescription {
icon: None,
override_title: None,
package_id: None,
version_id: None,
existing_ingress: None,
cluster_path,
},
})
}
/// Sets generated cluster attributes to the pack attributes (using `cluster::edit`).
/// This includes the pack name, icon, game version, loader version, and loader.
#[onelauncher_debug::debugger]
pub async fn set_cluster_information(
cluster_path: ClusterPath,
description: &CreatePackDescription,
backup_name: &str,
deps: &HashMap<PackDependency, String>,
ignore_lock: bool,
) -> crate::Result<()> {
let mut game_version: Option<&String> = None;
let mut mod_loader = None;
let mut loader_version = None;
for (key, value) in deps {
match key {
PackDependency::Forge => {
mod_loader = Some(Loader::Forge);
loader_version = Some(value);
}
PackDependency::NeoForge => {
mod_loader = Some(Loader::NeoForge);
loader_version = Some(value);
}
PackDependency::FabricLoader => {
mod_loader = Some(Loader::Fabric);
loader_version = Some(value);
}
PackDependency::QuiltLoader => {
mod_loader = Some(Loader::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(anyhow::anyhow!("pack did not specify Minecraft version").into());
};
let mod_loader = mod_loader.unwrap_or(Loader::Vanilla);
let loader_version = if mod_loader != Loader::Vanilla {
crate::cluster::create::get_loader_version(
game_version.clone(),
mod_loader,
loader_version.cloned(),
)
.await?
} else {
None
};
crate::api::cluster::edit(&cluster_path, |cl| {
cl.meta.name = description
.override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
cl.stage = ClusterStage::PackDownloading;
let package_id = description.package_id.clone();
let version_id = description.version_id.clone();
cl.meta.package_data = if package_id.is_some() && version_id.is_some() {
Some(PackageData {
package_id,
version_id,
locked: if !ignore_lock {
Some(true)
} else {
cl.meta.package_data.as_ref().and_then(|x| x.locked)
},
})
} else {
None
};
cl.meta.icon.clone_from(&description.icon);
cl.meta.mc_version.clone_from(game_version);
cl.meta.loader_version.clone_from(&loader_version);
cl.meta.loader = mod_loader;
async { Ok(()) }
})
.await?;
Ok(())
}

View file

@ -1 +1,237 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use crate::package::from::CreatePackDescription;
use crate::package::import::{self, copy_minecraft};
use crate::package::{self};
use crate::prelude::{Cluster, ClusterPath, Loader};
use crate::store::{ClusterStage, PackageData, State};
use crate::utils::io;
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ATInstance {
pub id: String, // minecraft version id ie: 1.12.1, not a name
pub launcher: ATLauncher,
pub java_version: ATJavaVersion,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ATLauncher {
pub name: String,
pub pack: String,
pub version: String,
pub loader_version: ATLauncherLoaderVersion,
pub managed_project: Option<ATLauncherManagedProject>,
pub managed_version: Option<ATLauncherManagedVersion>,
pub managed_manifest: Option<package::from::PackFormat>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ATJavaVersion {
pub major_version: u8,
pub component: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ATLauncherLoaderVersion {
pub r#type: String,
pub version: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ATLauncherManagedProject {
pub id: String,
pub slug: String,
pub project_type: String,
pub team: String,
pub client_side: Option<String>,
pub server_side: Option<String>,
pub categories: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ATLauncherManagedVersion {
pub id: String,
pub project_id: String,
pub name: String,
pub version_number: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ATLauncherManagedVersionFile {
pub hashes: HashMap<String, String>,
pub url: String,
pub filename: String,
pub primary: bool,
pub size: u64,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ATLauncherManagedVersionDependency {
pub project_id: Option<String>,
pub version_id: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ATLauncherMod {
pub name: String,
pub version: String,
pub file: String,
pub managed_project: Option<ATLauncherManagedProject>,
pub managed_version: Option<ATLauncherManagedVersion>,
}
pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
let instance: String = io::read_to_string(&instance_folder.join("instance.json"))
.await
.unwrap_or("".to_string());
let instance: Result<ATInstance, serde_json::Error> =
serde_json::from_str::<ATInstance>(&instance);
if let Err(e) = instance {
tracing::warn!(
"failed to parse instance.json at {}: {}",
instance_folder.display(),
e
);
false
} else {
true
}
}
#[tracing::instrument]
#[onelauncher_debug::debugger]
pub async fn import_atlauncher(
// The path to the base ATLauncher folder.
atlauncher_base_path: PathBuf,
// The instance folder in atlauncher_base_path
instance_folder: String,
// The path to the importing Cluster
cluster_path: ClusterPath,
) -> crate::Result<()> {
let atlauncher_instance_path = atlauncher_base_path
.join("instances")
.join(instance_folder.clone());
let atinstance: String =
io::read_to_string(&atlauncher_instance_path.join("instance.json")).await?;
let atinstance: ATInstance = serde_json::from_str::<ATInstance>(&atinstance)?;
// Icon path should be {instance_folder}/instance.png if it exists,
// Another possibility is ATLauncher/configs/images/{safe_pack_name}.png (safe pack name is alphanumeric lowercase)
let icon_path_primary = atlauncher_instance_path.join("instance.png");
let safe_pack_name = atinstance
.launcher
.pack
.replace(|c: char| !c.is_alphanumeric(), "")
.to_lowercase();
let icon_path_secondary = atlauncher_base_path
.join("configs")
.join("images")
.join(safe_pack_name + ".png");
let icon = match (icon_path_primary.exists(), icon_path_secondary.exists()) {
(true, _) => import::cache_icon(icon_path_primary).await?,
(_, true) => import::cache_icon(icon_path_secondary).await?,
_ => None,
};
// Create description from instance.cfg
let description = CreatePackDescription {
icon,
override_title: Some(atinstance.launcher.name.clone()),
package_id: None,
version_id: None,
existing_ingress: None,
cluster_path: cluster_path.clone(),
};
let backup_name = format!("ATLauncher-{}", instance_folder);
let minecraft_folder = atlauncher_instance_path;
import_atlauncher_unmanaged(
cluster_path,
minecraft_folder,
backup_name,
description,
atinstance,
)
.await?;
Ok(())
}
async fn import_atlauncher_unmanaged(
cluster_path: ClusterPath,
minecraft_folder: PathBuf,
backup_name: String,
description: CreatePackDescription,
atinstance: ATInstance,
) -> crate::Result<()> {
let mod_loader = format!(
"\"{}\"",
atinstance.launcher.loader_version.r#type.to_lowercase()
);
let mod_loader: Loader = serde_json::from_str::<Loader>(&mod_loader)?;
let game_version = atinstance.id;
let loader_version = if mod_loader != Loader::Vanilla {
crate::cluster::create::get_loader_version(
game_version.clone(),
mod_loader,
Some(atinstance.launcher.loader_version.version.clone()),
)
.await?
} else {
None
};
crate::api::cluster::edit(&cluster_path, |cl| {
cl.meta.name = description
.override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
cl.stage = ClusterStage::PackDownloading;
cl.meta.package_data = Some(PackageData {
package_id: description.package_id.clone(),
version_id: description.version_id.clone(),
locked: Some(description.package_id.is_some() && description.version_id.is_some()),
});
cl.meta.icon.clone_from(&description.icon);
cl.meta.mc_version.clone_from(&game_version);
cl.meta.loader_version.clone_from(&loader_version);
cl.meta.loader = mod_loader;
async { Ok(()) }
})
.await?;
// moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
let state = State::get().await?;
let ingress = copy_minecraft(
cluster_path.clone(),
minecraft_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path, None).await? {
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
{
let state = State::get().await?;
let mut watcher = state.watcher.write().await;
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
}
State::sync().await?;
}
Ok(())
}

View file

@ -1 +1,165 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use super::{cache_icon, copy_minecraft};
use crate::prelude::{Cluster, ClusterPath, Loader};
use crate::store::{ClusterStage, State};
use crate::utils::http::{fetch, write_icon};
use crate::utils::io;
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MinecraftInstance {
pub name: Option<String>,
pub base_mod_loader: Option<MinecraftInstanceModLoader>,
pub profile_image_path: Option<PathBuf>,
pub installed_modpack: Option<InstalledModpack>,
/// Minecraft game version. Non-prioritized, use this if Vanilla
pub game_version: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct MinecraftInstanceModLoader {
pub name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InstalledModpack {
pub thumbnail_url: Option<String>,
}
pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
let minecraftinstance: String =
io::read_to_string(&instance_folder.join("minecraftinstance.json"))
.await
.unwrap_or("".to_string());
let minecraftinstance: Result<MinecraftInstance, serde_json::Error> =
serde_json::from_str::<MinecraftInstance>(&minecraftinstance);
minecraftinstance.is_ok()
}
pub async fn import_curseforge(
// The path to the CurseForge instance
curseforge_instance_folder: PathBuf,
// The path to the cluster
cluster_path: ClusterPath,
) -> crate::Result<()> {
let minecraft_instance: String =
io::read_to_string(&curseforge_instance_folder.join("minecraftinstance.json")).await?;
let minecraft_instance: MinecraftInstance =
serde_json::from_str::<MinecraftInstance>(&minecraft_instance)?;
let override_title: Option<String> = minecraft_instance.name.clone();
let backup_name = format!(
"Curseforge-{}",
curseforge_instance_folder
.file_name()
.map(|a| a.to_string_lossy().to_string())
.unwrap_or("Unknown".to_string())
);
let state = State::get().await?;
let mut icon = None;
if let Some(icon_path) = minecraft_instance.profile_image_path.clone() {
icon = cache_icon(icon_path).await?;
} else if let Some(InstalledModpack {
thumbnail_url: Some(thumbnail_url),
}) = minecraft_instance.installed_modpack.clone()
{
let icon_bytes = fetch(&thumbnail_url, None, &state.fetch_semaphore).await?;
let filename = thumbnail_url.rsplit('/').last();
if let Some(filename) = filename {
icon = Some(
write_icon(
filename,
&state.directories.caches_dir().await,
icon_bytes,
&state.io_semaphore,
)
.await?,
);
}
}
if let Some(instance_mod_loader) = minecraft_instance.base_mod_loader {
let game_version = minecraft_instance.game_version;
let mut mod_loader = None;
let mut loader_version = None;
match instance_mod_loader.name.split('-').collect::<Vec<&str>>()[..] {
["forge", version] => {
mod_loader = Some(Loader::Forge);
loader_version = Some(version.to_string());
}
["fabric", version, _game_version] => {
mod_loader = Some(Loader::Fabric);
loader_version = Some(version.to_string());
}
_ => {}
}
let mod_loader = mod_loader.unwrap_or(Loader::Vanilla);
let loader_version = if mod_loader != Loader::Vanilla {
crate::cluster::create::get_loader_version(
game_version.clone(),
mod_loader,
loader_version,
)
.await?
} else {
None
};
crate::api::cluster::edit(&cluster_path, |cl| {
cl.meta.name = override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
cl.stage = ClusterStage::PackDownloading;
cl.meta.icon.clone_from(&icon);
cl.meta.mc_version.clone_from(&game_version);
cl.meta.loader_version.clone_from(&loader_version);
cl.meta.loader = mod_loader;
async { Ok(()) }
})
.await?;
} else {
crate::api::cluster::edit(&cluster_path, |cl| {
cl.meta.name = override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
cl.meta.icon.clone_from(&icon);
cl.meta
.mc_version
.clone_from(&minecraft_instance.game_version);
cl.meta.loader_version = None;
cl.meta.loader = Loader::Vanilla;
async { Ok(()) }
})
.await?;
}
let state = State::get().await?;
let ingress = copy_minecraft(
cluster_path.clone(),
curseforge_instance_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path, None).await? {
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
{
let state = State::get().await?;
let mut watcher = state.watcher.write().await;
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
}
State::sync().await?;
}
Ok(())
}

View file

@ -1 +1,107 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use super::{cache_icon, copy_minecraft};
use crate::prelude::{Cluster, ClusterPath, Loader};
use crate::store::{ClusterStage, State};
use crate::utils::io;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GDLauncherConfig {
pub background: Option<String>,
pub loader: GDLauncherLoader,
// pub mods: Vec<GDLauncherMod>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GDLauncherLoader {
pub loader_type: Loader,
pub loader_version: Option<String>,
pub mc_version: String,
pub source: Option<String>,
pub source_name: Option<String>,
}
pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool {
let config: String = io::read_to_string(&instance_folder.join("config.json"))
.await
.unwrap_or("".to_string());
let config: Result<GDLauncherConfig, serde_json::Error> =
serde_json::from_str::<GDLauncherConfig>(&config);
config.is_ok()
}
pub async fn import_gdlauncher(
gdlauncher_instance_folder: PathBuf,
cluster_path: ClusterPath,
) -> crate::Result<()> {
let config: String =
io::read_to_string(&gdlauncher_instance_folder.join("config.json")).await?;
let config: GDLauncherConfig = serde_json::from_str::<GDLauncherConfig>(&config)?;
let override_title: Option<String> = config.loader.source_name.clone();
let backup_name = format!(
"GDLauncher-{}",
gdlauncher_instance_folder
.file_name()
.map(|a| a.to_string_lossy().to_string())
.unwrap_or("Unknown".to_string())
);
let icon = config
.background
.clone()
.map(|b| gdlauncher_instance_folder.join(b));
let icon = if let Some(icon) = icon {
cache_icon(icon).await?
} else {
None
};
let game_version = config.loader.mc_version;
let mod_loader = config.loader.loader_type;
let loader_version = config.loader.loader_version;
let loader_version = if mod_loader != Loader::Vanilla {
crate::cluster::create::get_loader_version(game_version.clone(), mod_loader, loader_version)
.await?
} else {
None
};
crate::api::cluster::edit(&cluster_path, |cl| {
cl.meta.name = override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
cl.stage = ClusterStage::PackDownloading;
cl.meta.icon.clone_from(&icon);
cl.meta.mc_version.clone_from(&game_version);
cl.meta.loader_version.clone_from(&loader_version);
cl.meta.loader = mod_loader;
async { Ok(()) }
})
.await?;
let state = State::get().await?;
let ingress = copy_minecraft(
cluster_path.clone(),
gdlauncher_instance_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path, None).await? {
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
{
let state = State::get().await?;
let mut watcher = state.watcher.write().await;
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
}
State::sync().await?;
}
Ok(())
}

View file

@ -4,6 +4,7 @@ use crate::prelude::ClusterPath;
use crate::proxy::send::{init_or_edit_ingress, send_ingress};
use crate::proxy::IngressId;
use crate::store::Clusters;
use crate::utils::http::{self, IoSemaphore};
use crate::utils::io::{self, IOError};
use serde::{Deserialize, Serialize};
@ -52,10 +53,10 @@ impl fmt::Display for ImportType {
ImportType::ATLauncher => write!(f, "ATLauncher"),
ImportType::Curseforge => write!(f, "Curseforge"),
ImportType::GDLauncher => write!(f, "GDLauncher"),
ImportType::Modrinth => write!(f, "Modrinth"),
ImportType::Modrinth => write!(f, "Modrinth App"),
ImportType::PrismLauncher => write!(f, "PrismLauncher"),
ImportType::TLauncher => write!(f, "TLauncher"),
ImportType::FTBLauncher => write!(f, "FTB"),
ImportType::FTBLauncher => write!(f, "Feed The Beast"),
ImportType::Technic => write!(f, "Technic"),
ImportType::Unknown => write!(f, "Unknown"),
}
@ -63,22 +64,21 @@ impl fmt::Display for ImportType {
}
pub async fn import_instances(import: ImportType, path: PathBuf) -> crate::Result<Vec<String>> {
// TODO(pauline)(instance_import): Fix MultiMC based launcher import
let instances_path = match import {
ImportType::GDLauncher | ImportType::ATLauncher => "instances".to_string(),
ImportType::Curseforge => "Instances".to_string(),
// ImportType::MultiMC => multibased::get_instances_path(path.clone().join("multimc.cfg"))
// .await
// .unwrap_or_else(|| "instances".to_string()),
// ImportType::PrismLauncher => {
// multibased::get_instances_path(path.clone().join("prismlauncher.cfg"))
// .await
// .unwrap_or_else(|| "instances".to_string())
// }
ImportType::MultiMC => multibased::get_instances_subpath(path.clone().join("multimc.cfg"))
.await
.unwrap_or_else(|| "instances".to_string()),
ImportType::PrismLauncher => {
multibased::get_instances_subpath(path.clone().join("prismlauncher.cfg"))
.await
.unwrap_or_else(|| "instances".to_string())
}
ImportType::Modrinth => "profiles".to_string(),
ImportType::TLauncher => "instances".to_string(),
ImportType::Technic => "instances".to_string(),
ImportType::FTBLauncher => "instances".to_string(),
ImportType::Technic => "instances".to_string(),
_ => return Err(anyhow::anyhow!("launcher type unknown, cant import").into()),
};
@ -115,80 +115,54 @@ pub async fn import_instance(
) -> crate::Result<()> {
tracing::debug!("importing instance from {instance_path}");
// TODO(pauline)(instance_import): Finish this
// let result = match import {
// ImportType::MultiMC | ImportType::PrismLauncher => {
// multibased::import(path, instance_path, cluster_path.clone()).await
// }
// ImportType::ATLauncher => {
// atlauncher::import(path, instance_path, cluster_path.clone()).await
// }
// ImportType::GDLauncher => {
// gdlauncher::import(
// path.join("instances").join(instance_path),
// cluster_path.clone(),
// )
// .await
// }
// ImportType::Curseforge => {
// curseforge::import(
// path.join("Instances").join(instance_path),
// cluster_path.clone(),
// )
// .await
// }
// ImportType::Modrinth => {
// modrinth::import(
// path.join("profiles").join(instance_path),
// cluster_path.clone(),
// )
// .await
// }
// ImportType::TLauncher => {
// tlauncher::import(
// path.join("instances").join(instance_path),
// cluster_path.clone(),
// )
// .await
// }
// ImportType::Technic => {
// technic::import(
// path.join("instances").join(instance_path),
// cluster_path.clone(),
// )
// .await
// }
// ImportType::FTBLauncher => {
// ftb::import(
// path.join("instances").join(instance_path),
// cluster_path.clone(),
// )
// .await
// }
// ImportType::Unknown => {
// return Err(anyhow::anyhow!("unknown launcher type").into());
// }
// };
let result = match import {
ImportType::MultiMC | ImportType::PrismLauncher => {
multibased::import_mmc(path, instance_path, cluster_path.clone()).await
}
ImportType::ATLauncher => {
atlauncher::import_atlauncher(path, instance_path, cluster_path.clone()).await
}
ImportType::GDLauncher => {
gdlauncher::import_gdlauncher(
path.join("instances").join(instance_path),
cluster_path.clone(),
)
.await
}
ImportType::Curseforge => {
curseforge::import_curseforge(
path.join("Instances").join(instance_path),
cluster_path.clone(),
)
.await
}
// ImportType::Modrinth => modrinth::import_modrinth(path.join("profiles").join(instance_path), cluster_path.clone()).await,
// ImportType::TLauncher => tlauncher::import_tlauncher(path.join("instances").join(instance_path), cluster_path.clone()).await,
// ImportType::Technic => technic::import_technic(path.join("instances").join(instance_path), cluster_path.clone()).await,
// ImportType::FTBLauncher => ftb::import_ftb(path.join("instances").join(instance_path), cluster_path.clone()).await,
ImportType::Unknown => Err(anyhow::anyhow!("unknown launcher type").into()),
_ => todo!(),
};
// match result {
// Ok(_) => {}
// Err(e) => {
// tracing::warn!("failed to import modpack: {:?}", e);
// let _ = crate::api::cluster::remove(&cluster_path).await;
// return Err(e);
// }
// }
match result {
Ok(_) => {}
Err(e) => {
tracing::warn!("failed to import modpack: {:?}", e);
let _ = crate::api::cluster::remove(&cluster_path).await;
return Err(e);
}
}
// tokio::task::spawn(Clusters::update_versions());
tokio::task::spawn(Clusters::update_versions());
// tracing::debug!("completed import of instance.");
tracing::debug!("completed import of instance.");
Ok(())
}
/// returns the default path for a given [`ImportType`].
pub fn default_launcher_path(r#type: ImportType) -> Option<PathBuf> {
let path = match r#type {
ImportType::MultiMC => None,
ImportType::MultiMC => None, // MultiMC data is in it's application directory
ImportType::PrismLauncher => Some(dirs::data_dir()?.join("PrismLauncher")),
ImportType::ATLauncher => Some(dirs::data_dir()?.join("ATLauncher")),
ImportType::GDLauncher => Some(dirs::data_dir()?.join("gdlauncher_next")),
@ -208,26 +182,26 @@ pub fn default_launcher_path(r#type: ImportType) -> Option<PathBuf> {
}
}
/// checks if a [`PathBuf`] is a valid instance for a given [`ImportType`]
/// Checks if a [`PathBuf`] is a valid instance for a given [`ImportType`]
#[tracing::instrument]
#[onelauncher_debug::debugger]
pub async fn is_valid_instance(instance_path: PathBuf, r#type: ImportType) -> bool {
false
// TODO(pauline)(instance_import): Finish this
// match r#type {
// ImportType::MultiMC | ImportType::PrismLauncher => multibased::is_valid(insance_path).await,
// ImportType::ATLauncher => atlauncher::is_valid(instance_path).await,
// ImportType::GDLauncher => gdlauncher::is_valid(instance_path).await,
// ImportType::Curseforge => curseforge::is_valid(instance_path).await,
// ImportType::Modrinth => modrinth::is_valid(instance_path).await,
// ImportType::TLauncher => tlauncher::is_valid(instance_path).await,
// ImportType::FTBLauncher => ftb::is_valid(instance_path).await,
// ImportType::Technic => technic::is_valid(instance_path).await,
// ImportType::Unknown => false,
// }
match r#type {
ImportType::MultiMC | ImportType::PrismLauncher => {
multibased::is_valid_multibased(instance_path).await
}
ImportType::ATLauncher => atlauncher::is_valid_atlauncher(instance_path).await,
ImportType::GDLauncher => gdlauncher::is_valid_gdlauncher(instance_path).await,
ImportType::Curseforge => curseforge::is_valid_curseforge(instance_path).await,
// ImportType::Modrinth => modrinth::is_valid_modrinth(instance_path).await,
// ImportType::TLauncher => tlauncher::is_valid_tlauncher(instance_path).await,
// ImportType::FTBLauncher => ftb::is_valid_ftb(instance_path).await,
// ImportType::Technic => technic::is_valid_technic(instance_path).await,
ImportType::Unknown => false,
_ => todo!(),
}
}
/// caches an image file
#[tracing::instrument]
#[onelauncher_debug::debugger]
pub async fn cache_icon(icon_path: PathBuf) -> crate::Result<Option<PathBuf>> {
@ -251,7 +225,7 @@ pub async fn copy_minecraft(
io_semaphore: &IoSemaphore,
old_ingress: Option<IngressId>,
) -> crate::Result<IngressId> {
let _cluster_full = cluster_path.full_path().await?;
let cluster_path_full = cluster_path.full_path().await?;
let subfiles = sub(&minecraft_path).await?;
let total_subfiles = subfiles.len() as u64;
let ingress = init_or_edit_ingress(
@ -269,7 +243,7 @@ pub async fn copy_minecraft(
let child = sub
.strip_prefix(&minecraft_path)
.map_err(|_| anyhow::anyhow!("invalid .minecraft file {}", &sub.display()))?;
let child = cluster_path.0.join(child);
let child = cluster_path_full.join(child);
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
http::copy(&sub, &child, io_semaphore).await?;
send_ingress(&ingress, 1.0, None).await?;

View file

@ -1 +1,24 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::prelude::ClusterPath;
use crate::utils::io;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ModrinthProfile {}
pub async fn is_valid_modrinth(instance_folder: PathBuf) -> bool {
let config: String = io::read_to_string(&instance_folder.join("profile.json"))
.await
.unwrap_or("".to_string());
let config: Result<ModrinthProfile, serde_json::Error> =
serde_json::from_str::<ModrinthProfile>(&config);
config.is_ok()
}
pub async fn import_modrinth(
modrinth_instance_folder: PathBuf,
cluster_path: ClusterPath,
) -> crate::Result<()> {
Ok(())
}

View file

@ -1 +1,341 @@
use serde::{de, Deserialize, Serialize};
use std::path::{Path, PathBuf};
use crate::package::from::{self, CreatePackDescription, PackDependency};
use crate::package::import::{self, copy_minecraft};
use crate::prelude::{Cluster, ClusterPath, State};
use crate::utils::io;
// instance.cfg: https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/minecraft/MinecraftInstance.cpp
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[serde(untagged)]
enum MMCInstanceEnum {
General(MMCInstanceGeneral),
Instance(MMCInstance),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct MMCInstanceGeneral {
pub general: MMCInstance,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct MMCInstance {
pub java_path: Option<String>,
pub jvm_args: Option<String>,
#[serde(default)]
#[serde(deserialize_with = "deserialize_optional_bool")]
pub managed_pack: Option<bool>,
#[serde(rename = "ManagedPackID")]
pub managed_pack_id: Option<String>,
pub managed_pack_type: Option<MMCManagedPackType>,
#[serde(rename = "ManagedPackVersionID")]
pub managed_pack_version_id: Option<String>,
pub managed_pack_version_name: Option<String>,
#[serde(rename = "iconKey")]
pub icon_key: Option<String>,
#[serde(rename = "name")]
pub name: Option<String>,
}
/// Deserializes INI String Boolean values into Rust Boolean values for [`serde-ini`].
fn deserialize_optional_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
where
D: de::Deserializer<'de>,
{
let s = Option::<String>::deserialize(deserializer)?;
match s {
Some(string) => match string.as_str() {
"true" => Ok(Some(true)),
"false" => Ok(Some(false)),
_ => Err(de::Error::custom("expected 'true' or 'false'")),
},
None => Ok(None),
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum MMCManagedPackType {
Modrinth,
Flame,
ATLauncher,
#[serde(other)]
Unknown,
}
// mmc-pack.json
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MMCPack {
components: Vec<MMCComponent>,
format_version: u32,
}
// https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/minecraft/Component.h
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MMCComponent {
pub uid: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub dependency_only: bool,
#[serde(default)]
pub important: bool,
#[serde(default)]
pub disabled: bool,
pub cached_name: Option<String>,
pub cached_version: Option<String>,
#[serde(default)]
pub cached_requires: Vec<MMCComponentRequirement>,
#[serde(default)]
pub cached_conflicts: Vec<MMCComponentRequirement>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MMCComponentRequirement {
pub uid: String,
pub equals_version: Option<String>,
pub suggests: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[serde(untagged)]
enum MMCLauncherEnum {
General(MMCLauncherGeneral),
Instance(MMCLauncher),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct MMCLauncherGeneral {
pub general: MMCLauncher,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct MMCLauncher {
instance_dir: String,
}
#[tracing::instrument]
pub async fn is_valid_multibased(instance_folder: PathBuf) -> bool {
let instance_cfg = instance_folder.join("instance.cfg");
let mmc_pack = instance_folder.join("mmc-pack.json");
let mmc_pack = match io::read_to_string(&mmc_pack).await {
Ok(mmc_pack) => mmc_pack,
Err(_) => return false,
};
load_instance_cfg(&instance_cfg).await.is_ok()
&& serde_json::from_str::<MMCPack>(&mmc_pack).is_ok()
}
#[tracing::instrument]
pub async fn get_instances_subpath(config: PathBuf) -> Option<String> {
let launcher = io::read_to_string(&config).await.ok()?;
let launcher: MMCLauncherEnum = serde_ini::from_str(&launcher).ok()?;
match launcher {
MMCLauncherEnum::General(p) => Some(p.general.instance_dir),
MMCLauncherEnum::Instance(p) => Some(p.instance_dir),
}
}
async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> {
let instance_cfg: String = io::read_to_string(file_path).await?;
let instance_cfg_enum: MMCInstanceEnum = serde_ini::from_str::<MMCInstanceEnum>(&instance_cfg)?;
match instance_cfg_enum {
MMCInstanceEnum::General(instance_cfg) => Ok(instance_cfg.general),
MMCInstanceEnum::Instance(instance_cfg) => Ok(instance_cfg),
}
}
#[tracing::instrument]
#[onelauncher_debug::debugger]
pub async fn import_mmc(
mmc_base_path: PathBuf,
instance_folder: String,
cluster_path: ClusterPath,
) -> crate::Result<()> {
let mmc_instance_path = mmc_base_path
.join("instances")
.join(instance_folder.clone());
let mmc_pack = io::read_to_string(&mmc_instance_path.join("mmc-pack.json")).await?;
let mmc_pack: MMCPack = serde_json::from_str::<MMCPack>(&mmc_pack)?;
let instance_cfg = load_instance_cfg(&mmc_instance_path.join("instance.cfg")).await?;
let icon = if let Some(icon_key) = instance_cfg.icon_key {
let icon_path = mmc_base_path.join("icons").join(icon_key);
import::cache_icon(icon_path).await?
} else {
None
};
let description = CreatePackDescription {
icon,
override_title: instance_cfg.name,
package_id: instance_cfg.managed_pack_id,
version_id: instance_cfg.managed_pack_version_id,
existing_ingress: None,
cluster_path: cluster_path.clone(),
};
let backup_name = "Imported Modpack".to_string();
if instance_cfg.managed_pack.unwrap_or(false) {
match instance_cfg.managed_pack_type {
Some(MMCManagedPackType::Modrinth) => {
let backup_name = "Imported Modrinth Modpack".to_string();
let minecraft_folder = mmc_base_path
.join("instances")
.join(instance_folder)
.join(".minecraft");
import_mmc_unmanaged(
cluster_path,
minecraft_folder,
backup_name,
description,
mmc_pack,
)
.await?;
}
Some(MMCManagedPackType::Flame) | Some(MMCManagedPackType::ATLauncher) => {
// Same as unmanaged, but with 'minecraft' folder instead of '.minecraft'
let minecraft_folder = mmc_base_path
.join("instances")
.join(instance_folder)
.join("minecraft");
import_mmc_unmanaged(
cluster_path,
minecraft_folder,
backup_name,
description,
mmc_pack,
)
.await?;
}
Some(_) => {
let backup_name = "ImportedModpack".to_string();
let minecraft_folder = mmc_base_path
.join("instances")
.join(instance_folder)
.join(".minecraft");
import_mmc_unmanaged(
cluster_path,
minecraft_folder,
backup_name,
description,
mmc_pack,
)
.await?;
}
_ => {
return Err(anyhow::anyhow!(
"Instance is managed, but managed pack type not specified in instance.cfg"
)
.into())
}
}
} else {
let backup_name = "Imported Modpack".to_string();
let minecraft_folder = mmc_base_path
.join("instances")
.join(instance_folder)
.join(".minecraft");
import_mmc_unmanaged(
cluster_path,
minecraft_folder,
backup_name,
description,
mmc_pack,
)
.await?;
}
Ok(())
}
async fn import_mmc_unmanaged(
cluster_path: ClusterPath,
minecraft_folder: PathBuf,
backup_name: String,
description: CreatePackDescription,
mmc_pack: MMCPack,
) -> crate::Result<()> {
let dependencies = mmc_pack
.components
.iter()
.filter_map(|component| {
if component.uid.starts_with("net.fabricmc.fabric-loader") {
return Some((
PackDependency::FabricLoader,
component.version.clone().unwrap_or_default(),
));
}
if component.uid.starts_with("net.minecraftforge") {
return Some((
PackDependency::Forge,
component.version.clone().unwrap_or_default(),
));
}
if component.uid.starts_with("org.quiltmc.quilt-loader") {
return Some((
PackDependency::QuiltLoader,
component.version.clone().unwrap_or_default(),
));
}
if component.uid.starts_with("net.minecraft") {
return Some((
PackDependency::Minecraft,
component.version.clone().unwrap_or_default(),
));
}
None
})
.collect();
from::set_cluster_information(
cluster_path.clone(),
&description,
&backup_name,
&dependencies,
false,
)
.await?;
let state = State::get().await?;
let ingress = copy_minecraft(
cluster_path.clone(),
minecraft_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(cluster_val) = crate::api::cluster::get(&cluster_path, None).await? {
crate::game::install_minecraft(&cluster_val, Some(ingress), false).await?;
{
let state = State::get().await?;
let mut watcher = state.watcher.write().await;
Cluster::watch(&cluster_val.get_full_path().await?, &mut watcher).await?;
}
State::sync().await?;
}
Ok(())
}

View file

@ -1 +1,348 @@
//! Import and Export `.mrpack` mod packs.
use crate::package::from::{set_cluster_information, EnvType, PackFile, PackFileHash};
use crate::prelude::{ClusterPath, ManagedVersion};
use crate::proxy::ingress_try_for_each;
use crate::proxy::send::{init_or_edit_ingress, send_ingress};
use crate::store::{ClusterStage, Clusters, PackageMetadata, PackageSide};
use crate::utils::http::{fetch_from_mirrors, fetch_json, write};
use crate::utils::io;
use crate::{cluster, IngressType, State};
use async_zip::base::read::seek::ZipFileReader;
use reqwest::Method;
use serde_json::json;
use std::collections::HashMap;
use std::io::Cursor;
use std::path::{Component, PathBuf};
use super::from::{
generate_pack_from_file, generate_pack_from_version_id, CreatePack, CreatePackLocation,
PackFormat, MODRINTH_API_URL,
};
/// Install a pack
/// Wrapper around [`install_pack_files`] that generates a pack creation description, and
/// attempts to install the pack files. If it fails, it will remove the cluster (fail safely)
/// Install a modpack from a mrpack file (a modrinth .zip format)
#[onelauncher_debug::debugger]
pub async fn install_zipped_mrpack(
location: CreatePackLocation,
cluster_path: ClusterPath,
) -> crate::Result<ClusterPath> {
let create_pack: CreatePack = match location {
CreatePackLocation::FromModrinth {
package_id,
version_id,
title,
icon_url,
} => {
generate_pack_from_version_id(
package_id,
version_id,
title,
icon_url,
cluster_path.clone(),
None,
)
.await?
}
CreatePackLocation::FromFile { path } => {
generate_pack_from_file(path, cluster_path.clone()).await?
}
};
let result = install_zipped_mrpack_files(create_pack, false).await;
tokio::task::spawn(Clusters::update_versions());
match result {
Ok(cluster) => Ok(cluster),
Err(err) => {
let _ = crate::api::cluster::remove(&cluster_path).await;
Err(err)
}
}
}
/// Install all pack files from a description. Does not remove the cluster if it fails
#[onelauncher_debug::debugger]
pub async fn install_zipped_mrpack_files(
create_pack: CreatePack,
ignore_lock: bool,
) -> crate::Result<ClusterPath> {
let state = &State::get().await?;
let file = create_pack.file;
let description = create_pack.description.clone(); // make a copy for cluster::edit
let icon = create_pack.description.icon;
let package_id = create_pack.description.package_id;
let version_id = create_pack.description.version_id;
let existing_ingress = create_pack.description.existing_ingress;
let cluster_path = create_pack.description.cluster_path;
let icon_exists = icon.is_some();
let reader: Cursor<&bytes::Bytes> = Cursor::new(&file);
let mut zip_reader = ZipFileReader::with_tokio(reader).await?;
let zip_index_option = zip_reader
.file()
.entries()
.iter()
.position(|f| f.filename().as_str().unwrap_or_default() == "modrinth.index.json");
if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
let mut reader = zip_reader.reader_with_entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?;
if &*pack.game != "minecraft" {
return Err(anyhow::anyhow!("pack doesn't support minecraft").into());
}
set_cluster_information(
cluster_path.clone(),
&description,
&pack.name,
&pack.dependencies,
ignore_lock,
)
.await?;
let cluster_path = cluster_path.clone();
let ingress = init_or_edit_ingress(
existing_ingress,
IngressType::DownloadPackage {
cluster_path: cluster_path.full_path().await?.clone(),
package_name: pack.name.clone(),
icon,
package_id,
package_version: version_id,
},
100.0,
"downloading modpack",
)
.await?;
let num_files = pack.files.len();
use futures::StreamExt;
ingress_try_for_each(
futures::stream::iter(pack.files.into_iter()).map(Ok::<PackFile, crate::Error>),
None,
Some(&ingress),
70.0,
num_files,
None,
|pack| {
let cluster_path = cluster_path.clone();
async move {
// TODO: prompt user for optional files in a modpack
if let Some(env) = pack.env {
if env
.get(&EnvType::Client)
.map(|x| x == &PackageSide::Unsupported)
.unwrap_or(false)
{
return Ok(());
}
}
let file = fetch_from_mirrors(
&pack.downloads.iter().map(|x| &**x).collect::<Vec<&str>>(),
pack.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
&state.fetch_semaphore,
)
.await?;
let package_path = pack.path.to_string();
let path = std::path::Path::new(&package_path).components().next();
if let Some(path) = path {
match path {
Component::CurDir | Component::Normal(_) => {
let path = cluster_path.full_path().await?.join(&package_path);
write(&path, &file, &state.io_semaphore).await?;
}
_ => {}
};
}
Ok(())
}
},
)
.await?;
send_ingress(&ingress, 0.0, Some("extracting overrides")).await?;
let mut total_len = 0;
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader.file().entries().get(index).unwrap();
let filename = file.filename().as_str().unwrap_or_default();
if (filename.starts_with("overrides") || filename.starts_with("client-overrides"))
&& !filename.ends_with('/')
{
total_len += 1;
}
}
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader.file().entries().get(index).unwrap();
let filename = file.filename().as_str().unwrap_or_default();
let file_path = PathBuf::from(filename);
if (filename.starts_with("overrides") || filename.starts_with("client-overrides"))
&& !filename.ends_with('/')
{
let mut content = Vec::new();
let mut reader = zip_reader.reader_with_entry(index).await?;
reader.read_to_end_checked(&mut content).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() {
write(
&cluster_path.full_path().await?.join(new_path),
&content,
&state.io_semaphore,
)
.await?;
}
send_ingress(
&ingress,
30.0 / total_len as f64,
Some(&format!("extracting override {}/{}", index, total_len)),
)
.await?;
}
}
let potential_icon = cluster_path.full_path().await?.join("icon.png");
if !icon_exists && potential_icon.exists() {
cluster::edit_icon(&cluster_path, Some(&potential_icon)).await?;
}
if let Some(cluster) = cluster::get(&cluster_path, None).await? {
crate::game::install_minecraft(&cluster, Some(ingress), false).await?;
State::sync().await?;
}
Ok::<ClusterPath, crate::Error>(cluster_path.clone())
} else {
Err(anyhow::anyhow!("No pack manifest found in mrpack").into())
}
}
#[tracing::instrument(skip(mrpack_file))]
#[onelauncher_debug::debugger]
pub async fn remove_all_related_files(
cluster_path: ClusterPath,
mrpack_file: bytes::Bytes,
) -> crate::Result<()> {
let reader: Cursor<&bytes::Bytes> = Cursor::new(&mrpack_file);
let mut zip_reader = ZipFileReader::with_tokio(reader).await?;
let zip_index_option = zip_reader
.file()
.entries()
.iter()
.position(|f| f.filename().as_str().unwrap_or_default() == "modrinth.index.json");
if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
let mut reader = zip_reader.reader_with_entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?;
if &*pack.game != "minecraft" {
return Err(anyhow::anyhow!("package doesn't support Minecraft").into());
}
crate::api::cluster::edit(&cluster_path, |cl| {
cl.stage = ClusterStage::PackDownloading;
async { Ok(()) }
})
.await?;
let state = State::get().await?;
let all_hashes = pack
.files
.iter()
.filter_map(|f| Some(f.hashes.get(&PackFileHash::Sha512)?.clone()))
.collect::<Vec<_>>();
let files_url = format!("{}version_files", MODRINTH_API_URL);
let hash_packages = fetch_json::<HashMap<String, ManagedVersion>>(
Method::POST,
&files_url,
None,
Some(json!({
"hashes": all_hashes,
"algorithm": "sha512",
})),
&state.fetch_semaphore,
)
.await?;
let to_remove = hash_packages
.into_values()
.map(|p| p.package_id)
.collect::<Vec<_>>();
let cluster = cluster::get(&cluster_path, None).await?.ok_or_else(|| {
crate::ErrorKind::AnyhowError(anyhow::anyhow!(
"{} is an unmanaged cluster!",
cluster_path.to_string()
))
})?;
for (package_id, package) in &cluster.packages {
if let PackageMetadata::Managed { package, .. } = &package.meta {
if to_remove.contains(&package.id) {
let path = cluster.get_full_path().await?.join(package_id.0.clone());
if path.exists() {
io::remove_file(&path).await?;
}
}
}
}
for file in pack.files {
let path: PathBuf = cluster_path.full_path().await?.join(file.path.to_string());
if path.exists() {
io::remove_file(&path).await?;
}
}
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader.file().entries().get(index).unwrap();
let filename = file.filename().as_str().unwrap_or_default();
let file_path = PathBuf::from(filename);
if (filename.starts_with("overrides") || filename.starts_with("client-overrides"))
&& !filename.ends_with('/')
{
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
}
let existing_file = cluster_path.full_path().await?.join(&new_path);
if existing_file.exists() {
io::remove_file(&existing_file).await?;
}
}
}
Ok(())
} else {
Err(anyhow::anyhow!("No pack manifest found in mrpack").into())
}
}

View file

@ -176,6 +176,12 @@ pub enum IngressType {
package_id: Option<String>,
package_version: Option<String>,
},
DownloadPack {
cluster_path: PathBuf,
package_name: String,
icon: Option<String>,
package_version: String,
},
}
#[derive(Serialize, Clone)]

View file

@ -158,7 +158,7 @@ impl MinecraftState {
redirect_uri: redirect_uri.value.msa_oauth_redirect,
})
} else {
Err(crate::ErrorKind::from(err).into())
Err(err.into())
}
}
}

View file

@ -30,4 +30,10 @@ impl OneConfig {
.await?;
Ok(Self { socket })
}
pub async fn close(&mut self) -> crate::Result<()> {
self.socket.close(None).await?;
Ok(())
}
}

View file

@ -193,8 +193,8 @@ pub fn extract_archive(archive: &PathBuf, dest: &PathBuf) -> crate::Result<()> {
};
match ext {
"zip" => extract_zip(archive, dest).map_err(|err| crate::ErrorKind::IOError(err).into()),
"gz" => extract_tar_gz(archive, dest).map_err(|err| crate::ErrorKind::IOError(err).into()),
"zip" => Ok(extract_zip(archive, dest)?),
"gz" => Ok(extract_tar_gz(archive, dest)?),
_ => Err(anyhow!("unsupported file extension {:?}", ext).into()),
}
}