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

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