You've already forked AstralRinth
forked from didirus/AstralRinth
Folder names (#318)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use super::Profile;
|
||||
use super::{Profile, ProfilePathId};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitStatus;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
@@ -25,7 +25,7 @@ pub struct Children(HashMap<Uuid, Arc<RwLock<MinecraftChild>>>);
|
||||
#[derive(Debug)]
|
||||
pub struct MinecraftChild {
|
||||
pub uuid: Uuid,
|
||||
pub profile_path: PathBuf, //todo: make UUID when profiles are recognized by UUID
|
||||
pub profile_relative_path: ProfilePathId,
|
||||
pub manager: Option<JoinHandle<crate::Result<ExitStatus>>>, // None when future has completed and been handled
|
||||
pub current_child: Arc<RwLock<Child>>,
|
||||
pub output: SharedOutput,
|
||||
@@ -53,7 +53,7 @@ impl Children {
|
||||
pub async fn insert_process(
|
||||
&mut self,
|
||||
uuid: Uuid,
|
||||
profile_path: PathBuf,
|
||||
profile_relative_path: ProfilePathId,
|
||||
log_path: PathBuf,
|
||||
mut mc_command: Command,
|
||||
post_command: Option<Command>, // Command to run after minecraft.
|
||||
@@ -107,7 +107,7 @@ impl Children {
|
||||
// Create MinecraftChild
|
||||
let mchild = MinecraftChild {
|
||||
uuid,
|
||||
profile_path,
|
||||
profile_relative_path,
|
||||
current_child,
|
||||
output: shared_output,
|
||||
manager,
|
||||
@@ -266,7 +266,7 @@ impl Children {
|
||||
// Gets all PID keys of running children with a given profile path
|
||||
pub async fn running_keys_with_profile(
|
||||
&self,
|
||||
profile_path: &Path,
|
||||
profile_path: ProfilePathId,
|
||||
) -> crate::Result<Vec<Uuid>> {
|
||||
let running_keys = self.running_keys().await?;
|
||||
let mut keys = Vec::new();
|
||||
@@ -274,7 +274,7 @@ impl Children {
|
||||
if let Some(child) = self.get(&key) {
|
||||
let child = child.clone();
|
||||
let child = child.read().await;
|
||||
if child.profile_path == profile_path {
|
||||
if child.profile_relative_path == profile_path {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
@@ -283,7 +283,9 @@ impl Children {
|
||||
}
|
||||
|
||||
// Gets all profiles of running children
|
||||
pub async fn running_profile_paths(&self) -> crate::Result<Vec<PathBuf>> {
|
||||
pub async fn running_profile_paths(
|
||||
&self,
|
||||
) -> crate::Result<Vec<ProfilePathId>> {
|
||||
let mut profiles = Vec::new();
|
||||
for key in self.keys() {
|
||||
if let Some(child) = self.get(&key) {
|
||||
@@ -297,7 +299,7 @@ impl Children {
|
||||
.map_err(IOError::from)?
|
||||
.is_none()
|
||||
{
|
||||
profiles.push(child.profile_path.clone());
|
||||
profiles.push(child.profile_relative_path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,7 +323,7 @@ impl Children {
|
||||
.is_none()
|
||||
{
|
||||
if let Some(prof) = crate::api::profile::get(
|
||||
&child.profile_path.clone(),
|
||||
&child.profile_relative_path.clone(),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
|
||||
@@ -2,16 +2,42 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::{ProfilePathId, Settings};
|
||||
|
||||
pub const SETTINGS_FILE_NAME: &str = "settings.json";
|
||||
pub const CACHES_FOLDER_NAME: &str = "caches";
|
||||
pub const LAUNCHER_LOGS_FOLDER_NAME: &str = "launcher_logs";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirectoryInfo {
|
||||
pub config_dir: PathBuf,
|
||||
pub settings_dir: PathBuf, // Base settings directory- settings.json and icon cache.
|
||||
pub config_dir: RwLock<PathBuf>, // Base config directory- instances, minecraft downloads, etc. Changeable as a setting.
|
||||
pub working_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl DirectoryInfo {
|
||||
// Get the settings directory
|
||||
// init() is not needed for this function
|
||||
pub fn get_initial_settings_dir() -> Option<PathBuf> {
|
||||
Self::env_path("THESEUS_CONFIG_DIR")
|
||||
.or_else(|| Some(dirs::config_dir()?.join("com.modrinth.theseus")))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_initial_settings_file() -> crate::Result<PathBuf> {
|
||||
let settings_dir = Self::get_initial_settings_dir().ok_or(
|
||||
crate::ErrorKind::FSError(
|
||||
"Could not find valid config dir".to_string(),
|
||||
),
|
||||
)?;
|
||||
Ok(settings_dir.join("settings.json"))
|
||||
}
|
||||
|
||||
/// Get all paths needed for Theseus to operate properly
|
||||
#[tracing::instrument]
|
||||
pub fn init() -> crate::Result<Self> {
|
||||
pub fn init(settings: &Settings) -> crate::Result<Self> {
|
||||
// Working directory
|
||||
let working_dir = std::env::current_dir().map_err(|err| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
@@ -19,143 +45,153 @@ impl DirectoryInfo {
|
||||
))
|
||||
})?;
|
||||
|
||||
// Config directory
|
||||
let config_dir = Self::env_path("THESEUS_CONFIG_DIR")
|
||||
.or_else(|| Some(dirs::config_dir()?.join("com.modrinth.theseus")))
|
||||
.ok_or(crate::ErrorKind::FSError(
|
||||
"Could not find valid config dir".to_string(),
|
||||
))?;
|
||||
let settings_dir = Self::get_initial_settings_dir().ok_or(
|
||||
crate::ErrorKind::FSError(
|
||||
"Could not find valid settings dir".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
fs::create_dir_all(&config_dir).map_err(|err| {
|
||||
fs::create_dir_all(&settings_dir).map_err(|err| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Error creating Theseus config directory: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// config directory (for instances, etc.)
|
||||
// by default this is the same as the settings directory
|
||||
let config_dir = settings.loaded_config_dir.clone().ok_or(
|
||||
crate::ErrorKind::FSError(
|
||||
"Could not find valid config dir".to_string(),
|
||||
),
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
config_dir,
|
||||
settings_dir,
|
||||
config_dir: RwLock::new(config_dir),
|
||||
working_dir,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the Minecraft instance metadata directory
|
||||
#[inline]
|
||||
pub fn metadata_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("meta")
|
||||
pub async fn metadata_dir(&self) -> PathBuf {
|
||||
self.config_dir.read().await.join("meta")
|
||||
}
|
||||
|
||||
/// Get the Minecraft java versions metadata directory
|
||||
#[inline]
|
||||
pub fn java_versions_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("java_versions")
|
||||
pub async fn java_versions_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("java_versions")
|
||||
}
|
||||
|
||||
/// Get the Minecraft versions metadata directory
|
||||
#[inline]
|
||||
pub fn versions_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("versions")
|
||||
pub async fn versions_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("versions")
|
||||
}
|
||||
|
||||
/// Get the metadata directory for a given version
|
||||
#[inline]
|
||||
pub fn version_dir(&self, version: &str) -> PathBuf {
|
||||
self.versions_dir().join(version)
|
||||
pub async fn version_dir(&self, version: &str) -> PathBuf {
|
||||
self.versions_dir().await.join(version)
|
||||
}
|
||||
|
||||
/// Get the Minecraft libraries metadata directory
|
||||
#[inline]
|
||||
pub fn libraries_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("libraries")
|
||||
pub async fn libraries_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("libraries")
|
||||
}
|
||||
|
||||
/// Get the Minecraft assets metadata directory
|
||||
#[inline]
|
||||
pub fn assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("assets")
|
||||
pub async fn assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("assets")
|
||||
}
|
||||
|
||||
/// Get the assets index directory
|
||||
#[inline]
|
||||
pub fn assets_index_dir(&self) -> PathBuf {
|
||||
self.assets_dir().join("indexes")
|
||||
pub async fn assets_index_dir(&self) -> PathBuf {
|
||||
self.assets_dir().await.join("indexes")
|
||||
}
|
||||
|
||||
/// Get the assets objects directory
|
||||
#[inline]
|
||||
pub fn objects_dir(&self) -> PathBuf {
|
||||
self.assets_dir().join("objects")
|
||||
pub async fn objects_dir(&self) -> PathBuf {
|
||||
self.assets_dir().await.join("objects")
|
||||
}
|
||||
|
||||
/// Get the directory for a specific object
|
||||
#[inline]
|
||||
pub fn object_dir(&self, hash: &str) -> PathBuf {
|
||||
self.objects_dir().join(&hash[..2]).join(hash)
|
||||
pub async fn object_dir(&self, hash: &str) -> PathBuf {
|
||||
self.objects_dir().await.join(&hash[..2]).join(hash)
|
||||
}
|
||||
|
||||
/// Get the Minecraft legacy assets metadata directory
|
||||
#[inline]
|
||||
pub fn legacy_assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("resources")
|
||||
pub async fn legacy_assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("resources")
|
||||
}
|
||||
|
||||
/// Get the Minecraft legacy assets metadata directory
|
||||
#[inline]
|
||||
pub fn natives_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().join("natives")
|
||||
pub async fn natives_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("natives")
|
||||
}
|
||||
|
||||
/// Get the natives directory for a version of Minecraft
|
||||
#[inline]
|
||||
pub fn version_natives_dir(&self, version: &str) -> PathBuf {
|
||||
self.natives_dir().join(version)
|
||||
pub async fn version_natives_dir(&self, version: &str) -> PathBuf {
|
||||
self.natives_dir().await.join(version)
|
||||
}
|
||||
|
||||
/// Get the directory containing instance icons
|
||||
#[inline]
|
||||
pub fn icon_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("icons")
|
||||
pub async fn icon_dir(&self) -> PathBuf {
|
||||
self.config_dir.read().await.join("icons")
|
||||
}
|
||||
|
||||
/// Get the profiles directory for created profiles
|
||||
#[inline]
|
||||
pub fn profiles_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("profiles")
|
||||
pub async fn profiles_dir(&self) -> PathBuf {
|
||||
self.config_dir.read().await.join("profiles")
|
||||
}
|
||||
|
||||
/// Gets the logs dir for a given profile
|
||||
#[inline]
|
||||
pub fn profile_logs_dir(&self, profile: uuid::Uuid) -> PathBuf {
|
||||
self.profiles_dir()
|
||||
.join(profile.to_string())
|
||||
.join("modrinth_logs")
|
||||
pub async fn profile_logs_dir(
|
||||
&self,
|
||||
profile_id: &ProfilePathId,
|
||||
) -> crate::Result<PathBuf> {
|
||||
Ok(profile_id.get_full_path().await?.join("modrinth_logs"))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn launcher_logs_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("launcher_logs")
|
||||
pub fn launcher_logs_dir() -> Option<PathBuf> {
|
||||
Self::get_initial_settings_dir()
|
||||
.map(|d| d.join(LAUNCHER_LOGS_FOLDER_NAME))
|
||||
}
|
||||
|
||||
/// Get the file containing the global database
|
||||
#[inline]
|
||||
pub fn database_file(&self) -> PathBuf {
|
||||
self.config_dir.join("data.bin")
|
||||
pub async fn database_file(&self) -> PathBuf {
|
||||
self.config_dir.read().await.join("data.bin")
|
||||
}
|
||||
|
||||
/// Get the settings file for Theseus
|
||||
#[inline]
|
||||
pub fn settings_file(&self) -> PathBuf {
|
||||
self.config_dir.join("settings.json")
|
||||
self.settings_dir.join(SETTINGS_FILE_NAME)
|
||||
}
|
||||
|
||||
/// Get the cache directory for Theseus
|
||||
#[inline]
|
||||
pub fn caches_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("caches")
|
||||
self.settings_dir.join(CACHES_FOLDER_NAME)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn caches_meta_dir(&self) -> PathBuf {
|
||||
self.config_dir.join("caches").join("metadata")
|
||||
pub async fn caches_meta_dir(&self) -> PathBuf {
|
||||
self.caches_dir().join("metadata")
|
||||
}
|
||||
|
||||
/// Get path from environment variable
|
||||
|
||||
@@ -35,6 +35,10 @@ impl JavaGlobals {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> Vec<String> {
|
||||
self.0.keys().cloned().collect()
|
||||
}
|
||||
|
||||
// Validates that every path here is a valid Java version and that the version matches the version stored here
|
||||
// If false, when checked, the user should be prompted to reselect the Java version
|
||||
pub async fn is_all_valid(&self) -> bool {
|
||||
|
||||
@@ -61,7 +61,7 @@ impl Metadata {
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
let mut metadata = None;
|
||||
let metadata_path = dirs.caches_meta_dir().join("metadata.json");
|
||||
let metadata_path = dirs.caches_meta_dir().await.join("metadata.json");
|
||||
|
||||
if let Ok(metadata_json) =
|
||||
read_json::<Metadata>(&metadata_path, io_semaphore).await
|
||||
@@ -106,8 +106,11 @@ impl Metadata {
|
||||
let metadata_fetch = Metadata::fetch().await?;
|
||||
let state = State::get().await?;
|
||||
|
||||
let metadata_path =
|
||||
state.directories.caches_meta_dir().join("metadata.json");
|
||||
let metadata_path = state
|
||||
.directories
|
||||
.caches_meta_dir()
|
||||
.await
|
||||
.join("metadata.json");
|
||||
|
||||
write(
|
||||
&metadata_path,
|
||||
|
||||
@@ -49,7 +49,8 @@ mod safe_processes;
|
||||
pub use self::safe_processes::*;
|
||||
|
||||
// Global state
|
||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||
// RwLock on state only has concurrent reads, except for config dir change which takes control of the State
|
||||
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
|
||||
pub struct State {
|
||||
/// Information on the location of files used in the launcher
|
||||
pub directories: DirectoryInfo,
|
||||
@@ -86,83 +87,97 @@ pub struct State {
|
||||
|
||||
impl State {
|
||||
/// Get the current launcher state, initializing it if needed
|
||||
pub async fn get(
|
||||
) -> crate::Result<Arc<tokio::sync::RwLockReadGuard<'static, Self>>> {
|
||||
Ok(Arc::new(
|
||||
LAUNCHER_STATE
|
||||
.get_or_try_init(Self::initialize_state)
|
||||
.await?
|
||||
.read()
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
/// Get the current launcher state, initializing it if needed
|
||||
/// Takes writing control of the state, blocking all other uses of it
|
||||
/// Only used for state change such as changing the config directory
|
||||
pub async fn get_write(
|
||||
) -> crate::Result<tokio::sync::RwLockWriteGuard<'static, Self>> {
|
||||
Ok(LAUNCHER_STATE
|
||||
.get_or_try_init(Self::initialize_state)
|
||||
.await?
|
||||
.write()
|
||||
.await)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
LAUNCHER_STATE
|
||||
.get_or_try_init(|| {
|
||||
async {
|
||||
let loading_bar = init_loading_unsafe(
|
||||
LoadingBarType::StateInit,
|
||||
100.0,
|
||||
"Initializing launcher",
|
||||
)
|
||||
.await?;
|
||||
async fn initialize_state() -> crate::Result<RwLock<State>> {
|
||||
let loading_bar = init_loading_unsafe(
|
||||
LoadingBarType::StateInit,
|
||||
100.0,
|
||||
"Initializing launcher",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut file_watcher = init_watcher().await?;
|
||||
// Settings
|
||||
let settings =
|
||||
Settings::init(&DirectoryInfo::get_initial_settings_file()?)
|
||||
.await?;
|
||||
|
||||
let directories = DirectoryInfo::init()?;
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
let directories = DirectoryInfo::init(&settings)?;
|
||||
|
||||
// Settings
|
||||
let settings =
|
||||
Settings::init(&directories.settings_file()).await?;
|
||||
let fetch_semaphore = FetchSemaphore(RwLock::new(
|
||||
Semaphore::new(settings.max_concurrent_downloads),
|
||||
));
|
||||
let io_semaphore = IoSemaphore(RwLock::new(
|
||||
Semaphore::new(settings.max_concurrent_writes),
|
||||
));
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
|
||||
let metadata_fut =
|
||||
Metadata::init(&directories, &io_semaphore);
|
||||
let profiles_fut =
|
||||
Profiles::init(&directories, &mut file_watcher);
|
||||
let tags_fut = Tags::init(
|
||||
&directories,
|
||||
&io_semaphore,
|
||||
&fetch_semaphore,
|
||||
);
|
||||
let users_fut = Users::init(&directories, &io_semaphore);
|
||||
// Launcher data
|
||||
let (metadata, profiles, tags, users) = loading_join! {
|
||||
Some(&loading_bar), 70.0, Some("Loading metadata");
|
||||
metadata_fut,
|
||||
profiles_fut,
|
||||
tags_fut,
|
||||
users_fut,
|
||||
}?;
|
||||
let mut file_watcher = init_watcher().await?;
|
||||
|
||||
let children = Children::new();
|
||||
let auth_flow = AuthTask::new();
|
||||
let safety_processes = SafeProcesses::new();
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
let fetch_semaphore = FetchSemaphore(RwLock::new(Semaphore::new(
|
||||
settings.max_concurrent_downloads,
|
||||
)));
|
||||
let io_semaphore = IoSemaphore(RwLock::new(Semaphore::new(
|
||||
settings.max_concurrent_writes,
|
||||
)));
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
directories,
|
||||
fetch_semaphore,
|
||||
fetch_semaphore_max: RwLock::new(
|
||||
settings.max_concurrent_downloads as u32,
|
||||
),
|
||||
io_semaphore,
|
||||
io_semaphore_max: RwLock::new(
|
||||
settings.max_concurrent_writes as u32,
|
||||
),
|
||||
metadata: RwLock::new(metadata),
|
||||
settings: RwLock::new(settings),
|
||||
profiles: RwLock::new(profiles),
|
||||
users: RwLock::new(users),
|
||||
children: RwLock::new(children),
|
||||
auth_flow: RwLock::new(auth_flow),
|
||||
tags: RwLock::new(tags),
|
||||
safety_processes: RwLock::new(safety_processes),
|
||||
file_watcher: RwLock::new(file_watcher),
|
||||
}))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map(Arc::clone)
|
||||
let metadata_fut = Metadata::init(&directories, &io_semaphore);
|
||||
let profiles_fut = Profiles::init(&directories, &mut file_watcher);
|
||||
let tags_fut =
|
||||
Tags::init(&directories, &io_semaphore, &fetch_semaphore);
|
||||
let users_fut = Users::init(&directories, &io_semaphore);
|
||||
// Launcher data
|
||||
let (metadata, profiles, tags, users) = loading_join! {
|
||||
Some(&loading_bar), 70.0, Some("Loading metadata");
|
||||
metadata_fut,
|
||||
profiles_fut,
|
||||
tags_fut,
|
||||
users_fut,
|
||||
}?;
|
||||
|
||||
let children = Children::new();
|
||||
let auth_flow = AuthTask::new();
|
||||
let safety_processes = SafeProcesses::new();
|
||||
emit_loading(&loading_bar, 10.0, None).await?;
|
||||
|
||||
Ok::<RwLock<Self>, crate::Error>(RwLock::new(Self {
|
||||
directories,
|
||||
fetch_semaphore,
|
||||
fetch_semaphore_max: RwLock::new(
|
||||
settings.max_concurrent_downloads as u32,
|
||||
),
|
||||
io_semaphore,
|
||||
io_semaphore_max: RwLock::new(
|
||||
settings.max_concurrent_writes as u32,
|
||||
),
|
||||
metadata: RwLock::new(metadata),
|
||||
settings: RwLock::new(settings),
|
||||
profiles: RwLock::new(profiles),
|
||||
users: RwLock::new(users),
|
||||
children: RwLock::new(children),
|
||||
auth_flow: RwLock::new(auth_flow),
|
||||
tags: RwLock::new(tags),
|
||||
safety_processes: RwLock::new(safety_processes),
|
||||
file_watcher: RwLock::new(file_watcher),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Updates state with data from the web
|
||||
@@ -240,7 +255,7 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
pub async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
let (mut tx, mut rx) = channel(1);
|
||||
|
||||
let file_watcher = new_debouncer(
|
||||
@@ -256,13 +271,19 @@ async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
tokio::task::spawn(async move {
|
||||
while let Some(res) = rx.next().await {
|
||||
match res {
|
||||
Ok(events) => {
|
||||
Ok(mut events) => {
|
||||
let mut visited_paths = Vec::new();
|
||||
// sort events by e.path
|
||||
events.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
events.iter().for_each(|e| {
|
||||
tracing::debug!(
|
||||
"File watcher event: {:?}",
|
||||
serde_json::to_string(&e.path).unwrap()
|
||||
);
|
||||
let mut new_path = PathBuf::new();
|
||||
let mut components_iterator = e.path.components();
|
||||
let mut found = false;
|
||||
|
||||
for component in e.path.components() {
|
||||
for component in components_iterator.by_ref() {
|
||||
new_path.push(component);
|
||||
if found {
|
||||
break;
|
||||
@@ -271,6 +292,14 @@ async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
// if any remain, it's a subfile of the profile folder and not the profile folder itself
|
||||
let subfile = components_iterator.next().is_some();
|
||||
|
||||
// At this point, new_path is the path to the profile, and subfile is whether it's a subfile of the profile or not
|
||||
let profile_path_id =
|
||||
ProfilePathId::new(&PathBuf::from(
|
||||
new_path.file_name().unwrap_or_default(),
|
||||
));
|
||||
|
||||
if e.path
|
||||
.components()
|
||||
@@ -280,10 +309,16 @@ async fn init_watcher() -> crate::Result<Debouncer<RecommendedWatcher>> {
|
||||
.map(|x| x == "txt")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Profile::crash_task(new_path);
|
||||
Profile::crash_task(profile_path_id);
|
||||
} else if !visited_paths.contains(&new_path) {
|
||||
Profile::sync_projects_task(new_path.clone());
|
||||
visited_paths.push(new_path);
|
||||
if subfile {
|
||||
Profile::sync_projects_task(profile_path_id);
|
||||
visited_paths.push(new_path);
|
||||
} else {
|
||||
Profiles::sync_available_profiles_task(
|
||||
profile_path_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use uuid::Uuid;
|
||||
|
||||
const PROFILE_JSON_PATH: &str = "profile.json";
|
||||
|
||||
pub(crate) struct Profiles(pub HashMap<PathBuf, Profile>);
|
||||
pub(crate) struct Profiles(pub HashMap<ProfilePathId, Profile>);
|
||||
|
||||
#[derive(
|
||||
Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq,
|
||||
@@ -46,13 +46,101 @@ pub enum ProfileInstallStage {
|
||||
NotInstalled,
|
||||
}
|
||||
|
||||
/// newtype wrapper over a Profile path, to be usable as a clear identifier for the kind of path used
|
||||
/// eg: for "a/b/c/profiles/My Mod", the ProfilePathId would be "My Mod" (a relative path)
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[serde(transparent)]
|
||||
pub struct ProfilePathId(PathBuf);
|
||||
impl ProfilePathId {
|
||||
// Create a new ProfilePathId from a full file path
|
||||
pub async fn from_fs_path(path: PathBuf) -> crate::Result<Self> {
|
||||
let path: PathBuf = io::canonicalize(path)?;
|
||||
let profiles_dir = io::canonicalize(
|
||||
State::get().await?.directories.profiles_dir().await,
|
||||
)?;
|
||||
path.strip_prefix(profiles_dir)
|
||||
.ok()
|
||||
.and_then(|p| p.file_name())
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Path {path:?} does not correspond to a profile",
|
||||
path = path
|
||||
))
|
||||
})?;
|
||||
Ok(Self(path))
|
||||
}
|
||||
|
||||
// Create a new ProfilePathId from a relative path
|
||||
pub fn new(path: &Path) -> Self {
|
||||
ProfilePathId(PathBuf::from(path))
|
||||
}
|
||||
|
||||
pub async fn get_full_path(&self) -> crate::Result<PathBuf> {
|
||||
let state = State::get().await?;
|
||||
let profiles_dir = state.directories.profiles_dir().await;
|
||||
Ok(profiles_dir.join(&self.0))
|
||||
}
|
||||
|
||||
pub fn check_valid_utf(&self) -> crate::Result<&Self> {
|
||||
self.0
|
||||
.to_str()
|
||||
.ok_or(crate::ErrorKind::UTFError(self.0.clone()).as_error())?;
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for ProfilePathId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.display().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// newtype wrapper over a Profile path, to be usable as a clear identifier for the kind of path used
|
||||
/// eg: for "a/b/c/profiles/My Mod/mods/myproj", the ProjectPathId would be "mods/myproj"
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[serde(transparent)]
|
||||
pub struct ProjectPathId(pub PathBuf);
|
||||
impl ProjectPathId {
|
||||
// Create a new ProjectPathId from a full file path
|
||||
pub async fn from_fs_path(path: PathBuf) -> crate::Result<Self> {
|
||||
let path: PathBuf = io::canonicalize(path)?;
|
||||
let profiles_dir: PathBuf = io::canonicalize(
|
||||
State::get().await?.directories.profiles_dir().await,
|
||||
)?;
|
||||
path.strip_prefix(profiles_dir)
|
||||
.ok()
|
||||
.map(|p| p.components().skip(1).collect::<PathBuf>())
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Path {path:?} does not correspond to a profile",
|
||||
path = path
|
||||
))
|
||||
})?;
|
||||
Ok(Self(path))
|
||||
}
|
||||
|
||||
pub async fn get_full_path(
|
||||
&self,
|
||||
profile: ProfilePathId,
|
||||
) -> crate::Result<PathBuf> {
|
||||
let _state = State::get().await?;
|
||||
let profile_dir = profile.get_full_path().await?;
|
||||
Ok(profile_dir.join(&self.0))
|
||||
}
|
||||
|
||||
// Create a new ProjectPathId from a relative path
|
||||
pub fn new(path: &Path) -> Self {
|
||||
ProjectPathId(PathBuf::from(path))
|
||||
}
|
||||
}
|
||||
|
||||
// Represent a Minecraft instance.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Profile {
|
||||
pub uuid: Uuid, // todo: will be used in restructure to refer to profiles
|
||||
#[serde(default)]
|
||||
pub install_stage: ProfileInstallStage,
|
||||
pub path: PathBuf,
|
||||
#[serde(default)]
|
||||
pub path: PathBuf, // Relative path to the profile, to be used in ProfilePathId
|
||||
pub metadata: ProfileMetadata,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub java: Option<JavaSettings>,
|
||||
@@ -62,7 +150,7 @@ pub struct Profile {
|
||||
pub resolution: Option<WindowSize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hooks: Option<Hooks>,
|
||||
pub projects: HashMap<PathBuf, Project>,
|
||||
pub projects: HashMap<ProjectPathId, Project>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
@@ -148,7 +236,6 @@ impl Profile {
|
||||
uuid: Uuid,
|
||||
name: String,
|
||||
version: String,
|
||||
path: PathBuf,
|
||||
) -> crate::Result<Self> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(crate::ErrorKind::InputError(String::from(
|
||||
@@ -160,7 +247,7 @@ impl Profile {
|
||||
Ok(Self {
|
||||
uuid,
|
||||
install_stage: ProfileInstallStage::NotInstalled,
|
||||
path: io::canonicalize(path)?,
|
||||
path: PathBuf::new().join(&name),
|
||||
metadata: ProfileMetadata {
|
||||
name,
|
||||
icon: None,
|
||||
@@ -182,6 +269,12 @@ impl Profile {
|
||||
})
|
||||
}
|
||||
|
||||
// Gets the ProfilePathId for this profile
|
||||
#[inline]
|
||||
pub fn profile_id(&self) -> ProfilePathId {
|
||||
ProfilePathId::new(&self.path)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, semaphore, icon))]
|
||||
pub async fn set_icon<'a>(
|
||||
&'a mut self,
|
||||
@@ -197,7 +290,7 @@ impl Profile {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn crash_task(path: PathBuf) {
|
||||
pub fn crash_task(path: ProfilePathId) {
|
||||
tokio::task::spawn(async move {
|
||||
let res = async {
|
||||
let profile = crate::api::profile::get(&path, None).await?;
|
||||
@@ -221,38 +314,47 @@ impl Profile {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn sync_projects_task(path: PathBuf) {
|
||||
pub fn sync_projects_task(profile_path_id: ProfilePathId) {
|
||||
tokio::task::spawn(async move {
|
||||
let span =
|
||||
tracing::span!(tracing::Level::INFO, "sync_projects_task");
|
||||
tracing::debug!(
|
||||
parent: &span,
|
||||
"Syncing projects for profile {}",
|
||||
profile_path_id
|
||||
);
|
||||
let res = async {
|
||||
let _span = span.enter();
|
||||
let state = State::get().await?;
|
||||
let profile = crate::api::profile::get(&path, None).await?;
|
||||
let profile = crate::api::profile::get(&profile_path_id, None).await?;
|
||||
|
||||
if let Some(profile) = profile {
|
||||
let paths = profile.get_profile_project_paths()?;
|
||||
let paths = profile.get_profile_full_project_paths().await?;
|
||||
|
||||
let caches_dir = state.directories.caches_dir();
|
||||
let projects = crate::state::infer_data_from_files(
|
||||
profile.clone(),
|
||||
paths,
|
||||
state.directories.caches_dir(),
|
||||
caches_dir,
|
||||
&state.io_semaphore,
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut new_profiles = state.profiles.write().await;
|
||||
if let Some(profile) = new_profiles.0.get_mut(&path) {
|
||||
if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) {
|
||||
profile.projects = projects;
|
||||
}
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Synced,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Unable to fetch single profile projects: path {path:?} invalid",
|
||||
"Unable to fetch single profile projects: path {profile_path_id} invalid",
|
||||
);
|
||||
}
|
||||
Ok::<(), crate::Error>(())
|
||||
@@ -268,10 +370,21 @@ impl Profile {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_profile_project_paths(&self) -> crate::Result<Vec<PathBuf>> {
|
||||
// Get full path to profile
|
||||
pub async fn get_profile_full_path(&self) -> crate::Result<PathBuf> {
|
||||
let state = State::get().await?;
|
||||
let profiles_dir = state.directories.profiles_dir().await;
|
||||
Ok(profiles_dir.join(&self.path))
|
||||
}
|
||||
|
||||
/// Gets paths to projects as their full paths, not just their relative paths
|
||||
pub async fn get_profile_full_project_paths(
|
||||
&self,
|
||||
) -> crate::Result<Vec<PathBuf>> {
|
||||
let mut files = Vec::new();
|
||||
let profile_path = self.get_profile_full_path().await?;
|
||||
let mut read_paths = |path: &str| {
|
||||
let new_path = self.path.join(path);
|
||||
let new_path = profile_path.join(path);
|
||||
if new_path.exists() {
|
||||
let path = self.path.join(path);
|
||||
for path in std::fs::read_dir(&path)
|
||||
@@ -338,7 +451,7 @@ impl Profile {
|
||||
pub async fn add_project_version(
|
||||
&self,
|
||||
version_id: String,
|
||||
) -> crate::Result<(PathBuf, ModrinthVersion)> {
|
||||
) -> crate::Result<(ProjectPathId, ModrinthVersion)> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let version = fetch_json::<ModrinthVersion>(
|
||||
@@ -387,7 +500,7 @@ impl Profile {
|
||||
file_name: &str,
|
||||
bytes: bytes::Bytes,
|
||||
project_type: Option<ProjectType>,
|
||||
) -> crate::Result<PathBuf> {
|
||||
) -> crate::Result<ProjectPathId> {
|
||||
let project_type = if let Some(project_type) = project_type {
|
||||
project_type
|
||||
} else {
|
||||
@@ -419,16 +532,23 @@ impl Profile {
|
||||
};
|
||||
|
||||
let state = State::get().await?;
|
||||
let path = self.path.join(project_type.get_folder()).join(file_name);
|
||||
write(&path, &bytes, &state.io_semaphore).await?;
|
||||
let relative_name = PathBuf::new()
|
||||
.join(project_type.get_folder())
|
||||
.join(file_name);
|
||||
let file_path = self
|
||||
.get_profile_full_path()
|
||||
.await?
|
||||
.join(relative_name.clone());
|
||||
let project_path_id = ProjectPathId::new(&relative_name);
|
||||
write(&file_path, &bytes, &state.io_semaphore).await?;
|
||||
|
||||
let hash = get_hash(bytes).await?;
|
||||
{
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
if let Some(profile) = profiles.0.get_mut(&self.path) {
|
||||
if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
|
||||
profile.projects.insert(
|
||||
path.clone(),
|
||||
project_path_id.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
disabled: false,
|
||||
@@ -440,32 +560,40 @@ impl Profile {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
Ok(project_path_id)
|
||||
}
|
||||
|
||||
/// Toggle a project's disabled state.
|
||||
/// 'path' should be relative to the profile's path.
|
||||
#[tracing::instrument(skip(self))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn toggle_disable_project(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> crate::Result<PathBuf> {
|
||||
relative_path: &ProjectPathId,
|
||||
) -> crate::Result<ProjectPathId> {
|
||||
let state = State::get().await?;
|
||||
if let Some(mut project) = {
|
||||
let mut profiles = state.profiles.write().await;
|
||||
let mut profiles: tokio::sync::RwLockWriteGuard<'_, Profiles> =
|
||||
state.profiles.write().await;
|
||||
|
||||
if let Some(profile) = profiles.0.get_mut(&self.path) {
|
||||
profile.projects.remove(path)
|
||||
if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
|
||||
profile.projects.remove(relative_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} {
|
||||
let path = path.to_path_buf();
|
||||
let mut new_path = path.clone();
|
||||
// Get relative path from former ProjectPathId
|
||||
let relative_path = relative_path.0.to_path_buf();
|
||||
let mut new_path = relative_path.clone();
|
||||
|
||||
if path.extension().map_or(false, |ext| ext == "disabled") {
|
||||
if relative_path
|
||||
.extension()
|
||||
.map_or(false, |ext| ext == "disabled")
|
||||
{
|
||||
project.disabled = false;
|
||||
new_path.set_file_name(
|
||||
path.file_name()
|
||||
relative_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.replace(".disabled", ""),
|
||||
@@ -473,24 +601,35 @@ impl Profile {
|
||||
} else {
|
||||
new_path.set_file_name(format!(
|
||||
"{}.disabled",
|
||||
path.file_name().unwrap_or_default().to_string_lossy()
|
||||
relative_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
));
|
||||
project.disabled = true;
|
||||
}
|
||||
|
||||
io::rename(&path, &new_path).await?;
|
||||
let true_path =
|
||||
self.get_profile_full_path().await?.join(&relative_path);
|
||||
let true_new_path =
|
||||
self.get_profile_full_path().await?.join(&new_path);
|
||||
io::rename(&true_path, &true_new_path).await?;
|
||||
|
||||
let new_project_path_id = ProjectPathId::new(&new_path);
|
||||
|
||||
let mut profiles = state.profiles.write().await;
|
||||
if let Some(profile) = profiles.0.get_mut(&self.path) {
|
||||
profile.projects.insert(new_path.clone(), project);
|
||||
if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
|
||||
profile
|
||||
.projects
|
||||
.insert(new_project_path_id.clone(), project);
|
||||
profile.metadata.date_modified = Utc::now();
|
||||
}
|
||||
|
||||
Ok(new_path)
|
||||
Ok(new_project_path_id)
|
||||
} else {
|
||||
Err(crate::ErrorKind::InputError(format!(
|
||||
"Project path does not exist: {:?}",
|
||||
path
|
||||
relative_path
|
||||
))
|
||||
.into())
|
||||
}
|
||||
@@ -498,24 +637,29 @@ impl Profile {
|
||||
|
||||
pub async fn remove_project(
|
||||
&self,
|
||||
path: &Path,
|
||||
relative_path: &ProjectPathId,
|
||||
dont_remove_arr: Option<bool>,
|
||||
) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
if self.projects.contains_key(path) {
|
||||
io::remove_file(path).await?;
|
||||
if self.projects.contains_key(relative_path) {
|
||||
io::remove_file(
|
||||
self.get_profile_full_path()
|
||||
.await?
|
||||
.join(relative_path.0.clone()),
|
||||
)
|
||||
.await?;
|
||||
if !dont_remove_arr.unwrap_or(false) {
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
if let Some(profile) = profiles.0.get_mut(&self.path) {
|
||||
profile.projects.remove(path);
|
||||
if let Some(profile) = profiles.0.get_mut(&self.profile_id()) {
|
||||
profile.projects.remove(relative_path);
|
||||
profile.metadata.date_modified = Utc::now();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(crate::ErrorKind::InputError(format!(
|
||||
"Project path does not exist: {:?}",
|
||||
path
|
||||
relative_path
|
||||
))
|
||||
.into());
|
||||
}
|
||||
@@ -532,14 +676,21 @@ impl Profiles {
|
||||
file_watcher: &mut Debouncer<RecommendedWatcher>,
|
||||
) -> crate::Result<Self> {
|
||||
let mut profiles = HashMap::new();
|
||||
io::create_dir_all(&dirs.profiles_dir()).await?;
|
||||
let mut entries = io::read_dir(&dirs.profiles_dir()).await?;
|
||||
let profiles_dir = dirs.profiles_dir().await;
|
||||
io::create_dir_all(&&profiles_dir).await?;
|
||||
|
||||
file_watcher
|
||||
.watcher()
|
||||
.watch(&profiles_dir, RecursiveMode::NonRecursive)?;
|
||||
|
||||
let mut entries = io::read_dir(&dirs.profiles_dir().await).await?;
|
||||
while let Some(entry) =
|
||||
entries.next_entry().await.map_err(IOError::from)?
|
||||
{
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let prof = match Self::read_profile_from_dir(&path).await {
|
||||
let prof = match Self::read_profile_from_dir(&path, dirs).await
|
||||
{
|
||||
Ok(prof) => Some(prof),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
@@ -551,7 +702,7 @@ impl Profiles {
|
||||
if let Some(profile) = prof {
|
||||
let path = io::canonicalize(path)?;
|
||||
Profile::watch_fs(&path, file_watcher).await?;
|
||||
profiles.insert(path, profile);
|
||||
profiles.insert(profile.profile_id(), profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -570,26 +721,28 @@ impl Profiles {
|
||||
{
|
||||
let profiles = state.profiles.read().await;
|
||||
for (_profile_path, profile) in profiles.0.iter() {
|
||||
let paths = profile.get_profile_project_paths()?;
|
||||
let paths =
|
||||
profile.get_profile_full_project_paths().await?;
|
||||
|
||||
files.push((profile.clone(), paths));
|
||||
}
|
||||
}
|
||||
|
||||
let caches_dir = state.directories.caches_dir();
|
||||
future::try_join_all(files.into_iter().map(
|
||||
|(profile, files)| async {
|
||||
let profile_path = profile.path.clone();
|
||||
let profile_name = profile.profile_id();
|
||||
let inferred = super::projects::infer_data_from_files(
|
||||
profile,
|
||||
files,
|
||||
state.directories.caches_dir(),
|
||||
caches_dir.clone(),
|
||||
&state.io_semaphore,
|
||||
&state.fetch_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut new_profiles = state.profiles.write().await;
|
||||
if let Some(profile) = new_profiles.0.get_mut(&profile_path)
|
||||
if let Some(profile) = new_profiles.0.get_mut(&profile_name)
|
||||
{
|
||||
profile.projects = inferred;
|
||||
}
|
||||
@@ -622,7 +775,7 @@ impl Profiles {
|
||||
pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.path.clone(),
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Added,
|
||||
)
|
||||
@@ -630,30 +783,26 @@ impl Profiles {
|
||||
|
||||
let state = State::get().await?;
|
||||
let mut file_watcher = state.file_watcher.write().await;
|
||||
Profile::watch_fs(&profile.path, &mut file_watcher).await?;
|
||||
Profile::watch_fs(
|
||||
&profile.get_profile_full_path().await?,
|
||||
&mut file_watcher,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.0.insert(
|
||||
io::canonicalize(&profile.path)?
|
||||
.to_str()
|
||||
.ok_or(
|
||||
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
|
||||
)?
|
||||
.into(),
|
||||
profile,
|
||||
);
|
||||
let profile_name = profile.profile_id();
|
||||
profile_name.check_valid_utf()?;
|
||||
self.0.insert(profile_name, profile);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn remove(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
profile_path: &ProfilePathId,
|
||||
) -> crate::Result<Option<Profile>> {
|
||||
let path = PathBuf::from(
|
||||
&io::canonicalize(path)?.to_string_lossy().to_string(),
|
||||
);
|
||||
let profile = self.0.remove(&path);
|
||||
let profile = self.0.remove(profile_path);
|
||||
|
||||
let path = profile_path.get_full_path().await?;
|
||||
if path.exists() {
|
||||
io::remove_dir_all(&path).await?;
|
||||
}
|
||||
@@ -663,12 +812,15 @@ impl Profiles {
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn sync(&self) -> crate::Result<&Self> {
|
||||
let _state = State::get().await?;
|
||||
stream::iter(self.0.iter())
|
||||
.map(Ok::<_, crate::Error>)
|
||||
.try_for_each_concurrent(None, |(path, profile)| async move {
|
||||
.try_for_each_concurrent(None, |(_, profile)| async move {
|
||||
let json = serde_json::to_vec(&profile)?;
|
||||
|
||||
let json_path = Path::new(&path.to_string_lossy().to_string())
|
||||
let json_path = profile
|
||||
.get_profile_full_path()
|
||||
.await?
|
||||
.join(PROFILE_JSON_PATH);
|
||||
|
||||
io::write(&json_path, &json).await?;
|
||||
@@ -679,10 +831,68 @@ impl Profiles {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
async fn read_profile_from_dir(path: &Path) -> crate::Result<Profile> {
|
||||
async fn read_profile_from_dir(
|
||||
path: &Path,
|
||||
dirs: &DirectoryInfo,
|
||||
) -> crate::Result<Profile> {
|
||||
let json = io::read(&path.join(PROFILE_JSON_PATH)).await?;
|
||||
let mut profile = serde_json::from_slice::<Profile>(&json)?;
|
||||
profile.path = PathBuf::from(path);
|
||||
|
||||
// Get name from stripped path
|
||||
profile.path =
|
||||
PathBuf::from(path.strip_prefix(dirs.profiles_dir().await)?);
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
pub fn sync_available_profiles_task(profile_path_id: ProfilePathId) {
|
||||
tokio::task::spawn(async move {
|
||||
let span = tracing::span!(
|
||||
tracing::Level::INFO,
|
||||
"sync_available_profiles_task"
|
||||
);
|
||||
let res = async {
|
||||
let _span = span.enter();
|
||||
let state = State::get().await?;
|
||||
let dirs = &state.directories;
|
||||
let mut profiles = state.profiles.write().await;
|
||||
|
||||
if let Some(profile) = profiles.0.get_mut(&profile_path_id) {
|
||||
if !profile.get_profile_full_path().await?.exists() {
|
||||
// if path exists in the state but no longer in the filesystem, remove it from the state list
|
||||
emit_profile(
|
||||
profile.uuid,
|
||||
profile.get_profile_full_path().await?,
|
||||
&profile.metadata.name,
|
||||
ProfilePayloadType::Removed,
|
||||
)
|
||||
.await?;
|
||||
tracing::debug!("Removed!");
|
||||
profiles.0.remove(&profile_path_id);
|
||||
}
|
||||
} else if profile_path_id.get_full_path().await?.exists() {
|
||||
// if it exists in the filesystem but no longer in the state, add it to the state list
|
||||
profiles
|
||||
.insert(
|
||||
Self::read_profile_from_dir(
|
||||
&profile_path_id.get_full_path().await?,
|
||||
dirs,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await?;
|
||||
Profile::sync_projects_task(profile_path_id);
|
||||
}
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("Unable to fetch all profiles: {err}")
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::util::fetch::{
|
||||
use crate::util::io::IOError;
|
||||
use async_zip::tokio::read::fs::ZipFileReader;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::StreamExt;
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
@@ -16,6 +17,8 @@ use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::ProjectPathId;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProjectType {
|
||||
@@ -253,6 +256,9 @@ async fn read_icon_from_file(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Creates Project data from the existing files in the file system, for a given Profile
|
||||
// Paths must be the full paths to the files in the FS, and not the relative paths
|
||||
// eg: with get_profile_full_project_paths
|
||||
#[tracing::instrument(skip(paths, profile, io_semaphore, fetch_semaphore))]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn infer_data_from_files(
|
||||
@@ -261,7 +267,7 @@ pub async fn infer_data_from_files(
|
||||
cache_dir: PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<HashMap<PathBuf, Project>> {
|
||||
) -> crate::Result<HashMap<ProjectPathId, Project>> {
|
||||
let mut file_path_hashes = HashMap::new();
|
||||
|
||||
// TODO: Make this concurrent and use progressive hashing to avoid loading each JAR in memory
|
||||
@@ -342,7 +348,7 @@ pub async fn infer_data_from_files(
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let mut return_projects = HashMap::new();
|
||||
let mut return_projects: Vec<(PathBuf, Project)> = Vec::new();
|
||||
let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new();
|
||||
|
||||
for (hash, path) in file_path_hashes {
|
||||
@@ -356,7 +362,7 @@ pub async fn infer_data_from_files(
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path,
|
||||
Project {
|
||||
disabled: file_name.ends_with(".disabled"),
|
||||
@@ -392,7 +398,7 @@ pub async fn infer_data_from_files(
|
||||
sha512: hash,
|
||||
file_name,
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -412,7 +418,7 @@ pub async fn infer_data_from_files(
|
||||
{
|
||||
zip_file_reader
|
||||
} else {
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -420,7 +426,7 @@ pub async fn infer_data_from_files(
|
||||
metadata: ProjectMetadata::Unknown,
|
||||
file_name,
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
};
|
||||
let zip_index_option = zip_file_reader
|
||||
@@ -466,7 +472,7 @@ pub async fn infer_data_from_files(
|
||||
)
|
||||
.await?;
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -491,7 +497,7 @@ pub async fn infer_data_from_files(
|
||||
project_type: Some("mod".to_string()),
|
||||
},
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -533,7 +539,7 @@ pub async fn infer_data_from_files(
|
||||
)
|
||||
.await?;
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -552,7 +558,7 @@ pub async fn infer_data_from_files(
|
||||
project_type: Some("mod".to_string()),
|
||||
},
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -599,7 +605,7 @@ pub async fn infer_data_from_files(
|
||||
)
|
||||
.await?;
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -621,7 +627,7 @@ pub async fn infer_data_from_files(
|
||||
project_type: Some("mod".to_string()),
|
||||
},
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -665,7 +671,7 @@ pub async fn infer_data_from_files(
|
||||
)
|
||||
.await?;
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -697,7 +703,7 @@ pub async fn infer_data_from_files(
|
||||
project_type: Some("mod".to_string()),
|
||||
},
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -731,7 +737,7 @@ pub async fn infer_data_from_files(
|
||||
io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -746,13 +752,13 @@ pub async fn infer_data_from_files(
|
||||
project_type: None,
|
||||
},
|
||||
},
|
||||
);
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return_projects.insert(
|
||||
return_projects.push((
|
||||
path.clone(),
|
||||
Project {
|
||||
sha512: hash,
|
||||
@@ -760,8 +766,17 @@ pub async fn infer_data_from_files(
|
||||
file_name,
|
||||
metadata: ProjectMetadata::Unknown,
|
||||
},
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
Ok(return_projects)
|
||||
// Project paths should be relative
|
||||
let _profile_base_path = profile.get_profile_full_path().await?;
|
||||
let mut corrected_hashmap = HashMap::new();
|
||||
let mut stream = tokio_stream::iter(return_projects);
|
||||
while let Some((h, v)) = stream.next().await {
|
||||
let h = ProjectPathId::from_fs_path(h).await?;
|
||||
corrected_hashmap.insert(h, v);
|
||||
}
|
||||
|
||||
Ok(corrected_hashmap)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ use crate::{
|
||||
State,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
use super::JavaGlobals;
|
||||
use super::{DirectoryInfo, JavaGlobals};
|
||||
|
||||
// TODO: convert to semver?
|
||||
const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
@@ -15,7 +15,6 @@ const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||
// Types
|
||||
/// Global Theseus settings
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
pub theme: Theme,
|
||||
pub memory: MemorySettings,
|
||||
@@ -41,31 +40,8 @@ pub struct Settings {
|
||||
pub advanced_rendering: bool,
|
||||
#[serde(default)]
|
||||
pub onboarded: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: Theme::Dark,
|
||||
memory: MemorySettings::default(),
|
||||
game_resolution: WindowSize::default(),
|
||||
custom_java_args: Vec::new(),
|
||||
custom_env_args: Vec::new(),
|
||||
java_globals: JavaGlobals::new(),
|
||||
default_user: None,
|
||||
hooks: Hooks::default(),
|
||||
max_concurrent_downloads: 10,
|
||||
max_concurrent_writes: 10,
|
||||
version: CURRENT_FORMAT_VERSION,
|
||||
collapsed_navigation: false,
|
||||
hide_on_process: false,
|
||||
default_page: DefaultPage::Home,
|
||||
developer_mode: false,
|
||||
opt_out_analytics: false,
|
||||
advanced_rendering: true,
|
||||
onboarded: false,
|
||||
}
|
||||
}
|
||||
#[serde(default = "DirectoryInfo::get_initial_settings_dir")]
|
||||
pub loaded_config_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
@@ -85,7 +61,29 @@ impl Settings {
|
||||
.map_err(crate::Error::from)
|
||||
})
|
||||
} else {
|
||||
Ok(Settings::default())
|
||||
Ok(Self {
|
||||
theme: Theme::Dark,
|
||||
memory: MemorySettings::default(),
|
||||
game_resolution: WindowSize::default(),
|
||||
custom_java_args: Vec::new(),
|
||||
custom_env_args: Vec::new(),
|
||||
java_globals: JavaGlobals::new(),
|
||||
default_user: None,
|
||||
hooks: Hooks::default(),
|
||||
max_concurrent_downloads: 10,
|
||||
max_concurrent_writes: 10,
|
||||
version: CURRENT_FORMAT_VERSION,
|
||||
collapsed_navigation: false,
|
||||
hide_on_process: false,
|
||||
default_page: DefaultPage::Home,
|
||||
developer_mode: false,
|
||||
opt_out_analytics: false,
|
||||
advanced_rendering: true,
|
||||
onboarded: false,
|
||||
|
||||
// By default, the config directory is the same as the settings directory
|
||||
loaded_config_dir: DirectoryInfo::get_initial_settings_dir(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Tags {
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
let mut tags = None;
|
||||
let tags_path = dirs.caches_meta_dir().join("tags.json");
|
||||
let tags_path = dirs.caches_meta_dir().await.join("tags.json");
|
||||
|
||||
if let Ok(tags_json) = read_json::<Self>(&tags_path, io_semaphore).await
|
||||
{
|
||||
@@ -60,7 +60,7 @@ impl Tags {
|
||||
let tags_fetch = Tags::fetch(&state.fetch_semaphore).await?;
|
||||
|
||||
let tags_path =
|
||||
state.directories.caches_meta_dir().join("tags.json");
|
||||
state.directories.caches_meta_dir().await.join("tags.json");
|
||||
|
||||
write(
|
||||
&tags_path,
|
||||
|
||||
@@ -17,7 +17,7 @@ impl Users {
|
||||
dirs: &DirectoryInfo,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
let users_path = dirs.caches_meta_dir().join(USERS_JSON);
|
||||
let users_path = dirs.caches_meta_dir().await.join(USERS_JSON);
|
||||
let users = read_json(&users_path, io_semaphore).await.ok();
|
||||
|
||||
if let Some(users) = users {
|
||||
@@ -29,7 +29,8 @@ impl Users {
|
||||
|
||||
pub async fn save(&self) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let users_path = state.directories.caches_meta_dir().join(USERS_JSON);
|
||||
let users_path =
|
||||
state.directories.caches_meta_dir().await.join(USERS_JSON);
|
||||
write(
|
||||
&users_path,
|
||||
&serde_json::to_vec(&self.0)?,
|
||||
|
||||
Reference in New Issue
Block a user