You've already forked AstralRinth
forked from didirus/AstralRinth
Migrate to SQLite for Internal Launcher Data (#1300)
* initial migration * barebones profiles * Finish profiles * Add back file watcher * UI support progress * Finish most of cache * Fix options page * Fix forge, finish modrinth auth * Accounts, process cache * Run SQLX prepare * Finish * Run lint + actions * Fix version to be compat with windows * fix lint * actually fix lint * actually fix lint again
This commit is contained in:
42
packages/app-lib/src/api/cache.rs
Normal file
42
packages/app-lib/src/api/cache.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use crate::state::{
|
||||
CachedEntry, Organization, Project, SearchResults, TeamMember, User,
|
||||
Version,
|
||||
};
|
||||
|
||||
macro_rules! impl_cache_methods {
|
||||
($(($variant:ident, $type:ty)),*) => {
|
||||
$(
|
||||
paste::paste! {
|
||||
#[tracing::instrument]
|
||||
pub async fn [<get_ $variant:snake>](
|
||||
id: &str,
|
||||
) -> crate::Result<Option<$type>>
|
||||
{
|
||||
let state = crate::State::get().await?;
|
||||
Ok(CachedEntry::[<get_ $variant:snake _many>](&[id], None, &state.pool, &state.api_semaphore).await?.into_iter().next())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn [<get_ $variant:snake _many>](
|
||||
ids: &[&str],
|
||||
) -> crate::Result<Vec<$type>>
|
||||
{
|
||||
let state = crate::State::get().await?;
|
||||
let entries =
|
||||
CachedEntry::[<get_ $variant:snake _many>](ids, None, &state.pool, &state.api_semaphore).await?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
impl_cache_methods!(
|
||||
(Project, Project),
|
||||
(Version, Version),
|
||||
(User, User),
|
||||
(Team, Vec<TeamMember>),
|
||||
(Organization, Organization),
|
||||
(SearchResults, SearchResults)
|
||||
);
|
||||
@@ -1,19 +1,31 @@
|
||||
//! Authentication flow interface
|
||||
use crate::event::emit::{emit_loading, init_loading};
|
||||
use crate::state::JavaVersion;
|
||||
use crate::util::fetch::{fetch_advanced, fetch_json};
|
||||
use dashmap::DashMap;
|
||||
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},
|
||||
util::jre::{self},
|
||||
LoadingBarType, State,
|
||||
};
|
||||
|
||||
pub async fn get_java_versions() -> crate::Result<DashMap<u32, JavaVersion>> {
|
||||
let state = State::get().await?;
|
||||
|
||||
JavaVersion::get_all(&state.pool).await
|
||||
}
|
||||
|
||||
pub async fn set_java_version(java_version: JavaVersion) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
java_version.upsert(&state.pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 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(
|
||||
@@ -38,7 +50,6 @@ pub async fn find_filtered_jres(
|
||||
})
|
||||
}
|
||||
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
let state = State::get().await?;
|
||||
|
||||
@@ -67,7 +78,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&CredentialsStore(None),
|
||||
&state.pool,
|
||||
).await?;
|
||||
emit_loading(&loading_bar, 10.0, Some("Downloading java version")).await?;
|
||||
|
||||
@@ -80,11 +91,11 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
None,
|
||||
Some((&loading_bar, 80.0)),
|
||||
&state.fetch_semaphore,
|
||||
&CredentialsStore(None),
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let path = state.directories.java_versions_dir().await;
|
||||
let path = state.directories.java_versions_dir();
|
||||
|
||||
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))
|
||||
.map_err(|_| {
|
||||
|
||||
@@ -9,9 +9,9 @@ use tokio::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
prelude::{Credentials, DirectoryInfo},
|
||||
prelude::Credentials,
|
||||
util::io::{self, IOError},
|
||||
{state::ProfilePathId, State},
|
||||
State,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -66,7 +66,7 @@ impl Logs {
|
||||
async fn build(
|
||||
log_type: LogType,
|
||||
age: SystemTime,
|
||||
profile_subpath: &ProfilePathId,
|
||||
profile_subpath: &str,
|
||||
filename: String,
|
||||
clear_contents: Option<bool>,
|
||||
) -> crate::Result<Self> {
|
||||
@@ -95,19 +95,20 @@ impl Logs {
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs_from_type(
|
||||
profile_path: &ProfilePathId,
|
||||
profile_path: &str,
|
||||
log_type: LogType,
|
||||
clear_contents: Option<bool>,
|
||||
logs: &mut Vec<crate::Result<Logs>>,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let logs_folder = match log_type {
|
||||
LogType::InfoLog => {
|
||||
DirectoryInfo::profile_logs_dir(profile_path).await?
|
||||
}
|
||||
LogType::InfoLog => state.directories.profile_logs_dir(profile_path),
|
||||
LogType::CrashReport => {
|
||||
DirectoryInfo::crash_reports_dir(profile_path).await?
|
||||
state.directories.crash_reports_dir(profile_path)
|
||||
}
|
||||
};
|
||||
|
||||
if logs_folder.exists() {
|
||||
for entry in std::fs::read_dir(&logs_folder)
|
||||
.map_err(|e| IOError::with_path(e, &logs_folder))?
|
||||
@@ -142,21 +143,19 @@ pub async fn get_logs_from_type(
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs(
|
||||
profile_path_id: ProfilePathId,
|
||||
profile_path_id: &str,
|
||||
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,
|
||||
profile_path_id,
|
||||
LogType::InfoLog,
|
||||
clear_contents,
|
||||
&mut logs,
|
||||
)
|
||||
.await?;
|
||||
get_logs_from_type(
|
||||
&profile_path,
|
||||
profile_path_id,
|
||||
LogType::CrashReport,
|
||||
clear_contents,
|
||||
&mut logs,
|
||||
@@ -170,54 +169,47 @@ pub async fn get_logs(
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs_by_filename(
|
||||
profile_path_id: ProfilePathId,
|
||||
profile_path: &str,
|
||||
log_type: LogType,
|
||||
filename: String,
|
||||
) -> crate::Result<Logs> {
|
||||
let profile_path = profile_path_id.profile_path().await?;
|
||||
let state = State::get().await?;
|
||||
|
||||
let path = match log_type {
|
||||
LogType::InfoLog => {
|
||||
DirectoryInfo::profile_logs_dir(&profile_path).await
|
||||
}
|
||||
LogType::InfoLog => state.directories.profile_logs_dir(profile_path),
|
||||
LogType::CrashReport => {
|
||||
DirectoryInfo::crash_reports_dir(&profile_path).await
|
||||
state.directories.crash_reports_dir(profile_path)
|
||||
}
|
||||
}?
|
||||
}
|
||||
.join(&filename);
|
||||
|
||||
let metadata = std::fs::metadata(&path)?;
|
||||
let age = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
|
||||
Logs::build(log_type, age, &profile_path, filename, Some(true)).await
|
||||
Logs::build(log_type, age, profile_path, filename, Some(true)).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_output_by_filename(
|
||||
profile_subpath: &ProfilePathId,
|
||||
profile_subpath: &str,
|
||||
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::InfoLog => state.directories.profile_logs_dir(profile_subpath),
|
||||
LogType::CrashReport => {
|
||||
DirectoryInfo::crash_reports_dir(profile_subpath).await?
|
||||
state.directories.crash_reports_dir(profile_subpath)
|
||||
}
|
||||
};
|
||||
|
||||
let path = logs_folder.join(file_name);
|
||||
|
||||
let credentials: Vec<Credentials> = state
|
||||
.users
|
||||
.read()
|
||||
.await
|
||||
.users
|
||||
.clone()
|
||||
.into_values()
|
||||
let credentials = Credentials::get_all(&state.pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.1)
|
||||
.collect();
|
||||
|
||||
// Load .gz file into String
|
||||
@@ -265,10 +257,10 @@ pub async fn get_output_by_filename(
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs(profile_path_id: ProfilePathId) -> crate::Result<()> {
|
||||
let profile_path = profile_path_id.profile_path().await?;
|
||||
pub async fn delete_logs(profile_path_id: &str) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
|
||||
let logs_folder = state.directories.profile_logs_dir(profile_path_id);
|
||||
for entry in std::fs::read_dir(&logs_folder)
|
||||
.map_err(|e| IOError::with_path(e, &logs_folder))?
|
||||
{
|
||||
@@ -283,20 +275,18 @@ pub async fn delete_logs(profile_path_id: ProfilePathId) -> crate::Result<()> {
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs_by_filename(
|
||||
profile_path_id: ProfilePathId,
|
||||
profile_path_id: &str,
|
||||
log_type: LogType,
|
||||
filename: &str,
|
||||
) -> crate::Result<()> {
|
||||
let profile_path = profile_path_id.profile_path().await?;
|
||||
let state = State::get().await?;
|
||||
|
||||
let logs_folder = match log_type {
|
||||
LogType::InfoLog => {
|
||||
DirectoryInfo::profile_logs_dir(&profile_path).await
|
||||
}
|
||||
LogType::InfoLog => state.directories.profile_logs_dir(profile_path_id),
|
||||
LogType::CrashReport => {
|
||||
DirectoryInfo::crash_reports_dir(&profile_path).await
|
||||
state.directories.crash_reports_dir(profile_path_id)
|
||||
}
|
||||
}?;
|
||||
};
|
||||
|
||||
let path = logs_folder.join(filename);
|
||||
io::remove_dir_all(&path).await?;
|
||||
@@ -305,7 +295,7 @@ pub async fn delete_logs_by_filename(
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_latest_log_cursor(
|
||||
profile_path: ProfilePathId,
|
||||
profile_path: &str,
|
||||
cursor: u64, // 0 to start at beginning of file
|
||||
) -> crate::Result<LatestLogCursor> {
|
||||
get_generic_live_log_cursor(profile_path, "latest.log", cursor).await
|
||||
@@ -313,14 +303,12 @@ pub async fn get_latest_log_cursor(
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_generic_live_log_cursor(
|
||||
profile_path_id: ProfilePathId,
|
||||
profile_path_id: &str,
|
||||
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 logs_folder = state.directories.profile_logs_dir(profile_path_id);
|
||||
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)
|
||||
@@ -358,13 +346,10 @@ pub async fn get_generic_live_log_cursor(
|
||||
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()
|
||||
let credentials = Credentials::get_all(&state.pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.1)
|
||||
.collect();
|
||||
let output = CensoredString::censor(output, &credentials);
|
||||
Ok(LatestLogCursor {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::state::CachedEntry;
|
||||
use crate::State;
|
||||
pub use daedalus::minecraft::VersionManifest;
|
||||
pub use daedalus::modded::Manifest;
|
||||
@@ -5,39 +6,32 @@ 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();
|
||||
let minecraft_versions = CachedEntry::get_minecraft_manifest(
|
||||
None,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::NoValueFor("minecraft versions".to_string())
|
||||
})?;
|
||||
|
||||
Ok(tags)
|
||||
Ok(minecraft_versions)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_fabric_versions() -> crate::Result<Manifest> {
|
||||
pub async fn get_loader_versions(loader: &str) -> crate::Result<Manifest> {
|
||||
let state = State::get().await?;
|
||||
let tags = state.metadata.read().await.fabric.clone();
|
||||
let loaders = CachedEntry::get_loader_manifest(
|
||||
loader,
|
||||
None,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::NoValueFor(format!("{} loader versions", loader))
|
||||
})?;
|
||||
|
||||
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)
|
||||
Ok(loaders.manifest)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! 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
|
||||
crate::state::login_begin(&state.pool).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
@@ -16,34 +16,51 @@ pub async fn finish_login(
|
||||
flow: MinecraftLoginFlow,
|
||||
) -> crate::Result<Credentials> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
users.login_finish(code, flow).await
|
||||
crate::state::login_finish(code, flow, &state.pool).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)
|
||||
let users = Credentials::get_active(&state.pool).await?;
|
||||
Ok(users.map(|x| x.id))
|
||||
}
|
||||
|
||||
#[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?;
|
||||
let users = Credentials::get_all(&state.pool).await?;
|
||||
let (_, mut user) = users.remove(&user).ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to get nonexistent user with ID {user}"
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
user.active = true;
|
||||
user.upsert(&state.pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a user account from the database
|
||||
#[tracing::instrument]
|
||||
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||
pub async fn remove_user(uuid: uuid::Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
users.remove(user).await?;
|
||||
|
||||
let users = Credentials::get_all(&state.pool).await?;
|
||||
|
||||
if let Some((uuid, user)) = users.remove(&uuid) {
|
||||
Credentials::remove(uuid, &state.pool).await?;
|
||||
|
||||
if user.active {
|
||||
if let Some((_, mut user)) = users.into_iter().next() {
|
||||
user.active = true;
|
||||
user.upsert(&state.pool).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -52,25 +69,6 @@ pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||
#[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)
|
||||
let users = Credentials::get_all(&state.pool).await?;
|
||||
Ok(users.into_iter().map(|x| x.1).collect())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! API for interacting with Theseus
|
||||
pub mod cache;
|
||||
pub mod handler;
|
||||
pub mod jre;
|
||||
pub mod logs;
|
||||
@@ -8,17 +9,16 @@ 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,
|
||||
Credentials, Dependency, DirectoryInfo, Hooks, JavaVersion, LinkedData,
|
||||
MemorySettings, ModLoader, ModrinthCredentials,
|
||||
ModrinthCredentialsResult, ModrinthProject, ModrinthTeamMember,
|
||||
ModrinthUser, ModrinthVersion, ProfileMetadata, ProjectMetadata,
|
||||
Settings, Theme, WindowSize,
|
||||
ModrinthCredentialsResult, Organization, Process, ProfileFile, Project,
|
||||
ProjectType, SearchResult, SearchResults, Settings, TeamMember, Theme,
|
||||
User, Version, WindowSize,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,15 +26,10 @@ pub mod prelude {
|
||||
pub use crate::{
|
||||
data::*,
|
||||
event::CommandPayload,
|
||||
jre, metadata, minecraft_auth, pack, process,
|
||||
jre, metadata, minecraft_auth, mr_auth, pack, process,
|
||||
profile::{self, create, Profile},
|
||||
settings,
|
||||
state::JavaGlobals,
|
||||
state::{Dependency, ProfilePathId, ProjectPathId},
|
||||
util::{
|
||||
io::{canonicalize, IOError},
|
||||
jre::JavaVersion,
|
||||
},
|
||||
util::io::{canonicalize, IOError},
|
||||
State,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,56 +1,30 @@
|
||||
use crate::state::{
|
||||
ModrinthAuthFlow, ModrinthCredentials, ModrinthCredentialsResult,
|
||||
};
|
||||
use crate::ErrorKind;
|
||||
use crate::state::{ModrinthCredentials, ModrinthCredentialsResult};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[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)
|
||||
pub fn authenticate_begin_flow(provider: &str) -> String {
|
||||
crate::state::get_login_url(provider)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn authenticate_await_complete_flow(
|
||||
pub async fn authenticate_finish_flow(
|
||||
response: HashMap<String, Value>,
|
||||
) -> 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?;
|
||||
let creds = crate::state::finish_login_flow(
|
||||
response,
|
||||
&state.api_semaphore,
|
||||
&state.pool,
|
||||
)
|
||||
.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())
|
||||
if let ModrinthCredentialsResult::Credentials(creds) = &creds {
|
||||
creds.upsert(&state.pool).await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[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(())
|
||||
Ok(creds)
|
||||
}
|
||||
|
||||
pub async fn login_password(
|
||||
@@ -63,13 +37,13 @@ pub async fn login_password(
|
||||
username,
|
||||
password,
|
||||
challenge,
|
||||
&state.fetch_semaphore,
|
||||
&state.api_semaphore,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let ModrinthCredentialsResult::Credentials(creds) = &creds {
|
||||
let mut write = state.credentials.write().await;
|
||||
write.login(creds.clone()).await?;
|
||||
creds.upsert(&state.pool).await?;
|
||||
}
|
||||
|
||||
Ok(creds)
|
||||
@@ -82,10 +56,10 @@ pub async fn login_2fa(
|
||||
) -> crate::Result<ModrinthCredentials> {
|
||||
let state = crate::State::get().await?;
|
||||
let creds =
|
||||
crate::state::login_2fa(code, flow, &state.fetch_semaphore).await?;
|
||||
crate::state::login_2fa(code, flow, &state.api_semaphore, &state.pool)
|
||||
.await?;
|
||||
|
||||
let mut write = state.credentials.write().await;
|
||||
write.login(creds.clone()).await?;
|
||||
creds.upsert(&state.pool).await?;
|
||||
|
||||
Ok(creds)
|
||||
}
|
||||
@@ -105,32 +79,24 @@ pub async fn create_account(
|
||||
password,
|
||||
challenge,
|
||||
sign_up_newsletter,
|
||||
&state.fetch_semaphore,
|
||||
&state.api_semaphore,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut write = state.credentials.write().await;
|
||||
write.login(creds.clone()).await?;
|
||||
creds.upsert(&state.pool).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?;
|
||||
let current = ModrinthCredentials::get_active(&state.pool).await?;
|
||||
|
||||
if let Some(current) = current {
|
||||
ModrinthCredentials::remove(¤t.user_id, &state.pool).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -138,7 +104,9 @@ pub async fn logout() -> crate::Result<()> {
|
||||
#[tracing::instrument]
|
||||
pub async fn get_credentials() -> crate::Result<Option<ModrinthCredentials>> {
|
||||
let state = crate::State::get().await?;
|
||||
let read = state.credentials.read().await;
|
||||
let current =
|
||||
ModrinthCredentials::get_and_refresh(&state.pool, &state.api_semaphore)
|
||||
.await?;
|
||||
|
||||
Ok(read.0.clone())
|
||||
Ok(current)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
import::{self, copy_dotminecraft},
|
||||
install_from::CreatePackDescription,
|
||||
},
|
||||
prelude::{ModLoader, Profile, ProfilePathId},
|
||||
prelude::ModLoader,
|
||||
state::{LinkedData, ProfileInstallStage},
|
||||
util::io,
|
||||
State,
|
||||
@@ -116,11 +116,11 @@ pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
|
||||
}
|
||||
|
||||
#[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
|
||||
profile_path: &str, // path to profile
|
||||
) -> crate::Result<()> {
|
||||
let atlauncher_instance_path = atlauncher_base_path
|
||||
.join("instances")
|
||||
@@ -159,7 +159,7 @@ pub async fn import_atlauncher(
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
existing_loading_bar: None,
|
||||
profile_path: profile_path.clone(),
|
||||
profile_path: profile_path.to_string(),
|
||||
};
|
||||
|
||||
let backup_name = format!("ATLauncher-{}", instance_folder);
|
||||
@@ -177,7 +177,7 @@ pub async fn import_atlauncher(
|
||||
}
|
||||
|
||||
async fn import_atlauncher_unmanaged(
|
||||
profile_path: ProfilePathId,
|
||||
profile_path: &str,
|
||||
minecraft_folder: PathBuf,
|
||||
backup_name: String,
|
||||
description: CreatePackDescription,
|
||||
@@ -198,10 +198,10 @@ async fn import_atlauncher_unmanaged(
|
||||
let game_version = atinstance.id;
|
||||
|
||||
let loader_version = if mod_loader != ModLoader::Vanilla {
|
||||
crate::profile::create::get_loader_version_from_loader(
|
||||
game_version.clone(),
|
||||
crate::launcher::get_loader_version_from_profile(
|
||||
&game_version,
|
||||
mod_loader,
|
||||
Some(atinstance.launcher.loader_version.version.clone()),
|
||||
Some(&atinstance.launcher.loader_version.version),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -209,24 +209,30 @@ async fn import_atlauncher_unmanaged(
|
||||
};
|
||||
|
||||
// Set profile data to created default profile
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.metadata.name = description
|
||||
crate::api::profile::edit(profile_path, |prof| {
|
||||
prof.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.clone_from(&description.icon);
|
||||
prof.metadata.game_version.clone_from(&game_version);
|
||||
prof.metadata.loader_version.clone_from(&loader_version);
|
||||
prof.metadata.loader = mod_loader;
|
||||
|
||||
if let Some(ref project_id) = description.project_id {
|
||||
if let Some(ref version_id) = description.version_id {
|
||||
prof.linked_data = Some(LinkedData {
|
||||
project_id: project_id.clone(),
|
||||
version_id: version_id.clone(),
|
||||
locked: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
prof.icon_path = description
|
||||
.icon
|
||||
.clone()
|
||||
.map(|x| x.to_string_lossy().to_string());
|
||||
prof.game_version.clone_from(&game_version);
|
||||
prof.loader_version = loader_version.clone().map(|x| x.id);
|
||||
prof.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
@@ -235,32 +241,20 @@ async fn import_atlauncher_unmanaged(
|
||||
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
profile_path,
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
if let Some(profile_val) = crate::api::profile::get(profile_path).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(())
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::prelude::Profile;
|
||||
use crate::state::CredentialsStore;
|
||||
use crate::{
|
||||
prelude::{ModLoader, ProfilePathId},
|
||||
prelude::ModLoader,
|
||||
state::ProfileInstallStage,
|
||||
util::{
|
||||
fetch::{fetch, write_cached_icon},
|
||||
@@ -49,7 +47,7 @@ pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
|
||||
|
||||
pub async fn import_curseforge(
|
||||
curseforge_instance_folder: PathBuf, // instance's folder
|
||||
profile_path: ProfilePathId, // path to profile
|
||||
profile_path: &str, // path to profile
|
||||
) -> crate::Result<()> {
|
||||
// Load minecraftinstance.json
|
||||
let minecraft_instance: String = io::read_to_string(
|
||||
@@ -77,13 +75,9 @@ pub async fn import_curseforge(
|
||||
thumbnail_url: Some(thumbnail_url),
|
||||
}) = minecraft_instance.installed_modpack.clone()
|
||||
{
|
||||
let icon_bytes = fetch(
|
||||
&thumbnail_url,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&CredentialsStore(None),
|
||||
)
|
||||
.await?;
|
||||
let icon_bytes =
|
||||
fetch(&thumbnail_url, None, &state.fetch_semaphore, &state.pool)
|
||||
.await?;
|
||||
let filename = thumbnail_url.rsplit('/').last();
|
||||
if let Some(filename) = filename {
|
||||
icon = Some(
|
||||
@@ -121,10 +115,10 @@ pub async fn import_curseforge(
|
||||
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(),
|
||||
crate::launcher::get_loader_version_from_profile(
|
||||
&game_version,
|
||||
mod_loader,
|
||||
loader_version,
|
||||
loader_version.as_deref(),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -132,31 +126,32 @@ pub async fn import_curseforge(
|
||||
};
|
||||
|
||||
// Set profile data to created default profile
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.metadata.name = override_title
|
||||
crate::api::profile::edit(profile_path, |prof| {
|
||||
prof.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
prof.metadata.icon.clone_from(&icon);
|
||||
prof.metadata.game_version.clone_from(&game_version);
|
||||
prof.metadata.loader_version.clone_from(&loader_version);
|
||||
prof.metadata.loader = mod_loader;
|
||||
prof.icon_path =
|
||||
icon.clone().map(|x| x.to_string_lossy().to_string());
|
||||
prof.game_version.clone_from(&game_version);
|
||||
prof.loader_version = loader_version.clone().map(|x| x.id);
|
||||
prof.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
// create a vanilla profile
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.metadata.name = override_title
|
||||
crate::api::profile::edit(profile_path, |prof| {
|
||||
prof.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.metadata.icon.clone_from(&icon);
|
||||
prof.metadata
|
||||
.game_version
|
||||
prof.icon_path =
|
||||
icon.clone().map(|x| x.to_string_lossy().to_string());
|
||||
prof.game_version
|
||||
.clone_from(&minecraft_instance.game_version);
|
||||
prof.metadata.loader_version = None;
|
||||
prof.metadata.loader = ModLoader::Vanilla;
|
||||
prof.loader_version = None;
|
||||
prof.loader = ModLoader::Vanilla;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
@@ -166,33 +161,20 @@ pub async fn import_curseforge(
|
||||
// Copy in contained folders as overrides
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
profile_path,
|
||||
curseforge_instance_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
if let Some(profile_val) = crate::api::profile::get(profile_path).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(())
|
||||
|
||||
@@ -2,12 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
prelude::{ModLoader, Profile, ProfilePathId},
|
||||
state::ProfileInstallStage,
|
||||
util::io,
|
||||
State,
|
||||
};
|
||||
use crate::{prelude::ModLoader, state::ProfileInstallStage, util::io, State};
|
||||
|
||||
use super::{copy_dotminecraft, recache_icon};
|
||||
|
||||
@@ -41,7 +36,7 @@ pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool {
|
||||
|
||||
pub async fn import_gdlauncher(
|
||||
gdlauncher_instance_folder: PathBuf, // instance's folder
|
||||
profile_path: ProfilePathId, // path to profile
|
||||
profile_path: &str, // path to profile
|
||||
) -> crate::Result<()> {
|
||||
// Load config.json
|
||||
let config: String =
|
||||
@@ -74,10 +69,10 @@ pub async fn import_gdlauncher(
|
||||
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(),
|
||||
crate::launcher::get_loader_version_from_profile(
|
||||
&game_version,
|
||||
mod_loader,
|
||||
loader_version,
|
||||
loader_version.as_deref(),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -85,15 +80,15 @@ pub async fn import_gdlauncher(
|
||||
};
|
||||
|
||||
// Set profile data to created default profile
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.metadata.name = override_title
|
||||
crate::api::profile::edit(profile_path, |prof| {
|
||||
prof.name = override_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| backup_name.to_string());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
prof.metadata.icon.clone_from(&icon);
|
||||
prof.metadata.game_version.clone_from(&game_version);
|
||||
prof.metadata.loader_version.clone_from(&loader_version);
|
||||
prof.metadata.loader = mod_loader;
|
||||
prof.icon_path = icon.clone().map(|x| x.to_string_lossy().to_string());
|
||||
prof.game_version.clone_from(&game_version);
|
||||
prof.loader_version = loader_version.clone().map(|x| x.id);
|
||||
prof.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
@@ -102,32 +97,20 @@ pub async fn import_gdlauncher(
|
||||
// Copy in contained folders as overrides
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
profile_path,
|
||||
gdlauncher_instance_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
if let Some(profile_val) = crate::api::profile::get(profile_path).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(())
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::{
|
||||
import::{self, copy_dotminecraft},
|
||||
install_from::{self, CreatePackDescription, PackDependency},
|
||||
},
|
||||
prelude::{Profile, ProfilePathId},
|
||||
util::io,
|
||||
State,
|
||||
};
|
||||
@@ -176,11 +175,11 @@ async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> {
|
||||
}
|
||||
|
||||
#[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
|
||||
mmc_base_path: PathBuf, // path to base mmc folder
|
||||
instance_folder: String, // instance folder in mmc_base_path
|
||||
profile_path: &str, // path to profile
|
||||
) -> crate::Result<()> {
|
||||
let mmc_instance_path = mmc_base_path
|
||||
.join("instances")
|
||||
@@ -202,13 +201,13 @@ pub async fn import_mmc(
|
||||
};
|
||||
|
||||
// Create description from instance.cfg
|
||||
let description = CreatePackDescription {
|
||||
let mut description = CreatePackDescription {
|
||||
icon,
|
||||
override_title: instance_cfg.name,
|
||||
project_id: instance_cfg.managed_pack_id,
|
||||
version_id: instance_cfg.managed_pack_version_id,
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
existing_loading_bar: None,
|
||||
profile_path: profile_path.clone(),
|
||||
profile_path: profile_path.to_string(),
|
||||
};
|
||||
|
||||
// Managed pack
|
||||
@@ -217,6 +216,9 @@ pub async fn import_mmc(
|
||||
if instance_cfg.managed_pack.unwrap_or(false) {
|
||||
match instance_cfg.managed_pack_type {
|
||||
Some(MMCManagedPackType::Modrinth) => {
|
||||
description.project_id = instance_cfg.managed_pack_id;
|
||||
description.version_id = instance_cfg.managed_pack_version_id;
|
||||
|
||||
// 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();
|
||||
@@ -260,7 +262,7 @@ pub async fn import_mmc(
|
||||
}
|
||||
|
||||
async fn import_mmc_unmanaged(
|
||||
profile_path: ProfilePathId,
|
||||
profile_path: &str,
|
||||
minecraft_folder: PathBuf,
|
||||
backup_name: String,
|
||||
description: CreatePackDescription,
|
||||
@@ -302,7 +304,7 @@ async fn import_mmc_unmanaged(
|
||||
|
||||
// Sets profile information to be that loaded from mmc-pack.json and instance.cfg
|
||||
install_from::set_profile_information(
|
||||
profile_path.clone(),
|
||||
profile_path.to_string(),
|
||||
&description,
|
||||
&backup_name,
|
||||
&dependencies,
|
||||
@@ -313,32 +315,20 @@ async fn import_mmc_unmanaged(
|
||||
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
|
||||
let state = State::get().await?;
|
||||
let loading_bar = copy_dotminecraft(
|
||||
profile_path.clone(),
|
||||
profile_path,
|
||||
minecraft_folder,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
if let Some(profile_val) = crate::api::profile::get(profile_path).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(())
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ use crate::{
|
||||
emit::{emit_loading, init_or_edit_loading},
|
||||
LoadingBarId,
|
||||
},
|
||||
prelude::ProfilePathId,
|
||||
state::Profiles,
|
||||
util::{
|
||||
fetch::{self, IoSemaphore},
|
||||
io,
|
||||
@@ -105,10 +103,10 @@ pub async fn get_importable_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
|
||||
profile_path: &str, // This should be a blank profile
|
||||
launcher_type: ImportLauncherType,
|
||||
base_path: PathBuf,
|
||||
instance_folder: String,
|
||||
@@ -117,31 +115,31 @@ pub async fn import_instance(
|
||||
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
|
||||
base_path, // path to base mmc folder
|
||||
instance_folder, // instance folder in mmc_base_path
|
||||
profile_path, // 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
|
||||
base_path, // path to atlauncher folder
|
||||
instance_folder, // instance folder in atlauncher
|
||||
profile_path, // 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
|
||||
profile_path, // 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
|
||||
profile_path, // path to profile
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -158,14 +156,11 @@ pub async fn import_instance(
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
tracing::warn!("Import failed: {:?}", e);
|
||||
let _ = crate::api::profile::remove(&profile_path).await;
|
||||
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(())
|
||||
}
|
||||
@@ -200,7 +195,7 @@ pub fn get_default_launcher_path(
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -224,7 +219,7 @@ pub async fn is_valid_importable_instance(
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -252,13 +247,14 @@ pub async fn recache_icon(
|
||||
}
|
||||
|
||||
pub async fn copy_dotminecraft(
|
||||
profile_path_id: ProfilePathId,
|
||||
profile_path_id: &str,
|
||||
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?;
|
||||
let profile_path =
|
||||
crate::api::profile::get_full_path(profile_path_id).await?;
|
||||
|
||||
// Gets all subfiles recursively in src
|
||||
let subfiles = get_all_subfiles(&dotminecraft).await?;
|
||||
@@ -298,7 +294,7 @@ pub async fn copy_dotminecraft(
|
||||
|
||||
/// 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>> {
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
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::state::{CachedEntry, LinkedData, ProfileInstallStage, SideType};
|
||||
use crate::util::fetch::{fetch, fetch_advanced, write_cached_icon};
|
||||
use crate::util::io;
|
||||
use crate::{InnerProjectPathUnix, State};
|
||||
use crate::State;
|
||||
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -33,7 +27,7 @@ pub struct PackFormat {
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PackFile {
|
||||
pub path: InnerProjectPathUnix,
|
||||
pub path: String,
|
||||
pub hashes: HashMap<PackFileHash, String>,
|
||||
pub env: Option<HashMap<EnvType, SideType>>,
|
||||
pub downloads: Vec<String>,
|
||||
@@ -84,7 +78,7 @@ pub enum PackDependency {
|
||||
Minecraft,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum CreatePackLocation {
|
||||
// Create a pack from a modrinth version ID (such as a modpack)
|
||||
@@ -144,7 +138,7 @@ pub struct CreatePackDescription {
|
||||
pub project_id: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
pub existing_loading_bar: Option<LoadingBarId>,
|
||||
pub profile_path: ProfilePathId,
|
||||
pub profile_path: String,
|
||||
}
|
||||
|
||||
pub fn get_profile_from_pack(
|
||||
@@ -160,9 +154,9 @@ pub fn get_profile_from_pack(
|
||||
name: title,
|
||||
icon_url,
|
||||
linked_data: Some(LinkedData {
|
||||
project_id: Some(project_id),
|
||||
version_id: Some(version_id),
|
||||
locked: Some(true),
|
||||
project_id,
|
||||
version_id,
|
||||
locked: true,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -182,13 +176,13 @@ pub fn get_profile_from_pack(
|
||||
}
|
||||
|
||||
#[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,
|
||||
profile_path: String,
|
||||
|
||||
// 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
|
||||
@@ -202,7 +196,7 @@ pub async fn generate_pack_from_version_id(
|
||||
} else {
|
||||
init_loading(
|
||||
LoadingBarType::PackFileDownload {
|
||||
profile_path: profile_path.get_full_path().await?,
|
||||
profile_path: profile_path.clone(),
|
||||
pack_name: title,
|
||||
icon: icon_url,
|
||||
pack_version: version_id.clone(),
|
||||
@@ -214,16 +208,18 @@ pub async fn generate_pack_from_version_id(
|
||||
};
|
||||
|
||||
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),
|
||||
let version = CachedEntry::get_version(
|
||||
&version_id,
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(
|
||||
"Invalid version ID specified!".to_string(),
|
||||
)
|
||||
})?;
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
|
||||
let (url, hash) =
|
||||
@@ -249,27 +245,29 @@ pub async fn generate_pack_from_version_id(
|
||||
None,
|
||||
Some((&loading_bar, 70.0)),
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
&state.pool,
|
||||
)
|
||||
.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),
|
||||
let project = CachedEntry::get_project(
|
||||
&version.project_id,
|
||||
None,
|
||||
None,
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(
|
||||
"Invalid project ID specified!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
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);
|
||||
fetch(&icon_url, None, &state.fetch_semaphore, &state.pool).await?;
|
||||
|
||||
let filename = icon_url.rsplit('/').next();
|
||||
|
||||
@@ -305,10 +303,10 @@ pub async fn generate_pack_from_version_id(
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
|
||||
pub async fn generate_pack_from_file(
|
||||
path: PathBuf,
|
||||
profile_path: ProfilePathId,
|
||||
profile_path: String,
|
||||
) -> crate::Result<CreatePack> {
|
||||
let file = io::read(&path).await?;
|
||||
Ok(CreatePack {
|
||||
@@ -326,9 +324,9 @@ pub async fn generate_pack_from_file(
|
||||
|
||||
/// 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,
|
||||
profile_path: String,
|
||||
description: &CreatePackDescription,
|
||||
backup_name: &str,
|
||||
dependencies: &HashMap<PackDependency, String>,
|
||||
@@ -371,10 +369,10 @@ pub async fn set_profile_information(
|
||||
|
||||
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(),
|
||||
crate::launcher::get_loader_version_from_profile(
|
||||
game_version,
|
||||
mod_loader,
|
||||
loader_version.cloned(),
|
||||
loader_version.cloned().as_deref(),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -382,35 +380,36 @@ pub async fn set_profile_information(
|
||||
};
|
||||
// Sets values in profile
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.metadata.name = description
|
||||
prof.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();
|
||||
if let Some(ref project_id) = description.project_id {
|
||||
if let Some(ref version_id) = description.version_id {
|
||||
prof.linked_data = Some(LinkedData {
|
||||
project_id: project_id.clone(),
|
||||
version_id: version_id.clone(),
|
||||
locked: if !ignore_lock {
|
||||
true
|
||||
} else {
|
||||
prof.linked_data
|
||||
.as_ref()
|
||||
.map(|x| x.locked)
|
||||
.unwrap_or(true)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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.clone_from(&description.icon);
|
||||
prof.metadata.game_version.clone_from(game_version);
|
||||
prof.metadata.loader_version.clone_from(&loader_version);
|
||||
prof.metadata.loader = mod_loader;
|
||||
prof.icon_path = description
|
||||
.icon
|
||||
.clone()
|
||||
.map(|x| x.to_string_lossy().to_string());
|
||||
prof.game_version.clone_from(game_version);
|
||||
prof.loader_version = loader_version.clone().map(|x| x.id);
|
||||
prof.loader = mod_loader;
|
||||
|
||||
async { Ok(()) }
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::config::MODRINTH_API_URL;
|
||||
use crate::event::emit::{
|
||||
emit_loading, init_or_edit_loading, loading_try_for_each_concurrent,
|
||||
};
|
||||
@@ -6,16 +5,14 @@ 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::state::{
|
||||
cache_file_hash, CacheBehaviour, CachedEntry, ProfileInstallStage, SideType,
|
||||
};
|
||||
use crate::util::fetch::{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};
|
||||
|
||||
@@ -28,11 +25,11 @@ use super::install_from::{
|
||||
/// 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> {
|
||||
profile_path: String,
|
||||
) -> crate::Result<String> {
|
||||
// Get file from description
|
||||
let create_pack: CreatePack = match location {
|
||||
CreatePackLocation::FromVersionId {
|
||||
@@ -59,9 +56,6 @@ pub async fn install_zipped_mrpack(
|
||||
// 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) => {
|
||||
@@ -74,11 +68,11 @@ pub async fn install_zipped_mrpack(
|
||||
|
||||
/// 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> {
|
||||
) -> crate::Result<String> {
|
||||
let state = &State::get().await?;
|
||||
|
||||
let file = create_pack.file;
|
||||
@@ -132,7 +126,7 @@ pub async fn install_zipped_mrpack_files(
|
||||
let loading_bar = init_or_edit_loading(
|
||||
existing_loading_bar,
|
||||
LoadingBarType::PackDownload {
|
||||
profile_path: profile_path.get_full_path().await?.clone(),
|
||||
profile_path: profile_path.clone(),
|
||||
pack_name: pack.name.clone(),
|
||||
icon,
|
||||
pack_id: project_id,
|
||||
@@ -167,7 +161,6 @@ pub async fn install_zipped_mrpack_files(
|
||||
}
|
||||
}
|
||||
|
||||
let creds = state.credentials.read().await;
|
||||
let file = fetch_mirrors(
|
||||
&project
|
||||
.downloads
|
||||
@@ -176,10 +169,9 @@ pub async fn install_zipped_mrpack_files(
|
||||
.collect::<Vec<&str>>(),
|
||||
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
drop(creds);
|
||||
|
||||
let project_path = project.path.to_string();
|
||||
|
||||
@@ -188,10 +180,23 @@ pub async fn install_zipped_mrpack_files(
|
||||
if let Some(path) = path {
|
||||
match path {
|
||||
Component::CurDir | Component::Normal(_) => {
|
||||
let path = profile_path
|
||||
.get_full_path()
|
||||
.await?
|
||||
.join(&project_path);
|
||||
let path =
|
||||
profile::get_full_path(&profile_path)
|
||||
.await?
|
||||
.join(&project_path);
|
||||
|
||||
cache_file_hash(
|
||||
file.clone(),
|
||||
&profile_path,
|
||||
&project_path,
|
||||
project
|
||||
.hashes
|
||||
.get(&PackFileHash::Sha1)
|
||||
.map(|x| &**x),
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
write(&path, &file, &state.io_semaphore)
|
||||
.await?;
|
||||
}
|
||||
@@ -243,9 +248,22 @@ pub async fn install_zipped_mrpack_files(
|
||||
}
|
||||
|
||||
if new_path.file_name().is_some() {
|
||||
let bytes = bytes::Bytes::from(content);
|
||||
|
||||
cache_file_hash(
|
||||
bytes.clone(),
|
||||
&profile_path,
|
||||
&new_path.to_string_lossy(),
|
||||
None,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
write(
|
||||
&profile_path.get_full_path().await?.join(new_path),
|
||||
&content,
|
||||
&profile::get_full_path(&profile_path)
|
||||
.await?
|
||||
.join(new_path),
|
||||
&bytes,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
@@ -265,24 +283,23 @@ pub async fn install_zipped_mrpack_files(
|
||||
|
||||
// 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");
|
||||
let potential_icon = profile::get_full_path(&profile_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? {
|
||||
if let Some(profile_val) = profile::get(&profile_path).await? {
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
Some(loading_bar),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
Ok::<ProfilePathId, crate::Error>(profile_path.clone())
|
||||
Ok::<String, crate::Error>(profile_path.clone())
|
||||
} else {
|
||||
Err(crate::Error::from(crate::ErrorKind::InputError(
|
||||
"No pack manifest found in mrpack".to_string(),
|
||||
@@ -291,9 +308,9 @@ pub async fn install_zipped_mrpack_files(
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(mrpack_file))]
|
||||
#[theseus_macros::debug_pin]
|
||||
|
||||
pub async fn remove_all_related_files(
|
||||
profile_path: ProfilePathId,
|
||||
profile_path: String,
|
||||
mrpack_file: bytes::Bytes,
|
||||
) -> crate::Result<()> {
|
||||
let reader: Cursor<&bytes::Bytes> = Cursor::new(&mrpack_file);
|
||||
@@ -339,43 +356,39 @@ pub async fn remove_all_related_files(
|
||||
let all_hashes = pack
|
||||
.files
|
||||
.iter()
|
||||
.filter_map(|f| Some(f.hashes.get(&PackFileHash::Sha512)?.clone()))
|
||||
.filter_map(|f| Some(f.hashes.get(&PackFileHash::Sha1)?.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,
|
||||
let file_infos = CachedEntry::get_file_many(
|
||||
&all_hashes.iter().map(|x| &**x).collect::<Vec<_>>(),
|
||||
None,
|
||||
Some(json!({
|
||||
"hashes": all_hashes,
|
||||
"algorithm": "sha512",
|
||||
})),
|
||||
&state.fetch_semaphore,
|
||||
&creds,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?;
|
||||
let to_remove = hash_projects
|
||||
.into_values()
|
||||
|
||||
let to_remove = file_infos
|
||||
.into_iter()
|
||||
.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());
|
||||
|
||||
let profile = profile::get(&profile_path).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
})?;
|
||||
let profile_full_path = profile::get_full_path(&profile_path).await?;
|
||||
|
||||
for (file_path, project) in profile
|
||||
.get_projects(
|
||||
Some(CacheBehaviour::MustRevalidate),
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if let Some(metadata) = &project.metadata {
|
||||
if to_remove.contains(&metadata.project_id) {
|
||||
let path = profile_full_path.join(file_path);
|
||||
if path.exists() {
|
||||
io::remove_file(&path).await?;
|
||||
}
|
||||
@@ -386,10 +399,7 @@ pub async fn remove_all_related_files(
|
||||
// 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());
|
||||
let path: PathBuf = profile_full_path.join(file.path);
|
||||
if path.exists() {
|
||||
io::remove_file(&path).await?;
|
||||
}
|
||||
@@ -414,8 +424,9 @@ pub async fn remove_all_related_files(
|
||||
}
|
||||
|
||||
// Remove this file if a corresponding one exists in the filesystem
|
||||
let existing_file =
|
||||
profile_path.get_full_path().await?.join(&new_path);
|
||||
let existing_file = profile::get_full_path(&profile_path)
|
||||
.await?
|
||||
.join(&new_path);
|
||||
if existing_file.exists() {
|
||||
io::remove_file(&existing_file).await?;
|
||||
}
|
||||
|
||||
@@ -1,128 +1,56 @@
|
||||
//! Theseus process management interface
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::state::{MinecraftChild, ProfilePathId};
|
||||
use crate::state::Process;
|
||||
pub use crate::{
|
||||
state::{
|
||||
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
|
||||
},
|
||||
state::{Hooks, 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>>
|
||||
{
|
||||
pub async fn get_all() -> crate::Result<Vec<Process>> {
|
||||
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
|
||||
let processes = Process::get_all(&state.pool).await?;
|
||||
Ok(processes)
|
||||
}
|
||||
|
||||
// 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>> {
|
||||
pub async fn get_by_profile_path(
|
||||
profile_path: &str,
|
||||
) -> crate::Result<Vec<Process>> {
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
children.running_keys_with_profile(profile_path).await
|
||||
let processes =
|
||||
Process::get_from_profile(profile_path, &state.pool).await?;
|
||||
Ok(processes)
|
||||
}
|
||||
|
||||
// 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<()> {
|
||||
pub async fn kill(pid: i32) -> 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
|
||||
let process = Process::get(pid, &state.pool).await?;
|
||||
|
||||
if let Some(process) = process {
|
||||
process.kill().await?;
|
||||
|
||||
Ok(())
|
||||
} 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<()> {
|
||||
pub async fn wait_for(pid: i32) -> 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
|
||||
let process = Process::get(pid, &state.pool).await?;
|
||||
|
||||
if let Some(process) = process {
|
||||
process.wait_for().await?;
|
||||
|
||||
Ok(())
|
||||
} 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(())
|
||||
}
|
||||
|
||||
@@ -1,55 +1,45 @@
|
||||
//! Theseus profile management interface
|
||||
use crate::pack::install_from::CreatePackProfile;
|
||||
use crate::prelude::ProfilePathId;
|
||||
use crate::state::LinkedData;
|
||||
use crate::launcher::get_loader_version_from_profile;
|
||||
use crate::settings::Hooks;
|
||||
use crate::state::{LinkedData, ProfileInstallStage};
|
||||
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;
|
||||
pub use crate::{state::Profile, State};
|
||||
use chrono::Utc;
|
||||
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
|
||||
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)
|
||||
icon_path: Option<String>, // the icon for the profile
|
||||
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);
|
||||
|
||||
) -> crate::Result<String> {
|
||||
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 path = profile::sanitize_profile_name(&name);
|
||||
let mut full_path = state.directories.profiles_dir().join(&path);
|
||||
if full_path.exists() {
|
||||
let mut new_path;
|
||||
let mut new_full_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() {
|
||||
new_path = format!("{path} ({which})");
|
||||
new_full_path = state.directories.profiles_dir().join(&new_path);
|
||||
if !new_full_path.exists() {
|
||||
break;
|
||||
}
|
||||
which += 1;
|
||||
@@ -57,32 +47,60 @@ pub async fn profile_create(
|
||||
|
||||
tracing::debug!(
|
||||
"Folder collision: {}, renaming to: {}",
|
||||
path.display(),
|
||||
new_path.display()
|
||||
full_path.display(),
|
||||
new_full_path.display()
|
||||
);
|
||||
|
||||
path = new_path;
|
||||
name = new_name;
|
||||
full_path = new_full_path;
|
||||
}
|
||||
io::create_dir_all(&path).await?;
|
||||
io::create_dir_all(&full_path).await?;
|
||||
|
||||
info!(
|
||||
"Creating profile at path {}",
|
||||
&canonicalize(&path)?.display()
|
||||
&canonicalize(&full_path)?.display()
|
||||
);
|
||||
let loader = if modloader != ModLoader::Vanilla {
|
||||
get_loader_version_from_loader(
|
||||
game_version.clone(),
|
||||
get_loader_version_from_profile(
|
||||
&game_version,
|
||||
modloader,
|
||||
loader_version,
|
||||
loader_version.as_deref(),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut profile = Profile::new(uuid, name, game_version).await?;
|
||||
let mut profile = Profile {
|
||||
path: path.clone(),
|
||||
install_stage: ProfileInstallStage::NotInstalled,
|
||||
name,
|
||||
icon_path: None,
|
||||
game_version,
|
||||
loader: modloader,
|
||||
loader_version: loader.map(|x| x.id),
|
||||
groups: Vec::new(),
|
||||
linked_data,
|
||||
created: Utc::now(),
|
||||
modified: Utc::now(),
|
||||
last_played: None,
|
||||
submitted_time_played: 0,
|
||||
recent_time_played: 0,
|
||||
java_path: None,
|
||||
extra_launch_args: None,
|
||||
custom_env_vars: None,
|
||||
memory: None,
|
||||
force_fullscreen: None,
|
||||
game_resolution: None,
|
||||
hooks: Hooks {
|
||||
pre_launch: None,
|
||||
wrapper: None,
|
||||
post_exit: None,
|
||||
},
|
||||
};
|
||||
|
||||
let result = async {
|
||||
if let Some(ref icon) = icon {
|
||||
if let Some(ref icon) = icon_path {
|
||||
let bytes =
|
||||
io::read(state.directories.caches_dir().join(icon)).await?;
|
||||
profile
|
||||
@@ -90,93 +108,55 @@ pub async fn profile_create(
|
||||
&state.directories.caches_dir(),
|
||||
&state.io_semaphore,
|
||||
bytes::Bytes::from(bytes),
|
||||
&icon.to_string_lossy(),
|
||||
icon,
|
||||
)
|
||||
.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,
|
||||
crate::state::fs_watcher::watch_profile(
|
||||
&profile.path,
|
||||
&state.file_watcher,
|
||||
&state.directories,
|
||||
)
|
||||
.await?;
|
||||
|
||||
{
|
||||
let mut profiles = state.profiles.write().await;
|
||||
profiles
|
||||
.insert(profile.clone(), no_watch.unwrap_or_default())
|
||||
.await?;
|
||||
}
|
||||
profile.upsert(&state.pool).await?;
|
||||
|
||||
emit_profile(&profile.path, ProfilePayloadType::Created).await?;
|
||||
|
||||
if !skip_install_profile.unwrap_or(false) {
|
||||
crate::launcher::install_minecraft(&profile, None, false).await?;
|
||||
}
|
||||
State::sync().await?;
|
||||
|
||||
Ok(profile.profile_id())
|
||||
Ok(profile.path)
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(profile) => Ok(profile),
|
||||
Err(err) => {
|
||||
let _ = crate::api::profile::remove(&profile.profile_id()).await;
|
||||
let _ = profile::remove(&path).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> {
|
||||
copy_from: &str,
|
||||
) -> crate::Result<String> {
|
||||
// Original profile
|
||||
let profile = profile::get(©_from, None).await?.ok_or_else(|| {
|
||||
let profile = profile::get(copy_from).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),
|
||||
profile.name.clone(),
|
||||
profile.game_version.clone(),
|
||||
profile.loader,
|
||||
profile.loader_version.clone(),
|
||||
profile.icon_path.clone(),
|
||||
profile.linked_data.clone(),
|
||||
Some(true),
|
||||
)
|
||||
.await?;
|
||||
@@ -184,114 +164,26 @@ pub async fn profile_create_from_duplicate(
|
||||
// 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?,
|
||||
&profile_path_id,
|
||||
profile::get_full_path(copy_from).await?,
|
||||
&state.io_semaphore,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let duplicated_profile =
|
||||
profile::get(&profile_path_id, None).await?.ok_or_else(|| {
|
||||
profile::get(&profile_path_id).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?;
|
||||
emit_profile(&profile.path, ProfilePayloadType::Edited).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}")]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,24 @@
|
||||
use crate::state::CacheBehaviour;
|
||||
use crate::{
|
||||
event::{
|
||||
emit::{emit_profile, init_loading, loading_try_for_each_concurrent},
|
||||
emit::{emit_profile, init_loading},
|
||||
ProfilePayloadType,
|
||||
},
|
||||
pack::{self, install_from::generate_pack_from_version_id},
|
||||
prelude::{ProfilePathId, ProjectPathId},
|
||||
profile::get,
|
||||
state::{ProfileInstallStage, Project},
|
||||
LoadingBarType, State,
|
||||
state::ProfileInstallStage,
|
||||
LoadingBarType,
|
||||
};
|
||||
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,
|
||||
profile_path: &String,
|
||||
new_version_id: &String,
|
||||
) -> crate::Result<()> {
|
||||
let profile = get(profile_path, None).await?.ok_or_else(|| {
|
||||
let profile = get(profile_path).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error()
|
||||
})?;
|
||||
@@ -30,46 +30,29 @@ pub async fn update_managed_modrinth_version(
|
||||
};
|
||||
|
||||
// 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)?;
|
||||
let linked_data = profile.linked_data.as_ref().ok_or_else(unmanaged_err)?;
|
||||
|
||||
// Replace the pack with the new version
|
||||
replace_managed_modrinth(
|
||||
profile_path,
|
||||
&profile,
|
||||
project_id,
|
||||
version_id,
|
||||
&linked_data.project_id,
|
||||
&linked_data.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?;
|
||||
emit_profile(profile_path, 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(|| {
|
||||
|
||||
pub async fn repair_managed_modrinth(profile_path: &str) -> crate::Result<()> {
|
||||
let profile = get(profile_path).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error()
|
||||
})?;
|
||||
@@ -83,69 +66,44 @@ pub async fn repair_managed_modrinth(
|
||||
// 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?;
|
||||
let state = crate::State::get().await?;
|
||||
let projects_map = profile
|
||||
.get_projects(
|
||||
Some(CacheBehaviour::MustRevalidate),
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for (file, _) in projects_map {
|
||||
crate::state::Profile::remove_project(&profile.path, &file).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)?;
|
||||
let linked_data = profile.linked_data.as_ref().ok_or_else(unmanaged_err)?;
|
||||
|
||||
// Replace the pack with the same version
|
||||
replace_managed_modrinth(
|
||||
profile_path,
|
||||
&profile,
|
||||
project_id,
|
||||
version_id,
|
||||
&linked_data.project_id,
|
||||
&linked_data.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?;
|
||||
emit_profile(profile_path, 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_path: &str,
|
||||
profile: &crate::state::Profile,
|
||||
project_id: &String,
|
||||
version_id: &String,
|
||||
@@ -161,59 +119,63 @@ async fn replace_managed_modrinth(
|
||||
// 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?;
|
||||
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: crate::api::profile::get_full_path(profile_path)
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
pack_name: profile.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(
|
||||
// download in parallel, then join.
|
||||
try_join!(
|
||||
generate_pack_from_version_id(
|
||||
project_id.clone(),
|
||||
version_id.clone(),
|
||||
profile.metadata.name.clone(),
|
||||
profile.name.clone(),
|
||||
None,
|
||||
profile_path.clone(),
|
||||
profile_path.to_string(),
|
||||
Some(shared_loading_bar.clone())
|
||||
),
|
||||
generate_pack_from_version_id(
|
||||
project_id.clone(),
|
||||
new_version_id.clone(),
|
||||
profile.name.clone(),
|
||||
None,
|
||||
profile_path.to_string(),
|
||||
Some(shared_loading_bar)
|
||||
)
|
||||
.await?;
|
||||
old_pack_creator.description.existing_loading_bar = None;
|
||||
(old_pack_creator.clone(), old_pack_creator)
|
||||
};
|
||||
)?
|
||||
} 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.name.clone(),
|
||||
None,
|
||||
profile_path.to_string(),
|
||||
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(),
|
||||
profile_path.to_string(),
|
||||
old_pack_creator.file,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
use crate::state::{ProcessType, SafeProcesses};
|
||||
|
||||
pub async fn check_safe_loading_bars() -> crate::Result<bool> {
|
||||
SafeProcesses::is_complete(ProcessType::LoadingBar).await
|
||||
}
|
||||
@@ -1,21 +1,7 @@
|
||||
//! 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::{Hooks, MemorySettings, Profile, Settings, WindowSize},
|
||||
State,
|
||||
};
|
||||
|
||||
@@ -23,224 +9,15 @@ pub use crate::{
|
||||
#[tracing::instrument]
|
||||
pub async fn get() -> crate::Result<Settings> {
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
Ok(settings.clone())
|
||||
let settings = Settings::get(&state.pool).await?;
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
/// Sets entire settings
|
||||
#[tracing::instrument]
|
||||
pub async fn set(settings: Settings) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
settings.update(&state.pool).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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,64 @@
|
||||
//! Theseus tag management interface
|
||||
use crate::state::CachedEntry;
|
||||
pub use crate::{
|
||||
state::{Category, DonationPlatform, GameVersion, Loader, Tags},
|
||||
state::{Category, DonationPlatform, GameVersion, Loader},
|
||||
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;
|
||||
let categories =
|
||||
CachedEntry::get_categories(None, &state.pool, &state.api_semaphore)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::NoValueFor("category tags".to_string())
|
||||
})?;
|
||||
|
||||
Ok(tags.get_categories())
|
||||
Ok(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;
|
||||
let report_types =
|
||||
CachedEntry::get_report_types(None, &state.pool, &state.api_semaphore)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::NoValueFor("report type tags".to_string())
|
||||
})?;
|
||||
|
||||
Ok(tags.get_report_types())
|
||||
Ok(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;
|
||||
let loaders =
|
||||
CachedEntry::get_loaders(None, &state.pool, &state.api_semaphore)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::NoValueFor("loader tags".to_string())
|
||||
})?;
|
||||
|
||||
Ok(tags.get_loaders())
|
||||
Ok(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;
|
||||
let game_versions =
|
||||
CachedEntry::get_game_versions(None, &state.pool, &state.api_semaphore)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::NoValueFor("game version tags".to_string())
|
||||
})?;
|
||||
|
||||
Ok(tags.get_game_versions())
|
||||
Ok(game_versions)
|
||||
}
|
||||
|
||||
/// Get donation platform tags
|
||||
@@ -54,7 +66,15 @@ pub async fn get_game_version_tags() -> crate::Result<Vec<GameVersion>> {
|
||||
pub async fn get_donation_platform_tags() -> crate::Result<Vec<DonationPlatform>>
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let tags = state.tags.read().await;
|
||||
let donation_platforms = CachedEntry::get_donation_platforms(
|
||||
None,
|
||||
&state.pool,
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::NoValueFor("donation platform tags".to_string())
|
||||
})?;
|
||||
|
||||
Ok(tags.get_donation_platforms())
|
||||
Ok(donation_platforms)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user