Folder names (#318)

This commit is contained in:
Wyatt Verchere
2023-07-21 20:16:07 -07:00
committed by GitHub
parent 4941260805
commit 3fa33dc241
42 changed files with 1129 additions and 535 deletions

View File

@@ -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?;

View File

@@ -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(())

View File

@@ -29,6 +29,7 @@ pub mod prelude {
profile::{self, Profile},
profile_create, settings,
state::JavaGlobals,
state::{ProfilePathId, ProjectPathId},
util::{
io::{canonicalize, IOError},
jre::JavaVersion,

View File

@@ -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)
}

View File

@@ -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,
})
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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(())
}

View File

@@ -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}")]

View File

@@ -181,6 +181,9 @@ pub enum LoadingBarType {
profile_path: PathBuf,
profile_name: String,
},
ConfigChange {
new_path: PathBuf,
},
}
#[derive(Serialize, Clone)]

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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()

View File

@@ -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?

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,
);
}
}
});
}

View File

@@ -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}")
}
};
});
}
}

View File

@@ -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)
}

View File

@@ -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(),
})
}
}

View File

@@ -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,

View File

@@ -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)?,

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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?;

View File

@@ -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) {

View File

@@ -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

View File

@@ -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?)
}

View File

@@ -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?)
}

View File

@@ -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(())
}

View File

@@ -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,

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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`,

View File

@@ -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"
/>

View File

@@ -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 })
}

View File

@@ -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)
}
})

View File

@@ -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()

View File

@@ -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'

View File

@@ -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();