✨ feature(core): start package api
This commit is contained in:
parent
72c2f42c16
commit
3522142a2c
15 changed files with 1693 additions and 102 deletions
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
//! Utilities for updating Clusters
|
||||
|
||||
// TODO: requires `store/package.rs` to be implemented.
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ impl MinecraftState {
|
|||
redirect_uri: redirect_uri.value.msa_oauth_redirect,
|
||||
})
|
||||
} else {
|
||||
Err(crate::ErrorKind::from(err).into())
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,4 +30,10 @@ impl OneConfig {
|
|||
.await?;
|
||||
Ok(Self { socket })
|
||||
}
|
||||
|
||||
pub async fn close(&mut self) -> crate::Result<()> {
|
||||
self.socket.close(None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue