You've already forked AstralRinth
forked from didirus/AstralRinth
Migrate to Turborepo (#1251)
This commit is contained in:
75
packages/app-lib/src/api/handler.rs
Normal file
75
packages/app-lib/src/api/handler.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
event::{
|
||||
emit::{emit_command, emit_warning},
|
||||
CommandPayload,
|
||||
},
|
||||
util::io,
|
||||
};
|
||||
|
||||
/// Handles external functions (such as through URL deep linkage)
|
||||
/// Link is extracted value (link) in somewhat URL format, such as
|
||||
/// subdomain1/subdomain2
|
||||
/// (Does not include modrinth://)
|
||||
pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
|
||||
Ok(match sublink.split_once('/') {
|
||||
// /mod/{id} - Installs a mod of mod id
|
||||
Some(("mod", id)) => CommandPayload::InstallMod { id: id.to_string() },
|
||||
// /version/{id} - Installs a specific version of id
|
||||
Some(("version", id)) => {
|
||||
CommandPayload::InstallVersion { id: id.to_string() }
|
||||
}
|
||||
// /modpack/{id} - Installs a modpack of modpack id
|
||||
Some(("modpack", id)) => {
|
||||
CommandPayload::InstallModpack { id: id.to_string() }
|
||||
}
|
||||
_ => {
|
||||
emit_warning(&format!(
|
||||
"Invalid command, unrecognized path: {sublink}"
|
||||
))
|
||||
.await?;
|
||||
return Err(crate::ErrorKind::InputError(format!(
|
||||
"Invalid command, unrecognized path: {sublink}"
|
||||
))
|
||||
.into());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn parse_command(
|
||||
command_string: &str,
|
||||
) -> crate::Result<CommandPayload> {
|
||||
tracing::debug!("Parsing command: {}", &command_string);
|
||||
|
||||
// modrinth://some-command
|
||||
// This occurs when following a web redirect link
|
||||
if let Some(sublink) = command_string.strip_prefix("modrinth://") {
|
||||
Ok(handle_url(sublink).await?)
|
||||
} else {
|
||||
// We assume anything else is a filepath to an .mrpack file
|
||||
let path = PathBuf::from(command_string);
|
||||
let path = io::canonicalize(path)?;
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "mrpack" {
|
||||
return Ok(CommandPayload::RunMRPack { path });
|
||||
}
|
||||
}
|
||||
emit_warning(&format!(
|
||||
"Invalid command, unrecognized filetype: {}",
|
||||
path.display()
|
||||
))
|
||||
.await?;
|
||||
Err(crate::ErrorKind::InputError(format!(
|
||||
"Invalid command, unrecognized filetype: {}",
|
||||
path.display()
|
||||
))
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn parse_and_emit_command(command_string: &str) -> crate::Result<()> {
|
||||
let command = parse_command(command_string).await?;
|
||||
emit_command(command).await?;
|
||||
Ok(())
|
||||
}
|
||||
174
packages/app-lib/src/api/jre.rs
Normal file
174
packages/app-lib/src/api/jre.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
//! Authentication flow interface
|
||||
use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::event::emit::{emit_loading, init_loading};
|
||||
use crate::state::CredentialsStore;
|
||||
use crate::util::fetch::{fetch_advanced, fetch_json};
|
||||
|
||||
use crate::util::io;
|
||||
use crate::util::jre::extract_java_majorminor_version;
|
||||
use crate::{
|
||||
util::jre::{self, JavaVersion},
|
||||
LoadingBarType, State,
|
||||
};
|
||||
|
||||
// Searches for jres on the system given a java version (ex: 1.8, 1.17, 1.18)
|
||||
// Allow higher allows for versions higher than the given version to be returned ('at least')
|
||||
pub async fn find_filtered_jres(
|
||||
java_version: Option<u32>,
|
||||
) -> crate::Result<Vec<JavaVersion>> {
|
||||
let jres = jre::get_all_jre().await?;
|
||||
|
||||
// Filter out JREs that are not 1.17 or higher
|
||||
Ok(if let Some(java_version) = java_version {
|
||||
jres.into_iter()
|
||||
.filter(|jre| {
|
||||
let jre_version = extract_java_majorminor_version(&jre.version);
|
||||
if let Ok(jre_version) = jre_version {
|
||||
jre_version.1 == java_version
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
jres
|
||||
})
|
||||
}
|
||||
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let loading_bar = init_loading(
|
||||
LoadingBarType::JavaDownload {
|
||||
version: java_version,
|
||||
},
|
||||
100.0,
|
||||
"Downloading java version",
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Package {
|
||||
pub download_url: String,
|
||||
pub name: PathBuf,
|
||||
}
|
||||
|
||||
emit_loading(&loading_bar, 0.0, Some("Fetching java version")).await?;
|
||||
let packages = fetch_json::<Vec<Package>>(
|
||||
Method::GET,
|
||||
&format!(
|
||||
"https://api.azul.com/metadata/v1/zulu/packages?arch={}&java_version={}&os={}&archive_type=zip&javafx_bundled=false&java_package_type=jre&page_size=1",
|
||||
std::env::consts::ARCH, java_version, std::env::consts::OS
|
||||
),
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&CredentialsStore(None),
|
||||
).await?;
|
||||
emit_loading(&loading_bar, 10.0, Some("Downloading java version")).await?;
|
||||
|
||||
if let Some(download) = packages.first() {
|
||||
let file = fetch_advanced(
|
||||
Method::GET,
|
||||
&download.download_url,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some((&loading_bar, 80.0)),
|
||||
&state.fetch_semaphore,
|
||||
&CredentialsStore(None),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let path = state.directories.java_versions_dir().await;
|
||||
|
||||
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
|
||||
.map_err(|_| {
|
||||
crate::Error::from(crate::ErrorKind::InputError(
|
||||
"Failed to read java zip".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
// removes the old installation of java
|
||||
if let Some(file) = archive.file_names().next() {
|
||||
if let Some(dir) = file.split('/').next() {
|
||||
let path = path.join(dir);
|
||||
|
||||
if path.exists() {
|
||||
io::remove_dir_all(path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit_loading(&loading_bar, 0.0, Some("Extracting java")).await?;
|
||||
archive.extract(&path).map_err(|_| {
|
||||
crate::Error::from(crate::ErrorKind::InputError(
|
||||
"Failed to extract java zip".to_string(),
|
||||
))
|
||||
})?;
|
||||
emit_loading(&loading_bar, 10.0, Some("Done extracting java")).await?;
|
||||
let mut base_path = path.join(
|
||||
download
|
||||
.name
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
base_path = base_path
|
||||
.join(format!("zulu-{}.jre", java_version))
|
||||
.join("Contents")
|
||||
.join("Home")
|
||||
.join("bin")
|
||||
.join("java")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
base_path = base_path.join("bin").join(jre::JAVA_BIN)
|
||||
}
|
||||
|
||||
Ok(base_path)
|
||||
} else {
|
||||
Err(crate::ErrorKind::LauncherError(format!(
|
||||
"No Java Version found for Java version {}, OS {}, and Architecture {}",
|
||||
java_version, std::env::consts::OS, std::env::consts::ARCH,
|
||||
)).into())
|
||||
}
|
||||
}
|
||||
|
||||
// Validates JRE at a given at a given path
|
||||
pub async fn check_jre(path: PathBuf) -> crate::Result<Option<JavaVersion>> {
|
||||
Ok(jre::check_java_at_filepath(&path).await)
|
||||
}
|
||||
|
||||
// Test JRE at a given path
|
||||
pub async fn test_jre(
|
||||
path: PathBuf,
|
||||
major_version: u32,
|
||||
) -> crate::Result<bool> {
|
||||
let jre = match jre::check_java_at_filepath(&path).await {
|
||||
Some(jre) => jre,
|
||||
None => return Ok(false),
|
||||
};
|
||||
let (major, _) = extract_java_majorminor_version(&jre.version)?;
|
||||
Ok(major == major_version)
|
||||
}
|
||||
|
||||
// Gets maximum memory in KiB.
|
||||
pub async fn get_max_memory() -> crate::Result<u64> {
|
||||
Ok(sys_info::mem_info()
|
||||
.map_err(|_| {
|
||||
crate::Error::from(crate::ErrorKind::LauncherError(
|
||||
"Unable to get computer memory".to_string(),
|
||||
))
|
||||
})?
|
||||
.total)
|
||||
}
|
||||
372
packages/app-lib/src/api/logs.rs
Normal file
372
packages/app-lib/src/api/logs.rs
Normal file
@@ -0,0 +1,372 @@
|
||||
use std::io::{Read, SeekFrom};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use futures::TryFutureExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncReadExt, AsyncSeekExt},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
prelude::{Credentials, DirectoryInfo},
|
||||
util::io::{self, IOError},
|
||||
{state::ProfilePathId, State},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Logs {
|
||||
pub log_type: LogType,
|
||||
pub filename: String,
|
||||
pub age: u64,
|
||||
pub output: Option<CensoredString>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum LogType {
|
||||
InfoLog,
|
||||
CrashReport,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct LatestLogCursor {
|
||||
pub cursor: u64,
|
||||
pub output: CensoredString,
|
||||
pub new_file: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)] // Not deserialize
|
||||
#[serde(transparent)]
|
||||
pub struct CensoredString(String);
|
||||
impl CensoredString {
|
||||
pub fn censor(mut s: String, credentials_set: &Vec<Credentials>) -> Self {
|
||||
let username = whoami::username();
|
||||
s = s
|
||||
.replace(&format!("/{}/", username), "/{COMPUTER_USERNAME}/")
|
||||
.replace(&format!("\\{}\\", username), "\\{COMPUTER_USERNAME}\\");
|
||||
for credentials in credentials_set {
|
||||
s = s
|
||||
.replace(&credentials.access_token, "{MINECRAFT_ACCESS_TOKEN}")
|
||||
.replace(&credentials.username, "{MINECRAFT_USERNAME}")
|
||||
.replace(
|
||||
&credentials.id.as_simple().to_string(),
|
||||
"{MINECRAFT_UUID}",
|
||||
)
|
||||
.replace(
|
||||
&credentials.id.as_hyphenated().to_string(),
|
||||
"{MINECRAFT_UUID}",
|
||||
);
|
||||
}
|
||||
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Logs {
|
||||
async fn build(
|
||||
log_type: LogType,
|
||||
age: SystemTime,
|
||||
profile_subpath: &ProfilePathId,
|
||||
filename: String,
|
||||
clear_contents: Option<bool>,
|
||||
) -> crate::Result<Self> {
|
||||
Ok(Self {
|
||||
log_type,
|
||||
age: age
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
|
||||
.as_secs(),
|
||||
output: if clear_contents.unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
get_output_by_filename(
|
||||
profile_subpath,
|
||||
log_type,
|
||||
&filename,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
},
|
||||
filename,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs_from_type(
|
||||
profile_path: &ProfilePathId,
|
||||
log_type: LogType,
|
||||
clear_contents: Option<bool>,
|
||||
logs: &mut Vec<crate::Result<Logs>>,
|
||||
) -> crate::Result<()> {
|
||||
let logs_folder = match log_type {
|
||||
LogType::InfoLog => {
|
||||
DirectoryInfo::profile_logs_dir(profile_path).await?
|
||||
}
|
||||
LogType::CrashReport => {
|
||||
DirectoryInfo::crash_reports_dir(profile_path).await?
|
||||
}
|
||||
};
|
||||
if logs_folder.exists() {
|
||||
for entry in std::fs::read_dir(&logs_folder)
|
||||
.map_err(|e| IOError::with_path(e, &logs_folder))?
|
||||
{
|
||||
let entry: std::fs::DirEntry =
|
||||
entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
|
||||
let age = entry.metadata()?.created().unwrap_or_else(|_| SystemTime::UNIX_EPOCH);
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
if let Some(file_name) = path.file_name() {
|
||||
let file_name = file_name.to_string_lossy().to_string();
|
||||
logs.push(
|
||||
Logs::build(
|
||||
log_type,
|
||||
age,
|
||||
&profile_path,
|
||||
file_name,
|
||||
clear_contents,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs(
|
||||
profile_path_id: ProfilePathId,
|
||||
clear_contents: Option<bool>,
|
||||
) -> crate::Result<Vec<Logs>> {
|
||||
let profile_path = profile_path_id.profile_path().await?;
|
||||
|
||||
let mut logs = Vec::new();
|
||||
get_logs_from_type(
|
||||
&profile_path,
|
||||
LogType::InfoLog,
|
||||
clear_contents,
|
||||
&mut logs,
|
||||
)
|
||||
.await?;
|
||||
get_logs_from_type(
|
||||
&profile_path,
|
||||
LogType::CrashReport,
|
||||
clear_contents,
|
||||
&mut logs,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut logs = logs.into_iter().collect::<crate::Result<Vec<Logs>>>()?;
|
||||
logs.sort_by(|a, b| b.age.cmp(&a.age).then(b.filename.cmp(&a.filename)));
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs_by_filename(
|
||||
profile_path_id: ProfilePathId,
|
||||
log_type: LogType,
|
||||
filename: String,
|
||||
) -> crate::Result<Logs> {
|
||||
let profile_path = profile_path_id.profile_path().await?;
|
||||
|
||||
let path = match log_type {
|
||||
LogType::InfoLog => {
|
||||
DirectoryInfo::profile_logs_dir(&profile_path).await
|
||||
}
|
||||
LogType::CrashReport => {
|
||||
DirectoryInfo::crash_reports_dir(&profile_path).await
|
||||
}
|
||||
}?
|
||||
.join(&filename);
|
||||
|
||||
let metadata = std::fs::metadata(&path)?;
|
||||
let age = metadata.created().unwrap_or_else(|_| SystemTime::UNIX_EPOCH);
|
||||
|
||||
Logs::build(log_type, age, &profile_path, filename, Some(true)).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_output_by_filename(
|
||||
profile_subpath: &ProfilePathId,
|
||||
log_type: LogType,
|
||||
file_name: &str,
|
||||
) -> crate::Result<CensoredString> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let logs_folder = match log_type {
|
||||
LogType::InfoLog => {
|
||||
DirectoryInfo::profile_logs_dir(profile_subpath).await?
|
||||
}
|
||||
LogType::CrashReport => {
|
||||
DirectoryInfo::crash_reports_dir(profile_subpath).await?
|
||||
}
|
||||
};
|
||||
|
||||
let path = logs_folder.join(file_name);
|
||||
|
||||
let credentials: Vec<Credentials> = state
|
||||
.users
|
||||
.read()
|
||||
.await
|
||||
.users
|
||||
.clone()
|
||||
.into_values()
|
||||
.collect();
|
||||
|
||||
// Load .gz file into String
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "gz" {
|
||||
let file = std::fs::File::open(&path)
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
let mut contents = [0; 1024];
|
||||
let mut result = String::new();
|
||||
let mut gz =
|
||||
flate2::read::GzDecoder::new(std::io::BufReader::new(file));
|
||||
|
||||
while gz
|
||||
.read(&mut contents)
|
||||
.map_err(|e| IOError::with_path(e, &path))?
|
||||
> 0
|
||||
{
|
||||
result.push_str(&String::from_utf8_lossy(&contents));
|
||||
contents = [0; 1024];
|
||||
}
|
||||
return Ok(CensoredString::censor(result, &credentials));
|
||||
} else if ext == "log" || ext == "txt" {
|
||||
let mut result = String::new();
|
||||
let mut contents = [0; 1024];
|
||||
let mut file = std::fs::File::open(&path)
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
// iteratively read the file to a String
|
||||
while file
|
||||
.read(&mut contents)
|
||||
.map_err(|e| IOError::with_path(e, &path))?
|
||||
> 0
|
||||
{
|
||||
result.push_str(&String::from_utf8_lossy(&contents));
|
||||
contents = [0; 1024];
|
||||
}
|
||||
let result = CensoredString::censor(result, &credentials);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
Err(crate::ErrorKind::OtherError(format!(
|
||||
"File extension not supported: {}",
|
||||
path.display()
|
||||
))
|
||||
.into())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs(profile_path_id: ProfilePathId) -> crate::Result<()> {
|
||||
let profile_path = profile_path_id.profile_path().await?;
|
||||
|
||||
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
|
||||
for entry in std::fs::read_dir(&logs_folder)
|
||||
.map_err(|e| IOError::with_path(e, &logs_folder))?
|
||||
{
|
||||
let entry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
io::remove_dir_all(&path).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs_by_filename(
|
||||
profile_path_id: ProfilePathId,
|
||||
log_type: LogType,
|
||||
filename: &str,
|
||||
) -> crate::Result<()> {
|
||||
let profile_path = profile_path_id.profile_path().await?;
|
||||
|
||||
let logs_folder = match log_type {
|
||||
LogType::InfoLog => {
|
||||
DirectoryInfo::profile_logs_dir(&profile_path).await
|
||||
}
|
||||
LogType::CrashReport => {
|
||||
DirectoryInfo::crash_reports_dir(&profile_path).await
|
||||
}
|
||||
}?;
|
||||
|
||||
let path = logs_folder.join(filename);
|
||||
io::remove_dir_all(&path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_latest_log_cursor(
|
||||
profile_path: ProfilePathId,
|
||||
cursor: u64, // 0 to start at beginning of file
|
||||
) -> crate::Result<LatestLogCursor> {
|
||||
get_generic_live_log_cursor(profile_path, "latest.log", cursor).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_generic_live_log_cursor(
|
||||
profile_path_id: ProfilePathId,
|
||||
log_file_name: &str,
|
||||
mut cursor: u64, // 0 to start at beginning of file
|
||||
) -> crate::Result<LatestLogCursor> {
|
||||
let profile_path = profile_path_id.profile_path().await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
|
||||
let path = logs_folder.join(log_file_name);
|
||||
if !path.exists() {
|
||||
// Allow silent failure if latest.log doesn't exist (as the instance may have been launched, but not yet created the file)
|
||||
return Ok(LatestLogCursor {
|
||||
cursor: 0,
|
||||
new_file: false,
|
||||
output: CensoredString("".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
let mut file = File::open(&path)
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
let metadata = file
|
||||
.metadata()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &path))?;
|
||||
|
||||
let mut new_file = false;
|
||||
if cursor > metadata.len() {
|
||||
// Cursor is greater than file length, reset cursor to 0
|
||||
// Likely cause is that the file was rotated while the log was being read
|
||||
cursor = 0;
|
||||
new_file = true;
|
||||
}
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
file.seek(SeekFrom::Start(cursor))
|
||||
.map_err(|e| IOError::with_path(e, &path))
|
||||
.await?; // Seek to cursor
|
||||
let bytes_read = file
|
||||
.read_to_end(&mut buffer)
|
||||
.map_err(|e| IOError::with_path(e, &path))
|
||||
.await?; // Read to end of file
|
||||
let output = String::from_utf8_lossy(&buffer).to_string(); // Convert to String
|
||||
let cursor = cursor + bytes_read as u64; // Update cursor
|
||||
|
||||
let credentials: Vec<Credentials> = state
|
||||
.users
|
||||
.read()
|
||||
.await
|
||||
.users
|
||||
.clone()
|
||||
.into_values()
|
||||
.collect();
|
||||
let output = CensoredString::censor(output, &credentials);
|
||||
Ok(LatestLogCursor {
|
||||
cursor,
|
||||
new_file,
|
||||
output,
|
||||
})
|
||||
}
|
||||
43
packages/app-lib/src/api/metadata.rs
Normal file
43
packages/app-lib/src/api/metadata.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use crate::State;
|
||||
pub use daedalus::minecraft::VersionManifest;
|
||||
pub use daedalus::modded::Manifest;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_minecraft_versions() -> crate::Result<VersionManifest> {
|
||||
let state = State::get().await?;
|
||||
let tags = state.metadata.read().await.minecraft.clone();
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_fabric_versions() -> crate::Result<Manifest> {
|
||||
let state = State::get().await?;
|
||||
let tags = state.metadata.read().await.fabric.clone();
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_forge_versions() -> crate::Result<Manifest> {
|
||||
let state = State::get().await?;
|
||||
let tags = state.metadata.read().await.forge.clone();
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_quilt_versions() -> crate::Result<Manifest> {
|
||||
let state = State::get().await?;
|
||||
let tags = state.metadata.read().await.quilt.clone();
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_neoforge_versions() -> crate::Result<Manifest> {
|
||||
let state = State::get().await?;
|
||||
let tags = state.metadata.read().await.neoforge.clone();
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
76
packages/app-lib/src/api/minecraft_auth.rs
Normal file
76
packages/app-lib/src/api/minecraft_auth.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
//! Authentication flow interface
|
||||
use crate::state::{Credentials, MinecraftLoginFlow};
|
||||
use crate::State;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn begin_login() -> crate::Result<MinecraftLoginFlow> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
users.login_begin().await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn finish_login(
|
||||
code: &str,
|
||||
flow: MinecraftLoginFlow,
|
||||
) -> crate::Result<Credentials> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
users.login_finish(code, flow).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
Ok(users.default_user)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn set_default_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||
let user = get_user(user).await?;
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
users.default_user = Some(user.id);
|
||||
users.save().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a user account from the database
|
||||
#[tracing::instrument]
|
||||
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
users.remove(user).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a copy of the list of all user credentials
|
||||
#[tracing::instrument]
|
||||
pub async fn users() -> crate::Result<Vec<Credentials>> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
Ok(users.users.values().cloned().collect())
|
||||
}
|
||||
|
||||
/// Get a specific user by user ID
|
||||
/// Prefer to use 'refresh' instead of this function
|
||||
#[tracing::instrument]
|
||||
pub async fn get_user(user: uuid::Uuid) -> crate::Result<Credentials> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
let user = users
|
||||
.users
|
||||
.get(&user)
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to get nonexistent user with ID {user}"
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.clone();
|
||||
Ok(user)
|
||||
}
|
||||
40
packages/app-lib/src/api/mod.rs
Normal file
40
packages/app-lib/src/api/mod.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
//! API for interacting with Theseus
|
||||
pub mod handler;
|
||||
pub mod jre;
|
||||
pub mod logs;
|
||||
pub mod metadata;
|
||||
pub mod minecraft_auth;
|
||||
pub mod mr_auth;
|
||||
pub mod pack;
|
||||
pub mod process;
|
||||
pub mod profile;
|
||||
pub mod safety;
|
||||
pub mod settings;
|
||||
pub mod tags;
|
||||
|
||||
pub mod data {
|
||||
pub use crate::state::{
|
||||
Credentials, DirectoryInfo, Hooks, JavaSettings, LinkedData,
|
||||
MemorySettings, ModLoader, ModrinthCredentials,
|
||||
ModrinthCredentialsResult, ModrinthProject, ModrinthTeamMember,
|
||||
ModrinthUser, ModrinthVersion, ProfileMetadata, ProjectMetadata,
|
||||
Settings, Theme, WindowSize,
|
||||
};
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
data::*,
|
||||
event::CommandPayload,
|
||||
jre, metadata, minecraft_auth, pack, process,
|
||||
profile::{self, create, Profile},
|
||||
settings,
|
||||
state::JavaGlobals,
|
||||
state::{Dependency, ProfilePathId, ProjectPathId},
|
||||
util::{
|
||||
io::{canonicalize, IOError},
|
||||
jre::JavaVersion,
|
||||
},
|
||||
State,
|
||||
};
|
||||
}
|
||||
144
packages/app-lib/src/api/mr_auth.rs
Normal file
144
packages/app-lib/src/api/mr_auth.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use crate::state::{
|
||||
ModrinthAuthFlow, ModrinthCredentials, ModrinthCredentialsResult,
|
||||
};
|
||||
use crate::ErrorKind;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn authenticate_begin_flow(provider: &str) -> crate::Result<String> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
// Don't start an uncompleteable new flow if there's an existing locked one
|
||||
let mut write: tokio::sync::RwLockWriteGuard<'_, Option<ModrinthAuthFlow>> =
|
||||
state.modrinth_auth_flow.write().await;
|
||||
|
||||
let mut flow = ModrinthAuthFlow::new(provider).await?;
|
||||
let url = flow.prepare_login_url().await?;
|
||||
|
||||
*write = Some(flow);
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn authenticate_await_complete_flow(
|
||||
) -> crate::Result<ModrinthCredentialsResult> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let mut write = state.modrinth_auth_flow.write().await;
|
||||
if let Some(ref mut flow) = *write {
|
||||
let creds = flow.extract_credentials(&state.fetch_semaphore).await?;
|
||||
|
||||
if let ModrinthCredentialsResult::Credentials(creds) = &creds {
|
||||
let mut write = state.credentials.write().await;
|
||||
write.login(creds.clone()).await?;
|
||||
}
|
||||
|
||||
Ok(creds)
|
||||
} else {
|
||||
Err(ErrorKind::OtherError(
|
||||
"No active Modrinth authenication flow!".to_string(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn cancel_flow() -> crate::Result<()> {
|
||||
let state = crate::State::get().await?;
|
||||
let mut write = state.modrinth_auth_flow.write().await;
|
||||
if let Some(ref mut flow) = *write {
|
||||
flow.close().await?;
|
||||
}
|
||||
*write = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn login_password(
|
||||
username: &str,
|
||||
password: &str,
|
||||
challenge: &str,
|
||||
) -> crate::Result<ModrinthCredentialsResult> {
|
||||
let state = crate::State::get().await?;
|
||||
let creds = crate::state::login_password(
|
||||
username,
|
||||
password,
|
||||
challenge,
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let ModrinthCredentialsResult::Credentials(creds) = &creds {
|
||||
let mut write = state.credentials.write().await;
|
||||
write.login(creds.clone()).await?;
|
||||
}
|
||||
|
||||
Ok(creds)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn login_2fa(
|
||||
code: &str,
|
||||
flow: &str,
|
||||
) -> crate::Result<ModrinthCredentials> {
|
||||
let state = crate::State::get().await?;
|
||||
let creds =
|
||||
crate::state::login_2fa(code, flow, &state.fetch_semaphore).await?;
|
||||
|
||||
let mut write = state.credentials.write().await;
|
||||
write.login(creds.clone()).await?;
|
||||
|
||||
Ok(creds)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn create_account(
|
||||
username: &str,
|
||||
email: &str,
|
||||
password: &str,
|
||||
challenge: &str,
|
||||
sign_up_newsletter: bool,
|
||||
) -> crate::Result<ModrinthCredentials> {
|
||||
let state = crate::State::get().await?;
|
||||
let creds = crate::state::create_account(
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
challenge,
|
||||
sign_up_newsletter,
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut write = state.credentials.write().await;
|
||||
write.login(creds.clone()).await?;
|
||||
|
||||
Ok(creds)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn refresh() -> crate::Result<()> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let mut write = state.credentials.write().await;
|
||||
crate::state::refresh_credentials(&mut write, &state.fetch_semaphore)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn logout() -> crate::Result<()> {
|
||||
let state = crate::State::get().await?;
|
||||
let mut write = state.credentials.write().await;
|
||||
write.logout().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_credentials() -> crate::Result<Option<ModrinthCredentials>> {
|
||||
let state = crate::State::get().await?;
|
||||
let read = state.credentials.read().await;
|
||||
|
||||
Ok(read.0.clone())
|
||||
}
|
||||
266
packages/app-lib/src/api/pack/import/atlauncher.rs
Normal file
266
packages/app-lib/src/api/pack/import/atlauncher.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
pack::{
|
||||
self,
|
||||
import::{self, copy_dotminecraft},
|
||||
install_from::CreatePackDescription,
|
||||
},
|
||||
prelude::{ModLoader, Profile, ProfilePathId},
|
||||
state::{LinkedData, ProfileInstallStage},
|
||||
util::io,
|
||||
State,
|
||||
};
|
||||
|
||||
#[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, // ie: 1.6
|
||||
pub loader_version: ATLauncherLoaderVersion,
|
||||
|
||||
pub modrinth_project: Option<ATLauncherModrinthProject>,
|
||||
pub modrinth_version: Option<ATLauncherModrinthVersion>,
|
||||
pub modrinth_manifest: Option<pack::install_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 ATLauncherModrinthProject {
|
||||
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 ATLauncherModrinthVersion {
|
||||
pub id: String,
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ATLauncherModrinthVersionFile {
|
||||
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 ATLauncherModrinthVersionDependency {
|
||||
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 modrinth_project: Option<ATLauncherModrinthProject>,
|
||||
pub modrinth_version: Option<ATLauncherModrinthVersion>,
|
||||
}
|
||||
|
||||
// Check if folder has a instance.json that parses
|
||||
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!(
|
||||
"Could not parse instance.json at {}: {}",
|
||||
instance_folder.display(),
|
||||
e
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn import_atlauncher(
|
||||
atlauncher_base_path: PathBuf, // path to base atlauncher folder
|
||||
instance_folder: String, // instance folder in atlauncher_base_path
|
||||
profile_path: ProfilePathId, // path to profile
|
||||
) -> crate::Result<()> {
|
||||
let atlauncher_instance_path = atlauncher_base_path
|
||||
.join("instances")
|
||||
.join(instance_folder.clone());
|
||||
|
||||
// Load instance.json
|
||||
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,
|
||||
// Second 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::recache_icon(icon_path_primary).await?,
|
||||
(_, true) => import::recache_icon(icon_path_secondary).await?,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Create description from instance.cfg
|
||||
let description = CreatePackDescription {
|
||||
icon,
|
||||
override_title: Some(atinstance.launcher.name.clone()),
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
existing_loading_bar: None,
|
||||
profile_path: profile_path.clone(),
|
||||
};
|
||||
|
||||
let backup_name = format!("ATLauncher-{}", instance_folder);
|
||||
let minecraft_folder = atlauncher_instance_path;
|
||||
|
||||
import_atlauncher_unmanaged(
|
||||
profile_path,
|
||||
minecraft_folder,
|
||||
backup_name,
|
||||
description,
|
||||
atinstance,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn import_atlauncher_unmanaged(
|
||||
profile_path: ProfilePathId,
|
||||
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: ModLoader = serde_json::from_str::<ModLoader>(&mod_loader)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Could not parse mod loader type: {}",
|
||||
mod_loader
|
||||
))
|
||||
})?;
|
||||
|
||||
let game_version = atinstance.id;
|
||||
|
||||
let loader_version = if mod_loader != ModLoader::Vanilla {
|
||||
crate::profile::create::get_loader_version_from_loader(
|
||||
game_version.clone(),
|
||||
mod_loader,
|
||||
Some(atinstance.launcher.loader_version.version.clone()),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Set profile data to created default profile
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.metadata.name = description
|
||||
.override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
prof.metadata.linked_data = Some(LinkedData {
|
||||
project_id: description.project_id.clone(),
|
||||
version_id: description.version_id.clone(),
|
||||
locked: Some(
|
||||
description.project_id.is_some()
|
||||
&& description.version_id.is_some(),
|
||||
),
|
||||
});
|
||||
prof.metadata.icon = description.icon.clone();
|
||||
prof.metadata.game_version = game_version.clone();
|
||||
prof.metadata.loader_version = loader_version.clone();
|
||||
prof.metadata.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
Some(loading_bar),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile_val.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
198
packages/app-lib/src/api/pack/import/curseforge.rs
Normal file
198
packages/app-lib/src/api/pack/import/curseforge.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::prelude::Profile;
|
||||
use crate::state::CredentialsStore;
|
||||
use crate::{
|
||||
prelude::{ModLoader, ProfilePathId},
|
||||
state::ProfileInstallStage,
|
||||
util::{
|
||||
fetch::{fetch, write_cached_icon},
|
||||
io,
|
||||
},
|
||||
State,
|
||||
};
|
||||
|
||||
use super::{copy_dotminecraft, recache_icon};
|
||||
|
||||
#[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>,
|
||||
pub game_version: String, // Minecraft game version. Non-prioritized, use this if Vanilla
|
||||
}
|
||||
#[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>,
|
||||
}
|
||||
|
||||
// Check if folder has a minecraftinstance.json that parses
|
||||
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(
|
||||
curseforge_instance_folder: PathBuf, // instance's folder
|
||||
profile_path: ProfilePathId, // path to profile
|
||||
) -> crate::Result<()> {
|
||||
// Load minecraftinstance.json
|
||||
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?;
|
||||
// Recache Curseforge Icon if it exists
|
||||
let mut icon = None;
|
||||
|
||||
if let Some(icon_path) = minecraft_instance.profile_image_path.clone() {
|
||||
icon = recache_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,
|
||||
&CredentialsStore(None),
|
||||
)
|
||||
.await?;
|
||||
let filename = thumbnail_url.rsplit('/').last();
|
||||
if let Some(filename) = filename {
|
||||
icon = Some(
|
||||
write_cached_icon(
|
||||
filename,
|
||||
&state.directories.caches_dir(),
|
||||
icon_bytes,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// base mod loader is always None for vanilla
|
||||
if let Some(instance_mod_loader) = minecraft_instance.base_mod_loader {
|
||||
let game_version = minecraft_instance.game_version;
|
||||
|
||||
// CF allows Forge, Fabric, and Vanilla
|
||||
let mut mod_loader = None;
|
||||
let mut loader_version = None;
|
||||
|
||||
match instance_mod_loader.name.split('-').collect::<Vec<&str>>()[..] {
|
||||
["forge", version] => {
|
||||
mod_loader = Some(ModLoader::Forge);
|
||||
loader_version = Some(version.to_string());
|
||||
}
|
||||
["fabric", version, _game_version] => {
|
||||
mod_loader = Some(ModLoader::Fabric);
|
||||
loader_version = Some(version.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla);
|
||||
|
||||
let loader_version = if mod_loader != ModLoader::Vanilla {
|
||||
crate::profile::create::get_loader_version_from_loader(
|
||||
game_version.clone(),
|
||||
mod_loader,
|
||||
loader_version,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Set profile data to created default profile
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.metadata.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
prof.metadata.icon = icon.clone();
|
||||
prof.metadata.game_version = game_version.clone();
|
||||
prof.metadata.loader_version = loader_version.clone();
|
||||
prof.metadata.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
// create a vanilla profile
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.metadata.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.metadata.icon = icon.clone();
|
||||
prof.metadata.game_version =
|
||||
minecraft_instance.game_version.clone();
|
||||
prof.metadata.loader_version = None;
|
||||
prof.metadata.loader = ModLoader::Vanilla;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Copy in contained folders as overrides
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
curseforge_instance_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
Some(loading_bar),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile_val.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
134
packages/app-lib/src/api/pack/import/gdlauncher.rs
Normal file
134
packages/app-lib/src/api/pack/import/gdlauncher.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
prelude::{ModLoader, Profile, ProfilePathId},
|
||||
state::ProfileInstallStage,
|
||||
util::io,
|
||||
State,
|
||||
};
|
||||
|
||||
use super::{copy_dotminecraft, recache_icon};
|
||||
|
||||
#[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: ModLoader,
|
||||
pub loader_version: Option<String>,
|
||||
pub mc_version: String,
|
||||
pub source: Option<String>,
|
||||
pub source_name: Option<String>,
|
||||
}
|
||||
|
||||
// Check if folder has a config.json that parses
|
||||
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, // instance's folder
|
||||
profile_path: ProfilePathId, // path to profile
|
||||
) -> crate::Result<()> {
|
||||
// Load config.json
|
||||
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())
|
||||
);
|
||||
|
||||
// Re-cache icon
|
||||
let icon = config
|
||||
.background
|
||||
.clone()
|
||||
.map(|b| gdlauncher_instance_folder.join(b));
|
||||
let icon = if let Some(icon) = icon {
|
||||
recache_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 != ModLoader::Vanilla {
|
||||
crate::profile::create::get_loader_version_from_loader(
|
||||
game_version.clone(),
|
||||
mod_loader,
|
||||
loader_version,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Set profile data to created default profile
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.metadata.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
prof.metadata.icon = icon.clone();
|
||||
prof.metadata.game_version = game_version.clone();
|
||||
prof.metadata.loader_version = loader_version.clone();
|
||||
prof.metadata.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Copy in contained folders as overrides
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
gdlauncher_instance_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
Some(loading_bar),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile_val.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
344
packages/app-lib/src/api/pack/import/mmc.rs
Normal file
344
packages/app-lib/src/api/pack/import/mmc.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{de, Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
pack::{
|
||||
import::{self, copy_dotminecraft},
|
||||
install_from::{self, CreatePackDescription, PackDependency},
|
||||
},
|
||||
prelude::{Profile, ProfilePathId},
|
||||
util::io,
|
||||
State,
|
||||
};
|
||||
|
||||
// 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>,
|
||||
}
|
||||
|
||||
// serde_ini reads 'true' and 'false' as strings, so we need to convert them to booleans
|
||||
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,
|
||||
}
|
||||
|
||||
// Checks if if its a folder, and the folder contains instance.cfg and mmc-pack.json, and they both parse
|
||||
#[tracing::instrument]
|
||||
pub async fn is_valid_mmc(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),
|
||||
}
|
||||
}
|
||||
|
||||
// Loading the INI (instance.cfg) file
|
||||
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]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn import_mmc(
|
||||
mmc_base_path: PathBuf, // path to base mmc folder
|
||||
instance_folder: String, // instance folder in mmc_base_path
|
||||
profile_path: ProfilePathId, // path to profile
|
||||
) -> 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?;
|
||||
|
||||
// Re-cache icon
|
||||
let icon = if let Some(icon_key) = instance_cfg.icon_key {
|
||||
let icon_path = mmc_base_path.join("icons").join(icon_key);
|
||||
import::recache_icon(icon_path).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Create description from instance.cfg
|
||||
let description = CreatePackDescription {
|
||||
icon,
|
||||
override_title: instance_cfg.name,
|
||||
project_id: instance_cfg.managed_pack_id,
|
||||
version_id: instance_cfg.managed_pack_version_id,
|
||||
existing_loading_bar: None,
|
||||
profile_path: profile_path.clone(),
|
||||
};
|
||||
|
||||
// Managed pack
|
||||
let backup_name = "Imported Modpack".to_string();
|
||||
|
||||
if instance_cfg.managed_pack.unwrap_or(false) {
|
||||
match instance_cfg.managed_pack_type {
|
||||
Some(MMCManagedPackType::Modrinth) => {
|
||||
// Modrinth Managed Pack
|
||||
// Kept separate as we may in the future want to add special handling for modrinth managed packs
|
||||
let backup_name = "Imported Modrinth Modpack".to_string();
|
||||
let minecraft_folder = mmc_base_path.join("instances").join(instance_folder).join(".minecraft");
|
||||
import_mmc_unmanaged(profile_path, minecraft_folder, backup_name, description, mmc_pack).await?;
|
||||
}
|
||||
Some(MMCManagedPackType::Flame) | Some(MMCManagedPackType::ATLauncher) => {
|
||||
// For flame/atlauncher managed packs
|
||||
// Treat 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(profile_path, minecraft_folder, backup_name, description, mmc_pack).await?;
|
||||
},
|
||||
Some(_) => {
|
||||
// For managed packs that aren't modrinth, flame, atlauncher
|
||||
// Treat as unmanaged
|
||||
let backup_name = "ImportedModpack".to_string();
|
||||
let minecraft_folder = mmc_base_path.join("instances").join(instance_folder).join(".minecraft");
|
||||
import_mmc_unmanaged(profile_path, minecraft_folder, backup_name, description, mmc_pack).await?;
|
||||
},
|
||||
_ => return Err(crate::ErrorKind::InputError({
|
||||
"Instance is managed, but managed pack type not specified in instance.cfg".to_string()
|
||||
}).into())
|
||||
}
|
||||
} else {
|
||||
// Direclty import unmanaged pack
|
||||
let backup_name = "Imported Modpack".to_string();
|
||||
let minecraft_folder = mmc_base_path
|
||||
.join("instances")
|
||||
.join(instance_folder)
|
||||
.join(".minecraft");
|
||||
import_mmc_unmanaged(
|
||||
profile_path,
|
||||
minecraft_folder,
|
||||
backup_name,
|
||||
description,
|
||||
mmc_pack,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn import_mmc_unmanaged(
|
||||
profile_path: ProfilePathId,
|
||||
minecraft_folder: PathBuf,
|
||||
backup_name: String,
|
||||
description: CreatePackDescription,
|
||||
mmc_pack: MMCPack,
|
||||
) -> crate::Result<()> {
|
||||
// Pack dependencies stored in mmc-pack.json, we convert to .mrpack pack dependencies
|
||||
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();
|
||||
|
||||
// Sets profile information to be that loaded from mmc-pack.json and instance.cfg
|
||||
install_from::set_profile_information(
|
||||
profile_path.clone(),
|
||||
&description,
|
||||
&backup_name,
|
||||
&dependencies,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
Some(loading_bar),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile_val.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
320
packages/app-lib/src/api/pack/import/mod.rs
Normal file
320
packages/app-lib/src/api/pack/import/mod.rs
Normal file
@@ -0,0 +1,320 @@
|
||||
use std::{
|
||||
fmt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use io::IOError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
event::{
|
||||
emit::{emit_loading, init_or_edit_loading},
|
||||
LoadingBarId,
|
||||
},
|
||||
prelude::ProfilePathId,
|
||||
state::Profiles,
|
||||
util::{
|
||||
fetch::{self, IoSemaphore},
|
||||
io,
|
||||
},
|
||||
};
|
||||
|
||||
pub mod atlauncher;
|
||||
pub mod curseforge;
|
||||
pub mod gdlauncher;
|
||||
pub mod mmc;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ImportLauncherType {
|
||||
MultiMC,
|
||||
PrismLauncher,
|
||||
ATLauncher,
|
||||
GDLauncher,
|
||||
Curseforge,
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
// impl display
|
||||
impl fmt::Display for ImportLauncherType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ImportLauncherType::MultiMC => write!(f, "MultiMC"),
|
||||
ImportLauncherType::PrismLauncher => write!(f, "PrismLauncher"),
|
||||
ImportLauncherType::ATLauncher => write!(f, "ATLauncher"),
|
||||
ImportLauncherType::GDLauncher => write!(f, "GDLauncher"),
|
||||
ImportLauncherType::Curseforge => write!(f, "Curseforge"),
|
||||
ImportLauncherType::Unknown => write!(f, "Unknown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return a list of importable instances from a launcher type and base path, by iterating through the folder and checking
|
||||
pub async fn get_importable_instances(
|
||||
launcher_type: ImportLauncherType,
|
||||
base_path: PathBuf,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
// Some launchers have a different folder structure for instances
|
||||
let instances_subfolder = match launcher_type {
|
||||
ImportLauncherType::GDLauncher | ImportLauncherType::ATLauncher => {
|
||||
"instances".to_string()
|
||||
}
|
||||
ImportLauncherType::Curseforge => "Instances".to_string(),
|
||||
ImportLauncherType::MultiMC => {
|
||||
mmc::get_instances_subpath(base_path.clone().join("multimc.cfg"))
|
||||
.await
|
||||
.unwrap_or_else(|| "instances".to_string())
|
||||
}
|
||||
ImportLauncherType::PrismLauncher => mmc::get_instances_subpath(
|
||||
base_path.clone().join("prismlauncher.cfg"),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|| "instances".to_string()),
|
||||
ImportLauncherType::Unknown => {
|
||||
return Err(crate::ErrorKind::InputError(
|
||||
"Launcher type Unknown".to_string(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
let instances_folder = base_path.join(&instances_subfolder);
|
||||
let mut instances = Vec::new();
|
||||
let mut dir = io::read_dir(&instances_folder).await.map_err(| _ | {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Invalid {launcher_type} launcher path, could not find '{instances_subfolder}' subfolder."
|
||||
))
|
||||
})?;
|
||||
while let Some(entry) = dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &instances_folder))?
|
||||
{
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
// Check instance is valid of this launcher type
|
||||
if is_valid_importable_instance(path.clone(), launcher_type).await {
|
||||
let name = path.file_name();
|
||||
if let Some(name) = name {
|
||||
instances.push(name.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(instances)
|
||||
}
|
||||
|
||||
// Import an instance from a launcher type and base path
|
||||
// Note: this *deletes* the submitted empty profile
|
||||
#[theseus_macros::debug_pin]
|
||||
#[tracing::instrument]
|
||||
pub async fn import_instance(
|
||||
profile_path: ProfilePathId, // This should be a blank profile
|
||||
launcher_type: ImportLauncherType,
|
||||
base_path: PathBuf,
|
||||
instance_folder: String,
|
||||
) -> crate::Result<()> {
|
||||
tracing::debug!("Importing instance from {instance_folder}");
|
||||
let res = match launcher_type {
|
||||
ImportLauncherType::MultiMC | ImportLauncherType::PrismLauncher => {
|
||||
mmc::import_mmc(
|
||||
base_path, // path to base mmc folder
|
||||
instance_folder, // instance folder in mmc_base_path
|
||||
profile_path.clone(), // path to profile
|
||||
)
|
||||
.await
|
||||
}
|
||||
ImportLauncherType::ATLauncher => {
|
||||
atlauncher::import_atlauncher(
|
||||
base_path, // path to atlauncher folder
|
||||
instance_folder, // instance folder in atlauncher
|
||||
profile_path.clone(), // path to profile
|
||||
)
|
||||
.await
|
||||
}
|
||||
ImportLauncherType::GDLauncher => {
|
||||
gdlauncher::import_gdlauncher(
|
||||
base_path.join("instances").join(instance_folder), // path to gdlauncher folder
|
||||
profile_path.clone(), // path to profile
|
||||
)
|
||||
.await
|
||||
}
|
||||
ImportLauncherType::Curseforge => {
|
||||
curseforge::import_curseforge(
|
||||
base_path.join("Instances").join(instance_folder), // path to curseforge folder
|
||||
profile_path.clone(), // path to profile
|
||||
)
|
||||
.await
|
||||
}
|
||||
ImportLauncherType::Unknown => {
|
||||
return Err(crate::ErrorKind::InputError(
|
||||
"Launcher type Unknown".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
|
||||
// If import failed, delete the profile
|
||||
match res {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
tracing::warn!("Import failed: {:?}", e);
|
||||
let _ = crate::api::profile::remove(&profile_path).await;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Check existing managed packs for potential updates
|
||||
tokio::task::spawn(Profiles::update_modrinth_versions());
|
||||
|
||||
tracing::debug!("Completed import.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the default path for the given launcher type
|
||||
/// None if it can't be found or doesn't exist
|
||||
pub fn get_default_launcher_path(
|
||||
r#type: ImportLauncherType,
|
||||
) -> Option<PathBuf> {
|
||||
let path = match r#type {
|
||||
ImportLauncherType::MultiMC => None, // multimc data is *in* app dir
|
||||
ImportLauncherType::PrismLauncher => {
|
||||
Some(dirs::data_dir()?.join("PrismLauncher"))
|
||||
}
|
||||
ImportLauncherType::ATLauncher => {
|
||||
Some(dirs::data_dir()?.join("ATLauncher"))
|
||||
}
|
||||
ImportLauncherType::GDLauncher => {
|
||||
Some(dirs::data_dir()?.join("gdlauncher_next"))
|
||||
}
|
||||
ImportLauncherType::Curseforge => {
|
||||
Some(dirs::home_dir()?.join("curseforge").join("minecraft"))
|
||||
}
|
||||
ImportLauncherType::Unknown => None,
|
||||
};
|
||||
let path = path?;
|
||||
if path.exists() {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if this PathBuf is a valid instance for the given launcher type
|
||||
#[theseus_macros::debug_pin]
|
||||
#[tracing::instrument]
|
||||
pub async fn is_valid_importable_instance(
|
||||
instance_path: PathBuf,
|
||||
r#type: ImportLauncherType,
|
||||
) -> bool {
|
||||
match r#type {
|
||||
ImportLauncherType::MultiMC | ImportLauncherType::PrismLauncher => {
|
||||
mmc::is_valid_mmc(instance_path).await
|
||||
}
|
||||
ImportLauncherType::ATLauncher => {
|
||||
atlauncher::is_valid_atlauncher(instance_path).await
|
||||
}
|
||||
ImportLauncherType::GDLauncher => {
|
||||
gdlauncher::is_valid_gdlauncher(instance_path).await
|
||||
}
|
||||
ImportLauncherType::Curseforge => {
|
||||
curseforge::is_valid_curseforge(instance_path).await
|
||||
}
|
||||
ImportLauncherType::Unknown => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Caches an image file in the filesystem into the cache directory, and returns the path to the cached file.
|
||||
#[theseus_macros::debug_pin]
|
||||
#[tracing::instrument]
|
||||
pub async fn recache_icon(
|
||||
icon_path: PathBuf,
|
||||
) -> crate::Result<Option<PathBuf>> {
|
||||
let state = crate::State::get().await?;
|
||||
|
||||
let bytes = tokio::fs::read(&icon_path).await;
|
||||
if let Ok(bytes) = bytes {
|
||||
let bytes = bytes::Bytes::from(bytes);
|
||||
let cache_dir = &state.directories.caches_dir();
|
||||
let semaphore = &state.io_semaphore;
|
||||
Ok(Some(
|
||||
fetch::write_cached_icon(
|
||||
&icon_path.to_string_lossy(),
|
||||
cache_dir,
|
||||
bytes,
|
||||
semaphore,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
} else {
|
||||
// could not find icon (for instance, prism default icon, etc)
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn copy_dotminecraft(
|
||||
profile_path_id: ProfilePathId,
|
||||
dotminecraft: PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
existing_loading_bar: Option<LoadingBarId>,
|
||||
) -> crate::Result<LoadingBarId> {
|
||||
// Get full path to profile
|
||||
let profile_path = profile_path_id.get_full_path().await?;
|
||||
|
||||
// Gets all subfiles recursively in src
|
||||
let subfiles = get_all_subfiles(&dotminecraft).await?;
|
||||
let total_subfiles = subfiles.len() as u64;
|
||||
|
||||
let loading_bar = init_or_edit_loading(
|
||||
existing_loading_bar,
|
||||
crate::LoadingBarType::CopyProfile {
|
||||
import_location: dotminecraft.clone(),
|
||||
profile_name: profile_path_id.to_string(),
|
||||
},
|
||||
total_subfiles as f64,
|
||||
"Copying files in profile",
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Copy each file
|
||||
for src_child in subfiles {
|
||||
let dst_child =
|
||||
src_child.strip_prefix(&dotminecraft).map_err(|_| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Invalid file: {}",
|
||||
&src_child.display()
|
||||
))
|
||||
})?;
|
||||
let dst_child = profile_path.join(dst_child);
|
||||
|
||||
// sleep for cpu for 1 millisecond
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||
|
||||
fetch::copy(&src_child, &dst_child, io_semaphore).await?;
|
||||
|
||||
emit_loading(&loading_bar, 1.0, None).await?;
|
||||
}
|
||||
Ok(loading_bar)
|
||||
}
|
||||
|
||||
/// Recursively get a list of all subfiles in src
|
||||
/// uses async recursion
|
||||
#[theseus_macros::debug_pin]
|
||||
#[async_recursion::async_recursion]
|
||||
#[tracing::instrument]
|
||||
pub async fn get_all_subfiles(src: &Path) -> crate::Result<Vec<PathBuf>> {
|
||||
if !src.is_dir() {
|
||||
return Ok(vec![src.to_path_buf()]);
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
let mut dir = io::read_dir(&src).await?;
|
||||
while let Some(child) = dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, src))?
|
||||
{
|
||||
let src_child = child.path();
|
||||
files.append(&mut get_all_subfiles(&src_child).await?);
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
419
packages/app-lib/src/api/pack/install_from.rs
Normal file
419
packages/app-lib/src/api/pack/install_from.rs
Normal file
@@ -0,0 +1,419 @@
|
||||
use crate::config::MODRINTH_API_URL;
|
||||
use crate::data::ModLoader;
|
||||
use crate::event::emit::{emit_loading, init_loading};
|
||||
use crate::event::{LoadingBarId, LoadingBarType};
|
||||
use crate::prelude::ProfilePathId;
|
||||
use crate::state::{
|
||||
LinkedData, ModrinthProject, ModrinthVersion, ProfileInstallStage, SideType,
|
||||
};
|
||||
use crate::util::fetch::{
|
||||
fetch, fetch_advanced, fetch_json, write_cached_icon,
|
||||
};
|
||||
use crate::util::io;
|
||||
use crate::{InnerProjectPathUnix, State};
|
||||
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[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: InnerProjectPathUnix,
|
||||
pub hashes: HashMap<PackFileHash, String>,
|
||||
pub env: Option<HashMap<EnvType, SideType>>,
|
||||
pub downloads: Vec<String>,
|
||||
pub file_size: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
#[serde(rename_all = "camelCase", from = "String")]
|
||||
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 version ID (such as a modpack)
|
||||
FromVersionId {
|
||||
project_id: String,
|
||||
version_id: String,
|
||||
title: String,
|
||||
icon_url: Option<String>,
|
||||
},
|
||||
// Create a pack from a file (such as an .mrpack for installing from a file, or a folder name for importing)
|
||||
FromFile {
|
||||
path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatePackProfile {
|
||||
pub name: String, // the name of the profile, and relative path
|
||||
pub game_version: String, // the game version of the profile
|
||||
pub modloader: ModLoader, // the modloader to use
|
||||
pub loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader. defaults to latest
|
||||
pub icon: Option<PathBuf>, // the icon for the profile
|
||||
pub icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES)
|
||||
pub linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating
|
||||
pub skip_install_profile: Option<bool>,
|
||||
pub no_watch: Option<bool>,
|
||||
}
|
||||
|
||||
// default
|
||||
impl Default for CreatePackProfile {
|
||||
fn default() -> Self {
|
||||
CreatePackProfile {
|
||||
name: "Untitled".to_string(),
|
||||
game_version: "1.19.4".to_string(),
|
||||
modloader: ModLoader::Vanilla,
|
||||
loader_version: None,
|
||||
icon: None,
|
||||
icon_url: None,
|
||||
linked_data: None,
|
||||
skip_install_profile: Some(true),
|
||||
no_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 project_id: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
pub existing_loading_bar: Option<LoadingBarId>,
|
||||
pub profile_path: ProfilePathId,
|
||||
}
|
||||
|
||||
pub fn get_profile_from_pack(
|
||||
location: CreatePackLocation,
|
||||
) -> CreatePackProfile {
|
||||
match location {
|
||||
CreatePackLocation::FromVersionId {
|
||||
project_id,
|
||||
version_id,
|
||||
title,
|
||||
icon_url,
|
||||
} => CreatePackProfile {
|
||||
name: title,
|
||||
icon_url,
|
||||
linked_data: Some(LinkedData {
|
||||
project_id: Some(project_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();
|
||||
|
||||
CreatePackProfile {
|
||||
name: file_name,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn generate_pack_from_version_id(
|
||||
project_id: String,
|
||||
version_id: String,
|
||||
title: String,
|
||||
icon_url: Option<String>,
|
||||
profile_path: ProfilePathId,
|
||||
|
||||
// Existing loading bar. Unlike when existing_loading_bar is used, this one is pre-initialized with PackFileDownload
|
||||
// For example, you might use this if multiple packs are being downloaded at once and you want to use the same loading bar
|
||||
initialized_loading_bar: Option<LoadingBarId>,
|
||||
) -> crate::Result<CreatePack> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let loading_bar = if let Some(bar) = initialized_loading_bar {
|
||||
emit_loading(&bar, 0.0, Some("Downloading pack file")).await?;
|
||||
bar
|
||||
} else {
|
||||
init_loading(
|
||||
LoadingBarType::PackFileDownload {
|
||||
profile_path: profile_path.get_full_path().await?,
|
||||
pack_name: title,
|
||||
icon: icon_url,
|
||||
pack_version: version_id.clone(),
|
||||
},
|
||||
100.0,
|
||||
"Downloading pack file",
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?;
|
||||
let creds = state.credentials.read().await;
|
||||
let version: ModrinthVersion = fetch_json(
|
||||
Method::GET,
|
||||
&format!("{}version/{}", MODRINTH_API_URL, version_id),
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
emit_loading(&loading_bar, 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::InputError(
|
||||
"Specified version has no files".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let file = fetch_advanced(
|
||||
Method::GET,
|
||||
&url,
|
||||
hash.map(|x| &**x),
|
||||
None,
|
||||
None,
|
||||
Some((&loading_bar, 70.0)),
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
emit_loading(&loading_bar, 0.0, Some("Fetching project metadata")).await?;
|
||||
|
||||
let project: ModrinthProject = fetch_json(
|
||||
Method::GET,
|
||||
&format!("{}project/{}", MODRINTH_API_URL, version.project_id),
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
|
||||
emit_loading(&loading_bar, 10.0, Some("Retrieving icon")).await?;
|
||||
let icon = if let Some(icon_url) = project.icon_url {
|
||||
let state = State::get().await?;
|
||||
let icon_bytes =
|
||||
fetch(&icon_url, None, &state.fetch_semaphore, &creds).await?;
|
||||
drop(creds);
|
||||
|
||||
let filename = icon_url.rsplit('/').next();
|
||||
|
||||
if let Some(filename) = filename {
|
||||
Some(
|
||||
write_cached_icon(
|
||||
filename,
|
||||
&state.directories.caches_dir(),
|
||||
icon_bytes,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
|
||||
Ok(CreatePack {
|
||||
file,
|
||||
description: CreatePackDescription {
|
||||
icon,
|
||||
override_title: None,
|
||||
project_id: Some(project_id),
|
||||
version_id: Some(version_id),
|
||||
existing_loading_bar: Some(loading_bar),
|
||||
profile_path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn generate_pack_from_file(
|
||||
path: PathBuf,
|
||||
profile_path: ProfilePathId,
|
||||
) -> crate::Result<CreatePack> {
|
||||
let file = io::read(&path).await?;
|
||||
Ok(CreatePack {
|
||||
file: bytes::Bytes::from(file),
|
||||
description: CreatePackDescription {
|
||||
icon: None,
|
||||
override_title: None,
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
existing_loading_bar: None,
|
||||
profile_path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets generated profile attributes to the pack ones (using profile::edit)
|
||||
/// This includes the pack name, icon, game version, loader version, and loader
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn set_profile_information(
|
||||
profile_path: ProfilePathId,
|
||||
description: &CreatePackDescription,
|
||||
backup_name: &str,
|
||||
dependencies: &HashMap<PackDependency, String>,
|
||||
ignore_lock: bool, // do not change locked status
|
||||
) -> crate::Result<()> {
|
||||
let mut game_version: Option<&String> = None;
|
||||
let mut mod_loader = None;
|
||||
let mut loader_version = None;
|
||||
|
||||
for (key, value) in dependencies {
|
||||
match key {
|
||||
PackDependency::Forge => {
|
||||
mod_loader = Some(ModLoader::Forge);
|
||||
loader_version = Some(value);
|
||||
}
|
||||
PackDependency::NeoForge => {
|
||||
mod_loader = Some(ModLoader::NeoForge);
|
||||
loader_version = Some(value);
|
||||
}
|
||||
PackDependency::FabricLoader => {
|
||||
mod_loader = Some(ModLoader::Fabric);
|
||||
loader_version = Some(value);
|
||||
}
|
||||
PackDependency::QuiltLoader => {
|
||||
mod_loader = Some(ModLoader::Quilt);
|
||||
loader_version = Some(value);
|
||||
}
|
||||
PackDependency::Minecraft => game_version = Some(value),
|
||||
}
|
||||
}
|
||||
|
||||
let game_version = if let Some(game_version) = game_version {
|
||||
game_version
|
||||
} else {
|
||||
return Err(crate::ErrorKind::InputError(
|
||||
"Pack did not specify Minecraft version".to_string(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla);
|
||||
let loader_version = if mod_loader != ModLoader::Vanilla {
|
||||
crate::profile::create::get_loader_version_from_loader(
|
||||
game_version.clone(),
|
||||
mod_loader,
|
||||
loader_version.cloned(),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Sets values in profile
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.metadata.name = description
|
||||
.override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
|
||||
let project_id = description.project_id.clone();
|
||||
let version_id = description.version_id.clone();
|
||||
|
||||
prof.metadata.linked_data = if project_id.is_some()
|
||||
&& version_id.is_some()
|
||||
{
|
||||
Some(LinkedData {
|
||||
project_id,
|
||||
version_id,
|
||||
locked: if !ignore_lock {
|
||||
Some(true)
|
||||
} else {
|
||||
prof.metadata.linked_data.as_ref().and_then(|x| x.locked)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
prof.metadata.icon = description.icon.clone();
|
||||
prof.metadata.game_version = game_version.clone();
|
||||
prof.metadata.loader_version = loader_version.clone();
|
||||
prof.metadata.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
430
packages/app-lib/src/api/pack/install_mrpack.rs
Normal file
430
packages/app-lib/src/api/pack/install_mrpack.rs
Normal file
@@ -0,0 +1,430 @@
|
||||
use crate::config::MODRINTH_API_URL;
|
||||
use crate::event::emit::{
|
||||
emit_loading, init_or_edit_loading, loading_try_for_each_concurrent,
|
||||
};
|
||||
use crate::event::LoadingBarType;
|
||||
use crate::pack::install_from::{
|
||||
set_profile_information, EnvType, PackFile, PackFileHash,
|
||||
};
|
||||
use crate::prelude::{ModrinthVersion, ProfilePathId, ProjectMetadata};
|
||||
use crate::state::{ProfileInstallStage, Profiles, SideType};
|
||||
use crate::util::fetch::{fetch_json, fetch_mirrors, write};
|
||||
use crate::util::io;
|
||||
use crate::{profile, 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::install_from::{
|
||||
generate_pack_from_file, generate_pack_from_version_id, CreatePack,
|
||||
CreatePackLocation, PackFormat,
|
||||
};
|
||||
|
||||
/// 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 profile (fail safely)
|
||||
/// Install a modpack from a mrpack file (a modrinth .zip format)
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn install_zipped_mrpack(
|
||||
location: CreatePackLocation,
|
||||
profile_path: ProfilePathId,
|
||||
) -> crate::Result<ProfilePathId> {
|
||||
// Get file from description
|
||||
let create_pack: CreatePack = match location {
|
||||
CreatePackLocation::FromVersionId {
|
||||
project_id,
|
||||
version_id,
|
||||
title,
|
||||
icon_url,
|
||||
} => {
|
||||
generate_pack_from_version_id(
|
||||
project_id,
|
||||
version_id,
|
||||
title,
|
||||
icon_url,
|
||||
profile_path.clone(),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
CreatePackLocation::FromFile { path } => {
|
||||
generate_pack_from_file(path, profile_path.clone()).await?
|
||||
}
|
||||
};
|
||||
|
||||
// Install pack files, and if it fails, fail safely by removing the profile
|
||||
let result = install_zipped_mrpack_files(create_pack, false).await;
|
||||
|
||||
// Check existing managed packs for potential updates
|
||||
tokio::task::spawn(Profiles::update_modrinth_versions());
|
||||
|
||||
match result {
|
||||
Ok(profile) => Ok(profile),
|
||||
Err(err) => {
|
||||
let _ = crate::api::profile::remove(&profile_path).await;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Install all pack files from a description
|
||||
/// Does not remove the profile if it fails
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn install_zipped_mrpack_files(
|
||||
create_pack: CreatePack,
|
||||
ignore_lock: bool,
|
||||
) -> crate::Result<ProfilePathId> {
|
||||
let state = &State::get().await?;
|
||||
|
||||
let file = create_pack.file;
|
||||
let description = create_pack.description.clone(); // make a copy for profile edit function
|
||||
let icon = create_pack.description.icon;
|
||||
let project_id = create_pack.description.project_id;
|
||||
let version_id = create_pack.description.version_id;
|
||||
let existing_loading_bar = create_pack.description.existing_loading_bar;
|
||||
let profile_path = create_pack.description.profile_path;
|
||||
let icon_exists = icon.is_some();
|
||||
|
||||
let reader: Cursor<&bytes::Bytes> = Cursor::new(&file);
|
||||
|
||||
// Create zip reader around file
|
||||
let mut zip_reader =
|
||||
ZipFileReader::with_tokio(reader).await.map_err(|_| {
|
||||
crate::Error::from(crate::ErrorKind::InputError(
|
||||
"Failed to read input modpack zip".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
// Extract index of modrinth.index.json
|
||||
let zip_index_option = zip_reader.file().entries().iter().position(|f| {
|
||||
f.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(crate::ErrorKind::InputError(
|
||||
"Pack does not support Minecraft".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Sets generated profile attributes to the pack ones (using profile::edit)
|
||||
set_profile_information(
|
||||
profile_path.clone(),
|
||||
&description,
|
||||
&pack.name,
|
||||
&pack.dependencies,
|
||||
ignore_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let profile_path = profile_path.clone();
|
||||
let loading_bar = init_or_edit_loading(
|
||||
existing_loading_bar,
|
||||
LoadingBarType::PackDownload {
|
||||
profile_path: profile_path.get_full_path().await?.clone(),
|
||||
pack_name: pack.name.clone(),
|
||||
icon,
|
||||
pack_id: project_id,
|
||||
pack_version: version_id,
|
||||
},
|
||||
100.0,
|
||||
"Downloading modpack",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let num_files = pack.files.len();
|
||||
use futures::StreamExt;
|
||||
loading_try_for_each_concurrent(
|
||||
futures::stream::iter(pack.files.into_iter())
|
||||
.map(Ok::<PackFile, crate::Error>),
|
||||
None,
|
||||
Some(&loading_bar),
|
||||
70.0,
|
||||
num_files,
|
||||
None,
|
||||
|project| {
|
||||
let profile_path = profile_path.clone();
|
||||
async move {
|
||||
//TODO: Future update: prompt user for optional files in a modpack
|
||||
if let Some(env) = project.env {
|
||||
if env
|
||||
.get(&EnvType::Client)
|
||||
.map(|x| x == &SideType::Unsupported)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let creds = state.credentials.read().await;
|
||||
let file = fetch_mirrors(
|
||||
&project
|
||||
.downloads
|
||||
.iter()
|
||||
.map(|x| &**x)
|
||||
.collect::<Vec<&str>>(),
|
||||
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
drop(creds);
|
||||
|
||||
let project_path = project.path.to_string();
|
||||
|
||||
let path =
|
||||
std::path::Path::new(&project_path).components().next();
|
||||
if let Some(path) = path {
|
||||
match path {
|
||||
Component::CurDir | Component::Normal(_) => {
|
||||
let path = profile_path
|
||||
.get_full_path()
|
||||
.await?
|
||||
.join(&project_path);
|
||||
write(&path, &file, &state.io_semaphore)
|
||||
.await?;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
emit_loading(&loading_bar, 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('/')
|
||||
{
|
||||
// Reads the file into the 'content' variable
|
||||
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(
|
||||
&profile_path.get_full_path().await?.join(new_path),
|
||||
&content,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
emit_loading(
|
||||
&loading_bar,
|
||||
30.0 / total_len as f64,
|
||||
Some(&format!(
|
||||
"Extracting override {}/{}",
|
||||
index, total_len
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// If the icon doesn't exist, we expect icon.png to be a potential icon.
|
||||
// If it doesn't exist, and an override to icon.png exists, cache and use that
|
||||
let potential_icon =
|
||||
profile_path.get_full_path().await?.join("icon.png");
|
||||
if !icon_exists && potential_icon.exists() {
|
||||
profile::edit_icon(&profile_path, Some(&potential_icon)).await?;
|
||||
}
|
||||
|
||||
if let Some(profile_val) = profile::get(&profile_path, None).await? {
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
Some(loading_bar),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
Ok::<ProfilePathId, crate::Error>(profile_path.clone())
|
||||
} else {
|
||||
Err(crate::Error::from(crate::ErrorKind::InputError(
|
||||
"No pack manifest found in mrpack".to_string(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(mrpack_file))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn remove_all_related_files(
|
||||
profile_path: ProfilePathId,
|
||||
mrpack_file: bytes::Bytes,
|
||||
) -> crate::Result<()> {
|
||||
let reader: Cursor<&bytes::Bytes> = Cursor::new(&mrpack_file);
|
||||
|
||||
// Create zip reader around file
|
||||
let mut zip_reader =
|
||||
ZipFileReader::with_tokio(reader).await.map_err(|_| {
|
||||
crate::Error::from(crate::ErrorKind::InputError(
|
||||
"Failed to read input modpack zip".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
// Extract index of modrinth.index.json
|
||||
let zip_index_option = zip_reader.file().entries().iter().position(|f| {
|
||||
f.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(crate::ErrorKind::InputError(
|
||||
"Pack does not support Minecraft".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Set install stage to installing, and do not change it back (as files are being removed and are not being reinstalled here)
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
// First, remove all modrinth projects by their version hashes
|
||||
// Remove all modrinth projects by their version hashes
|
||||
// We need to do a fetch to get the project ids from Modrinth
|
||||
let state = State::get().await?;
|
||||
let all_hashes = pack
|
||||
.files
|
||||
.iter()
|
||||
.filter_map(|f| Some(f.hashes.get(&PackFileHash::Sha512)?.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let creds = state.credentials.read().await;
|
||||
|
||||
// First, get project info by hash
|
||||
let files_url = format!("{}version_files", MODRINTH_API_URL);
|
||||
|
||||
let hash_projects = fetch_json::<HashMap<String, ModrinthVersion>>(
|
||||
Method::POST,
|
||||
&files_url,
|
||||
None,
|
||||
Some(json!({
|
||||
"hashes": all_hashes,
|
||||
"algorithm": "sha512",
|
||||
})),
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
)
|
||||
.await?;
|
||||
let to_remove = hash_projects
|
||||
.into_values()
|
||||
.map(|p| p.project_id)
|
||||
.collect::<Vec<_>>();
|
||||
let profile =
|
||||
profile::get(&profile_path, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_path.to_string(),
|
||||
)
|
||||
})?;
|
||||
for (project_id, project) in &profile.projects {
|
||||
if let ProjectMetadata::Modrinth { project, .. } = &project.metadata
|
||||
{
|
||||
if to_remove.contains(&project.id) {
|
||||
let path = profile
|
||||
.get_profile_full_path()
|
||||
.await?
|
||||
.join(project_id.0.clone());
|
||||
if path.exists() {
|
||||
io::remove_file(&path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over all Modrinth project file paths in the json, and remove them
|
||||
// (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized)
|
||||
for file in pack.files {
|
||||
let path: PathBuf = profile_path
|
||||
.get_full_path()
|
||||
.await?
|
||||
.join(file.path.to_string());
|
||||
if path.exists() {
|
||||
io::remove_file(&path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over each 'overrides' file and remove it
|
||||
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);
|
||||
}
|
||||
|
||||
// Remove this file if a corresponding one exists in the filesystem
|
||||
let existing_file =
|
||||
profile_path.get_full_path().await?.join(&new_path);
|
||||
if existing_file.exists() {
|
||||
io::remove_file(&existing_file).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(crate::Error::from(crate::ErrorKind::InputError(
|
||||
"No pack manifest found in mrpack".to_string(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
3
packages/app-lib/src/api/pack/mod.rs
Normal file
3
packages/app-lib/src/api/pack/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod import;
|
||||
pub mod install_from;
|
||||
pub mod install_mrpack;
|
||||
130
packages/app-lib/src/api/process.rs
Normal file
130
packages/app-lib/src/api/process.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
//! Theseus process management interface
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::state::{MinecraftChild, ProfilePathId};
|
||||
pub use crate::{
|
||||
state::{
|
||||
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
|
||||
},
|
||||
State,
|
||||
};
|
||||
|
||||
// Gets whether a child process stored in the state by UUID has finished
|
||||
#[tracing::instrument]
|
||||
pub async fn has_finished_by_uuid(uuid: Uuid) -> crate::Result<bool> {
|
||||
Ok(get_exit_status_by_uuid(uuid).await?.is_some())
|
||||
}
|
||||
|
||||
// Gets the exit status of a child process stored in the state by UUID
|
||||
#[tracing::instrument]
|
||||
pub async fn get_exit_status_by_uuid(
|
||||
uuid: Uuid,
|
||||
) -> crate::Result<Option<i32>> {
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
children.exit_status(uuid).await
|
||||
}
|
||||
|
||||
// Gets the UUID of each stored process in the state
|
||||
#[tracing::instrument]
|
||||
pub async fn get_all_uuids() -> crate::Result<Vec<Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
Ok(children.keys())
|
||||
}
|
||||
|
||||
// Gets the UUID of each *running* stored process in the state
|
||||
#[tracing::instrument]
|
||||
pub async fn get_all_running_uuids() -> crate::Result<Vec<Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
children.running_keys().await
|
||||
}
|
||||
|
||||
// Gets the Profile paths of each *running* stored process in the state
|
||||
#[tracing::instrument]
|
||||
pub async fn get_all_running_profile_paths() -> crate::Result<Vec<ProfilePathId>>
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
children.running_profile_paths().await
|
||||
}
|
||||
|
||||
// Gets the Profiles (cloned) of each *running* stored process in the state
|
||||
#[tracing::instrument]
|
||||
pub async fn get_all_running_profiles() -> crate::Result<Vec<Profile>> {
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
children.running_profiles().await
|
||||
}
|
||||
|
||||
// Gets the UUID of each stored process in the state by profile path
|
||||
#[tracing::instrument]
|
||||
pub async fn get_uuids_by_profile_path(
|
||||
profile_path: ProfilePathId,
|
||||
) -> crate::Result<Vec<Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
children.running_keys_with_profile(profile_path).await
|
||||
}
|
||||
|
||||
// Kill a child process stored in the state by UUID, as a string
|
||||
#[tracing::instrument]
|
||||
pub async fn kill_by_uuid(uuid: Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
if let Some(mchild) = children.get(uuid) {
|
||||
let mut mchild = mchild.write().await;
|
||||
kill(&mut mchild).await
|
||||
} else {
|
||||
// No error returned for already finished process
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for a child process stored in the state by UUID
|
||||
#[tracing::instrument]
|
||||
pub async fn wait_for_by_uuid(uuid: Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
// No error returned for already killed process
|
||||
if let Some(mchild) = children.get(uuid) {
|
||||
let mut mchild = mchild.write().await;
|
||||
wait_for(&mut mchild).await
|
||||
} else {
|
||||
// No error returned for already finished process
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Kill a running child process directly
|
||||
#[tracing::instrument(skip(running))]
|
||||
pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> {
|
||||
running.current_child.write().await.kill().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Await on the completion of a child process directly
|
||||
#[tracing::instrument(skip(running))]
|
||||
pub async fn wait_for(running: &mut MinecraftChild) -> crate::Result<()> {
|
||||
// We do not wait on the Child directly, but wait on the thread manager.
|
||||
// This way we can still run all cleanup hook functions that happen after.
|
||||
running
|
||||
.manager
|
||||
.take()
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Process manager already completed or missing for process {}",
|
||||
running.uuid
|
||||
))
|
||||
})?
|
||||
.await?
|
||||
.map_err(|err| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Error running minecraft: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
315
packages/app-lib/src/api/profile/create.rs
Normal file
315
packages/app-lib/src/api/profile/create.rs
Normal file
@@ -0,0 +1,315 @@
|
||||
//! Theseus profile management interface
|
||||
use crate::pack::install_from::CreatePackProfile;
|
||||
use crate::prelude::ProfilePathId;
|
||||
use crate::state::LinkedData;
|
||||
use crate::util::io::{self, canonicalize};
|
||||
use crate::{
|
||||
event::{emit::emit_profile, ProfilePayloadType},
|
||||
prelude::ModLoader,
|
||||
};
|
||||
use crate::{pack, profile, ErrorKind};
|
||||
pub use crate::{
|
||||
state::{JavaSettings, Profile},
|
||||
State,
|
||||
};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tracing::{info, trace};
|
||||
use uuid::Uuid;
|
||||
|
||||
// Creates a profile of a given name and adds it to the in-memory state
|
||||
// Returns relative filepath as ProfilePathId which can be used to access it in the State
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn profile_create(
|
||||
mut name: String, // the name of the profile, and relative path
|
||||
game_version: String, // the game version of the profile
|
||||
modloader: ModLoader, // the modloader to use
|
||||
loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader. defaults to latest
|
||||
icon: Option<PathBuf>, // the icon for the profile
|
||||
icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES)
|
||||
linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating
|
||||
skip_install_profile: Option<bool>,
|
||||
no_watch: Option<bool>,
|
||||
) -> crate::Result<ProfilePathId> {
|
||||
name = profile::sanitize_profile_name(&name);
|
||||
|
||||
trace!("Creating new profile. {}", name);
|
||||
let state = State::get().await?;
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
let mut path = state.directories.profiles_dir().await.join(&name);
|
||||
|
||||
if path.exists() {
|
||||
let mut new_name;
|
||||
let mut new_path;
|
||||
let mut which = 1;
|
||||
loop {
|
||||
new_name = format!("{name} ({which})");
|
||||
new_path = state.directories.profiles_dir().await.join(&new_name);
|
||||
if !new_path.exists() {
|
||||
break;
|
||||
}
|
||||
which += 1;
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"Folder collision: {}, renaming to: {}",
|
||||
path.display(),
|
||||
new_path.display()
|
||||
);
|
||||
path = new_path;
|
||||
name = new_name;
|
||||
}
|
||||
io::create_dir_all(&path).await?;
|
||||
|
||||
info!(
|
||||
"Creating profile at path {}",
|
||||
&canonicalize(&path)?.display()
|
||||
);
|
||||
let loader = if modloader != ModLoader::Vanilla {
|
||||
get_loader_version_from_loader(
|
||||
game_version.clone(),
|
||||
modloader,
|
||||
loader_version,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut profile = Profile::new(uuid, name, game_version).await?;
|
||||
let result = async {
|
||||
if let Some(ref icon) = icon {
|
||||
let bytes =
|
||||
io::read(state.directories.caches_dir().join(icon)).await?;
|
||||
profile
|
||||
.set_icon(
|
||||
&state.directories.caches_dir(),
|
||||
&state.io_semaphore,
|
||||
bytes::Bytes::from(bytes),
|
||||
&icon.to_string_lossy(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
profile.metadata.icon_url = icon_url;
|
||||
if let Some(loader_version) = loader {
|
||||
profile.metadata.loader = modloader;
|
||||
profile.metadata.loader_version = Some(loader_version);
|
||||
}
|
||||
|
||||
profile.metadata.linked_data = linked_data;
|
||||
if let Some(linked_data) = &mut profile.metadata.linked_data {
|
||||
linked_data.locked = Some(
|
||||
linked_data.project_id.is_some()
|
||||
&& linked_data.version_id.is_some(),
|
||||
);
|
||||
}
|
||||
|
||||
emit_profile(
|
||||
uuid,
|
||||
&profile.profile_id(),
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Created,
|
||||
)
|
||||
.await?;
|
||||
|
||||
{
|
||||
let mut profiles = state.profiles.write().await;
|
||||
profiles
|
||||
.insert(profile.clone(), no_watch.unwrap_or_default())
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !skip_install_profile.unwrap_or(false) {
|
||||
crate::launcher::install_minecraft(&profile, None, false).await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
|
||||
Ok(profile.profile_id())
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(profile) => Ok(profile),
|
||||
Err(err) => {
|
||||
let _ = crate::api::profile::remove(&profile.profile_id()).await;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn profile_create_from_creator(
|
||||
profile: CreatePackProfile,
|
||||
) -> crate::Result<ProfilePathId> {
|
||||
profile_create(
|
||||
profile.name,
|
||||
profile.game_version,
|
||||
profile.modloader,
|
||||
profile.loader_version,
|
||||
profile.icon,
|
||||
profile.icon_url,
|
||||
profile.linked_data,
|
||||
profile.skip_install_profile,
|
||||
profile.no_watch,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn profile_create_from_duplicate(
|
||||
copy_from: ProfilePathId,
|
||||
) -> crate::Result<ProfilePathId> {
|
||||
// Original profile
|
||||
let profile = profile::get(©_from, None).await?.ok_or_else(|| {
|
||||
ErrorKind::UnmanagedProfileError(copy_from.to_string())
|
||||
})?;
|
||||
|
||||
let profile_path_id = profile_create(
|
||||
profile.metadata.name.clone(),
|
||||
profile.metadata.game_version.clone(),
|
||||
profile.metadata.loader,
|
||||
profile.metadata.loader_version.clone().map(|it| it.id),
|
||||
profile.metadata.icon.clone(),
|
||||
profile.metadata.icon_url.clone(),
|
||||
profile.metadata.linked_data.clone(),
|
||||
Some(true),
|
||||
Some(true),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Copy it over using the import system (essentially importing from the same profile)
|
||||
let state = State::get().await?;
|
||||
let bar = pack::import::copy_dotminecraft(
|
||||
profile_path_id.clone(),
|
||||
copy_from.get_full_path().await?,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let duplicated_profile =
|
||||
profile::get(&profile_path_id, None).await?.ok_or_else(|| {
|
||||
ErrorKind::UnmanagedProfileError(profile_path_id.to_string())
|
||||
})?;
|
||||
|
||||
crate::launcher::install_minecraft(&duplicated_profile, Some(bar), false)
|
||||
.await?;
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(
|
||||
&profile.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// emit profile edited
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
&profile.profile_id(),
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
.await?;
|
||||
State::sync().await?;
|
||||
Ok(profile_path_id)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub(crate) async fn get_loader_version_from_loader(
|
||||
game_version: String,
|
||||
loader: ModLoader,
|
||||
loader_version: Option<String>,
|
||||
) -> crate::Result<Option<LoaderVersion>> {
|
||||
let state = State::get().await?;
|
||||
let metadata = state.metadata.read().await;
|
||||
|
||||
let version = loader_version.unwrap_or_else(|| "latest".to_string());
|
||||
|
||||
let filter = |it: &LoaderVersion| match version.as_str() {
|
||||
"latest" => true,
|
||||
"stable" => it.stable,
|
||||
id => {
|
||||
it.id == *id
|
||||
|| format!("{}-{}", game_version, id) == it.id
|
||||
|| format!("{}-{}-{}", game_version, id, game_version) == it.id
|
||||
}
|
||||
};
|
||||
|
||||
let loader_data = match loader {
|
||||
ModLoader::Forge => &metadata.forge,
|
||||
ModLoader::Fabric => &metadata.fabric,
|
||||
ModLoader::Quilt => &metadata.quilt,
|
||||
ModLoader::NeoForge => &metadata.neoforge,
|
||||
_ => {
|
||||
return Err(
|
||||
ProfileCreationError::NoManifest(loader.to_string()).into()
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let loaders = &loader_data
|
||||
.game_versions
|
||||
.iter()
|
||||
.find(|it| {
|
||||
it.id
|
||||
.replace(daedalus::modded::DUMMY_REPLACE_STRING, &game_version)
|
||||
== game_version
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
ProfileCreationError::ModloaderUnsupported(
|
||||
loader.to_string(),
|
||||
game_version.clone(),
|
||||
)
|
||||
})?
|
||||
.loaders;
|
||||
|
||||
let loader_version = loaders
|
||||
.iter()
|
||||
.find(|&x| filter(x))
|
||||
.cloned()
|
||||
.or(
|
||||
// If stable was searched for but not found, return latest by default
|
||||
if version == "stable" {
|
||||
loaders.iter().next().cloned()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
ProfileCreationError::InvalidVersionModloader(
|
||||
version,
|
||||
loader.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Some(loader_version))
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ProfileCreationError {
|
||||
#[error("Profile .json exists: {0}")]
|
||||
ProfileExistsError(PathBuf),
|
||||
#[error("Modloader {0} unsupported for Minecraft version {1}")]
|
||||
ModloaderUnsupported(String, String),
|
||||
#[error("Invalid version {0} for modloader {1}")]
|
||||
InvalidVersionModloader(String, String),
|
||||
#[error("Could not get manifest for loader {0}. This is a bug in the GUI")]
|
||||
NoManifest(String),
|
||||
#[error("Could not get State.")]
|
||||
NoState,
|
||||
|
||||
#[error("Attempted to create project in something other than a folder.")]
|
||||
NotFolder,
|
||||
#[error("You are trying to create a profile in a non-empty directory")]
|
||||
NotEmptyFolder,
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
}
|
||||
1082
packages/app-lib/src/api/profile/mod.rs
Normal file
1082
packages/app-lib/src/api/profile/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
233
packages/app-lib/src/api/profile/update.rs
Normal file
233
packages/app-lib/src/api/profile/update.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
use crate::{
|
||||
event::{
|
||||
emit::{emit_profile, init_loading, loading_try_for_each_concurrent},
|
||||
ProfilePayloadType,
|
||||
},
|
||||
pack::{self, install_from::generate_pack_from_version_id},
|
||||
prelude::{ProfilePathId, ProjectPathId},
|
||||
profile::get,
|
||||
state::{ProfileInstallStage, Project},
|
||||
LoadingBarType, State,
|
||||
};
|
||||
use futures::try_join;
|
||||
|
||||
/// Updates a managed modrinth pack to the version specified by new_version_id
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn update_managed_modrinth_version(
|
||||
profile_path: &ProfilePathId,
|
||||
new_version_id: &String,
|
||||
) -> crate::Result<()> {
|
||||
let profile = get(profile_path, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let unmanaged_err = || {
|
||||
crate::ErrorKind::InputError(
|
||||
format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path),
|
||||
)
|
||||
};
|
||||
|
||||
// Extract modrinth pack information, if appropriate
|
||||
let linked_data = profile
|
||||
.metadata
|
||||
.linked_data
|
||||
.as_ref()
|
||||
.ok_or_else(unmanaged_err)?;
|
||||
let project_id: &String =
|
||||
linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?;
|
||||
let version_id =
|
||||
linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?;
|
||||
|
||||
// Replace the pack with the new version
|
||||
replace_managed_modrinth(
|
||||
profile_path,
|
||||
&profile,
|
||||
project_id,
|
||||
version_id,
|
||||
Some(new_version_id),
|
||||
true, // switching versions should ignore the lock
|
||||
)
|
||||
.await?;
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
.await?;
|
||||
|
||||
State::sync().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repair a managed modrinth pack by 'updating' it to the current version
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn repair_managed_modrinth(
|
||||
profile_path: &ProfilePathId,
|
||||
) -> crate::Result<()> {
|
||||
let profile = get(profile_path, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let unmanaged_err = || {
|
||||
crate::ErrorKind::InputError(
|
||||
format!("Profile at {} is not a managed modrinth pack, or has been disconnected.", profile_path),
|
||||
)
|
||||
};
|
||||
|
||||
// For repairing specifically, first we remove all installed projects (to ensure we do remove ones that aren't in the pack)
|
||||
// We do a project removal followed by removing everything in the .mrpack, to ensure we only
|
||||
// remove relevant projects and not things like save files
|
||||
let projects_map = profile.projects.clone();
|
||||
let stream = futures::stream::iter(
|
||||
projects_map
|
||||
.into_iter()
|
||||
.map(Ok::<(ProjectPathId, Project), crate::Error>),
|
||||
);
|
||||
loading_try_for_each_concurrent(
|
||||
stream,
|
||||
None,
|
||||
None,
|
||||
0.0,
|
||||
0,
|
||||
None,
|
||||
|(project_id, _)| {
|
||||
let profile = profile.clone();
|
||||
async move {
|
||||
profile.remove_project(&project_id, Some(true)).await?;
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Extract modrinth pack information, if appropriate
|
||||
let linked_data = profile
|
||||
.metadata
|
||||
.linked_data
|
||||
.as_ref()
|
||||
.ok_or_else(unmanaged_err)?;
|
||||
let project_id: &String =
|
||||
linked_data.project_id.as_ref().ok_or_else(unmanaged_err)?;
|
||||
let version_id =
|
||||
linked_data.version_id.as_ref().ok_or_else(unmanaged_err)?;
|
||||
|
||||
// Replace the pack with the same version
|
||||
replace_managed_modrinth(
|
||||
profile_path,
|
||||
&profile,
|
||||
project_id,
|
||||
version_id,
|
||||
None,
|
||||
false, // do not ignore lock, as repairing can reset the lock
|
||||
)
|
||||
.await?;
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
.await?;
|
||||
|
||||
State::sync().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replace a managed modrinth pack with a new version
|
||||
/// If new_version_id is None, the pack is 'reinstalled' in-place
|
||||
#[tracing::instrument(skip(profile))]
|
||||
#[theseus_macros::debug_pin]
|
||||
async fn replace_managed_modrinth(
|
||||
profile_path: &ProfilePathId,
|
||||
profile: &crate::state::Profile,
|
||||
project_id: &String,
|
||||
version_id: &String,
|
||||
new_version_id: Option<&String>,
|
||||
ignore_lock: bool,
|
||||
) -> crate::Result<()> {
|
||||
crate::profile::edit(profile_path, |profile| {
|
||||
profile.install_stage = ProfileInstallStage::Installing;
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Fetch .mrpacks for both old and new versions
|
||||
// TODO: this will need to be updated if we revert the hacky pack method we needed for compiler speed
|
||||
|
||||
let (old_pack_creator, new_pack_creator) =
|
||||
if let Some(new_version_id) = new_version_id {
|
||||
let shared_loading_bar = init_loading(
|
||||
LoadingBarType::PackFileDownload {
|
||||
profile_path: profile_path.get_full_path().await?,
|
||||
pack_name: profile.metadata.name.clone(),
|
||||
icon: None,
|
||||
pack_version: version_id.clone(),
|
||||
},
|
||||
200.0, // These two downloads will share the same loading bar
|
||||
"Downloading pack file",
|
||||
)
|
||||
.await?;
|
||||
|
||||
// download in parallel, then join.
|
||||
try_join!(
|
||||
generate_pack_from_version_id(
|
||||
project_id.clone(),
|
||||
version_id.clone(),
|
||||
profile.metadata.name.clone(),
|
||||
None,
|
||||
profile_path.clone(),
|
||||
Some(shared_loading_bar.clone())
|
||||
),
|
||||
generate_pack_from_version_id(
|
||||
project_id.clone(),
|
||||
new_version_id.clone(),
|
||||
profile.metadata.name.clone(),
|
||||
None,
|
||||
profile_path.clone(),
|
||||
Some(shared_loading_bar)
|
||||
)
|
||||
)?
|
||||
} else {
|
||||
// If new_version_id is None, we don't need to download the new pack, so we clone the old one
|
||||
let mut old_pack_creator = generate_pack_from_version_id(
|
||||
project_id.clone(),
|
||||
version_id.clone(),
|
||||
profile.metadata.name.clone(),
|
||||
None,
|
||||
profile_path.clone(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
old_pack_creator.description.existing_loading_bar = None;
|
||||
(old_pack_creator.clone(), old_pack_creator)
|
||||
};
|
||||
|
||||
// Removal - remove all files that were added by the old pack
|
||||
// - remove all installed projects
|
||||
// - remove all overrides
|
||||
pack::install_mrpack::remove_all_related_files(
|
||||
profile_path.clone(),
|
||||
old_pack_creator.file,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Reinstallation - install all files that are added by the new pack
|
||||
// - install all projects
|
||||
// - install all overrides
|
||||
// - edits the profile to update the new data
|
||||
// - (functionals almost identically to rteinstalling the pack 'in-place')
|
||||
pack::install_mrpack::install_zipped_mrpack_files(
|
||||
new_pack_creator,
|
||||
ignore_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
5
packages/app-lib/src/api/safety.rs
Normal file
5
packages/app-lib/src/api/safety.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use crate::state::{ProcessType, SafeProcesses};
|
||||
|
||||
pub async fn check_safe_loading_bars() -> crate::Result<bool> {
|
||||
SafeProcesses::is_complete(ProcessType::LoadingBar).await
|
||||
}
|
||||
246
packages/app-lib/src/api/settings.rs
Normal file
246
packages/app-lib/src/api/settings.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
//! Theseus profile management interface
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
use io::IOError;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
event::emit::{emit_loading, init_loading},
|
||||
prelude::DirectoryInfo,
|
||||
state::{self, Profiles},
|
||||
util::{fetch, io},
|
||||
};
|
||||
pub use crate::{
|
||||
state::{
|
||||
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
|
||||
},
|
||||
State,
|
||||
};
|
||||
|
||||
/// Gets entire settings
|
||||
#[tracing::instrument]
|
||||
pub async fn get() -> crate::Result<Settings> {
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
Ok(settings.clone())
|
||||
}
|
||||
|
||||
/// Sets entire settings
|
||||
#[tracing::instrument]
|
||||
pub async fn set(settings: Settings) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
|
||||
if settings.loaded_config_dir
|
||||
!= state.settings.read().await.loaded_config_dir
|
||||
{
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Cannot change config directory as setting".to_string(),
|
||||
)
|
||||
.as_error());
|
||||
}
|
||||
|
||||
let (reset_io, reset_fetch) = async {
|
||||
let read = state.settings.read().await;
|
||||
(
|
||||
settings.max_concurrent_writes != read.max_concurrent_writes,
|
||||
settings.max_concurrent_downloads != read.max_concurrent_downloads,
|
||||
)
|
||||
}
|
||||
.await;
|
||||
|
||||
let updated_discord_rpc = {
|
||||
let read = state.settings.read().await;
|
||||
settings.disable_discord_rpc != read.disable_discord_rpc
|
||||
};
|
||||
|
||||
{
|
||||
*state.settings.write().await = settings;
|
||||
}
|
||||
|
||||
if updated_discord_rpc {
|
||||
state.discord_rpc.clear_to_default(true).await?;
|
||||
}
|
||||
|
||||
if reset_io {
|
||||
state.reset_io_semaphore().await;
|
||||
}
|
||||
if reset_fetch {
|
||||
state.reset_fetch_semaphore().await;
|
||||
}
|
||||
|
||||
State::sync().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the new config dir, the location of all Theseus data except for the settings.json and caches
|
||||
/// Takes control of the entire state and blocks until completion
|
||||
pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
|
||||
tracing::trace!("Changing config dir to: {}", new_config_dir.display());
|
||||
if !new_config_dir.is_dir() {
|
||||
return Err(crate::ErrorKind::FSError(format!(
|
||||
"New config dir is not a folder: {}",
|
||||
new_config_dir.display()
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
|
||||
if !is_dir_writeable(new_config_dir.clone()).await? {
|
||||
return Err(crate::ErrorKind::FSError(format!(
|
||||
"New config dir is not writeable: {}",
|
||||
new_config_dir.display()
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
|
||||
let loading_bar = init_loading(
|
||||
crate::LoadingBarType::ConfigChange {
|
||||
new_path: new_config_dir.clone(),
|
||||
},
|
||||
100.0,
|
||||
"Changing configuration directory",
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::trace!("Changing config dir, taking control of the state");
|
||||
// Take control of the state
|
||||
let mut state_write = State::get_write().await?;
|
||||
let old_config_dir =
|
||||
state_write.directories.config_dir.read().await.clone();
|
||||
|
||||
// Reset file watcher
|
||||
tracing::trace!("Reset file watcher");
|
||||
let file_watcher = state::init_watcher().await?;
|
||||
state_write.file_watcher = RwLock::new(file_watcher);
|
||||
|
||||
// Getting files to be moved
|
||||
let mut config_entries = io::read_dir(&old_config_dir).await?;
|
||||
let across_drives = is_different_drive(&old_config_dir, &new_config_dir);
|
||||
let mut entries = vec![];
|
||||
let mut deletable_entries = vec![];
|
||||
while let Some(entry) = config_entries
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &old_config_dir))?
|
||||
{
|
||||
let entry_path = entry.path();
|
||||
if let Some(file_name) = entry_path.file_name() {
|
||||
// We are only moving the profiles and metadata folders
|
||||
if file_name == state::PROFILES_FOLDER_NAME
|
||||
|| file_name == state::METADATA_FOLDER_NAME
|
||||
{
|
||||
if across_drives {
|
||||
entries.extend(
|
||||
crate::pack::import::get_all_subfiles(&entry_path)
|
||||
.await?,
|
||||
);
|
||||
deletable_entries.push(entry_path.clone());
|
||||
} else {
|
||||
entries.push(entry_path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::trace!("Moving files");
|
||||
let semaphore = &state_write.io_semaphore;
|
||||
let num_entries = entries.len() as f64;
|
||||
for entry_path in entries {
|
||||
let relative_path = entry_path.strip_prefix(&old_config_dir)?;
|
||||
let new_path = new_config_dir.join(relative_path);
|
||||
if across_drives {
|
||||
fetch::copy(&entry_path, &new_path, semaphore).await?;
|
||||
} else {
|
||||
io::rename(entry_path.clone(), new_path.clone()).await?;
|
||||
}
|
||||
emit_loading(&loading_bar, 80.0 * (1.0 / num_entries), None).await?;
|
||||
}
|
||||
|
||||
tracing::trace!("Setting configuration setting");
|
||||
// Set load config dir setting
|
||||
let settings = {
|
||||
let mut settings = state_write.settings.write().await;
|
||||
settings.loaded_config_dir = Some(new_config_dir.clone());
|
||||
|
||||
// Some java paths are hardcoded to within our config dir, so we need to update them
|
||||
tracing::trace!("Updating java keys");
|
||||
for key in settings.java_globals.keys() {
|
||||
if let Some(java) = settings.java_globals.get_mut(&key) {
|
||||
// If the path is within the old config dir path, update it to the new config dir
|
||||
if let Ok(relative_path) = PathBuf::from(java.path.clone())
|
||||
.strip_prefix(&old_config_dir)
|
||||
{
|
||||
java.path = new_config_dir
|
||||
.join(relative_path)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::trace!("Syncing settings");
|
||||
|
||||
settings
|
||||
.sync(&state_write.directories.settings_file())
|
||||
.await?;
|
||||
settings.clone()
|
||||
};
|
||||
|
||||
tracing::trace!("Reinitializing directory");
|
||||
// Set new state information
|
||||
state_write.directories = DirectoryInfo::init(&settings)?;
|
||||
|
||||
// Delete entries that were from a different drive
|
||||
let deletable_entries_len = deletable_entries.len();
|
||||
if deletable_entries_len > 0 {
|
||||
tracing::trace!("Deleting old files");
|
||||
}
|
||||
for entry in deletable_entries {
|
||||
io::remove_dir_all(entry).await?;
|
||||
emit_loading(
|
||||
&loading_bar,
|
||||
10.0 * (1.0 / deletable_entries_len as f64),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Reset file watcher
|
||||
tracing::trace!("Reset file watcher");
|
||||
let mut file_watcher = state::init_watcher().await?;
|
||||
|
||||
// Reset profiles (for filepaths, file watcher, etc)
|
||||
state_write.profiles = RwLock::new(
|
||||
Profiles::init(&state_write.directories, &mut file_watcher).await?,
|
||||
);
|
||||
state_write.file_watcher = RwLock::new(file_watcher);
|
||||
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
|
||||
tracing::info!(
|
||||
"Successfully switched config folder to: {}",
|
||||
new_config_dir.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Function to check if two paths are on different drives/roots
|
||||
fn is_different_drive(path1: &Path, path2: &Path) -> bool {
|
||||
let root1 = path1.components().next();
|
||||
let root2 = path2.components().next();
|
||||
root1 != root2
|
||||
}
|
||||
|
||||
pub async fn is_dir_writeable(new_config_dir: PathBuf) -> crate::Result<bool> {
|
||||
let temp_path = new_config_dir.join(".tmp");
|
||||
match fs::write(temp_path.clone(), "test").await {
|
||||
Ok(_) => {
|
||||
fs::remove_file(temp_path).await?;
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error writing to new config dir: {}", e);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
60
packages/app-lib/src/api/tags.rs
Normal file
60
packages/app-lib/src/api/tags.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
//! Theseus tag management interface
|
||||
pub use crate::{
|
||||
state::{Category, DonationPlatform, GameVersion, Loader, Tags},
|
||||
State,
|
||||
};
|
||||
|
||||
// Get bundled set of tags
|
||||
#[tracing::instrument]
|
||||
pub async fn get_tag_bundle() -> crate::Result<Tags> {
|
||||
let state = State::get().await?;
|
||||
let tags = state.tags.read().await;
|
||||
|
||||
Ok(tags.get_tag_bundle())
|
||||
}
|
||||
|
||||
/// Get category tags
|
||||
#[tracing::instrument]
|
||||
pub async fn get_category_tags() -> crate::Result<Vec<Category>> {
|
||||
let state = State::get().await?;
|
||||
let tags = state.tags.read().await;
|
||||
|
||||
Ok(tags.get_categories())
|
||||
}
|
||||
|
||||
/// Get report type tags
|
||||
#[tracing::instrument]
|
||||
pub async fn get_report_type_tags() -> crate::Result<Vec<String>> {
|
||||
let state = State::get().await?;
|
||||
let tags = state.tags.read().await;
|
||||
|
||||
Ok(tags.get_report_types())
|
||||
}
|
||||
|
||||
/// Get loader tags
|
||||
#[tracing::instrument]
|
||||
pub async fn get_loader_tags() -> crate::Result<Vec<Loader>> {
|
||||
let state = State::get().await?;
|
||||
let tags = state.tags.read().await;
|
||||
|
||||
Ok(tags.get_loaders())
|
||||
}
|
||||
|
||||
/// Get game version tags
|
||||
#[tracing::instrument]
|
||||
pub async fn get_game_version_tags() -> crate::Result<Vec<GameVersion>> {
|
||||
let state = State::get().await?;
|
||||
let tags = state.tags.read().await;
|
||||
|
||||
Ok(tags.get_game_versions())
|
||||
}
|
||||
|
||||
/// Get donation platform tags
|
||||
#[tracing::instrument]
|
||||
pub async fn get_donation_platform_tags() -> crate::Result<Vec<DonationPlatform>>
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let tags = state.tags.read().await;
|
||||
|
||||
Ok(tags.get_donation_platforms())
|
||||
}
|
||||
Reference in New Issue
Block a user