forked from didirus/AstralRinth
Folder names (#318)
This commit is contained in:
@@ -112,7 +112,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let path = state.directories.java_versions_dir();
|
||||
let path = state.directories.java_versions_dir().await;
|
||||
|
||||
if path.exists() {
|
||||
io::remove_dir_all(&path).await?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
util::io::{self, IOError},
|
||||
State,
|
||||
{state::ProfilePathId, State},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -11,7 +11,7 @@ pub struct Logs {
|
||||
}
|
||||
impl Logs {
|
||||
async fn build(
|
||||
profile_uuid: uuid::Uuid,
|
||||
profile_subpath: &ProfilePathId,
|
||||
datetime_string: String,
|
||||
clear_contents: Option<bool>,
|
||||
) -> crate::Result<Self> {
|
||||
@@ -20,7 +20,7 @@ impl Logs {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
get_output_by_datetime(profile_uuid, &datetime_string)
|
||||
get_output_by_datetime(profile_subpath, &datetime_string)
|
||||
.await?,
|
||||
)
|
||||
},
|
||||
@@ -35,7 +35,18 @@ pub async fn get_logs(
|
||||
clear_contents: Option<bool>,
|
||||
) -> crate::Result<Vec<Logs>> {
|
||||
let state = State::get().await?;
|
||||
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
|
||||
let profile_path = if let Some(p) =
|
||||
crate::profile::get_by_uuid(profile_uuid, None).await?
|
||||
{
|
||||
p.profile_id()
|
||||
} else {
|
||||
return Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_uuid.to_string(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
|
||||
let mut logs = Vec::new();
|
||||
if logs_folder.exists() {
|
||||
for entry in std::fs::read_dir(&logs_folder)
|
||||
@@ -48,7 +59,7 @@ pub async fn get_logs(
|
||||
if let Some(datetime_string) = path.file_name() {
|
||||
logs.push(
|
||||
Logs::build(
|
||||
profile_uuid,
|
||||
&profile_path,
|
||||
datetime_string.to_string_lossy().to_string(),
|
||||
clear_contents,
|
||||
)
|
||||
@@ -69,9 +80,19 @@ pub async fn get_logs_by_datetime(
|
||||
profile_uuid: uuid::Uuid,
|
||||
datetime_string: String,
|
||||
) -> crate::Result<Logs> {
|
||||
let profile_path = if let Some(p) =
|
||||
crate::profile::get_by_uuid(profile_uuid, None).await?
|
||||
{
|
||||
p.profile_id()
|
||||
} else {
|
||||
return Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_uuid.to_string(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
Ok(Logs {
|
||||
output: Some(
|
||||
get_output_by_datetime(profile_uuid, &datetime_string).await?,
|
||||
get_output_by_datetime(&profile_path, &datetime_string).await?,
|
||||
),
|
||||
datetime_string,
|
||||
})
|
||||
@@ -79,19 +100,31 @@ pub async fn get_logs_by_datetime(
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_output_by_datetime(
|
||||
profile_uuid: uuid::Uuid,
|
||||
profile_subpath: &ProfilePathId,
|
||||
datetime_string: &str,
|
||||
) -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
|
||||
let logs_folder =
|
||||
state.directories.profile_logs_dir(profile_subpath).await?;
|
||||
let path = logs_folder.join(datetime_string).join("stdout.log");
|
||||
Ok(io::read_to_string(&path).await?)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs(profile_uuid: uuid::Uuid) -> crate::Result<()> {
|
||||
let profile_path = if let Some(p) =
|
||||
crate::profile::get_by_uuid(profile_uuid, None).await?
|
||||
{
|
||||
p.profile_id()
|
||||
} else {
|
||||
return Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_uuid.to_string(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
let state = State::get().await?;
|
||||
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
|
||||
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
|
||||
for entry in std::fs::read_dir(&logs_folder)
|
||||
.map_err(|e| IOError::with_path(e, &logs_folder))?
|
||||
{
|
||||
@@ -109,8 +142,19 @@ pub async fn delete_logs_by_datetime(
|
||||
profile_uuid: uuid::Uuid,
|
||||
datetime_string: &str,
|
||||
) -> crate::Result<()> {
|
||||
let profile_path = if let Some(p) =
|
||||
crate::profile::get_by_uuid(profile_uuid, None).await?
|
||||
{
|
||||
p.profile_id()
|
||||
} else {
|
||||
return Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_uuid.to_string(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
let state = State::get().await?;
|
||||
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
|
||||
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
|
||||
let path = logs_folder.join(datetime_string);
|
||||
io::remove_dir_all(&path).await?;
|
||||
Ok(())
|
||||
|
||||
@@ -29,6 +29,7 @@ pub mod prelude {
|
||||
profile::{self, Profile},
|
||||
profile_create, settings,
|
||||
state::JavaGlobals,
|
||||
state::{ProfilePathId, ProjectPathId},
|
||||
util::{
|
||||
io::{canonicalize, IOError},
|
||||
jre::JavaVersion,
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::event::emit::{
|
||||
};
|
||||
use crate::event::LoadingBarType;
|
||||
use crate::pack::install_from::{EnvType, PackFile, PackFileHash};
|
||||
use crate::state::{LinkedData, ProfileInstallStage, SideType};
|
||||
use crate::state::{LinkedData, ProfileInstallStage, ProfilePathId, SideType};
|
||||
use crate::util::fetch::{fetch_mirrors, write};
|
||||
use crate::State;
|
||||
use async_zip::tokio::read::seek::ZipFileReader;
|
||||
@@ -20,8 +20,8 @@ use super::install_from::{
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn install_pack(
|
||||
location: CreatePackLocation,
|
||||
profile: PathBuf,
|
||||
) -> crate::Result<PathBuf> {
|
||||
profile_path: ProfilePathId,
|
||||
) -> crate::Result<ProfilePathId> {
|
||||
// Get file from description
|
||||
let description: CreatePackDescription = match location {
|
||||
CreatePackLocation::FromVersionId {
|
||||
@@ -31,12 +31,16 @@ pub async fn install_pack(
|
||||
icon_url,
|
||||
} => {
|
||||
generate_pack_from_version_id(
|
||||
project_id, version_id, title, icon_url, profile,
|
||||
project_id,
|
||||
version_id,
|
||||
title,
|
||||
icon_url,
|
||||
profile_path,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
CreatePackLocation::FromFile { path } => {
|
||||
generate_pack_from_file(path, profile).await?
|
||||
generate_pack_from_file(path, profile_path).await?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,7 +50,7 @@ pub async fn install_pack(
|
||||
let project_id = description.project_id;
|
||||
let version_id = description.version_id;
|
||||
let existing_loading_bar = description.existing_loading_bar;
|
||||
let profile = description.profile;
|
||||
let profile_path = description.profile_path;
|
||||
|
||||
let state = &State::get().await?;
|
||||
|
||||
@@ -125,7 +129,7 @@ pub async fn install_pack(
|
||||
loader_version.cloned(),
|
||||
)
|
||||
.await?;
|
||||
crate::api::profile::edit(&profile, |prof| {
|
||||
crate::api::profile::edit(&profile_path, |prof| {
|
||||
prof.metadata.name =
|
||||
override_title.clone().unwrap_or_else(|| pack.name.clone());
|
||||
prof.install_stage = ProfileInstallStage::PackInstalling;
|
||||
@@ -142,12 +146,15 @@ pub async fn install_pack(
|
||||
})
|
||||
.await?;
|
||||
|
||||
let profile = profile.clone();
|
||||
let profile_path = profile_path.clone();
|
||||
let result = async {
|
||||
let loading_bar = init_or_edit_loading(
|
||||
existing_loading_bar,
|
||||
LoadingBarType::PackDownload {
|
||||
profile_path: profile.clone(),
|
||||
profile_path: profile_path
|
||||
.get_full_path()
|
||||
.await?
|
||||
.clone(),
|
||||
pack_name: pack.name.clone(),
|
||||
icon,
|
||||
pack_id: project_id,
|
||||
@@ -169,7 +176,7 @@ pub async fn install_pack(
|
||||
num_files,
|
||||
None,
|
||||
|project| {
|
||||
let profile = profile.clone();
|
||||
let profile_path = profile_path.clone();
|
||||
async move {
|
||||
//TODO: Future update: prompt user for optional files in a modpack
|
||||
if let Some(env) = project.env {
|
||||
@@ -203,7 +210,10 @@ pub async fn install_pack(
|
||||
match path {
|
||||
Component::CurDir
|
||||
| Component::Normal(_) => {
|
||||
let path = profile.join(project.path);
|
||||
let path = profile_path
|
||||
.get_full_path()
|
||||
.await?
|
||||
.join(project.path);
|
||||
write(
|
||||
&path,
|
||||
&file,
|
||||
@@ -265,7 +275,10 @@ pub async fn install_pack(
|
||||
|
||||
if new_path.file_name().is_some() {
|
||||
write(
|
||||
&profile.join(new_path),
|
||||
&profile_path
|
||||
.get_full_path()
|
||||
.await?
|
||||
.join(new_path),
|
||||
&content,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
@@ -285,7 +298,7 @@ pub async fn install_pack(
|
||||
}
|
||||
|
||||
if let Some(profile_val) =
|
||||
crate::api::profile::get(&profile, None).await?
|
||||
crate::api::profile::get(&profile_path, None).await?
|
||||
{
|
||||
crate::launcher::install_minecraft(
|
||||
&profile_val,
|
||||
@@ -296,14 +309,14 @@ pub async fn install_pack(
|
||||
State::sync().await?;
|
||||
}
|
||||
|
||||
Ok::<PathBuf, crate::Error>(profile.clone())
|
||||
Ok::<ProfilePathId, crate::Error>(profile_path.clone())
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(profile) => Ok(profile),
|
||||
Err(err) => {
|
||||
let _ = crate::api::profile::remove(&profile).await;
|
||||
let _ = crate::api::profile::remove(&profile_path).await;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
@@ -319,7 +332,7 @@ pub async fn install_pack(
|
||||
match result {
|
||||
Ok(profile) => Ok(profile),
|
||||
Err(err) => {
|
||||
let _ = crate::api::profile::remove(&profile).await;
|
||||
let _ = crate::api::profile::remove(&profile_path).await;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ 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::state::{LinkedData, ModrinthProject, ModrinthVersion, SideType};
|
||||
use crate::state::{
|
||||
LinkedData, ModrinthProject, ModrinthVersion, ProfilePathId, SideType,
|
||||
};
|
||||
use crate::util::fetch::{
|
||||
fetch, fetch_advanced, fetch_json, write_cached_icon,
|
||||
};
|
||||
@@ -71,7 +73,7 @@ pub enum PackDependency {
|
||||
Minecraft,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum CreatePackLocation {
|
||||
FromVersionId {
|
||||
@@ -98,6 +100,7 @@ pub struct CreatePackProfile {
|
||||
pub skip_install_profile: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreatePackDescription {
|
||||
pub file: bytes::Bytes,
|
||||
pub icon: Option<PathBuf>,
|
||||
@@ -105,7 +108,7 @@ pub struct CreatePackDescription {
|
||||
pub project_id: Option<String>,
|
||||
pub version_id: Option<String>,
|
||||
pub existing_loading_bar: Option<LoadingBarId>,
|
||||
pub profile: PathBuf,
|
||||
pub profile_path: ProfilePathId,
|
||||
}
|
||||
|
||||
pub fn get_profile_from_pack(
|
||||
@@ -158,13 +161,13 @@ pub async fn generate_pack_from_version_id(
|
||||
version_id: String,
|
||||
title: String,
|
||||
icon_url: Option<String>,
|
||||
profile: PathBuf,
|
||||
profile_path: ProfilePathId,
|
||||
) -> crate::Result<CreatePackDescription> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let loading_bar = init_loading(
|
||||
LoadingBarType::PackFileDownload {
|
||||
profile_path: profile.clone(),
|
||||
profile_path: profile_path.get_full_path().await?,
|
||||
pack_name: title,
|
||||
icon: icon_url,
|
||||
pack_version: version_id.clone(),
|
||||
@@ -253,7 +256,7 @@ pub async fn generate_pack_from_version_id(
|
||||
project_id: Some(project_id),
|
||||
version_id: Some(version_id),
|
||||
existing_loading_bar: Some(loading_bar),
|
||||
profile,
|
||||
profile_path,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -261,7 +264,7 @@ pub async fn generate_pack_from_version_id(
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn generate_pack_from_file(
|
||||
path: PathBuf,
|
||||
profile: PathBuf,
|
||||
profile_path: ProfilePathId,
|
||||
) -> crate::Result<CreatePackDescription> {
|
||||
let file = io::read(&path).await?;
|
||||
Ok(CreatePackDescription {
|
||||
@@ -271,6 +274,6 @@ pub async fn generate_pack_from_file(
|
||||
project_id: None,
|
||||
version_id: None,
|
||||
existing_loading_bar: None,
|
||||
profile,
|
||||
profile_path,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
//! Theseus process management interface
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{state::MinecraftChild, util::io::IOError};
|
||||
pub use crate::{
|
||||
state::{
|
||||
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
|
||||
},
|
||||
State,
|
||||
};
|
||||
use crate::{
|
||||
state::{MinecraftChild, ProfilePathId},
|
||||
util::io::IOError,
|
||||
};
|
||||
|
||||
// Gets whether a child process stored in the state by UUID has finished
|
||||
#[tracing::instrument]
|
||||
@@ -45,7 +47,8 @@ pub async fn get_all_running_uuids() -> crate::Result<Vec<Uuid>> {
|
||||
|
||||
// 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<PathBuf>> {
|
||||
pub async fn get_all_running_profile_paths() -> crate::Result<Vec<ProfilePathId>>
|
||||
{
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
children.running_profile_paths().await
|
||||
@@ -62,7 +65,7 @@ pub async fn get_all_running_profiles() -> crate::Result<Vec<Profile>> {
|
||||
// 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: &Path,
|
||||
profile_path: ProfilePathId,
|
||||
) -> crate::Result<Vec<Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let children = state.children.read().await;
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::pack::install_from::{
|
||||
EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
|
||||
};
|
||||
use crate::prelude::JavaVersion;
|
||||
use crate::state::ProjectMetadata;
|
||||
use crate::state::{ProfilePathId, ProjectMetadata, ProjectPathId};
|
||||
|
||||
use crate::util::io::{self, IOError};
|
||||
use crate::{
|
||||
@@ -32,14 +32,14 @@ use tokio::{fs::File, process::Command, sync::RwLock};
|
||||
|
||||
/// Remove a profile
|
||||
#[tracing::instrument]
|
||||
pub async fn remove(path: &Path) -> crate::Result<()> {
|
||||
pub async fn remove(path: &ProfilePathId) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
if let Some(profile) = profiles.remove(path).await? {
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path.clone(),
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Removed,
|
||||
)
|
||||
@@ -49,13 +49,14 @@ pub async fn remove(path: &Path) -> crate::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a profile by path,
|
||||
/// Get a profile by relative path (or, name)
|
||||
#[tracing::instrument]
|
||||
pub async fn get(
|
||||
path: &Path,
|
||||
path: &ProfilePathId,
|
||||
clear_projects: Option<bool>,
|
||||
) -> crate::Result<Option<Profile>> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let profiles = state.profiles.read().await;
|
||||
let mut profile = profiles.0.get(path).cloned();
|
||||
|
||||
@@ -68,9 +69,29 @@ pub async fn get(
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
/// Get a profile by uuid
|
||||
#[tracing::instrument]
|
||||
pub async fn get_by_uuid(
|
||||
uuid: uuid::Uuid,
|
||||
clear_projects: Option<bool>,
|
||||
) -> crate::Result<Option<Profile>> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let profiles = state.profiles.read().await;
|
||||
let mut profile = profiles.0.values().find(|x| x.uuid == uuid).cloned();
|
||||
|
||||
if clear_projects.unwrap_or(false) {
|
||||
if let Some(profile) = &mut profile {
|
||||
profile.projects = HashMap::new();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
/// Edit a profile using a given asynchronous closure
|
||||
pub async fn edit<Fut>(
|
||||
path: &Path,
|
||||
path: &ProfilePathId,
|
||||
action: impl Fn(&mut Profile) -> Fut,
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
@@ -85,7 +106,7 @@ where
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path.clone(),
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@@ -93,16 +114,14 @@ where
|
||||
|
||||
Ok(())
|
||||
}
|
||||
None => Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
path.display().to_string(),
|
||||
)
|
||||
.as_error()),
|
||||
None => Err(crate::ErrorKind::UnmanagedProfileError(path.to_string())
|
||||
.as_error()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Edits a profile's icon
|
||||
pub async fn edit_icon(
|
||||
path: &Path,
|
||||
path: &ProfilePathId,
|
||||
icon_path: Option<&Path>,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
@@ -125,17 +144,17 @@ pub async fn edit_icon(
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path.clone(),
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
path.display().to_string(),
|
||||
)
|
||||
.as_error()),
|
||||
None => {
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(path.to_string())
|
||||
.as_error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
edit(path, |profile| {
|
||||
@@ -155,7 +174,7 @@ pub async fn edit_icon(
|
||||
// Generally this would be used for profile_create, to get the optimal JRE key
|
||||
// this can be overwritten by the user a profile-by-profile basis
|
||||
pub async fn get_optimal_jre_key(
|
||||
path: &Path,
|
||||
path: &ProfilePathId,
|
||||
) -> crate::Result<Option<JavaVersion>> {
|
||||
let state = State::get().await?;
|
||||
|
||||
@@ -193,10 +212,8 @@ pub async fn get_optimal_jre_key(
|
||||
|
||||
Ok(version)
|
||||
} else {
|
||||
Err(
|
||||
crate::ErrorKind::UnmanagedProfileError(path.display().to_string())
|
||||
.as_error(),
|
||||
)
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(path.to_string())
|
||||
.as_error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +221,7 @@ pub async fn get_optimal_jre_key(
|
||||
#[tracing::instrument]
|
||||
pub async fn list(
|
||||
clear_projects: Option<bool>,
|
||||
) -> crate::Result<HashMap<PathBuf, Profile>> {
|
||||
) -> crate::Result<HashMap<ProfilePathId, Profile>> {
|
||||
let state = State::get().await?;
|
||||
let profiles = state.profiles.read().await;
|
||||
Ok(profiles
|
||||
@@ -223,14 +240,12 @@ pub async fn list(
|
||||
|
||||
/// Installs/Repairs a profile
|
||||
#[tracing::instrument]
|
||||
pub async fn install(path: &Path) -> crate::Result<()> {
|
||||
pub async fn install(path: &ProfilePathId) -> crate::Result<()> {
|
||||
if let Some(profile) = get(path, None).await? {
|
||||
crate::launcher::install_minecraft(&profile, None).await?;
|
||||
} else {
|
||||
return Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
path.display().to_string(),
|
||||
)
|
||||
.as_error());
|
||||
return Err(crate::ErrorKind::UnmanagedProfileError(path.to_string())
|
||||
.as_error());
|
||||
}
|
||||
State::sync().await?;
|
||||
Ok(())
|
||||
@@ -239,12 +254,12 @@ pub async fn install(path: &Path) -> crate::Result<()> {
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn update_all(
|
||||
profile_path: &Path,
|
||||
) -> crate::Result<HashMap<PathBuf, PathBuf>> {
|
||||
profile_path: &ProfilePathId,
|
||||
) -> crate::Result<HashMap<ProjectPathId, ProjectPathId>> {
|
||||
if let Some(profile) = get(profile_path, None).await? {
|
||||
let loading_bar = init_loading(
|
||||
LoadingBarType::ProfileUpdate {
|
||||
profile_path: profile.path.clone(),
|
||||
profile_path: profile.get_profile_full_path().await?,
|
||||
profile_name: profile.metadata.name.clone(),
|
||||
},
|
||||
100.0,
|
||||
@@ -252,6 +267,7 @@ pub async fn update_all(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let profile_base_path = profile.get_profile_full_path().await?;
|
||||
let keys = profile
|
||||
.projects
|
||||
.into_iter()
|
||||
@@ -272,7 +288,7 @@ pub async fn update_all(
|
||||
|
||||
use futures::StreamExt;
|
||||
loading_try_for_each_concurrent(
|
||||
futures::stream::iter(keys).map(Ok::<PathBuf, crate::Error>),
|
||||
futures::stream::iter(keys).map(Ok::<ProjectPathId, crate::Error>),
|
||||
None,
|
||||
Some(&loading_bar),
|
||||
100.0,
|
||||
@@ -297,7 +313,7 @@ pub async fn update_all(
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path,
|
||||
profile_base_path,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@@ -306,20 +322,22 @@ pub async fn update_all(
|
||||
|
||||
Ok(Arc::try_unwrap(map).unwrap().into_inner())
|
||||
} else {
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_path.display().to_string(),
|
||||
Err(
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error(),
|
||||
)
|
||||
.as_error())
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates a project to the latest version
|
||||
/// Uses and returns the relative path to the project
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn update_project(
|
||||
profile_path: &Path,
|
||||
project_path: &Path,
|
||||
profile_path: &ProfilePathId,
|
||||
project_path: &ProjectPathId,
|
||||
skip_send_event: Option<bool>,
|
||||
) -> crate::Result<PathBuf> {
|
||||
) -> crate::Result<ProjectPathId> {
|
||||
if let Some(profile) = get(profile_path, None).await? {
|
||||
if let Some(project) = profile.projects.get(project_path) {
|
||||
if let ProjectMetadata::Modrinth {
|
||||
@@ -331,13 +349,13 @@ pub async fn update_project(
|
||||
.add_project_version(update_version.id.clone())
|
||||
.await?;
|
||||
|
||||
if path != project_path {
|
||||
if path != project_path.clone() {
|
||||
profile.remove_project(project_path, Some(true)).await?;
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let mut profiles = state.profiles.write().await;
|
||||
if let Some(profile) = profiles.0.get_mut(project_path) {
|
||||
if let Some(profile) = profiles.0.get_mut(profile_path) {
|
||||
let value = profile.projects.remove(project_path);
|
||||
if let Some(mut project) = value {
|
||||
if let ProjectMetadata::Modrinth {
|
||||
@@ -354,7 +372,7 @@ pub async fn update_project(
|
||||
if !skip_send_event.unwrap_or(false) {
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@@ -371,25 +389,26 @@ pub async fn update_project(
|
||||
)
|
||||
.as_error())
|
||||
} else {
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_path.display().to_string(),
|
||||
Err(
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error(),
|
||||
)
|
||||
.as_error())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a project from a version
|
||||
/// Returns the relative path to the project as a ProjectPathId
|
||||
#[tracing::instrument]
|
||||
pub async fn add_project_from_version(
|
||||
profile_path: &Path,
|
||||
profile_path: &ProfilePathId,
|
||||
version_id: String,
|
||||
) -> crate::Result<PathBuf> {
|
||||
) -> crate::Result<ProjectPathId> {
|
||||
if let Some(profile) = get(profile_path, None).await? {
|
||||
let (path, _) = profile.add_project_version(version_id).await?;
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@@ -398,20 +417,21 @@ pub async fn add_project_from_version(
|
||||
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_path.display().to_string(),
|
||||
Err(
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error(),
|
||||
)
|
||||
.as_error())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a project from an FS path
|
||||
/// Uses and returns the relative path to the project as a ProjectPathId
|
||||
#[tracing::instrument]
|
||||
pub async fn add_project_from_path(
|
||||
profile_path: &Path,
|
||||
profile_path: &ProfilePathId,
|
||||
path: &Path,
|
||||
project_type: Option<String>,
|
||||
) -> crate::Result<PathBuf> {
|
||||
) -> crate::Result<ProjectPathId> {
|
||||
if let Some(profile) = get(profile_path, None).await? {
|
||||
let file = io::read(path).await?;
|
||||
let file_name = path
|
||||
@@ -430,7 +450,7 @@ pub async fn add_project_from_path(
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@@ -439,25 +459,27 @@ pub async fn add_project_from_path(
|
||||
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
profile_path.display().to_string(),
|
||||
Err(
|
||||
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
|
||||
.as_error(),
|
||||
)
|
||||
.as_error())
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle whether a project is disabled or not
|
||||
/// Project path should be relative to the profile
|
||||
/// returns the new state, relative to the profile
|
||||
#[tracing::instrument]
|
||||
pub async fn toggle_disable_project(
|
||||
profile: &Path,
|
||||
project: &Path,
|
||||
) -> crate::Result<PathBuf> {
|
||||
profile: &ProfilePathId,
|
||||
project: &ProjectPathId,
|
||||
) -> crate::Result<ProjectPathId> {
|
||||
if let Some(profile) = get(profile, None).await? {
|
||||
let res = profile.toggle_disable_project(project).await?;
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@@ -466,25 +488,24 @@ pub async fn toggle_disable_project(
|
||||
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
profile.display().to_string(),
|
||||
)
|
||||
.as_error())
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(profile.to_string())
|
||||
.as_error())
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a project from a profile
|
||||
/// Uses and returns the relative path to the project
|
||||
#[tracing::instrument]
|
||||
pub async fn remove_project(
|
||||
profile: &Path,
|
||||
project: &Path,
|
||||
profile: &ProfilePathId,
|
||||
project: &ProjectPathId,
|
||||
) -> crate::Result<()> {
|
||||
if let Some(profile) = get(profile, None).await? {
|
||||
profile.remove_project(project, None).await?;
|
||||
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Edited,
|
||||
)
|
||||
@@ -493,10 +514,8 @@ pub async fn remove_project(
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(
|
||||
profile.display().to_string(),
|
||||
)
|
||||
.as_error())
|
||||
Err(crate::ErrorKind::UnmanagedProfileError(profile.to_string())
|
||||
.as_error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +524,7 @@ pub async fn remove_project(
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn export_mrpack(
|
||||
profile_path: &Path,
|
||||
profile_path: &ProfilePathId,
|
||||
export_path: PathBuf,
|
||||
included_overrides: Vec<String>, // which folders to include in the overrides
|
||||
version_id: Option<String>,
|
||||
@@ -516,11 +535,24 @@ pub async fn export_mrpack(
|
||||
let profile = get(profile_path, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to export a nonexistent or unloaded profile at path {}!",
|
||||
profile_path.display()
|
||||
profile_path
|
||||
))
|
||||
})?;
|
||||
|
||||
let profile_base_path = &profile.path;
|
||||
// remove .DS_Store files from included_overrides
|
||||
let included_overrides = included_overrides
|
||||
.into_iter()
|
||||
.filter(|x| {
|
||||
if let Some(f) = PathBuf::from(x).file_name() {
|
||||
if f.to_string_lossy().starts_with(".DS_Store") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let profile_base_path = &profile.get_profile_full_path().await?;
|
||||
|
||||
let mut file = File::create(&export_path)
|
||||
.await
|
||||
@@ -529,7 +561,7 @@ pub async fn export_mrpack(
|
||||
|
||||
// Create mrpack json configuration file
|
||||
let version_id = version_id.unwrap_or("1.0.0".to_string());
|
||||
let packfile = create_mrpack_json(&profile, version_id)?;
|
||||
let packfile = create_mrpack_json(&profile, version_id).await?;
|
||||
let modrinth_path_list = get_modrinth_pack_list(&packfile);
|
||||
|
||||
// Build vec of all files in the folder
|
||||
@@ -539,7 +571,7 @@ pub async fn export_mrpack(
|
||||
// Initialize loading bar
|
||||
let loading_bar = init_loading(
|
||||
LoadingBarType::ZipExtract {
|
||||
profile_path: profile.path.to_path_buf(),
|
||||
profile_path: profile.get_profile_full_path().await?,
|
||||
profile_name: profile.metadata.name.clone(),
|
||||
},
|
||||
path_list.len() as f64,
|
||||
@@ -628,25 +660,28 @@ pub async fn export_mrpack(
|
||||
// => [folder1, folder2]
|
||||
#[tracing::instrument]
|
||||
pub async fn get_potential_override_folders(
|
||||
profile_path: PathBuf,
|
||||
profile_path: ProfilePathId,
|
||||
) -> crate::Result<Vec<PathBuf>> {
|
||||
// First, get a dummy mrpack json for the files within
|
||||
let profile: Profile =
|
||||
get(&profile_path, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to export a nonexistent or unloaded profile at path {}!",
|
||||
profile_path.display()
|
||||
profile_path
|
||||
))
|
||||
})?;
|
||||
let mrpack = create_mrpack_json(&profile, "0".to_string())?;
|
||||
// dummy mrpack to get pack list
|
||||
let mrpack = create_mrpack_json(&profile, "0".to_string()).await?;
|
||||
let mrpack_files = get_modrinth_pack_list(&mrpack);
|
||||
|
||||
let mut path_list: Vec<PathBuf> = Vec::new();
|
||||
let mut read_dir = io::read_dir(&profile_path).await?;
|
||||
|
||||
let profile_base_dir = profile.get_profile_full_path().await?;
|
||||
let mut read_dir = io::read_dir(&profile_base_dir).await?;
|
||||
while let Some(entry) = read_dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &profile_path))?
|
||||
.map_err(|e| IOError::with_path(e, &profile_base_dir))?
|
||||
{
|
||||
let path: PathBuf = entry.path();
|
||||
if path.is_dir() {
|
||||
@@ -655,17 +690,17 @@ pub async fn get_potential_override_folders(
|
||||
while let Some(entry) = read_dir
|
||||
.next_entry()
|
||||
.await
|
||||
.map_err(|e| IOError::with_path(e, &profile_path))?
|
||||
.map_err(|e| IOError::with_path(e, &profile_base_dir))?
|
||||
{
|
||||
let path: PathBuf = entry.path();
|
||||
let name = path.strip_prefix(&profile_path)?.to_path_buf();
|
||||
let name = path.strip_prefix(&profile_base_dir)?.to_path_buf();
|
||||
if !mrpack_files.contains(&name.to_string_lossy().to_string()) {
|
||||
path_list.push(name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// One layer of files/folders if its a file
|
||||
let name = path.strip_prefix(&profile_path)?.to_path_buf();
|
||||
let name = path.strip_prefix(&profile_base_dir)?.to_path_buf();
|
||||
if !mrpack_files.contains(&name.to_string_lossy().to_string()) {
|
||||
path_list.push(name);
|
||||
}
|
||||
@@ -677,7 +712,9 @@ pub async fn get_potential_override_folders(
|
||||
/// Run Minecraft using a profile and the default credentials, logged in credentials,
|
||||
/// failing with an error if no credentials are available
|
||||
#[tracing::instrument]
|
||||
pub async fn run(path: &Path) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
||||
pub async fn run(
|
||||
path: &ProfilePathId,
|
||||
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
||||
let state = State::get().await?;
|
||||
|
||||
// Get default account and refresh credentials (preferred way to log in)
|
||||
@@ -702,7 +739,7 @@ pub async fn run(path: &Path) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
||||
#[tracing::instrument(skip(credentials))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn run_credentials(
|
||||
path: &Path,
|
||||
path: &ProfilePathId,
|
||||
credentials: &auth::Credentials,
|
||||
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
||||
let state = State::get().await?;
|
||||
@@ -710,7 +747,7 @@ pub async fn run_credentials(
|
||||
let profile = get(path, None).await?.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to run a nonexistent or unloaded profile at path {}!",
|
||||
path.display()
|
||||
path
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -720,11 +757,12 @@ pub async fn run_credentials(
|
||||
// TODO: hook parameters
|
||||
let mut cmd = hook.split(' ');
|
||||
if let Some(command) = cmd.next() {
|
||||
let full_path = path.get_full_path().await?;
|
||||
let result = Command::new(command)
|
||||
.args(&cmd.collect::<Vec<&str>>())
|
||||
.current_dir(path)
|
||||
.current_dir(&full_path)
|
||||
.spawn()
|
||||
.map_err(|e| IOError::with_path(e, path))?
|
||||
.map_err(|e| IOError::with_path(e, &full_path))?
|
||||
.wait()
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
@@ -767,7 +805,9 @@ pub async fn run_credentials(
|
||||
let mut cmd = hook.split(' ');
|
||||
if let Some(command) = cmd.next() {
|
||||
let mut command = Command::new(command);
|
||||
command.args(&cmd.collect::<Vec<&str>>()).current_dir(path);
|
||||
command
|
||||
.args(&cmd.collect::<Vec<&str>>())
|
||||
.current_dir(path.get_full_path().await?);
|
||||
Some(command)
|
||||
} else {
|
||||
None
|
||||
@@ -806,7 +846,7 @@ fn get_modrinth_pack_list(packfile: &PackFormat) -> Vec<String> {
|
||||
/// Creates a json configuration for a .mrpack zipped file
|
||||
// Version ID of uploaded version (ie 1.1.5), not the unique identifying ID of the version (nvrqJg44)
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn create_mrpack_json(
|
||||
pub async fn create_mrpack_json(
|
||||
profile: &Profile,
|
||||
version_id: String,
|
||||
) -> crate::Result<PackFormat> {
|
||||
@@ -845,17 +885,15 @@ pub fn create_mrpack_json(
|
||||
.map(|(k, v)| (k, sanitize_loader_version_string(&v).to_string()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let base_path = &profile.path;
|
||||
let profile_base_path = profile.get_profile_full_path().await?;
|
||||
let files: Result<Vec<PackFile>, crate::ErrorKind> = profile
|
||||
.projects
|
||||
.iter()
|
||||
.filter_map(|(mod_path, project)| {
|
||||
let path = match mod_path.strip_prefix(base_path) {
|
||||
Ok(path) => path.to_string_lossy().to_string(),
|
||||
Err(e) => {
|
||||
return Some(Err(e.into()));
|
||||
}
|
||||
};
|
||||
let path: String = profile_base_path
|
||||
.join(mod_path.0.clone())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// Only Modrinth projects have a modrinth metadata field for the modrinth.json
|
||||
Some(Ok(match project.metadata {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Theseus profile management interface
|
||||
use crate::state::LinkedData;
|
||||
use crate::state::{LinkedData, ProfilePathId};
|
||||
use crate::util::io::{self, canonicalize};
|
||||
use crate::{
|
||||
event::{emit::emit_profile, ProfilePayloadType},
|
||||
@@ -10,20 +10,18 @@ pub use crate::{
|
||||
State,
|
||||
};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use futures::prelude::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
|
||||
use tracing::{info, trace};
|
||||
use uuid::Uuid;
|
||||
|
||||
// Creates a profile at the given filepath and adds it to the in-memory state
|
||||
// Returns filepath at which it can be accessed in the State
|
||||
// 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(
|
||||
name: String, // the name of the profile, and relative path
|
||||
mut name: String, // the name of the profile, and relative path
|
||||
game_version: String, // the game version of the profile
|
||||
modloader: ModLoader, // the modloader to use
|
||||
loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader. defaults to latest
|
||||
@@ -31,32 +29,34 @@ pub async fn profile_create(
|
||||
icon_url: Option<String>, // the URL icon for a profile (ONLY USED FOR TEMPORARY PROFILES)
|
||||
linked_data: Option<LinkedData>, // the linked project ID (mainly for modpacks)- used for updating
|
||||
skip_install_profile: Option<bool>,
|
||||
) -> crate::Result<PathBuf> {
|
||||
) -> crate::Result<ProfilePathId> {
|
||||
trace!("Creating new profile. {}", name);
|
||||
let state = State::get().await?;
|
||||
let uuid = Uuid::new_v4();
|
||||
let path = state.directories.profiles_dir().join(uuid.to_string());
|
||||
|
||||
let mut path = state.directories.profiles_dir().await.join(&name);
|
||||
if path.exists() {
|
||||
if !path.is_dir() {
|
||||
return Err(ProfileCreationError::NotFolder.into());
|
||||
}
|
||||
if path.join("profile.json").exists() {
|
||||
return Err(ProfileCreationError::ProfileExistsError(
|
||||
path.join("profile.json"),
|
||||
)
|
||||
.into());
|
||||
let mut new_name;
|
||||
let mut new_path;
|
||||
let mut which = 1;
|
||||
loop {
|
||||
new_name = format!("{name} ({which})");
|
||||
new_path = state.directories.profiles_dir().await.join(&new_name);
|
||||
if !new_path.exists() {
|
||||
break;
|
||||
}
|
||||
which += 1;
|
||||
}
|
||||
|
||||
if ReadDirStream::new(io::read_dir(&path).await?)
|
||||
.next()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
return Err(ProfileCreationError::NotEmptyFolder.into());
|
||||
}
|
||||
} else {
|
||||
io::create_dir_all(&path).await?;
|
||||
tracing::debug!(
|
||||
"Folder collision: {}, renaming to: {}",
|
||||
path.display(),
|
||||
new_path.display()
|
||||
);
|
||||
path = new_path;
|
||||
name = new_name;
|
||||
}
|
||||
io::create_dir_all(&path).await?;
|
||||
|
||||
info!(
|
||||
"Creating profile at path {}",
|
||||
@@ -73,13 +73,11 @@ pub async fn profile_create(
|
||||
None
|
||||
};
|
||||
|
||||
// Fully canonicalize now that its created for storing purposes
|
||||
let path = canonicalize(&path)?;
|
||||
let mut profile =
|
||||
Profile::new(uuid, name, game_version, path.clone()).await?;
|
||||
let mut profile = Profile::new(uuid, name, game_version).await?;
|
||||
let result = async {
|
||||
if let Some(ref icon) = icon {
|
||||
let bytes = io::read(icon).await?;
|
||||
let bytes =
|
||||
io::read(state.directories.caches_dir().join(icon)).await?;
|
||||
profile
|
||||
.set_icon(
|
||||
&state.directories.caches_dir(),
|
||||
@@ -100,7 +98,7 @@ pub async fn profile_create(
|
||||
|
||||
emit_profile(
|
||||
uuid,
|
||||
path.clone(),
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Created,
|
||||
)
|
||||
@@ -116,14 +114,14 @@ pub async fn profile_create(
|
||||
}
|
||||
State::sync().await?;
|
||||
|
||||
Ok(path)
|
||||
Ok(profile.profile_id())
|
||||
}
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(profile) => Ok(profile),
|
||||
Err(err) => {
|
||||
let _ = crate::api::profile::remove(&profile.path).await;
|
||||
let _ = crate::api::profile::remove(&profile.profile_id()).await;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
//! Theseus profile management interface
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use io::IOError;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
event::emit::{emit_loading, init_loading},
|
||||
prelude::DirectoryInfo,
|
||||
state::{self, Profiles},
|
||||
util::io,
|
||||
};
|
||||
pub use crate::{
|
||||
state::{
|
||||
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
|
||||
@@ -19,6 +30,16 @@ pub async fn get() -> crate::Result<Settings> {
|
||||
#[tracing::instrument]
|
||||
pub async fn set(settings: Settings) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
|
||||
if settings.loaded_config_dir
|
||||
!= state.settings.read().await.loaded_config_dir
|
||||
{
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Cannot change config directory as setting".to_string(),
|
||||
)
|
||||
.as_error());
|
||||
}
|
||||
|
||||
let (reset_io, reset_fetch) = async {
|
||||
let read = state.settings.read().await;
|
||||
(
|
||||
@@ -42,3 +63,119 @@ pub async fn set(settings: Settings) -> crate::Result<()> {
|
||||
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<()> {
|
||||
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());
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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)?;
|
||||
let total_entries = std::fs::read_dir(&old_config_dir)
|
||||
.map_err(|e| IOError::with_path(e, &old_config_dir))?
|
||||
.count() as f64;
|
||||
|
||||
// Move all files over from state_write.directories.config_dir to new_config_dir
|
||||
tracing::trace!("Renaming folder structure");
|
||||
let mut i = 0.0;
|
||||
let mut entries = io::read_dir(&old_config_dir).await?;
|
||||
while let Some(entry) = 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() {
|
||||
// Ignore settings.json
|
||||
if file_name == state::SETTINGS_FILE_NAME {
|
||||
continue;
|
||||
}
|
||||
// Ignore caches folder
|
||||
if file_name == state::CACHES_FOLDER_NAME {
|
||||
continue;
|
||||
}
|
||||
// Ignore modrinth_logs folder
|
||||
if file_name == state::LAUNCHER_LOGS_FOLDER_NAME {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_path = new_config_dir.join(file_name);
|
||||
io::rename(entry_path, new_path).await?;
|
||||
|
||||
i += 1.0;
|
||||
emit_loading(&loading_bar, 90.0 * (i / total_entries), 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?;
|
||||
|
||||
// TODO: need to be able to safely error out of this function, reverting the changes
|
||||
tracing::info!(
|
||||
"Successfully switched config folder to: {}",
|
||||
new_config_dir.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ pub enum ErrorKind {
|
||||
#[error("Error acquiring semaphore: {0}")]
|
||||
AcquireError(#[from] tokio::sync::AcquireError),
|
||||
|
||||
#[error("Profile {0} is not managed by Theseus!")]
|
||||
#[error("Profile {0} is not managed by the app!")]
|
||||
UnmanagedProfileError(String),
|
||||
|
||||
#[error("Could not create profile: {0}")]
|
||||
|
||||
@@ -181,6 +181,9 @@ pub enum LoadingBarType {
|
||||
profile_path: PathBuf,
|
||||
profile_name: String,
|
||||
},
|
||||
ConfigChange {
|
||||
new_path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
|
||||
@@ -68,6 +68,7 @@ pub async fn download_version_info(
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(&version_id)
|
||||
.await
|
||||
.join(format!("{version_id}.json"));
|
||||
|
||||
let res = if path.exists() && !force.unwrap_or(false) {
|
||||
@@ -118,6 +119,7 @@ pub async fn download_client(
|
||||
let path = st
|
||||
.directories
|
||||
.version_dir(version)
|
||||
.await
|
||||
.join(format!("{version}.jar"));
|
||||
|
||||
if !path.exists() {
|
||||
@@ -149,6 +151,7 @@ pub async fn download_assets_index(
|
||||
let path = st
|
||||
.directories
|
||||
.assets_index_dir()
|
||||
.await
|
||||
.join(format!("{}.json", &version.asset_index.id));
|
||||
|
||||
let res = if path.exists() {
|
||||
@@ -192,7 +195,7 @@ pub async fn download_assets(
|
||||
None,
|
||||
|(name, asset)| async move {
|
||||
let hash = &asset.hash;
|
||||
let resource_path = st.directories.object_dir(hash);
|
||||
let resource_path = st.directories.object_dir(hash).await;
|
||||
let url = format!(
|
||||
"https://resources.download.minecraft.net/{sub_hash}/{hash}",
|
||||
sub_hash = &hash[..2]
|
||||
@@ -215,7 +218,7 @@ pub async fn download_assets(
|
||||
let resource = fetch_cell
|
||||
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore))
|
||||
.await?;
|
||||
let resource_path = st.directories.legacy_assets_dir().join(
|
||||
let resource_path = st.directories.legacy_assets_dir().await.join(
|
||||
name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
|
||||
);
|
||||
write(&resource_path, resource, &st.io_semaphore).await?;
|
||||
@@ -245,8 +248,8 @@ pub async fn download_libraries(
|
||||
tracing::debug!("Loading libraries");
|
||||
|
||||
tokio::try_join! {
|
||||
io::create_dir_all(st.directories.libraries_dir()),
|
||||
io::create_dir_all(st.directories.version_natives_dir(version))
|
||||
io::create_dir_all(st.directories.libraries_dir().await),
|
||||
io::create_dir_all(st.directories.version_natives_dir(version).await)
|
||||
}?;
|
||||
let num_files = libraries.len();
|
||||
loading_try_for_each_concurrent(
|
||||
@@ -262,7 +265,7 @@ pub async fn download_libraries(
|
||||
tokio::try_join! {
|
||||
async {
|
||||
let artifact_path = d::get_path_from_artifact(&library.name)?;
|
||||
let path = st.directories.libraries_dir().join(&artifact_path);
|
||||
let path = st.directories.libraries_dir().await.join(&artifact_path);
|
||||
|
||||
match library.downloads {
|
||||
_ if path.exists() => Ok(()),
|
||||
@@ -314,7 +317,7 @@ pub async fn download_libraries(
|
||||
let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?;
|
||||
let reader = std::io::Cursor::new(&data);
|
||||
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
|
||||
match archive.extract(&st.directories.version_natives_dir(version)) {
|
||||
match archive.extract(st.directories.version_natives_dir(version).await) {
|
||||
Ok(_) => tracing::info!("Fetched native {}", &library.name),
|
||||
Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err)
|
||||
}
|
||||
|
||||
@@ -108,14 +108,14 @@ pub async fn install_minecraft(
|
||||
LoadingBarType::MinecraftDownload {
|
||||
// If we are downloading minecraft for a profile, provide its name and uuid
|
||||
profile_name: profile.metadata.name.clone(),
|
||||
profile_path: profile.path.clone(),
|
||||
profile_path: profile.get_profile_full_path().await?,
|
||||
},
|
||||
100.0,
|
||||
"Downloading Minecraft",
|
||||
)
|
||||
.await?;
|
||||
|
||||
crate::api::profile::edit(&profile.path, |prof| {
|
||||
crate::api::profile::edit(&profile.profile_id(), |prof| {
|
||||
prof.install_stage = ProfileInstallStage::Installing;
|
||||
|
||||
async { Ok(()) }
|
||||
@@ -124,7 +124,8 @@ pub async fn install_minecraft(
|
||||
State::sync().await?;
|
||||
|
||||
let state = State::get().await?;
|
||||
let instance_path = &io::canonicalize(&profile.path)?;
|
||||
let instance_path =
|
||||
&io::canonicalize(&profile.get_profile_full_path().await?)?;
|
||||
let metadata = state.metadata.read().await;
|
||||
|
||||
let version = metadata
|
||||
@@ -176,8 +177,11 @@ pub async fn install_minecraft(
|
||||
let client_path = state
|
||||
.directories
|
||||
.version_dir(&version_jar)
|
||||
.await
|
||||
.join(format!("{version_jar}.jar"));
|
||||
|
||||
let libraries_dir = state.directories.libraries_dir().await;
|
||||
|
||||
if let Some(ref mut data) = version_info.data {
|
||||
processor_rules! {
|
||||
data;
|
||||
@@ -194,7 +198,7 @@ pub async fn install_minecraft(
|
||||
client => instance_path.to_string_lossy(),
|
||||
server => "";
|
||||
"LIBRARY_DIR":
|
||||
client => state.directories.libraries_dir().to_string_lossy(),
|
||||
client => libraries_dir.to_string_lossy(),
|
||||
server => "";
|
||||
}
|
||||
|
||||
@@ -217,13 +221,13 @@ pub async fn install_minecraft(
|
||||
let child = Command::new(&java_version.path)
|
||||
.arg("-cp")
|
||||
.arg(args::get_class_paths_jar(
|
||||
&state.directories.libraries_dir(),
|
||||
&libraries_dir,
|
||||
&cp,
|
||||
&java_version.architecture,
|
||||
)?)
|
||||
.arg(
|
||||
args::get_processor_main_class(args::get_lib_path(
|
||||
&state.directories.libraries_dir(),
|
||||
&libraries_dir,
|
||||
&processor.jar,
|
||||
false,
|
||||
)?)
|
||||
@@ -236,7 +240,7 @@ pub async fn install_minecraft(
|
||||
})?,
|
||||
)
|
||||
.args(args::get_processor_arguments(
|
||||
&state.directories.libraries_dir(),
|
||||
&libraries_dir,
|
||||
&processor.args,
|
||||
data,
|
||||
)?)
|
||||
@@ -269,7 +273,7 @@ pub async fn install_minecraft(
|
||||
}
|
||||
}
|
||||
|
||||
crate::api::profile::edit(&profile.path, |prof| {
|
||||
crate::api::profile::edit(&profile.profile_id(), |prof| {
|
||||
prof.install_stage = ProfileInstallStage::Installed;
|
||||
|
||||
async { Ok(()) }
|
||||
@@ -309,7 +313,9 @@ pub async fn launch_minecraft(
|
||||
|
||||
let state = State::get().await?;
|
||||
let metadata = state.metadata.read().await;
|
||||
let instance_path = &io::canonicalize(&profile.path)?;
|
||||
|
||||
let instance_path = profile.get_profile_full_path().await?;
|
||||
let instance_path = &io::canonicalize(instance_path)?;
|
||||
|
||||
let version = metadata
|
||||
.minecraft
|
||||
@@ -359,6 +365,7 @@ pub async fn launch_minecraft(
|
||||
let client_path = state
|
||||
.directories
|
||||
.version_dir(&version_jar)
|
||||
.await
|
||||
.join(format!("{version_jar}.jar"));
|
||||
|
||||
let args = version_info.arguments.clone().unwrap_or_default();
|
||||
@@ -374,11 +381,11 @@ pub async fn launch_minecraft(
|
||||
// Check if profile has a running profile, and reject running the command if it does
|
||||
// Done late so a quick double call doesn't launch two instances
|
||||
let existing_processes =
|
||||
process::get_uuids_by_profile_path(instance_path).await?;
|
||||
process::get_uuids_by_profile_path(profile.profile_id()).await?;
|
||||
if let Some(uuid) = existing_processes.first() {
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
"Profile {} is already running at UUID: {uuid}",
|
||||
instance_path.display()
|
||||
profile.profile_id()
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
@@ -388,10 +395,10 @@ pub async fn launch_minecraft(
|
||||
args::get_jvm_arguments(
|
||||
args.get(&d::minecraft::ArgumentType::Jvm)
|
||||
.map(|x| x.as_slice()),
|
||||
&state.directories.version_natives_dir(&version_jar),
|
||||
&state.directories.libraries_dir(),
|
||||
&state.directories.version_natives_dir(&version_jar).await,
|
||||
&state.directories.libraries_dir().await,
|
||||
&args::get_class_paths(
|
||||
&state.directories.libraries_dir(),
|
||||
&state.directories.libraries_dir().await,
|
||||
version_info.libraries.as_slice(),
|
||||
&client_path,
|
||||
&java_version.architecture,
|
||||
@@ -414,7 +421,7 @@ pub async fn launch_minecraft(
|
||||
&version.id,
|
||||
&version_info.asset_index.id,
|
||||
instance_path,
|
||||
&state.directories.assets_dir(),
|
||||
&state.directories.assets_dir().await,
|
||||
&version.type_,
|
||||
*resolution,
|
||||
&java_version.architecture,
|
||||
@@ -439,14 +446,15 @@ pub async fn launch_minecraft(
|
||||
let logs_dir = {
|
||||
let st = State::get().await?;
|
||||
st.directories
|
||||
.profile_logs_dir(profile.uuid)
|
||||
.profile_logs_dir(&profile.profile_id())
|
||||
.await?
|
||||
.join(&datetime_string)
|
||||
};
|
||||
io::create_dir_all(&logs_dir).await?;
|
||||
|
||||
let stdout_log_path = logs_dir.join("stdout.log");
|
||||
|
||||
crate::api::profile::edit(&profile.path, |prof| {
|
||||
crate::api::profile::edit(&profile.profile_id(), |prof| {
|
||||
prof.metadata.last_played = Some(Utc::now());
|
||||
|
||||
async { Ok(()) }
|
||||
@@ -499,7 +507,7 @@ pub async fn launch_minecraft(
|
||||
state_children
|
||||
.insert_process(
|
||||
Uuid::new_v4(),
|
||||
instance_path.to_path_buf(),
|
||||
profile.profile_id(),
|
||||
stdout_log_path,
|
||||
command,
|
||||
post_exit_hook,
|
||||
|
||||
@@ -44,10 +44,10 @@ pub fn start_logger() -> Option<WorkerGuard> {
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
// Initialize and get logs directory path
|
||||
let path = if let Some(dir) = DirectoryInfo::init().ok() {
|
||||
dir.launcher_logs_dir()
|
||||
let logs_dir = if let Some(d) = DirectoryInfo::launcher_logs_dir() {
|
||||
d
|
||||
} else {
|
||||
eprintln!("Could not create logger.");
|
||||
eprintln!("Could not start logger");
|
||||
return None;
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ pub fn start_logger() -> Option<WorkerGuard> {
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("theseus=info"));
|
||||
|
||||
let file_appender =
|
||||
RollingFileAppender::new(Rotation::DAILY, path, "theseus.log");
|
||||
RollingFileAppender::new(Rotation::DAILY, logs_dir, "theseus.log");
|
||||
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::Profile;
|
||||
use super::{Profile, ProfilePathId};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitStatus;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
@@ -25,7 +25,7 @@ pub struct Children(HashMap<Uuid, Arc<RwLock<MinecraftChild>>>);
|
||||
#[derive(Debug)]
|
||||
pub struct MinecraftChild {
|
||||
pub uuid: Uuid,
|
||||
pub profile_path: PathBuf, //todo: make UUID when profiles are recognized by UUID
|
||||
pub profile_relative_path: ProfilePathId,
|
||||
pub manager: Option<JoinHandle<crate::Result<ExitStatus>>>, // None when future has completed and been handled
|
||||
pub current_child: Arc<RwLock<Child>>,
|
||||
pub output: SharedOutput,
|
||||
@@ -53,7 +53,7 @@ impl Children {
|
||||
pub async fn insert_process(
|
||||
&mut self,
|
||||
uuid: Uuid,
|
||||
profile_path: PathBuf,
|
||||
profile_relative_path: ProfilePathId,
|
||||
log_path: PathBuf,
|
||||
mut mc_command: Command,
|
||||
post_command: Option<Command>, // Command to run after minecraft.
|
||||
@@ -107,7 +107,7 @@ impl Children {
|
||||
// Create MinecraftChild
|
||||
let mchild = MinecraftChild {
|
||||
uuid,
|
||||
profile_path,
|
||||
profile_relative_path,
|
||||
current_child,
|
||||
output: shared_output,
|
||||
manager,
|
||||
@@ -266,7 +266,7 @@ impl Children {
|
||||
// Gets all PID keys of running children with a given profile path
|
||||
pub async fn running_keys_with_profile(
|
||||
&self,
|
||||
profile_path: &Path,
|
||||
profile_path: ProfilePathId,
|
||||
) -> crate::Result<Vec<Uuid>> {
|
||||
let running_keys = self.running_keys().await?;
|
||||
let mut keys = Vec::new();
|
||||
@@ -274,7 +274,7 @@ impl Children {
|
||||
if let Some(child) = self.get(&key) {
|
||||
let child = child.clone();
|
||||
let child = child.read().await;
|
||||
if child.profile_path == profile_path {
|
||||
if child.profile_relative_path == profile_path {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
@@ -283,7 +283,9 @@ impl Children {
|
||||
}
|
||||
|
||||
// Gets all profiles of running children
|
||||
pub async fn running_profile_paths(&self) -> crate::Result<Vec<PathBuf>> {
|
||||
pub async fn running_profile_paths(
|
||||
&self,
|
||||
) -> crate::Result<Vec<ProfilePathId>> {
|
||||
let mut profiles = Vec::new();
|
||||
for key in self.keys() {
|
||||
if let Some(child) = self.get(&key) {
|
||||
@@ -297,7 +299,7 @@ impl Children {
|
||||
.map_err(IOError::from)?
|
||||
.is_none()
|
||||
{
|
||||
profiles.push(child.profile_path.clone());
|
||||
profiles.push(child.profile_relative_path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,7 +323,7 @@ impl Children {
|
||||
.is_none()
|
||||
{
|
||||
if let Some(prof) = crate::api::profile::get(
|
||||
&child.profile_path.clone(),
|
||||
&child.profile_relative_path.clone(),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
|
||||
@@ -2,16 +2,42 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::{ProfilePathId, Settings};
|
||||
|
||||
pub const SETTINGS_FILE_NAME: &str = "settings.json";
|
||||
pub const CACHES_FOLDER_NAME: &str = "caches";
|
||||
pub const LAUNCHER_LOGS_FOLDER_NAME: &str = "launcher_logs";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirectoryInfo {
|
||||
pub config_dir: PathBuf,
|
||||
pub settings_dir: PathBuf, // Base settings directory- settings.json and icon cache.
|
||||
pub config_dir: RwLock<PathBuf>, // Base config directory- instances, minecraft downloads, etc. Changeable as a setting.
|
||||
pub working_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl DirectoryInfo {
|
||||
// Get the settings directory
|
||||
// init() is not needed for this function
|
||||
pub fn get_initial_settings_dir() -> Option<PathBuf> {
|
||||
Self::env_path("THESEUS_CONFIG_DIR")
|
||||
.or_else(|| Some(dirs::config_dir()?.join("com.modrinth.theseus")))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_initial_settings_file() -> crate::Result<PathBuf> {
|
||||
let settings_dir = Self::get_initial_settings_dir().ok_or(
|
||||
crate::ErrorKind::FSError(
|
||||
"Could not find valid config dir".to_string(),
|
||||
),
|
||||
)?;
|
||||
Ok(settings_dir.join("settings.json"))
|
||||
}
|
||||
|
||||
/// Get all paths needed for Theseus to operate properly
|
||||
#[tracing::instrument]
|
||||
pub fn init() -> crate::Result<Self> {
|
||||
pub fn init(settings: &Settings) -> crate::Result<Self> {
|
||||
// Working directory
|
||||
let working_dir = std::env::current_dir().map_err(|err| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
@@ -19,143 +45,153 @@ impl DirectoryInfo {
|
||||
))
|
||||
})?;
|
||||
|
||||
// Config directory
|
||||
let config_dir = Self::env_path("THESEUS_CONFIG_DIR")
|
||||
.or_else(|| Some(dirs::config_dir()?.join("com.modrinth.theseus")))
|
||||
.ok_or(crate::ErrorKind::FSError(
|
||||
"Could not find valid config dir".to_string(),
|
||||
))?;
|
||||
let settings_dir = Self::get_initial_settings_dir().ok_or(
|
||||
crate::ErrorKind::FSError(
|
||||
"Could not find valid settings dir".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
fs::create_dir_all(&config_dir).map_err(|err| {
|
||||
fs::create_dir_all(&settings_dir).map_err(|err| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Error creating Theseus config directory: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// config directory (for instances, etc.)
|
||||
// by default this is the same as the settings directory
|
||||
let config_dir = settings.loaded_config_dir.clone().ok_or(
|
||||
crate::ErrorKind::FSError(
|
||||
"Could not find valid config dir".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
config_dir,
|
||||
settings_dir,
|
||||
config_dir: RwLock::new(config_dir),
|
||||
working_dir,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the Minecraft instance metadata directory
|
||||
#[inline]
|
||||
pub fn metadata_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("meta")
|
||||
pub async fn metadata_dir(&self) -> PathBuf {
|
||||
self.config_dir.read().await.join("meta")
|
||||
}
|
||||
|
||||
/// Get the Minecraft java versions metadata directory
|
||||
#[inline]
|
||||
pub fn java_versions_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("java_versions")
|
||||
pub async fn java_versions_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("java_versions")
|
||||
}
|
||||
|
||||
/// Get the Minecraft versions metadata directory
|
||||
#[inline]
|
||||
pub fn versions_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("versions")
|
||||
pub async fn versions_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("versions")
|
||||
}
|
||||
|
||||
/// Get the metadata directory for a given version
|
||||
#[inline]
|
||||
pub fn version_dir(&self, version: &str) -> PathBuf {
|
||||
self.versions_dir().join(version)
|
||||
pub async fn version_dir(&self, version: &str) -> PathBuf {
|
||||
self.versions_dir().await.join(version)
|
||||
}
|
||||
|
||||
/// Get the Minecraft libraries metadata directory
|
||||
#[inline]
|
||||
pub fn libraries_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("libraries")
|
||||
pub async fn libraries_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("libraries")
|
||||
}
|
||||
|
||||
/// Get the Minecraft assets metadata directory
|
||||
#[inline]
|
||||
pub fn assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("assets")
|
||||
pub async fn assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("assets")
|
||||
}
|
||||
|
||||
/// Get the assets index directory
|
||||
#[inline]
|
||||
pub fn assets_index_dir(&self) -> PathBuf {
|
||||
self.assets_dir().join("indexes")
|
||||
pub async fn assets_index_dir(&self) -> PathBuf {
|
||||
self.assets_dir().await.join("indexes")
|
||||
}
|
||||
|
||||
/// Get the assets objects directory
|
||||
#[inline]
|
||||
pub fn objects_dir(&self) -> PathBuf {
|
||||
self.assets_dir().join("objects")
|
||||
pub async fn objects_dir(&self) -> PathBuf {
|
||||
self.assets_dir().await.join("objects")
|
||||
}
|
||||
|
||||
/// Get the directory for a specific object
|
||||
#[inline]
|
||||
pub fn object_dir(&self, hash: &str) -> PathBuf {
|
||||
self.objects_dir().join(&hash[..2]).join(hash)
|
||||
pub async fn object_dir(&self, hash: &str) -> PathBuf {
|
||||
self.objects_dir().await.join(&hash[..2]).join(hash)
|
||||
}
|
||||
|
||||
/// Get the Minecraft legacy assets metadata directory
|
||||
#[inline]
|
||||
pub fn legacy_assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("resources")
|
||||
pub async fn legacy_assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("resources")
|
||||
}
|
||||
|
||||
/// Get the Minecraft legacy assets metadata directory
|
||||
#[inline]
|
||||
pub fn natives_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("natives")
|
||||
pub async fn natives_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("natives")
|
||||
}
|
||||
|
||||
/// Get the natives directory for a version of Minecraft
|
||||
#[inline]
|
||||
pub fn version_natives_dir(&self, version: &str) -> PathBuf {
|
||||
self.natives_dir().join(version)
|
||||
pub async fn version_natives_dir(&self, version: &str) -> PathBuf {
|
||||
self.natives_dir().await.join(version)
|
||||
}
|
||||
|
||||
/// Get the directory containing instance icons
|
||||
#[inline]
|
||||
pub fn icon_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("icons")
|
||||
pub async fn icon_dir(&self) -> PathBuf {
|
||||
self.config_dir.read().await.join("icons")
|
||||
}
|
||||
|
||||
/// Get the profiles directory for created profiles
|
||||
#[inline]
|
||||
pub fn profiles_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("profiles")
|
||||
pub async fn profiles_dir(&self) -> PathBuf {
|
||||
self.config_dir.read().await.join("profiles")
|
||||
}
|
||||
|
||||
/// Gets the logs dir for a given profile
|
||||
#[inline]
|
||||
pub fn profile_logs_dir(&self, profile: uuid::Uuid) -> PathBuf {
|
||||
self.profiles_dir()
|
||||
.join(profile.to_string())
|
||||
.join("modrinth_logs")
|
||||
pub async fn profile_logs_dir(
|
||||
&self,
|
||||
profile_id: &ProfilePathId,
|
||||
) -> crate::Result<PathBuf> {
|
||||
Ok(profile_id.get_full_path().await?.join("modrinth_logs"))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn launcher_logs_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("launcher_logs")
|
||||
pub fn launcher_logs_dir() -> Option<PathBuf> {
|
||||
Self::get_initial_settings_dir()
|
||||
.map(|d| d.join(LAUNCHER_LOGS_FOLDER_NAME))
|
||||
}
|
||||
|
||||
/// Get the file containing the global database
|
||||
#[inline]
|
||||
pub fn database_file(&self) -> PathBuf {
|
||||
self.config_dir.join("data.bin")
|
||||
pub async fn database_file(&self) -> PathBuf {
|
||||
self.config_dir.read().await.join("data.bin")
|
||||
}
|
||||
|
||||
/// Get the settings file for Theseus
|
||||
#[inline]
|
||||
pub fn settings_file(&self) -> PathBuf {
|
||||
self.config_dir.join("settings.json")
|
||||
self.settings_dir.join(SETTINGS_FILE_NAME)
|
||||
}
|
||||
|
||||
/// Get the cache directory for Theseus
|
||||
#[inline]
|
||||
pub fn caches_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("caches")
|
||||
self.settings_dir.join(CACHES_FOLDER_NAME)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn caches_meta_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("caches").join("metadata")
|
||||
pub async fn caches_meta_dir(&self) -> PathBuf {
|
||||
self.caches_dir().join("metadata")
|
||||
}
|
||||
|
||||
/// Get path from environment variable
|
||||
|
||||
@@ -35,6 +35,10 @@ impl JavaGlobals {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> Vec<String> {
|
||||
self.0.keys().cloned().collect()
|
||||
}
|
||||
|
||||
// Validates that every path here is a valid Java version and that the version matches the version stored here
|
||||
// If false, when checked, the user should be prompted to reselect the Java version
|
||||
pub async fn is_all_valid(&self) -> bool {
|
||||
|
||||
@@ -61,7 +61,7 @@ impl Metadata {
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
let mut metadata = None;
|
||||
let metadata_path = dirs.caches_meta_dir().join("metadata.json");
|
||||
let metadata_path = dirs.caches_meta_dir().await.join("metadata.json");
|
||||
|
||||
if let Ok(metadata_json) =
|
||||
read_json::<Metadata>(&metadata_path, io_semaphore).await
|
||||
@@ -106,8 +106,11 @@ impl Metadata {
|
||||
let metadata_fetch = Metadata::fetch().await?;
|
||||
let state = State::get().await?;
|
||||
|
||||
let metadata_path =
|
||||
state.directories.caches_meta_dir().join("metadata.json");
|
||||
let metadata_path = state
|
||||
.directories
|
||||
.caches_meta_dir()
|
||||
.await
|
||||
.join("metadata.json");
|
||||
|
||||
write(
|
||||
&metadata_path,
|
||||
|
||||
@@ -49,7 +49,8 @@ mod safe_processes;
|
||||
pub use self::safe_processes::*;
|
||||
|
||||
// Global state
|
||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||
// RwLock on state only has concurrent reads, except for config dir change which takes control of the State
|
||||
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
|
||||
pub struct State {
|
||||
/// Information on the location of files used in the launcher
|
||||
pub directories: DirectoryInfo,
|
||||
@@ -86,83 +87,97 @@ pub struct State {
|
||||
|
||||
impl State {
|
||||
/// Get the current launcher state, initializing it if needed
|
||||
pub async fn get(
|
||||
) -> crate::Result<Arc<tokio::sync::RwLockReadGuard<'static, Self>>> {
|
||||
Ok(Arc::new(
|
||||
LAUNCHER_STATE
|
||||
.get_or_try_init(Self::initialize_state)
|
||||
.await?
|
||||
.read()
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
/// Get the current launcher state, initializing it if needed
|
||||
/// Takes writing control of the state, blocking all other uses of it
|
||||
/// Only used for state change such as changing the config directory
|
||||
pub async fn get_write(
|
||||
) -> crate::Result<tokio::sync::RwLockWriteGuard<'static, Self>> {
|
||||
Ok(LAUNCHER_STATE
|
||||
.get_or_try_init(Self::initialize_state)
|
||||
.await?
|
||||
.write()
|
||||
.await)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
LAUNCHER_STATE
|
||||
.get_or_try_init(|| {
|
||||
async {
|
||||
let loading_bar = init_loading_unsafe(
|
||||
LoadingBarType::StateInit,
|
||||
100.0,
|
||||
"Initializing launcher",
|
||||
)
|
||||
.await?;
|
||||
async fn initialize_state() -> crate::Result<RwLock<State>> {
|
||||
let loading_bar = init_loading_unsafe(
|
||||
LoadingBarType::StateInit,
|
||||
100.0,
|
||||
"Initializing launcher",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut file_watcher = init_watcher().await?;
|
||||
// Settings
|
||||
let settings =
|
||||
Settings::init(&DirectoryInfo::get_initial_settings_file()?)
|
||||
.await?;
|
||||
|
||||
let directories = DirectoryInfo::init()?;
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
let directories = DirectoryInfo::init(&settings)?;
|
||||
|
||||
// Settings
|
||||
let settings =
|
||||
Settings::init(&directories.settings_file()).await?;
|
||||
let fetch_semaphore = FetchSemaphore(RwLock::new(
|
||||
Semaphore::new(settings.max_concurrent_downloads),
|
||||
));
|
||||
let io_semaphore = IoSemaphore(RwLock::new(
|
||||
Semaphore::new(settings.max_concurrent_writes),
|
||||
));
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
|
||||
let metadata_fut =
|
||||
Metadata::init(&directories, &io_semaphore);
|
||||
let profiles_fut =
|
||||
Profiles::init(&directories, &mut file_watcher);
|
||||
let tags_fut = Tags::init(
|
||||
&directories,
|
||||
&io_semaphore,
|
||||
&fetch_semaphore,
|
||||
);
|
||||
let users_fut = Users::init(&directories, &io_semaphore);
|
||||
// Launcher data
|
||||
let (metadata, profiles, tags, users) = loading_join! {
|
||||
Some(&loading_bar), 70.0, Some("Loading metadata");
|
||||
metadata_fut,
|
||||
profiles_fut,
|
||||
tags_fut,
|
||||
users_fut,
|
||||
}?;
|
||||
let mut file_watcher = init_watcher().await?;
|
||||
|
||||
let children = Children::new();
|
||||
let auth_flow = AuthTask::new();
|
||||
let safety_processes = SafeProcesses::new();
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
let fetch_semaphore = FetchSemaphore(RwLock::new(Semaphore::new(
|
||||
settings.max_concurrent_downloads,
|
||||
)));
|
||||
let io_semaphore = IoSemaphore(RwLock::new(Semaphore::new(
|
||||
settings.max_concurrent_writes,
|
||||
)));
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
directories,
|
||||
fetch_semaphore,
|
||||
fetch_semaphore_max: RwLock::new(
|
||||
settings.max_concurrent_downloads as u32,
|
||||
),
|
||||
io_semaphore,
|
||||
io_semaphore_max: RwLock::new(
|
||||
settings.max_concurrent_writes as u32,
|
||||
),
|
||||
metadata: RwLock::new(metadata),
|
||||
settings: RwLock::new(settings),
|
||||
profiles: RwLock::new(profiles),
|
||||
users: RwLock::new(users),
|
||||
children: RwLock::new(children),
|
||||
auth_flow: RwLock::new(auth_flow),
|
||||
tags: RwLock::new(tags),
|
||||
safety_processes: RwLock::new(safety_processes),
|
||||
file_watcher: RwLock::new(file_watcher),
|
||||
}))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map(Arc::clone)
|
||||
let metadata_fut = Metadata::init(&directories, &io_semaphore);
|
||||
let profiles_fut = Profiles::init(&directories, &mut file_watcher);
|
||||
let tags_fut =
|
||||
Tags::init(&directories, &io_semaphore, &fetch_semaphore);
|
||||
let users_fut = Users::init(&directories, &io_semaphore);
|
||||
// Launcher data
|
||||
let (metadata, profiles, tags, users) = loading_join! {
|
||||
Some(&loading_bar), 70.0, Some("Loading metadata");
|
||||
metadata_fut,
|
||||
profiles_fut,
|
||||
tags_fut,
|
||||
users_fut,
|
||||
}?;
|
||||
|
||||
let children = Children::new();
|
||||
let auth_flow = AuthTask::new();
|
||||
let safety_processes = SafeProcesses::new();
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
|
||||
Ok::<RwLock<Self>, crate::Error>(RwLock::new(Self {
|
||||
directories,
|
||||
fetch_semaphore,
|
||||
fetch_semaphore_max: RwLock::new(
|
||||
settings.max_concurrent_downloads as u32,
|
||||
),
|
||||
io_semaphore,
|
||||
io_semaphore_max: RwLock::new(
|
||||
settings.max_concurrent_writes as u32,
|
||||
),
|
||||
metadata: RwLock::new(metadata),
|
||||
settings: RwLock::new(settings),
|
||||
profiles: RwLock::new(profiles),
|
||||
users: RwLock::new(users),
|
||||
children: RwLock::new(children),
|
||||
auth_flow: RwLock::new(auth_flow),
|
||||
tags: RwLock::new(tags),
|
||||
safety_processes: RwLock::new(safety_processes),
|
||||
file_watcher: RwLock::new(file_watcher),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Updates state with data from the web
|
||||
@@ -240,7 +255,7 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
let (mut tx, mut rx) = channel(1);
|
||||
|
||||
let file_watcher = new_debouncer(
|
||||
@@ -256,13 +271,19 @@ async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
tokio::task::spawn(async move {
|
||||
while let Some(res) = rx.next().await {
|
||||
match res {
|
||||
Ok(events) => {
|
||||
Ok(mut events) => {
|
||||
let mut visited_paths = Vec::new();
|
||||
// sort events by e.path
|
||||
events.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
events.iter().for_each(|e| {
|
||||
tracing::debug!(
|
||||
"File watcher event: {:?}",
|
||||
serde_json::to_string(&e.path).unwrap()
|
||||
);
|
||||
let mut new_path = PathBuf::new();
|
||||
let mut components_iterator = e.path.components();
|
||||
let mut found = false;
|
||||
|
||||
for component in e.path.components() {
|
||||
for component in components_iterator.by_ref() {
|
||||
new_path.push(component);
|
||||
if found {
|
||||
break;
|
||||
@@ -271,6 +292,14 @@ async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
// if any remain, it's a subfile of the profile folder and not the profile folder itself
|
||||
let subfile = components_iterator.next().is_some();
|
||||
|
||||
// At this point, new_path is the path to the profile, and subfile is whether it's a subfile of the profile or not
|
||||
let profile_path_id =
|
||||
ProfilePathId::new(&PathBuf::from(
|
||||
new_path.file_name().unwrap_or_default(),
|
||||
));
|
||||
|
||||
if e.path
|
||||
.components()
|
||||
@@ -280,10 +309,16 @@ async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
.map(|x| x == "txt")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Profile::crash_task(new_path);
|
||||
Profile::crash_task(profile_path_id);
|
||||
} else if !visited_paths.contains(&new_path) {
|
||||
Profile::sync_projects_task(new_path.clone());
|
||||
visited_paths.push(new_path);
|
||||
if subfile {
|
||||
Profile::sync_projects_task(profile_path_id);
|
||||
visited_paths.push(new_path);
|
||||
} else {
|
||||
Profiles::sync_available_profiles_task(
|
||||
profile_path_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use uuid::Uuid;
|
||||
|
||||
const PROFILE_JSON_PATH: &str = "profile.json";
|
||||
|
||||
pub(crate) struct Profiles(pub HashMap<PathBuf, Profile>);
|
||||
pub(crate) struct Profiles(pub HashMap<ProfilePathId, Profile>);
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq,
|
||||
@@ -46,13 +46,101 @@ pub enum ProfileInstallStage {
|
||||
NotInstalled,
|
||||
}
|
||||
|
||||
/// newtype wrapper over a Profile path, to be usable as a clear identifier for the kind of path used
|
||||
/// eg: for "a/b/c/profiles/My Mod", the ProfilePathId would be "My Mod" (a relative path)
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[serde(transparent)]
|
||||
pub struct ProfilePathId(PathBuf);
|
||||
impl ProfilePathId {
|
||||
// Create a new ProfilePathId from a full file path
|
||||
pub async fn from_fs_path(path: PathBuf) -> crate::Result<Self> {
|
||||
let path: PathBuf = io::canonicalize(path)?;
|
||||
let profiles_dir = io::canonicalize(
|
||||
State::get().await?.directories.profiles_dir().await,
|
||||
)?;
|
||||
path.strip_prefix(profiles_dir)
|
||||
.ok()
|
||||
.and_then(|p| p.file_name())
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Path {path:?} does not correspond to a profile",
|
||||
path = path
|
||||
))
|
||||
})?;
|
||||
Ok(Self(path))
|
||||
}
|
||||
|
||||
// Create a new ProfilePathId from a relative path
|
||||
pub fn new(path: &Path) -> Self {
|
||||
ProfilePathId(PathBuf::from(path))
|
||||
}
|
||||
|
||||
pub async fn get_full_path(&self) -> crate::Result<PathBuf> {
|
||||
let state = State::get().await?;
|
||||
let profiles_dir = state.directories.profiles_dir().await;
|
||||
Ok(profiles_dir.join(&self.0))
|
||||
}
|
||||
|
||||
pub fn check_valid_utf(&self) -> crate::Result<&Self> {
|
||||
self.0
|
||||
.to_str()
|
||||
.ok_or(crate::ErrorKind::UTFError(self.0.clone()).as_error())?;
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for ProfilePathId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.display().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// newtype wrapper over a Profile path, to be usable as a clear identifier for the kind of path used
|
||||
/// eg: for "a/b/c/profiles/My Mod/mods/myproj", the ProjectPathId would be "mods/myproj"
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[serde(transparent)]
|
||||
pub struct ProjectPathId(pub PathBuf);
|
||||
impl ProjectPathId {
|
||||
// Create a new ProjectPathId from a full file path
|
||||
pub async fn from_fs_path(path: PathBuf) -> crate::Result<Self> {
|
||||
let path: PathBuf = io::canonicalize(path)?;
|
||||
let profiles_dir: PathBuf = io::canonicalize(
|
||||
State::get().await?.directories.profiles_dir().await,
|
||||
)?;
|
||||
path.strip_prefix(profiles_dir)
|
||||
.ok()
|
||||
.map(|p| p.components().skip(1).collect::<PathBuf>())
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Path {path:?} does not correspond to a profile",
|
||||
path = path
|
||||
))
|
||||
})?;
|
||||
Ok(Self(path))
|
||||
}
|
||||
|
||||
pub async fn get_full_path(
|
||||
&self,
|
||||
profile: ProfilePathId,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let _state = State::get().await?;
|
||||
let profile_dir = profile.get_full_path().await?;
|
||||
Ok(profile_dir.join(&self.0))
|
||||
}
|
||||
|
||||
// Create a new ProjectPathId from a relative path
|
||||
pub fn new(path: &Path) -> Self {
|
||||
ProjectPathId(PathBuf::from(path))
|
||||
}
|
||||
}
|
||||
|
||||
// Represent a Minecraft instance.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Profile {
|
||||
pub uuid: Uuid, // todo: will be used in restructure to refer to profiles
|
||||
#[serde(default)]
|
||||
pub install_stage: ProfileInstallStage,
|
||||
pub path: PathBuf,
|
||||
#[serde(default)]
|
||||
pub path: PathBuf, // Relative path to the profile, to be used in ProfilePathId
|
||||
pub metadata: ProfileMetadata,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub java: Option<JavaSettings>,
|
||||
@@ -62,7 +150,7 @@ pub struct Profile {
|
||||
pub resolution: Option<WindowSize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hooks: Option<Hooks>,
|
||||
pub projects: HashMap<PathBuf, Project>,
|
||||
pub projects: HashMap<ProjectPathId, Project>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
@@ -148,7 +236,6 @@ impl Profile {
|
||||
uuid: Uuid,
|
||||
name: String,
|
||||
version: String,
|
||||
path: PathBuf,
|
||||
) -> crate::Result<Self> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(crate::ErrorKind::InputError(String::from(
|
||||
@@ -160,7 +247,7 @@ impl Profile {
|
||||
Ok(Self {
|
||||
uuid,
|
||||
install_stage: ProfileInstallStage::NotInstalled,
|
||||
path: io::canonicalize(path)?,
|
||||
path: PathBuf::new().join(&name),
|
||||
metadata: ProfileMetadata {
|
||||
name,
|
||||
icon: None,
|
||||
@@ -182,6 +269,12 @@ impl Profile {
|
||||
})
|
||||
}
|
||||
|
||||
// Gets the ProfilePathId for this profile
|
||||
#[inline]
|
||||
pub fn profile_id(&self) -> ProfilePathId {
|
||||
ProfilePathId::new(&self.path)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, semaphore, icon))]
|
||||
pub async fn set_icon<'a>(
|
||||
&'a mut self,
|
||||
@@ -197,7 +290,7 @@ impl Profile {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn crash_task(path: PathBuf) {
|
||||
pub fn crash_task(path: ProfilePathId) {
|
||||
tokio::task::spawn(async move {
|
||||
let res = async {
|
||||
let profile = crate::api::profile::get(&path, None).await?;
|
||||
@@ -221,38 +314,47 @@ impl Profile {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn sync_projects_task(path: PathBuf) {
|
||||
pub fn sync_projects_task(profile_path_id: ProfilePathId) {
|
||||
tokio::task::spawn(async move {
|
||||
let span =
|
||||
tracing::span!(tracing::Level::INFO, "sync_projects_task");
|
||||
tracing::debug!(
|
||||
parent: &span,
|
||||
"Syncing projects for profile {}",
|
||||
profile_path_id
|
||||
);
|
||||
let res = async {
|
||||
let _span = span.enter();
|
||||
let state = State::get().await?;
|
||||
let profile = crate::api::profile::get(&path, None).await?;
|
||||
let profile = crate::api::profile::get(&profile_path_id, None).await?;
|
||||
|
||||
if let Some(profile) = profile {
|
||||
let paths = profile.get_profile_project_paths()?;
|
||||
let paths = profile.get_profile_full_project_paths().await?;
|
||||
|
||||
let caches_dir = state.directories.caches_dir();
|
||||
let projects = crate::state::infer_data_from_files(
|
||||
profile.clone(),
|
||||
paths,
|
||||
state.directories.caches_dir(),
|
||||
caches_dir,
|
||||
&state.io_semaphore,
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut new_profiles = state.profiles.write().await;
|
||||
if let Some(profile) = new_profiles.0.get_mut(&path) {
|
||||
if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) {
|
||||
profile.projects = projects;
|
||||
}
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Synced,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Unable to fetch single profile projects: path {path:?} invalid",
|
||||
"Unable to fetch single profile projects: path {profile_path_id} invalid",
|
||||
);
|
||||
}
|
||||
Ok::<(), crate::Error>(())
|
||||
@@ -268,10 +370,21 @@ impl Profile {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_profile_project_paths(&self) -> crate::Result<Vec<PathBuf>> {
|
||||
// Get full path to profile
|
||||
pub async fn get_profile_full_path(&self) -> crate::Result<PathBuf> {
|
||||
let state = State::get().await?;
|
||||
let profiles_dir = state.directories.profiles_dir().await;
|
||||
Ok(profiles_dir.join(&self.path))
|
||||
}
|
||||
|
||||
/// Gets paths to projects as their full paths, not just their relative paths
|
||||
pub async fn get_profile_full_project_paths(
|
||||
&self,
|
||||
) -> crate::Result<Vec<PathBuf>> {
|
||||
let mut files = Vec::new();
|
||||
let profile_path = self.get_profile_full_path().await?;
|
||||
let mut read_paths = |path: &str| {
|
||||
let new_path = self.path.join(path);
|
||||
let new_path = profile_path.join(path);
|
||||
if new_path.exists() {
|
||||
let path = self.path.join(path);
|
||||
for path in std::fs::read_dir(&path)
|
||||
@@ -338,7 +451,7 @@ impl Profile {
|
||||
pub async fn add_project_version(
|
||||
&self,
|
||||
version_id: String,
|
||||
) -> crate::Result<(PathBuf, ModrinthVersion)> {
|
||||
) -> crate::Result<(ProjectPathId, ModrinthVersion)> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let version = fetch_json::<ModrinthVersion>(
|
||||
@@ -387,7 +500,7 @@ impl Profile {
|
||||
file_name: &str,
|
||||
bytes: bytes::Bytes,
|
||||
project_type: Option<ProjectType>,
|
||||
) -> crate::Result<PathBuf> {
|
||||
) -> crate::Result<ProjectPathId> {
|
||||
let project_type = if let Some(project_type) = project_type {
|
||||
project_type
|
||||
} else {
|
||||
@@ -419,16 +532,23 @@ impl Profile {
|
||||
};
|
||||
|
||||
let state = State::get().await?;
|
||||
let path = self.path.join(project_type.get_folder()).join(file_name);
|
||||
write(&path, &bytes, &state.io_semaphore).await?;
|
||||
let relative_name = PathBuf::new()
|
||||
.join(project_type.get_folder())
|
||||
.join(file_name);
|
||||
let file_path = self
|
||||
.get_profile_full_path()
|
||||
.await?
|
||||
.join(relative_name.clone());
|
||||
let project_path_id = ProjectPathId::new(&relative_name);
|
||||
write(&file_path, &bytes, &state.io_semaphore).await?;
|
||||
|
||||
let hash = get_hash(bytes).await?;
|
||||
{
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
if let Some(profile) = profiles.0.get_mut(&self.path) {
|
||||
if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
|
||||
profile.projects.insert(
|
||||
path.clone(),
|
||||
project_path_id.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
disabled: false,
|
||||
@@ -440,32 +560,40 @@ impl Profile {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
Ok(project_path_id)
|
||||
}
|
||||
|
||||
/// Toggle a project's disabled state.
|
||||
/// 'path' should be relative to the profile's path.
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn toggle_disable_project(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> crate::Result<PathBuf> {
|
||||
relative_path: &ProjectPathId,
|
||||
) -> crate::Result<ProjectPathId> {
|
||||
let state = State::get().await?;
|
||||
if let Some(mut project) = {
|
||||
let mut profiles = state.profiles.write().await;
|
||||
let mut profiles: tokio::sync::RwLockWriteGuard<'_, Profiles> =
|
||||
state.profiles.write().await;
|
||||
|
||||
if let Some(profile) = profiles.0.get_mut(&self.path) {
|
||||
profile.projects.remove(path)
|
||||
if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
|
||||
profile.projects.remove(relative_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} {
|
||||
let path = path.to_path_buf();
|
||||
let mut new_path = path.clone();
|
||||
// Get relative path from former ProjectPathId
|
||||
let relative_path = relative_path.0.to_path_buf();
|
||||
let mut new_path = relative_path.clone();
|
||||
|
||||
if path.extension().map_or(false, |ext| ext == "disabled") {
|
||||
if relative_path
|
||||
.extension()
|
||||
.map_or(false, |ext| ext == "disabled")
|
||||
{
|
||||
project.disabled = false;
|
||||
new_path.set_file_name(
|
||||
path.file_name()
|
||||
relative_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.replace(".disabled", ""),
|
||||
@@ -473,24 +601,35 @@ impl Profile {
|
||||
} else {
|
||||
new_path.set_file_name(format!(
|
||||
"{}.disabled",
|
||||
path.file_name().unwrap_or_default().to_string_lossy()
|
||||
relative_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
));
|
||||
project.disabled = true;
|
||||
}
|
||||
|
||||
io::rename(&path, &new_path).await?;
|
||||
let true_path =
|
||||
self.get_profile_full_path().await?.join(&relative_path);
|
||||
let true_new_path =
|
||||
self.get_profile_full_path().await?.join(&new_path);
|
||||
io::rename(&true_path, &true_new_path).await?;
|
||||
|
||||
let new_project_path_id = ProjectPathId::new(&new_path);
|
||||
|
||||
let mut profiles = state.profiles.write().await;
|
||||
if let Some(profile) = profiles.0.get_mut(&self.path) {
|
||||
profile.projects.insert(new_path.clone(), project);
|
||||
if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
|
||||
profile
|
||||
.projects
|
||||
.insert(new_project_path_id.clone(), project);
|
||||
profile.metadata.date_modified = Utc::now();
|
||||
}
|
||||
|
||||
Ok(new_path)
|
||||
Ok(new_project_path_id)
|
||||
} else {
|
||||
Err(crate::ErrorKind::InputError(format!(
|
||||
"Project path does not exist: {:?}",
|
||||
path
|
||||
relative_path
|
||||
))
|
||||
.into())
|
||||
}
|
||||
@@ -498,24 +637,29 @@ impl Profile {
|
||||
|
||||
pub async fn remove_project(
|
||||
&self,
|
||||
path: &Path,
|
||||
relative_path: &ProjectPathId,
|
||||
dont_remove_arr: Option<bool>,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
if self.projects.contains_key(path) {
|
||||
io::remove_file(path).await?;
|
||||
if self.projects.contains_key(relative_path) {
|
||||
io::remove_file(
|
||||
self.get_profile_full_path()
|
||||
.await?
|
||||
.join(relative_path.0.clone()),
|
||||
)
|
||||
.await?;
|
||||
if !dont_remove_arr.unwrap_or(false) {
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
if let Some(profile) = profiles.0.get_mut(&self.path) {
|
||||
profile.projects.remove(path);
|
||||
if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
|
||||
profile.projects.remove(relative_path);
|
||||
profile.metadata.date_modified = Utc::now();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(crate::ErrorKind::InputError(format!(
|
||||
"Project path does not exist: {:?}",
|
||||
path
|
||||
relative_path
|
||||
))
|
||||
.into());
|
||||
}
|
||||
@@ -532,14 +676,21 @@ impl Profiles {
|
||||
file_watcher: &mut Debouncer<RecommendedWatcher>,
|
||||
) -> crate::Result<Self> {
|
||||
let mut profiles = HashMap::new();
|
||||
io::create_dir_all(&dirs.profiles_dir()).await?;
|
||||
let mut entries = io::read_dir(&dirs.profiles_dir()).await?;
|
||||
let profiles_dir = dirs.profiles_dir().await;
|
||||
io::create_dir_all(&&profiles_dir).await?;
|
||||
|
||||
file_watcher
|
||||
.watcher()
|
||||
.watch(&profiles_dir, RecursiveMode::NonRecursive)?;
|
||||
|
||||
let mut entries = io::read_dir(&dirs.profiles_dir().await).await?;
|
||||
while let Some(entry) =
|
||||
entries.next_entry().await.map_err(IOError::from)?
|
||||
{
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let prof = match Self::read_profile_from_dir(&path).await {
|
||||
let prof = match Self::read_profile_from_dir(&path, dirs).await
|
||||
{
|
||||
Ok(prof) => Some(prof),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
@@ -551,7 +702,7 @@ impl Profiles {
|
||||
if let Some(profile) = prof {
|
||||
let path = io::canonicalize(path)?;
|
||||
Profile::watch_fs(&path, file_watcher).await?;
|
||||
profiles.insert(path, profile);
|
||||
profiles.insert(profile.profile_id(), profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -570,26 +721,28 @@ impl Profiles {
|
||||
{
|
||||
let profiles = state.profiles.read().await;
|
||||
for (_profile_path, profile) in profiles.0.iter() {
|
||||
let paths = profile.get_profile_project_paths()?;
|
||||
let paths =
|
||||
profile.get_profile_full_project_paths().await?;
|
||||
|
||||
files.push((profile.clone(), paths));
|
||||
}
|
||||
}
|
||||
|
||||
let caches_dir = state.directories.caches_dir();
|
||||
future::try_join_all(files.into_iter().map(
|
||||
|(profile, files)| async {
|
||||
let profile_path = profile.path.clone();
|
||||
let profile_name = profile.profile_id();
|
||||
let inferred = super::projects::infer_data_from_files(
|
||||
profile,
|
||||
files,
|
||||
state.directories.caches_dir(),
|
||||
caches_dir.clone(),
|
||||
&state.io_semaphore,
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut new_profiles = state.profiles.write().await;
|
||||
if let Some(profile) = new_profiles.0.get_mut(&profile_path)
|
||||
if let Some(profile) = new_profiles.0.get_mut(&profile_name)
|
||||
{
|
||||
profile.projects = inferred;
|
||||
}
|
||||
@@ -622,7 +775,7 @@ impl Profiles {
|
||||
pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path.clone(),
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Added,
|
||||
)
|
||||
@@ -630,30 +783,26 @@ impl Profiles {
|
||||
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(&profile.path, &mut file_watcher).await?;
|
||||
Profile::watch_fs(
|
||||
&profile.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.0.insert(
|
||||
io::canonicalize(&profile.path)?
|
||||
.to_str()
|
||||
.ok_or(
|
||||
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
|
||||
)?
|
||||
.into(),
|
||||
profile,
|
||||
);
|
||||
let profile_name = profile.profile_id();
|
||||
profile_name.check_valid_utf()?;
|
||||
self.0.insert(profile_name, profile);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn remove(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
profile_path: &ProfilePathId,
|
||||
) -> crate::Result<Option<Profile>> {
|
||||
let path = PathBuf::from(
|
||||
&io::canonicalize(path)?.to_string_lossy().to_string(),
|
||||
);
|
||||
let profile = self.0.remove(&path);
|
||||
let profile = self.0.remove(profile_path);
|
||||
|
||||
let path = profile_path.get_full_path().await?;
|
||||
if path.exists() {
|
||||
io::remove_dir_all(&path).await?;
|
||||
}
|
||||
@@ -663,12 +812,15 @@ impl Profiles {
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn sync(&self) -> crate::Result<&Self> {
|
||||
let _state = State::get().await?;
|
||||
stream::iter(self.0.iter())
|
||||
.map(Ok::<_, crate::Error>)
|
||||
.try_for_each_concurrent(None, |(path, profile)| async move {
|
||||
.try_for_each_concurrent(None, |(_, profile)| async move {
|
||||
let json = serde_json::to_vec(&profile)?;
|
||||
|
||||
let json_path = Path::new(&path.to_string_lossy().to_string())
|
||||
let json_path = profile
|
||||
.get_profile_full_path()
|
||||
.await?
|
||||
.join(PROFILE_JSON_PATH);
|
||||
|
||||
io::write(&json_path, &json).await?;
|
||||
@@ -679,10 +831,68 @@ impl Profiles {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
async fn read_profile_from_dir(path: &Path) -> crate::Result<Profile> {
|
||||
async fn read_profile_from_dir(
|
||||
path: &Path,
|
||||
dirs: &DirectoryInfo,
|
||||
) -> crate::Result<Profile> {
|
||||
let json = io::read(&path.join(PROFILE_JSON_PATH)).await?;
|
||||
let mut profile = serde_json::from_slice::<Profile>(&json)?;
|
||||
profile.path = PathBuf::from(path);
|
||||
|
||||
// Get name from stripped path
|
||||
profile.path =
|
||||
PathBuf::from(path.strip_prefix(dirs.profiles_dir().await)?);
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
pub fn sync_available_profiles_task(profile_path_id: ProfilePathId) {
|
||||
tokio::task::spawn(async move {
|
||||
let span = tracing::span!(
|
||||
tracing::Level::INFO,
|
||||
"sync_available_profiles_task"
|
||||
);
|
||||
let res = async {
|
||||
let _span = span.enter();
|
||||
let state = State::get().await?;
|
||||
let dirs = &state.directories;
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
if let Some(profile) = profiles.0.get_mut(&profile_path_id) {
|
||||
if !profile.get_profile_full_path().await?.exists() {
|
||||
// if path exists in the state but no longer in the filesystem, remove it from the state list
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Removed,
|
||||
)
|
||||
.await?;
|
||||
tracing::debug!("Removed!");
|
||||
profiles.0.remove(&profile_path_id);
|
||||
}
|
||||
} else if profile_path_id.get_full_path().await?.exists() {
|
||||
// if it exists in the filesystem but no longer in the state, add it to the state list
|
||||
profiles
|
||||
.insert(
|
||||
Self::read_profile_from_dir(
|
||||
&profile_path_id.get_full_path().await?,
|
||||
dirs,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await?;
|
||||
Profile::sync_projects_task(profile_path_id);
|
||||
}
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("Unable to fetch all profiles: {err}")
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::util::fetch::{
|
||||
use crate::util::io::IOError;
|
||||
use async_zip::tokio::read::fs::ZipFileReader;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::StreamExt;
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
@@ -16,6 +17,8 @@ use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::ProjectPathId;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProjectType {
|
||||
@@ -253,6 +256,9 @@ async fn read_icon_from_file(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Creates Project data from the existing files in the file system, for a given Profile
|
||||
// Paths must be the full paths to the files in the FS, and not the relative paths
|
||||
// eg: with get_profile_full_project_paths
|
||||
#[tracing::instrument(skip(paths, profile, io_semaphore, fetch_semaphore))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn infer_data_from_files(
|
||||
@@ -261,7 +267,7 @@ pub async fn infer_data_from_files(
|
||||
cache_dir: PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<HashMap<PathBuf, Project>> {
|
||||
) -> crate::Result<HashMap<ProjectPathId, Project>> {
|
||||
let mut file_path_hashes = HashMap::new();
|
||||
|
||||
// TODO: Make this concurrent and use progressive hashing to avoid loading each JAR in memory
|
||||
@@ -342,7 +348,7 @@ pub async fn infer_data_from_files(
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let mut return_projects = HashMap::new();
|
||||
let mut return_projects: Vec<(PathBuf, Project)> = Vec::new();
|
||||
let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new();
|
||||
|
||||
for (hash, path) in file_path_hashes {
|
||||
@@ -356,7 +362,7 @@ pub async fn infer_data_from_files(
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path,
|
||||
Project {
|
||||
disabled: file_name.ends_with(".disabled"),
|
||||
@@ -392,7 +398,7 @@ pub async fn infer_data_from_files(
|
||||
sha512: hash,
|
||||
file_name,
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -412,7 +418,7 @@ pub async fn infer_data_from_files(
|
||||
{
|
||||
zip_file_reader
|
||||
} else {
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -420,7 +426,7 @@ pub async fn infer_data_from_files(
|
||||
metadata: ProjectMetadata::Unknown,
|
||||
file_name,
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
};
|
||||
let zip_index_option = zip_file_reader
|
||||
@@ -466,7 +472,7 @@ pub async fn infer_data_from_files(
|
||||
)
|
||||
.await?;
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -491,7 +497,7 @@ pub async fn infer_data_from_files(
|
||||
project_type: Some("mod".to_string()),
|
||||
},
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -533,7 +539,7 @@ pub async fn infer_data_from_files(
|
||||
)
|
||||
.await?;
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -552,7 +558,7 @@ pub async fn infer_data_from_files(
|
||||
project_type: Some("mod".to_string()),
|
||||
},
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -599,7 +605,7 @@ pub async fn infer_data_from_files(
|
||||
)
|
||||
.await?;
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -621,7 +627,7 @@ pub async fn infer_data_from_files(
|
||||
project_type: Some("mod".to_string()),
|
||||
},
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -665,7 +671,7 @@ pub async fn infer_data_from_files(
|
||||
)
|
||||
.await?;
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -697,7 +703,7 @@ pub async fn infer_data_from_files(
|
||||
project_type: Some("mod".to_string()),
|
||||
},
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -731,7 +737,7 @@ pub async fn infer_data_from_files(
|
||||
io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -746,13 +752,13 @@ pub async fn infer_data_from_files(
|
||||
project_type: None,
|
||||
},
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -760,8 +766,17 @@ pub async fn infer_data_from_files(
|
||||
file_name,
|
||||
metadata: ProjectMetadata::Unknown,
|
||||
},
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
Ok(return_projects)
|
||||
// Project paths should be relative
|
||||
let _profile_base_path = profile.get_profile_full_path().await?;
|
||||
let mut corrected_hashmap = HashMap::new();
|
||||
let mut stream = tokio_stream::iter(return_projects);
|
||||
while let Some((h, v)) = stream.next().await {
|
||||
let h = ProjectPathId::from_fs_path(h).await?;
|
||||
corrected_hashmap.insert(h, v);
|
||||
}
|
||||
|
||||
Ok(corrected_hashmap)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ use crate::{
|
||||
State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
use super::JavaGlobals;
|
||||
use super::{DirectoryInfo, JavaGlobals};
|
||||
|
||||
// TODO: convert to semver?
|
||||
const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
@@ -15,7 +15,6 @@ const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
// Types
|
||||
/// Global Theseus settings
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
pub theme: Theme,
|
||||
pub memory: MemorySettings,
|
||||
@@ -41,31 +40,8 @@ pub struct Settings {
|
||||
pub advanced_rendering: bool,
|
||||
#[serde(default)]
|
||||
pub onboarded: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: Theme::Dark,
|
||||
memory: MemorySettings::default(),
|
||||
game_resolution: WindowSize::default(),
|
||||
custom_java_args: Vec::new(),
|
||||
custom_env_args: Vec::new(),
|
||||
java_globals: JavaGlobals::new(),
|
||||
default_user: None,
|
||||
hooks: Hooks::default(),
|
||||
max_concurrent_downloads: 10,
|
||||
max_concurrent_writes: 10,
|
||||
version: CURRENT_FORMAT_VERSION,
|
||||
collapsed_navigation: false,
|
||||
hide_on_process: false,
|
||||
default_page: DefaultPage::Home,
|
||||
developer_mode: false,
|
||||
opt_out_analytics: false,
|
||||
advanced_rendering: true,
|
||||
onboarded: false,
|
||||
}
|
||||
}
|
||||
#[serde(default = "DirectoryInfo::get_initial_settings_dir")]
|
||||
pub loaded_config_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
@@ -85,7 +61,29 @@ impl Settings {
|
||||
.map_err(crate::Error::from)
|
||||
})
|
||||
} else {
|
||||
Ok(Settings::default())
|
||||
Ok(Self {
|
||||
theme: Theme::Dark,
|
||||
memory: MemorySettings::default(),
|
||||
game_resolution: WindowSize::default(),
|
||||
custom_java_args: Vec::new(),
|
||||
custom_env_args: Vec::new(),
|
||||
java_globals: JavaGlobals::new(),
|
||||
default_user: None,
|
||||
hooks: Hooks::default(),
|
||||
max_concurrent_downloads: 10,
|
||||
max_concurrent_writes: 10,
|
||||
version: CURRENT_FORMAT_VERSION,
|
||||
collapsed_navigation: false,
|
||||
hide_on_process: false,
|
||||
default_page: DefaultPage::Home,
|
||||
developer_mode: false,
|
||||
opt_out_analytics: false,
|
||||
advanced_rendering: true,
|
||||
onboarded: false,
|
||||
|
||||
// By default, the config directory is the same as the settings directory
|
||||
loaded_config_dir: DirectoryInfo::get_initial_settings_dir(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Tags {
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
let mut tags = None;
|
||||
let tags_path = dirs.caches_meta_dir().join("tags.json");
|
||||
let tags_path = dirs.caches_meta_dir().await.join("tags.json");
|
||||
|
||||
if let Ok(tags_json) = read_json::<Self>(&tags_path, io_semaphore).await
|
||||
{
|
||||
@@ -60,7 +60,7 @@ impl Tags {
|
||||
let tags_fetch = Tags::fetch(&state.fetch_semaphore).await?;
|
||||
|
||||
let tags_path =
|
||||
state.directories.caches_meta_dir().join("tags.json");
|
||||
state.directories.caches_meta_dir().await.join("tags.json");
|
||||
|
||||
write(
|
||||
&tags_path,
|
||||
|
||||
@@ -17,7 +17,7 @@ impl Users {
|
||||
dirs: &DirectoryInfo,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
let users_path = dirs.caches_meta_dir().join(USERS_JSON);
|
||||
let users_path = dirs.caches_meta_dir().await.join(USERS_JSON);
|
||||
let users = read_json(&users_path, io_semaphore).await.ok();
|
||||
|
||||
if let Some(users) = users {
|
||||
@@ -29,7 +29,8 @@ impl Users {
|
||||
|
||||
pub async fn save(&self) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let users_path = state.directories.caches_meta_dir().join(USERS_JSON);
|
||||
let users_path =
|
||||
state.directories.caches_meta_dir().await.join(USERS_JSON);
|
||||
write(
|
||||
&users_path,
|
||||
&serde_json::to_vec(&self.0)?,
|
||||
|
||||
@@ -221,6 +221,7 @@ pub async fn write<'a>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Writes a icon to the cache and returns the absolute path of the icon within the cache directory
|
||||
#[tracing::instrument(skip(bytes, semaphore))]
|
||||
pub async fn write_cached_icon(
|
||||
icon_path: &str,
|
||||
|
||||
@@ -201,7 +201,7 @@ async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError>
|
||||
let state = State::get().await.map_err(|_| JREError::StateError)?;
|
||||
|
||||
let mut jre_paths = HashSet::new();
|
||||
let base_path = state.directories.java_versions_dir();
|
||||
let base_path = state.directories.java_versions_dir().await;
|
||||
|
||||
if base_path.is_dir() {
|
||||
if let Ok(dir) = std::fs::read_dir(base_path) {
|
||||
|
||||
@@ -227,7 +227,7 @@ impl<'a> From<&'a Profile> for ProfileRow<'a> {
|
||||
fn from(it: &'a Profile) -> Self {
|
||||
Self {
|
||||
name: &it.metadata.name,
|
||||
path: &it.path,
|
||||
path: Path::new(&it.metadata.name),
|
||||
game_version: &it.metadata.game_version,
|
||||
loader: &it.metadata.loader,
|
||||
loader_version: it
|
||||
@@ -285,7 +285,8 @@ impl ProfileRemove {
|
||||
_args: &crate::Args,
|
||||
_largs: &ProfileCommand,
|
||||
) -> Result<()> {
|
||||
let profile = canonicalize(&self.profile)?;
|
||||
let profile =
|
||||
ProfilePathId::from_fs_path(canonicalize(&self.profile)?).await?;
|
||||
info!("Removing profile {} from Theseus", self.profile.display());
|
||||
|
||||
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
||||
@@ -335,7 +336,9 @@ impl ProfileRun {
|
||||
.await?;
|
||||
let credentials = auth::refresh(id).await?;
|
||||
|
||||
let proc_lock = profile::run_credentials(&path, &credentials).await?;
|
||||
let profile_path_id = ProfilePathId::from_fs_path(path).await?;
|
||||
let proc_lock =
|
||||
profile::run_credentials(&profile_path_id, &credentials).await?;
|
||||
let mut proc = proc_lock.write().await;
|
||||
process::wait_for(&mut proc).await?;
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ impl UserDefault {
|
||||
) -> Result<()> {
|
||||
info!("Setting user {} as default", self.user.as_hyphenated());
|
||||
|
||||
let state: std::sync::Arc<State> = State::get().await?;
|
||||
let state = State::get().await?;
|
||||
let mut settings = state.settings.write().await;
|
||||
|
||||
if settings.default_user == Some(self.user) {
|
||||
|
||||
@@ -50,7 +50,18 @@ pub async fn logs_get_output_by_datetime(
|
||||
profile_uuid: Uuid,
|
||||
datetime_string: String,
|
||||
) -> Result<String> {
|
||||
Ok(logs::get_output_by_datetime(profile_uuid, &datetime_string).await?)
|
||||
let profile_path = if let Some(p) =
|
||||
crate::profile::get_by_uuid(profile_uuid, None).await?
|
||||
{
|
||||
p.profile_id()
|
||||
} else {
|
||||
return Err(theseus::Error::from(
|
||||
theseus::ErrorKind::UnmanagedProfileError(profile_uuid.to_string()),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
Ok(logs::get_output_by_datetime(&profile_path, &datetime_string).await?)
|
||||
}
|
||||
|
||||
/// Delete all logs for a profile by profile id
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::Result;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use theseus::{
|
||||
pack::{
|
||||
install::install_pack,
|
||||
@@ -20,8 +20,8 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
#[tauri::command]
|
||||
pub async fn pack_install(
|
||||
location: CreatePackLocation,
|
||||
profile: PathBuf,
|
||||
) -> Result<PathBuf> {
|
||||
profile: ProfilePathId,
|
||||
) -> Result<ProfilePathId> {
|
||||
Ok(install_pack(location, profile).await?)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::api::Result;
|
||||
use theseus::prelude::*;
|
||||
use uuid::Uuid;
|
||||
@@ -50,14 +48,15 @@ pub async fn process_get_all_running_uuids() -> Result<Vec<Uuid>> {
|
||||
// Gets all process UUIDs by profile path
|
||||
#[tauri::command]
|
||||
pub async fn process_get_uuids_by_profile_path(
|
||||
profile_path: &Path,
|
||||
profile_path: ProfilePathId,
|
||||
) -> Result<Vec<Uuid>> {
|
||||
Ok(process::get_uuids_by_profile_path(profile_path).await?)
|
||||
}
|
||||
|
||||
// Gets the Profile paths of each *running* stored process in the state
|
||||
#[tauri::command]
|
||||
pub async fn process_get_all_running_profile_paths() -> Result<Vec<PathBuf>> {
|
||||
pub async fn process_get_all_running_profile_paths(
|
||||
) -> Result<Vec<ProfilePathId>> {
|
||||
Ok(process::get_all_running_profile_paths().await?)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
// Remove a profile
|
||||
// invoke('plugin:profile|profile_add_path',path)
|
||||
#[tauri::command]
|
||||
pub async fn profile_remove(path: &Path) -> Result<()> {
|
||||
profile::remove(path).await?;
|
||||
pub async fn profile_remove(path: ProfilePathId) -> Result<()> {
|
||||
profile::remove(&path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -45,19 +45,19 @@ pub async fn profile_remove(path: &Path) -> Result<()> {
|
||||
// invoke('plugin:profile|profile_add_path',path)
|
||||
#[tauri::command]
|
||||
pub async fn profile_get(
|
||||
path: &Path,
|
||||
path: ProfilePathId,
|
||||
clear_projects: Option<bool>,
|
||||
) -> Result<Option<Profile>> {
|
||||
let res = profile::get(path, clear_projects).await?;
|
||||
let res = profile::get(&path, clear_projects).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// Get optimal java version from profile
|
||||
#[tauri::command]
|
||||
pub async fn profile_get_optimal_jre_key(
|
||||
path: &Path,
|
||||
path: ProfilePathId,
|
||||
) -> Result<Option<JavaVersion>> {
|
||||
let res = profile::get_optimal_jre_key(path).await?;
|
||||
let res = profile::get_optimal_jre_key(&path).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -66,14 +66,14 @@ pub async fn profile_get_optimal_jre_key(
|
||||
#[tauri::command]
|
||||
pub async fn profile_list(
|
||||
clear_projects: Option<bool>,
|
||||
) -> Result<HashMap<PathBuf, Profile>> {
|
||||
) -> Result<HashMap<ProfilePathId, Profile>> {
|
||||
let res = profile::list(clear_projects).await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn profile_check_installed(
|
||||
path: &Path,
|
||||
path: ProfilePathId,
|
||||
project_id: String,
|
||||
) -> Result<bool> {
|
||||
let profile = profile_get(path, None).await?;
|
||||
@@ -94,8 +94,8 @@ pub async fn profile_check_installed(
|
||||
/// Installs/Repairs a profile
|
||||
/// invoke('plugin:profile|profile_install')
|
||||
#[tauri::command]
|
||||
pub async fn profile_install(path: &Path) -> Result<()> {
|
||||
profile::install(path).await?;
|
||||
pub async fn profile_install(path: ProfilePathId) -> Result<()> {
|
||||
profile::install(&path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -103,40 +103,40 @@ pub async fn profile_install(path: &Path) -> Result<()> {
|
||||
/// invoke('plugin:profile|profile_update_all')
|
||||
#[tauri::command]
|
||||
pub async fn profile_update_all(
|
||||
path: &Path,
|
||||
) -> Result<HashMap<PathBuf, PathBuf>> {
|
||||
Ok(profile::update_all(path).await?)
|
||||
path: ProfilePathId,
|
||||
) -> Result<HashMap<ProjectPathId, ProjectPathId>> {
|
||||
Ok(profile::update_all(&path).await?)
|
||||
}
|
||||
|
||||
/// Updates a specified project
|
||||
/// invoke('plugin:profile|profile_update_project')
|
||||
#[tauri::command]
|
||||
pub async fn profile_update_project(
|
||||
path: &Path,
|
||||
project_path: &Path,
|
||||
) -> Result<PathBuf> {
|
||||
Ok(profile::update_project(path, project_path, None).await?)
|
||||
path: ProfilePathId,
|
||||
project_path: ProjectPathId,
|
||||
) -> Result<ProjectPathId> {
|
||||
Ok(profile::update_project(&path, &project_path, None).await?)
|
||||
}
|
||||
|
||||
// Adds a project to a profile from a version ID
|
||||
// invoke('plugin:profile|profile_add_project_from_version')
|
||||
#[tauri::command]
|
||||
pub async fn profile_add_project_from_version(
|
||||
path: &Path,
|
||||
path: ProfilePathId,
|
||||
version_id: String,
|
||||
) -> Result<PathBuf> {
|
||||
Ok(profile::add_project_from_version(path, version_id).await?)
|
||||
) -> Result<ProjectPathId> {
|
||||
Ok(profile::add_project_from_version(&path, version_id).await?)
|
||||
}
|
||||
|
||||
// Adds a project to a profile from a path
|
||||
// invoke('plugin:profile|profile_add_project_from_path')
|
||||
#[tauri::command]
|
||||
pub async fn profile_add_project_from_path(
|
||||
path: &Path,
|
||||
path: ProfilePathId,
|
||||
project_path: &Path,
|
||||
project_type: Option<String>,
|
||||
) -> Result<PathBuf> {
|
||||
let res = profile::add_project_from_path(path, project_path, project_type)
|
||||
) -> Result<ProjectPathId> {
|
||||
let res = profile::add_project_from_path(&path, project_path, project_type)
|
||||
.await?;
|
||||
Ok(res)
|
||||
}
|
||||
@@ -145,20 +145,20 @@ pub async fn profile_add_project_from_path(
|
||||
// invoke('plugin:profile|profile_toggle_disable_project')
|
||||
#[tauri::command]
|
||||
pub async fn profile_toggle_disable_project(
|
||||
path: &Path,
|
||||
project_path: &Path,
|
||||
) -> Result<PathBuf> {
|
||||
Ok(profile::toggle_disable_project(path, project_path).await?)
|
||||
path: ProfilePathId,
|
||||
project_path: ProjectPathId,
|
||||
) -> Result<ProjectPathId> {
|
||||
Ok(profile::toggle_disable_project(&path, &project_path).await?)
|
||||
}
|
||||
|
||||
// Removes a project from a profile
|
||||
// invoke('plugin:profile|profile_remove_project')
|
||||
#[tauri::command]
|
||||
pub async fn profile_remove_project(
|
||||
path: &Path,
|
||||
project_path: &Path,
|
||||
path: ProfilePathId,
|
||||
project_path: ProjectPathId,
|
||||
) -> Result<()> {
|
||||
profile::remove_project(path, project_path).await?;
|
||||
profile::remove_project(&path, &project_path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -166,13 +166,13 @@ pub async fn profile_remove_project(
|
||||
// invoke('profile_export_mrpack')
|
||||
#[tauri::command]
|
||||
pub async fn profile_export_mrpack(
|
||||
path: &Path,
|
||||
path: ProfilePathId,
|
||||
export_location: PathBuf,
|
||||
included_overrides: Vec<String>,
|
||||
version_id: Option<String>,
|
||||
) -> Result<()> {
|
||||
profile::export_mrpack(
|
||||
path,
|
||||
&path,
|
||||
export_location,
|
||||
included_overrides,
|
||||
version_id,
|
||||
@@ -190,7 +190,7 @@ pub async fn profile_export_mrpack(
|
||||
// => [folder1, folder2]
|
||||
#[tauri::command]
|
||||
pub async fn profile_get_potential_override_folders(
|
||||
profile_path: PathBuf,
|
||||
profile_path: ProfilePathId,
|
||||
) -> Result<Vec<PathBuf>> {
|
||||
let overrides =
|
||||
profile::get_potential_override_folders(profile_path).await?;
|
||||
@@ -202,8 +202,8 @@ pub async fn profile_get_potential_override_folders(
|
||||
// for the actual Child in the state.
|
||||
// invoke('plugin:profile|profile_run', path)
|
||||
#[tauri::command]
|
||||
pub async fn profile_run(path: &Path) -> Result<Uuid> {
|
||||
let minecraft_child = profile::run(path).await?;
|
||||
pub async fn profile_run(path: ProfilePathId) -> Result<Uuid> {
|
||||
let minecraft_child = profile::run(&path).await?;
|
||||
let uuid = minecraft_child.read().await.uuid;
|
||||
Ok(uuid)
|
||||
}
|
||||
@@ -211,8 +211,8 @@ pub async fn profile_run(path: &Path) -> Result<Uuid> {
|
||||
// Run Minecraft using a profile using the default credentials, and wait for the result
|
||||
// invoke('plugin:profile|profile_run_wait', path)
|
||||
#[tauri::command]
|
||||
pub async fn profile_run_wait(path: &Path) -> Result<()> {
|
||||
let proc_lock = profile::run(path).await?;
|
||||
pub async fn profile_run_wait(path: ProfilePathId) -> Result<()> {
|
||||
let proc_lock = profile::run(&path).await?;
|
||||
let mut proc = proc_lock.write().await;
|
||||
Ok(process::wait_for(&mut proc).await?)
|
||||
}
|
||||
@@ -223,11 +223,12 @@ pub async fn profile_run_wait(path: &Path) -> Result<()> {
|
||||
// invoke('plugin:profile|profile_run_credentials', {path, credentials})')
|
||||
#[tauri::command]
|
||||
pub async fn profile_run_credentials(
|
||||
path: &Path,
|
||||
path: ProfilePathId,
|
||||
credentials: Credentials,
|
||||
) -> Result<Uuid> {
|
||||
let minecraft_child = profile::run_credentials(path, &credentials).await?;
|
||||
let minecraft_child = profile::run_credentials(&path, &credentials).await?;
|
||||
let uuid = minecraft_child.read().await.uuid;
|
||||
|
||||
Ok(uuid)
|
||||
}
|
||||
|
||||
@@ -235,10 +236,10 @@ pub async fn profile_run_credentials(
|
||||
// invoke('plugin:profile|profile_run_wait', {path, credentials)
|
||||
#[tauri::command]
|
||||
pub async fn profile_run_wait_credentials(
|
||||
path: &Path,
|
||||
path: ProfilePathId,
|
||||
credentials: Credentials,
|
||||
) -> Result<()> {
|
||||
let proc_lock = profile::run_credentials(path, &credentials).await?;
|
||||
let proc_lock = profile::run_credentials(&path, &credentials).await?;
|
||||
let mut proc = proc_lock.write().await;
|
||||
Ok(process::wait_for(&mut proc).await?)
|
||||
}
|
||||
@@ -265,10 +266,10 @@ pub struct EditProfileMetadata {
|
||||
// invoke('plugin:profile|profile_edit', {path, editProfile})
|
||||
#[tauri::command]
|
||||
pub async fn profile_edit(
|
||||
path: &Path,
|
||||
path: ProfilePathId,
|
||||
edit_profile: EditProfile,
|
||||
) -> Result<()> {
|
||||
profile::edit(path, |prof| {
|
||||
profile::edit(&path, |prof| {
|
||||
if let Some(metadata) = edit_profile.metadata.clone() {
|
||||
if let Some(name) = metadata.name {
|
||||
prof.metadata.name = name;
|
||||
@@ -305,9 +306,9 @@ pub async fn profile_edit(
|
||||
// invoke('plugin:profile|profile_edit_icon')
|
||||
#[tauri::command]
|
||||
pub async fn profile_edit_icon(
|
||||
path: &Path,
|
||||
path: ProfilePathId,
|
||||
icon_path: Option<&Path>,
|
||||
) -> Result<()> {
|
||||
profile::edit_icon(path, icon_path).await?;
|
||||
profile::edit_icon(&path, icon_path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub async fn profile_create(
|
||||
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
|
||||
icon: Option<PathBuf>, // the icon for the profile
|
||||
) -> Result<PathBuf> {
|
||||
) -> Result<ProfilePathId> {
|
||||
let res = profile_create::profile_create(
|
||||
name,
|
||||
game_version,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::api::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use theseus::prelude::*;
|
||||
@@ -22,7 +24,11 @@ pub struct FrontendSettings {
|
||||
|
||||
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
tauri::plugin::Builder::new("settings")
|
||||
.invoke_handler(tauri::generate_handler![settings_get, settings_set,])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
settings_get,
|
||||
settings_set,
|
||||
settings_change_config_dir
|
||||
])
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -41,3 +47,12 @@ pub async fn settings_set(settings: Settings) -> Result<()> {
|
||||
settings::set(settings).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Change config directory
|
||||
// Seizes the entire State to do it
|
||||
// invoke('plugin:settings|settings_change_config_dir', new_dir)
|
||||
#[tauri::command]
|
||||
pub async fn settings_change_config_dir(new_config_dir: PathBuf) -> Result<()> {
|
||||
settings::set_config_dir(new_config_dir).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -122,6 +122,6 @@ pub async fn handle_command(command: String) -> Result<()> {
|
||||
#[tauri::command]
|
||||
pub async fn await_sync() -> Result<()> {
|
||||
State::sync().await?;
|
||||
tracing::info!("State synced");
|
||||
tracing::debug!("State synced");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -69,14 +69,12 @@ const exportPack = async () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
console.log(filesToExport)
|
||||
const outputPath = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
})
|
||||
|
||||
if (outputPath) {
|
||||
console.log(outputPath)
|
||||
export_profile_mrpack(
|
||||
props.instance.path,
|
||||
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,
|
||||
|
||||
@@ -18,14 +18,13 @@ import {
|
||||
get,
|
||||
list,
|
||||
} from '@/helpers/profile'
|
||||
import { tauri } from '@tauri-apps/api'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { create } from '@/helpers/profile'
|
||||
import { installVersionDependencies } from '@/helpers/utils'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import mixpanel from 'mixpanel-browser'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { tauri } from '@tauri-apps/api'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -227,7 +226,7 @@ const check_valid = computed(() => {
|
||||
!profile.metadata.icon ||
|
||||
(profile.metadata.icon && profile.metadata.icon.startsWith('http'))
|
||||
? profile.metadata.icon
|
||||
: convertFileSrc(profile.metadata?.icon)
|
||||
: tauri.convertFileSrc(profile.metadata?.icon)
|
||||
"
|
||||
class="profile-image"
|
||||
/>
|
||||
|
||||
@@ -37,3 +37,9 @@ export async function get() {
|
||||
export async function set(settings) {
|
||||
return await invoke('plugin:settings|settings_set', { settings })
|
||||
}
|
||||
|
||||
// Changes the config dir
|
||||
// Seizes the entire application state until its done
|
||||
export async function change_config_dir(newConfigDir) {
|
||||
return await invoke('plugin:settings|settings_change_config_dir', { newConfigDir })
|
||||
}
|
||||
|
||||
@@ -147,13 +147,13 @@ import {
|
||||
import { process_listener, profile_listener } from '@/helpers/events'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
|
||||
import { showInFolder } from '@/helpers/utils.js'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import mixpanel from 'mixpanel-browser'
|
||||
import { PackageIcon } from '@/assets/icons/index.js'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -278,6 +278,12 @@ const handleOptionsClick = async (args) => {
|
||||
|
||||
const unlistenProfiles = await profile_listener(async (event) => {
|
||||
if (event.path === route.params.id) {
|
||||
if (event.event === 'removed') {
|
||||
await router.push({
|
||||
path: '/',
|
||||
})
|
||||
return
|
||||
}
|
||||
instance.value = await get(route.params.id).catch(handleError)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -107,7 +107,7 @@ async function getLiveLog() {
|
||||
}
|
||||
|
||||
async function getLogs() {
|
||||
return (await get_logs(props.instance.uuid, true).catch(handleError)).reverse().map((log) => {
|
||||
return (await get_logs(props.instance.path, true).catch(handleError)).reverse().map((log) => {
|
||||
log.name = dayjs(
|
||||
log.datetime_string.slice(0, 8) + 'T' + log.datetime_string.slice(9)
|
||||
).calendar()
|
||||
@@ -149,7 +149,7 @@ watch(selectedLogIndex, async (newIndex) => {
|
||||
if (logs.value.length > 1 && newIndex !== 0) {
|
||||
logs.value[newIndex].stdout = 'Loading...'
|
||||
logs.value[newIndex].stdout = await get_output_by_datetime(
|
||||
props.instance.uuid,
|
||||
props.instance.path,
|
||||
logs.value[newIndex].datetime_string
|
||||
).catch(handleError)
|
||||
}
|
||||
@@ -164,7 +164,7 @@ const deleteLog = async () => {
|
||||
let deleteIndex = selectedLogIndex.value
|
||||
selectedLogIndex.value = deleteIndex - 1
|
||||
await delete_logs_by_datetime(
|
||||
props.instance.uuid,
|
||||
props.instance.path,
|
||||
logs.value[deleteIndex].datetime_string
|
||||
).catch(handleError)
|
||||
await setLogs()
|
||||
|
||||
@@ -331,7 +331,6 @@ import {
|
||||
CodeIcon,
|
||||
} from 'omorphia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
add_project_from_path,
|
||||
@@ -345,6 +344,7 @@ import { handleError } from '@/store/notifications.js'
|
||||
import mixpanel from 'mixpanel-browser'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { showInFolder } from '@/helpers/utils.js'
|
||||
import { MenuIcon, ToggleIcon, TextInputIcon, AddProjectImage } from '@/assets/icons'
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ async fn main() -> theseus::Result<()> {
|
||||
|
||||
println!("running");
|
||||
// Run a profile, running minecraft and store the RwLock to the process
|
||||
let proc_lock = profile::run(&canonicalize(&profile_path)?).await?;
|
||||
let proc_lock = profile::run(&profile_path).await?;
|
||||
let uuid = proc_lock.read().await.uuid;
|
||||
let pid = proc_lock.read().await.current_child.read().await.id();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user