fmt clippy prettier

This commit is contained in:
Wyatt Verchere 2024-01-30 19:39:46 -08:00
parent 719aded698
commit f9beea8ef2
22 changed files with 616 additions and 385 deletions

View File

@ -14,8 +14,8 @@ use crate::{
/// (Does not include modrinth://) /// (Does not include modrinth://)
pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> { pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
Ok(match sublink.split_once('/') { Ok(match sublink.split_once('/') {
Some(("shared_profile", link)) => { Some(("shared_profile", link)) => CommandPayload::OpenSharedProfile {
CommandPayload::OpenSharedProfile { link: link.to_string() } link: link.to_string(),
}, },
// /mod/{id} - Installs a mod of mod id // /mod/{id} - Installs a mod of mod id
Some(("mod", id)) => CommandPayload::InstallMod { id: id.to_string() }, Some(("mod", id)) => CommandPayload::InstallMod { id: id.to_string() },

View File

@ -10,8 +10,8 @@ pub mod pack;
pub mod process; pub mod process;
pub mod profile; pub mod profile;
pub mod safety; pub mod safety;
pub mod shared_profile;
pub mod settings; pub mod settings;
pub mod shared_profile;
pub mod tags; pub mod tags;
pub mod data { pub mod data {
@ -30,7 +30,7 @@ pub mod prelude {
event::CommandPayload, event::CommandPayload,
jre, metadata, pack, process, jre, metadata, pack, process,
profile::{self, create, Profile}, profile::{self, create, Profile},
settings, settings, shared_profile,
state::JavaGlobals, state::JavaGlobals,
state::{Dependency, ProfilePathId, ProjectPathId}, state::{Dependency, ProfilePathId, ProjectPathId},
util::{ util::{
@ -38,6 +38,5 @@ pub mod prelude {
jre::JavaVersion, jre::JavaVersion,
}, },
State, State,
shared_profile,
}; };
} }

View File

@ -391,29 +391,30 @@ pub async fn set_profile_information(
let project_id = description.project_id.clone(); let project_id = description.project_id.clone();
let version_id = description.version_id.clone(); let version_id = description.version_id.clone();
prof.metadata.linked_data = if project_id.is_some() prof.metadata.linked_data =
&& version_id.is_some() if project_id.is_some() && version_id.is_some() {
{ Some(LinkedData::ModrinthModpack {
Some(LinkedData::ModrinthModpack { project_id,
project_id, version_id,
version_id, locked: if !ignore_lock {
locked: if !ignore_lock { Some(true)
Some(true)
} else {
prof.metadata.linked_data.as_ref().and_then(|x| if let LinkedData::ModrinthModpack {
locked: Some(locked),
..
} = x
{
Some(*locked)
} else { } else {
None prof.metadata.linked_data.as_ref().and_then(|x| {
}) if let LinkedData::ModrinthModpack {
}, locked: Some(locked),
}) ..
} else { } = x
None {
}; Some(*locked)
} else {
None
}
})
},
})
} else {
None
};
prof.metadata.icon = description.icon.clone(); prof.metadata.icon = description.icon.clone();
prof.metadata.game_version = game_version.clone(); prof.metadata.game_version = game_version.clone();

View File

@ -102,11 +102,14 @@ pub async fn profile_create(
} }
profile.metadata.linked_data = linked_data; profile.metadata.linked_data = linked_data;
if let Some(LinkedData::ModrinthModpack{ project_id, version_id, locked, .. }) = &mut profile.metadata.linked_data { if let Some(LinkedData::ModrinthModpack {
*locked = Some( project_id,
project_id.is_some() version_id,
&& version_id.is_some(), locked,
); ..
}) = &mut profile.metadata.linked_data
{
*locked = Some(project_id.is_some() && version_id.is_some());
} }
emit_profile( emit_profile(

View File

@ -3,12 +3,12 @@
use crate::event::emit::{ use crate::event::emit::{
emit_loading, init_loading, loading_try_for_each_concurrent, emit_loading, init_loading, loading_try_for_each_concurrent,
}; };
use crate::state::LinkedData;
use crate::event::LoadingBarType; use crate::event::LoadingBarType;
use crate::pack::install_from::{ use crate::pack::install_from::{
EnvType, PackDependency, PackFile, PackFileHash, PackFormat, EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
}; };
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId}; use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
use crate::state::LinkedData;
use crate::state::{InnerProjectPathUnix, ProjectMetadata, SideType}; use crate::state::{InnerProjectPathUnix, ProjectMetadata, SideType};
use crate::util::fetch; use crate::util::fetch;
@ -116,15 +116,14 @@ pub async fn get_mod_full_path(
project_path: &ProjectPathId, project_path: &ProjectPathId,
) -> crate::Result<PathBuf> { ) -> crate::Result<PathBuf> {
if get(profile_path, Some(true)).await?.is_some() { if get(profile_path, Some(true)).await?.is_some() {
let full_path = io::canonicalize( let full_path =
project_path.get_full_path(&profile_path).await?, io::canonicalize(project_path.get_full_path(profile_path).await?)?;
)?;
return Ok(full_path); return Ok(full_path);
} }
Err(crate::ErrorKind::OtherError(format!( Err(crate::ErrorKind::OtherError(format!(
"Tried to get the full path of a nonexistent or unloaded project at path {}!", "Tried to get the full path of a nonexistent or unloaded project at path {}!",
project_path.get_full_path(&profile_path).await?.display() project_path.get_full_path(profile_path).await?.display()
)) ))
.into()) .into())
} }
@ -873,13 +872,13 @@ pub async fn try_update_playtime(path: &ProfilePathId) -> crate::Result<()> {
let res = if updated_recent_playtime > 0 { let res = if updated_recent_playtime > 0 {
// Create update struct to send to Labrinth // Create update struct to send to Labrinth
let modrinth_pack_version_id = let modrinth_pack_version_id =
profile.metadata.linked_data.and_then(|l| profile.metadata.linked_data.and_then(|l| {
if let LinkedData::ModrinthModpack { if let LinkedData::ModrinthModpack { version_id, .. } = l {
version_id, Some(version_id)
.. } else {
} = l { None
Some(version_id) }
} else { None }); });
let playtime_update_json = json!({ let playtime_update_json = json!({
"seconds": updated_recent_playtime, "seconds": updated_recent_playtime,
"loader": profile.metadata.loader.to_string(), "loader": profile.metadata.loader.to_string(),

View File

@ -6,7 +6,7 @@ use crate::{
pack::{self, install_from::generate_pack_from_version_id}, pack::{self, install_from::generate_pack_from_version_id},
prelude::{ProfilePathId, ProjectPathId}, prelude::{ProfilePathId, ProjectPathId},
profile::get, profile::get,
state::{ProfileInstallStage, Project, LinkedData}, state::{LinkedData, ProfileInstallStage, Project},
LoadingBarType, State, LoadingBarType, State,
}; };
use futures::try_join; use futures::try_join;
@ -30,11 +30,12 @@ pub async fn update_managed_modrinth_version(
}; };
// Extract modrinth pack information, if appropriate // Extract modrinth pack information, if appropriate
let Some(LinkedData::ModrinthModpack{ let Some(LinkedData::ModrinthModpack {
project_id: Some(ref project_id), project_id: Some(ref project_id),
version_id: Some(ref version_id), version_id: Some(ref version_id),
.. ..
}) = profile.metadata.linked_data else { }) = profile.metadata.linked_data
else {
return Err(unmanaged_err().into()); return Err(unmanaged_err().into());
}; };
@ -105,11 +106,12 @@ pub async fn repair_managed_modrinth(
.await?; .await?;
// Extract modrinth pack information, if appropriate // Extract modrinth pack information, if appropriate
let Some(LinkedData::ModrinthModpack{ let Some(LinkedData::ModrinthModpack {
project_id: Some(ref project_id), project_id: Some(ref project_id),
version_id: Some(ref version_id), version_id: Some(ref version_id),
.. ..
}) = profile.metadata.linked_data else { }) = profile.metadata.linked_data
else {
return Err(unmanaged_err().into()); return Err(unmanaged_err().into());
}; };

View File

@ -1,6 +1,6 @@
//! Theseus profile management interface //! Theseus profile management interface
use std::path::{PathBuf, Path}; use std::path::{Path, PathBuf};
use tokio::fs; use tokio::fs;
use io::IOError; use io::IOError;
@ -10,7 +10,7 @@ use crate::{
event::emit::{emit_loading, init_loading}, event::emit::{emit_loading, init_loading},
prelude::DirectoryInfo, prelude::DirectoryInfo,
state::{self, Profiles}, state::{self, Profiles},
util::{io, fetch}, util::{fetch, io},
}; };
pub use crate::{ pub use crate::{
state::{ state::{
@ -109,7 +109,6 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
let old_config_dir = let old_config_dir =
state_write.directories.config_dir.read().await.clone(); state_write.directories.config_dir.read().await.clone();
// Reset file watcher // Reset file watcher
tracing::trace!("Reset file watcher"); tracing::trace!("Reset file watcher");
let file_watcher = state::init_watcher().await?; let file_watcher = state::init_watcher().await?;
@ -125,13 +124,17 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
.await .await
.map_err(|e| IOError::with_path(e, &old_config_dir))? .map_err(|e| IOError::with_path(e, &old_config_dir))?
{ {
let entry_path = entry.path(); let entry_path = entry.path();
if let Some(file_name) = entry_path.file_name() { if let Some(file_name) = entry_path.file_name() {
// We are only moving the profiles and metadata folders // We are only moving the profiles and metadata folders
if file_name == state::PROFILES_FOLDER_NAME || file_name == state::METADATA_FOLDER_NAME { if file_name == state::PROFILES_FOLDER_NAME
|| file_name == state::METADATA_FOLDER_NAME
{
if across_drives { if across_drives {
entries.extend(crate::pack::import::get_all_subfiles(&entry_path).await?); entries.extend(
crate::pack::import::get_all_subfiles(&entry_path)
.await?,
);
deletable_entries.push(entry_path.clone()); deletable_entries.push(entry_path.clone());
} else { } else {
entries.push(entry_path.clone()); entries.push(entry_path.clone());
@ -151,8 +154,7 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
} else { } else {
io::rename(entry_path.clone(), new_path.clone()).await?; io::rename(entry_path.clone(), new_path.clone()).await?;
} }
emit_loading(&loading_bar, 80.0 * (1.0 / num_entries), None) emit_loading(&loading_bar, 80.0 * (1.0 / num_entries), None).await?;
.await?;
} }
tracing::trace!("Setting configuration setting"); tracing::trace!("Setting configuration setting");
@ -199,7 +201,8 @@ pub async fn set_config_dir(new_config_dir: PathBuf) -> crate::Result<()> {
&loading_bar, &loading_bar,
10.0 * (1.0 / deletable_entries_len as f64), 10.0 * (1.0 / deletable_entries_len as f64),
None, None,
).await?; )
.await?;
} }
// Reset file watcher // Reset file watcher
@ -228,7 +231,6 @@ fn is_different_drive(path1: &Path, path2: &Path) -> bool {
root1 != root2 root1 != root2
} }
pub async fn is_dir_writeable(new_config_dir: PathBuf) -> crate::Result<bool> { pub async fn is_dir_writeable(new_config_dir: PathBuf) -> crate::Result<bool> {
let temp_path = new_config_dir.join(".tmp"); let temp_path = new_config_dir.join(".tmp");
match fs::write(temp_path.clone(), "test").await { match fs::write(temp_path.clone(), "test").await {

View File

@ -1,9 +1,20 @@
use std::path::PathBuf; use std::path::PathBuf;
use chrono::{DateTime,Utc}; use crate::{
config::MODRINTH_API_URL_INTERNAL,
prelude::{
LinkedData, ModLoader, ProfilePathId, ProjectMetadata, ProjectPathId,
},
profile,
state::{Profile, Profiles},
util::{
fetch::{fetch_advanced, REQWEST_CLIENT},
io,
},
};
use chrono::{DateTime, Utc};
use reqwest::Method; use reqwest::Method;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{config::MODRINTH_API_URL_INTERNAL, prelude::{LinkedData, ModLoader, ProfilePathId, ProjectMetadata, ProjectPathId}, profile, util::{fetch::{fetch_advanced, REQWEST_CLIENT}, io}, state::{Profile, Profiles}};
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
pub struct SharedProfile { pub struct SharedProfile {
@ -23,7 +34,7 @@ pub struct SharedProfile {
pub overrides: Vec<SharedProfileOverride>, pub overrides: Vec<SharedProfileOverride>,
pub share_links: Option<Vec<SharedProfileLink>>, pub share_links: Option<Vec<SharedProfileLink>>,
pub users: Option<Vec<String>> pub users: Option<Vec<String>>,
} }
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
@ -49,23 +60,33 @@ pub struct SharedProfileOverrideHashes {
// Create a new shared profile from ProfilePathId // Create a new shared profile from ProfilePathId
// This converts the LinkedData to a SharedProfile and uploads it to the Labrinth API // This converts the LinkedData to a SharedProfile and uploads it to the Labrinth API
#[tracing::instrument] #[tracing::instrument]
pub async fn create( pub async fn create(profile_id: ProfilePathId) -> crate::Result<()> {
profile_id: ProfilePathId,
) -> crate::Result<()> {
let state = crate::State::get().await?; let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?; let profile: Profile =
profile::get(&profile_id, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
})?;
let creds = state.credentials.read().await; let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?; let creds = creds
.0
.as_ref()
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
// Currently existing linked data should fail // Currently existing linked data should fail
match profile.metadata.linked_data { match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { .. }) => { Some(LinkedData::SharedProfile { .. }) => {
return Err(crate::ErrorKind::OtherError("Profile already linked to a shared profile".to_string()).as_error()); return Err(crate::ErrorKind::OtherError(
}, "Profile already linked to a shared profile".to_string(),
)
.as_error());
}
Some(LinkedData::ModrinthModpack { .. }) => { Some(LinkedData::ModrinthModpack { .. }) => {
return Err(crate::ErrorKind::OtherError("Profile already linked to a modrinth project".to_string()).as_error()); return Err(crate::ErrorKind::OtherError(
}, "Profile already linked to a modrinth project".to_string(),
)
.as_error());
}
None => {} None => {}
}; };
@ -74,39 +95,58 @@ pub async fn create(
let loader_version = profile.metadata.loader_version; let loader_version = profile.metadata.loader_version;
let game_version = profile.metadata.game_version; let game_version = profile.metadata.game_version;
let modrinth_projects : Vec<_> = profile.projects.iter() let modrinth_projects: Vec<_> = profile
.filter_map(|(_, project)|if let ProjectMetadata::Modrinth { ref version, .. } = project.metadata { .projects
Some(&version.id) .iter()
} else { .filter_map(|(_, project)| {
None if let ProjectMetadata::Modrinth { ref version, .. } =
}).collect(); project.metadata
{
Some(&version.id)
} else {
None
}
})
.collect();
let override_files : Vec<_> = profile.projects.iter() let override_files: Vec<_> = profile
.filter_map(|(id, project)|if let ProjectMetadata::Inferred { ..} = project.metadata { .projects
Some(id) .iter()
} else { .filter_map(|(id, project)| {
None if let ProjectMetadata::Inferred { .. } = project.metadata {
}).collect(); Some(id)
} else {
None
}
})
.collect();
// Create the profile on the Labrinth API // Create the profile on the Labrinth API
let response = REQWEST_CLIENT let response = REQWEST_CLIENT
.post( .post(format!("{MODRINTH_API_URL_INTERNAL}client/profile"))
format!("{MODRINTH_API_URL_INTERNAL}client/profile"), .header("Authorization", &creds.session)
).header("Authorization", &creds.session) .json(&serde_json::json!({
.json(&serde_json::json!({ "name": name,
"name": name, "loader": loader.as_api_str(),
"loader": loader.as_api_str(), "loader_version": loader_version.map(|x| x.id).unwrap_or_default(),
"loader_version": loader_version.map(|x| x.id).unwrap_or_default(), "game": "minecraft-java",
"game": "minecraft-java", "game_version": game_version,
"game_version": game_version, "versions": modrinth_projects,
"versions": modrinth_projects, }))
})) .send()
.send().await?; .await?;
let profile_response = response.json::<serde_json::Value>().await?; let profile_response = response.json::<serde_json::Value>().await?;
// Extract the profile ID from the response // Extract the profile ID from the response
let shared_profile_id = profile_response["id"].as_str().ok_or_else(|| crate::ErrorKind::OtherError("Could not parse response from Labrinth API".to_string()))?.to_string(); let shared_profile_id = profile_response["id"]
.as_str()
.ok_or_else(|| {
crate::ErrorKind::OtherError(
"Could not parse response from Labrinth API".to_string(),
)
})?
.to_string();
// Unmanaged projects // Unmanaged projects
let mut data = vec![]; // 'data' field, giving installation context to labrinth let mut data = vec![]; // 'data' field, giving installation context to labrinth
@ -114,26 +154,39 @@ pub async fn create(
for override_file in override_files { for override_file in override_files {
let path = override_file.get_inner_path_unix(); let path = override_file.get_inner_path_unix();
let Some(name) = path.0.split('/').last().map(|x| x.to_string()) else { continue }; let Some(name) = path.0.split('/').last().map(|x| x.to_string()) else {
continue;
};
// Load override to file // Load override to file
let full_path = &override_file.get_full_path(&profile_id).await?; let full_path = &override_file.get_full_path(&profile_id).await?;
let file_bytes = io::read(full_path).await?; let file_bytes = io::read(full_path).await?;
let ext = full_path.extension().and_then(|x| x.to_str()).unwrap_or_default(); let ext = full_path
let mime = project_file_type(ext).ok_or_else(|| crate::ErrorKind::OtherError(format!("Could not determine file type for {}", ext)))?; .extension()
.and_then(|x| x.to_str())
.unwrap_or_default();
let mime = project_file_type(ext).ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Could not determine file type for {}",
ext
))
})?;
data.push(serde_json::json!({ data.push(serde_json::json!({
"file_name": name.clone(), "file_name": name.clone(),
"install_path": path "install_path": path
})); }));
let part = reqwest::multipart::Part::bytes(file_bytes).file_name(name.clone()).mime_str(mime)?; let part = reqwest::multipart::Part::bytes(file_bytes)
.file_name(name.clone())
.mime_str(mime)?;
parts.push((name.clone(), part)); parts.push((name.clone(), part));
} }
// Build multipart with 'data' field first // Build multipart with 'data' field first
let mut multipart = reqwest::multipart::Form::new().percent_encode_noop(); let mut multipart = reqwest::multipart::Form::new().percent_encode_noop();
let json_part = reqwest::multipart::Part::text(serde_json::to_string(&data)?);//mime_str("application/json")?; let json_part =
reqwest::multipart::Part::text(serde_json::to_string(&data)?); //mime_str("application/json")?;
multipart = multipart.part("data", json_part); multipart = multipart.part("data", json_part);
for (name, part) in parts { for (name, part) in parts {
multipart = multipart.part(name, part); multipart = multipart.part(name, part);
@ -154,7 +207,8 @@ pub async fn create(
is_owner: true, is_owner: true,
}); });
async { Ok(()) } async { Ok(()) }
}).await?; })
.await?;
// Sync // Sync
crate::State::sync().await?; crate::State::sync().await?;
@ -175,7 +229,10 @@ pub fn project_file_type(ext: &str) -> Option<&str> {
pub async fn get_all() -> crate::Result<Vec<SharedProfile>> { pub async fn get_all() -> crate::Result<Vec<SharedProfile>> {
let state = crate::State::get().await?; let state = crate::State::get().await?;
let creds = state.credentials.read().await; let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?; let creds = creds
.0
.as_ref()
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
// First, get list of shared profiles the user has access to // First, get list of shared profiles the user has access to
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
@ -188,7 +245,7 @@ pub async fn get_all() -> crate::Result<Vec<SharedProfile>> {
pub icon_url: Option<String>, pub icon_url: Option<String>,
pub loader: ModLoader, pub loader: ModLoader,
pub game : String, pub game: String,
pub loader_version: String, pub loader_version: String,
pub game_version: String, pub game_version: String,
@ -199,11 +256,11 @@ pub async fn get_all() -> crate::Result<Vec<SharedProfile>> {
} }
let response = REQWEST_CLIENT let response = REQWEST_CLIENT
.get( .get(format!("{MODRINTH_API_URL_INTERNAL}client/user"))
format!("{MODRINTH_API_URL_INTERNAL}client/user"), .header("Authorization", &creds.session)
) .send()
.header("Authorization", &creds.session) .await?
.send().await?.error_for_status()?; .error_for_status()?;
let profiles = response.json::<Vec<SharedProfileResponse>>().await?; let profiles = response.json::<Vec<SharedProfileResponse>>().await?;
@ -223,18 +280,28 @@ pub async fn get_all() -> crate::Result<Vec<SharedProfile>> {
let id = profile.id; let id = profile.id;
let response = REQWEST_CLIENT let response = REQWEST_CLIENT
.get( .get(format!(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{id}/files"), "{MODRINTH_API_URL_INTERNAL}client/profile/{id}/files"
) ))
.header("Authorization", &creds.session) .header("Authorization", &creds.session)
.send().await?.error_for_status()?; .send()
.await?
.error_for_status()?;
let files = response.json::<SharedFiles>().await?; let files = response.json::<SharedFiles>().await?;
shared_profiles.push(SharedProfile { shared_profiles.push(SharedProfile {
id, id,
name: profile.name, name: profile.name,
is_owned: profile.owner_id == state.credentials.read().await.0.as_ref().map(|x| x.user.id.as_str()).unwrap_or_default(), is_owned: profile.owner_id
== state
.credentials
.read()
.await
.0
.as_ref()
.map(|x| x.user.id.as_str())
.unwrap_or_default(),
owner_id: profile.owner_id, owner_id: profile.owner_id,
loader: profile.loader, loader: profile.loader,
loader_version: profile.loader_version, loader_version: profile.loader_version,
@ -253,7 +320,9 @@ pub async fn get_all() -> crate::Result<Vec<SharedProfile>> {
} }
#[tracing::instrument] #[tracing::instrument]
pub async fn install(shared_profile : SharedProfile) -> crate::Result<ProfilePathId> { pub async fn install(
shared_profile: SharedProfile,
) -> crate::Result<ProfilePathId> {
let state = crate::State::get().await?; let state = crate::State::get().await?;
let linked_data = LinkedData::SharedProfile { let linked_data = LinkedData::SharedProfile {
@ -272,10 +341,14 @@ pub async fn install(shared_profile : SharedProfile) -> crate::Result<ProfilePat
Some(linked_data), Some(linked_data),
None, None,
None, None,
).await?; )
.await?;
// Get the profile // Get the profile
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?; let profile: Profile =
profile::get(&profile_id, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
})?;
let creds = state.credentials.read().await; let creds = state.credentials.read().await;
// TODO: concurrent requests // TODO: concurrent requests
@ -297,14 +370,14 @@ pub async fn install(shared_profile : SharedProfile) -> crate::Result<ProfilePat
) )
.await?; .await?;
profile.add_project_bytes_directly(&file_override.install_path, file).await?; profile
.add_project_bytes_directly(&file_override.install_path, file)
.await?;
} }
Ok(profile_id) Ok(profile_id)
} }
// Structure repesenting a synchronization difference between a local profile and a shared profile // Structure repesenting a synchronization difference between a local profile and a shared profile
#[derive(Default, Serialize, Deserialize, Clone, Debug)] #[derive(Default, Serialize, Deserialize, Clone, Debug)]
pub struct SharedModpackFileUpdate { pub struct SharedModpackFileUpdate {
@ -321,81 +394,115 @@ pub struct SharedModpackFileUpdate {
} }
#[tracing::instrument] #[tracing::instrument]
pub async fn check_updated(profile_id: &ProfilePathId, shared_profile : &SharedProfile) -> crate::Result<SharedModpackFileUpdate> { pub async fn check_updated(
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?; profile_id: &ProfilePathId,
shared_profile: &SharedProfile,
) -> crate::Result<SharedModpackFileUpdate> {
let profile: Profile =
profile::get(profile_id, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
})?;
// Check if the metadata is the same- if different, we return false with no file updates // Check if the metadata is the same- if different, we return false with no file updates
if profile.metadata.name != shared_profile.name || if profile.metadata.name != shared_profile.name
profile.metadata.loader != shared_profile.loader || || profile.metadata.loader != shared_profile.loader
profile.metadata.loader_version.map(|x| x.id).unwrap_or_default() != shared_profile.loader_version || || profile
profile.metadata.game_version != shared_profile.game_version { .metadata
.loader_version
.map(|x| x.id)
.unwrap_or_default()
!= shared_profile.loader_version
|| profile.metadata.game_version != shared_profile.game_version
{
return Ok(SharedModpackFileUpdate::default()); return Ok(SharedModpackFileUpdate::default());
} }
// Check if the projects are the same- we check each override by hash and each modrinth project by version id // Check if the projects are the same- we check each override by hash and each modrinth project by version id
let mut modrinth_projects = shared_profile.versions.clone(); let mut modrinth_projects = shared_profile.versions.clone();
let mut overrides = shared_profile.overrides.clone(); let mut overrides = shared_profile.overrides.clone();
let unsynced_projects : Vec<_> = profile.projects.into_iter().filter_map(|(id, project)|{ let unsynced_projects: Vec<_> = profile
match project.metadata { .projects
ProjectMetadata::Modrinth { ref version, .. } => { .into_iter()
if modrinth_projects.contains(&version.id) { .filter_map(|(id, project)| {
modrinth_projects.retain(|x| x != &version.id); match project.metadata {
} ProjectMetadata::Modrinth { ref version, .. } => {
else { if modrinth_projects.contains(&version.id) {
return Some(id); modrinth_projects.retain(|x| x != &version.id);
} } else {
},
ProjectMetadata::Inferred { .. } => {
let Some(matching_override) = overrides.iter().position(|o| o.install_path.to_string_lossy().to_string() == id.get_inner_path_unix().0)
else {
return Some(id);
};
if let Some(o) = overrides.get(matching_override) {
if o.hashes.sha512 != project.sha512 {
return Some(id); return Some(id);
} }
} else { }
ProjectMetadata::Inferred { .. } => {
let Some(matching_override) =
overrides.iter().position(|o| {
o.install_path.to_string_lossy()
== id.get_inner_path_unix().0
})
else {
return Some(id);
};
if let Some(o) = overrides.get(matching_override) {
if o.hashes.sha512 != project.sha512 {
return Some(id);
}
} else {
return Some(id);
}
overrides.remove(matching_override);
}
ProjectMetadata::Unknown => {
// TODO: What to do for unknown projects?
return Some(id); return Some(id);
} }
overrides.remove(matching_override);
} }
ProjectMetadata::Unknown => { None
// TODO: What to do for unknown projects? })
return Some(id) .collect();
}
}
None
}).collect();
Ok(SharedModpackFileUpdate { Ok(SharedModpackFileUpdate {
is_synced: modrinth_projects.is_empty() && overrides.is_empty() && unsynced_projects.is_empty(), is_synced: modrinth_projects.is_empty()
&& overrides.is_empty()
&& unsynced_projects.is_empty(),
unsynced_projects, unsynced_projects,
missing_versions: modrinth_projects, missing_versions: modrinth_projects,
missing_overrides: overrides, missing_overrides: overrides,
}) })
} }
// Updates projects for a given ProfilePathId from a SharedProfile // Updates projects for a given ProfilePathId from a SharedProfile
// This updates the local profile to match the shared profile on the Labrinth API // This updates the local profile to match the shared profile on the Labrinth API
#[tracing::instrument] #[tracing::instrument]
pub async fn inbound_sync( pub async fn inbound_sync(profile_id: ProfilePathId) -> crate::Result<()> {
profile_id: ProfilePathId,
) -> crate::Result<()> {
let state = crate::State::get().await?; let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?; let profile: Profile =
profile::get(&profile_id, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
})?;
let creds = state.credentials.read().await; let creds = state.credentials.read().await;
// Get linked // Get linked
let shared_profile = match profile.metadata.linked_data { let shared_profile = match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { ref profile_id, .. }) => profile_id, Some(LinkedData::SharedProfile { ref profile_id, .. }) => profile_id,
_ => return Err(crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()).as_error()), _ => {
return Err(crate::ErrorKind::OtherError(
"Profile is not linked to a shared profile".to_string(),
)
.as_error())
}
}; };
// Get updated shared profile // Get updated shared profile
let shared_profile = get_all().await?.into_iter().find(|x| &x.id == shared_profile).ok_or_else(|| crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()))?; let shared_profile = get_all()
.await?
.into_iter()
.find(|x| &x.id == shared_profile)
.ok_or_else(|| {
crate::ErrorKind::OtherError(
"Profile is not linked to a shared profile".to_string(),
)
})?;
let update_data = check_updated(&profile_id, &shared_profile).await?; let update_data = check_updated(&profile_id, &shared_profile).await?;
if update_data.is_synced { if update_data.is_synced {
@ -426,7 +533,9 @@ pub async fn inbound_sync(
) )
.await?; .await?;
profile.add_project_bytes_directly(&file_override.install_path, file).await?; profile
.add_project_bytes_directly(&file_override.install_path, file)
.await?;
} }
Ok(()) Ok(())
@ -435,27 +544,47 @@ pub async fn inbound_sync(
// Updates metadata for a given ProfilePathId to the Labrinth API // Updates metadata for a given ProfilePathId to the Labrinth API
// Must be an owner of the shared profile // Must be an owner of the shared profile
#[tracing::instrument] #[tracing::instrument]
pub async fn outbound_sync( pub async fn outbound_sync(profile_id: ProfilePathId) -> crate::Result<()> {
profile_id: ProfilePathId,
) -> crate::Result<()> {
let state = crate::State::get().await?; let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?; let profile: Profile =
profile::get(&profile_id, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
})?;
let creds = state.credentials.read().await; let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?; let creds = creds
.0
.as_ref()
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
// Get linked // Get linked
let shared_profile = match profile.metadata.linked_data { let shared_profile = match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id, Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
_ => return Err(crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()).as_error()), _ => {
return Err(crate::ErrorKind::OtherError(
"Profile is not linked to a shared profile".to_string(),
)
.as_error())
}
}; };
// Get updated shared profile // Get updated shared profile
let shared_profile = get_all().await?.into_iter().find(|x| x.id == shared_profile).ok_or_else(|| crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()))?; let shared_profile = get_all()
.await?
.into_iter()
.find(|x| x.id == shared_profile)
.ok_or_else(|| {
crate::ErrorKind::OtherError(
"Profile is not linked to a shared profile".to_string(),
)
})?;
// Check owner // Check owner
if !shared_profile.is_owned { if !shared_profile.is_owned {
return Err(crate::ErrorKind::OtherError("Profile is not owned by the current user".to_string()).as_error()); return Err(crate::ErrorKind::OtherError(
"Profile is not owned by the current user".to_string(),
)
.as_error());
} }
// Check if we are synced // Check if we are synced
@ -466,20 +595,35 @@ pub async fn outbound_sync(
} }
let unsynced = update_data.unsynced_projects; let unsynced = update_data.unsynced_projects;
let projects : Vec<_> = profile.projects.clone().into_iter().filter(|(id, _)| unsynced.contains(id)).collect(); let projects: Vec<_> = profile
let unsynced_modrinth_projects : Vec<_> = projects.iter() .projects
.filter_map(|(_, project)|if let ProjectMetadata::Modrinth { ref version, .. } = project.metadata { .clone()
Some(&version.id) .into_iter()
} else { .filter(|(id, _)| unsynced.contains(id))
None .collect();
}).collect(); let unsynced_modrinth_projects: Vec<_> = projects
.iter()
.filter_map(|(_, project)| {
if let ProjectMetadata::Modrinth { ref version, .. } =
project.metadata
{
Some(&version.id)
} else {
None
}
})
.collect();
let unsynced_override_files : Vec<_> = projects.iter() let unsynced_override_files: Vec<_> = projects
.filter_map(|(id, project)|if let ProjectMetadata::Inferred { ..} = project.metadata { .iter()
Some(id) .filter_map(|(id, project)| {
} else { if let ProjectMetadata::Inferred { .. } = project.metadata {
None Some(id)
}).collect(); } else {
None
}
})
.collect();
// Generate new version set // Generate new version set
let mut new_version_set = shared_profile.versions; let mut new_version_set = shared_profile.versions;
@ -511,35 +655,49 @@ pub async fn outbound_sync(
let mut data = vec![]; // 'data' field, giving installation context to labrinth let mut data = vec![]; // 'data' field, giving installation context to labrinth
for override_file in unsynced_override_files { for override_file in unsynced_override_files {
let path = override_file.get_inner_path_unix(); let path = override_file.get_inner_path_unix();
let Some(name) = path.0.split('/').last().map(|x| x.to_string()) else { continue }; let Some(name) = path.0.split('/').last().map(|x| x.to_string()) else {
continue;
};
// Load override to file // Load override to file
let full_path = &override_file.get_full_path(&profile_id).await?; let full_path = &override_file.get_full_path(&profile_id).await?;
let file_bytes = io::read(full_path).await?; let file_bytes = io::read(full_path).await?;
let ext = full_path.extension().and_then(|x| x.to_str()).unwrap_or_default(); let ext = full_path
let mime = project_file_type(ext).ok_or_else(|| crate::ErrorKind::OtherError(format!("Could not determine file type for {}", ext)))?; .extension()
.and_then(|x| x.to_str())
.unwrap_or_default();
let mime = project_file_type(ext).ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Could not determine file type for {}",
ext
))
})?;
data.push(serde_json::json!({ data.push(serde_json::json!({
"file_name": name.clone(), "file_name": name.clone(),
"install_path": path "install_path": path
})); }));
let part = reqwest::multipart::Part::bytes(file_bytes).file_name(name.clone()).mime_str(mime)?; let part = reqwest::multipart::Part::bytes(file_bytes)
.file_name(name.clone())
.mime_str(mime)?;
parts.push((name.clone(), part)); parts.push((name.clone(), part));
} }
// Build multipart with 'data' field first // Build multipart with 'data' field first
let mut multipart = reqwest::multipart::Form::new().percent_encode_noop(); let mut multipart = reqwest::multipart::Form::new().percent_encode_noop();
let json_part = reqwest::multipart::Part::text(serde_json::to_string(&data)?);//mime_str("application/json")?; let json_part =
reqwest::multipart::Part::text(serde_json::to_string(&data)?); //mime_str("application/json")?;
multipart = multipart.part("data", json_part); multipart = multipart.part("data", json_part);
for (name, part) in parts { for (name, part) in parts {
multipart = multipart.part(name, part); multipart = multipart.part(name, part);
} }
let response = REQWEST_CLIENT.post( let response = REQWEST_CLIENT
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{id}/override"), .post(format!(
) "{MODRINTH_API_URL_INTERNAL}client/profile/{id}/override"
.header("Authorization", &creds.session) ))
.multipart(multipart); .header("Authorization", &creds.session)
.multipart(multipart);
response.send().await?.error_for_status()?; response.send().await?.error_for_status()?;
@ -555,24 +713,37 @@ pub async fn remove_shared_profile_users(
) -> crate::Result<()> { ) -> crate::Result<()> {
let state = crate::State::get().await?; let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?; let profile: Profile =
profile::get(&profile_id, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
})?;
let creds = state.credentials.read().await; let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?; let creds = creds
.0
.as_ref()
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
let shared_profile = match profile.metadata.linked_data { let shared_profile = match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id, Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
_ => return Err(crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()).as_error()), _ => {
return Err(crate::ErrorKind::OtherError(
"Profile is not linked to a shared profile".to_string(),
)
.as_error())
}
}; };
REQWEST_CLIENT REQWEST_CLIENT
.patch( .patch(format!(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}"), "{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}"
) ))
.header("Authorization", &creds.session) .header("Authorization", &creds.session)
.json(&serde_json::json!({ .json(&serde_json::json!({
"remove_users": users, "remove_users": users,
})) }))
.send().await?.error_for_status()?; .send()
.await?
.error_for_status()?;
Ok(()) Ok(())
} }
@ -583,24 +754,37 @@ pub async fn remove_shared_profile_links(
) -> crate::Result<()> { ) -> crate::Result<()> {
let state = crate::State::get().await?; let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?; let profile: Profile =
profile::get(&profile_id, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
})?;
let creds = state.credentials.read().await; let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?; let creds = creds
.0
.as_ref()
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
let shared_profile = match profile.metadata.linked_data { let shared_profile = match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id, Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
_ => return Err(crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()).as_error()), _ => {
return Err(crate::ErrorKind::OtherError(
"Profile is not linked to a shared profile".to_string(),
)
.as_error())
}
}; };
REQWEST_CLIENT REQWEST_CLIENT
.patch( .patch(format!(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}"), "{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}"
) ))
.header("Authorization", &creds.session) .header("Authorization", &creds.session)
.json(&serde_json::json!({ .json(&serde_json::json!({
"remove_links": links, "remove_links": links,
})) }))
.send().await?.error_for_status()?; .send()
.await?
.error_for_status()?;
Ok(()) Ok(())
} }
@ -610,47 +794,61 @@ pub async fn generate_share_link(
) -> crate::Result<String> { ) -> crate::Result<String> {
let state = crate::State::get().await?; let state = crate::State::get().await?;
let profile : Profile = profile::get(&profile_id, None).await?.ok_or_else(|| crate::ErrorKind::UnmanagedProfileError(profile_id.to_string()))?; let profile: Profile =
profile::get(&profile_id, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_id.to_string())
})?;
let creds = state.credentials.read().await; let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?; let creds = creds
.0
.as_ref()
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
let shared_profile = match profile.metadata.linked_data { let shared_profile = match profile.metadata.linked_data {
Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id, Some(LinkedData::SharedProfile { profile_id, .. }) => profile_id,
_ => return Err(crate::ErrorKind::OtherError("Profile is not linked to a shared profile".to_string()).as_error()), _ => {
return Err(crate::ErrorKind::OtherError(
"Profile is not linked to a shared profile".to_string(),
)
.as_error())
}
}; };
let response = REQWEST_CLIENT let response = REQWEST_CLIENT
.post( .post(format!(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}/share"), "{MODRINTH_API_URL_INTERNAL}client/profile/{shared_profile}/share"
) ))
.header("Authorization", &creds.session) .header("Authorization", &creds.session)
.send().await?.error_for_status()?; .send()
.await?
.error_for_status()?;
let link = response.json::<SharedProfileLink>().await?; let link = response.json::<SharedProfileLink>().await?;
Ok(generate_deep_link(&link)) Ok(generate_deep_link(&link))
} }
fn generate_deep_link( fn generate_deep_link(link: &SharedProfileLink) -> String {
link: &SharedProfileLink
) -> String {
format!("modrinth://shared_profile/{}", link.id) format!("modrinth://shared_profile/{}", link.id)
} }
pub async fn accept_share_link( pub async fn accept_share_link(link: String) -> crate::Result<()> {
link: String,
) -> crate::Result<()> {
let state = crate::State::get().await?; let state = crate::State::get().await?;
let creds = state.credentials.read().await; let creds = state.credentials.read().await;
let creds = creds.0.as_ref().ok_or_else(|| crate::ErrorKind::NoCredentialsError)?; let creds = creds
.0
.as_ref()
.ok_or_else(|| crate::ErrorKind::NoCredentialsError)?;
REQWEST_CLIENT REQWEST_CLIENT
.post( .post(format!(
format!("{MODRINTH_API_URL_INTERNAL}client/profile/share/{link}/accept"), "{MODRINTH_API_URL_INTERNAL}client/profile/share/{link}/accept"
) ))
.header("Authorization", &creds.session) .header("Authorization", &creds.session)
.send().await?.error_for_status()?; .send()
.await?
.error_for_status()?;
Ok(()) Ok(())
} }

View File

@ -1,4 +1,5 @@
//! Configuration structs //! Configuration structs
pub const MODRINTH_API_URL: &str = "https://staging-api.modrinth.com/v2/"; pub const MODRINTH_API_URL: &str = "https://staging-api.modrinth.com/v2/";
pub const MODRINTH_API_URL_INTERNAL: &str = "https://staging-api.modrinth.com/_internal/"; pub const MODRINTH_API_URL_INTERNAL: &str =
"https://staging-api.modrinth.com/_internal/";

View File

@ -606,21 +606,18 @@ impl Profile {
relative_path: &Path, relative_path: &Path,
bytes: bytes::Bytes, bytes: bytes::Bytes,
) -> crate::Result<ProjectPathId> { ) -> crate::Result<ProjectPathId> {
let state = State::get().await?; let state = State::get().await?;
let file_path = self let file_path = self.get_profile_full_path().await?.join(relative_path);
.get_profile_full_path() let project_path_id = ProjectPathId::new(relative_path);
.await?
.join(relative_path);
let project_path_id = ProjectPathId::new(&relative_path);
write(&file_path, &bytes, &state.io_semaphore).await?; write(&file_path, &bytes, &state.io_semaphore).await?;
let file_name = relative_path let file_name = relative_path
.file_name() .file_name()
.ok_or_else(|| { .ok_or_else(|| {
crate::ErrorKind::InputError( crate::ErrorKind::InputError(format!(
format!("Could not find file name for {:?}", relative_path), "Could not find file name for {:?}",
) relative_path
))
})? })?
.to_string_lossy(); .to_string_lossy();
@ -945,13 +942,15 @@ impl Profiles {
{ {
let profiles = state.profiles.read().await; let profiles = state.profiles.read().await;
for (profile_path, profile) in profiles.0.iter() { for (profile_path, profile) in profiles.0.iter() {
if let Some(LinkedData::ModrinthModpack { project_id, .. }) = &profile.metadata.linked_data { if let Some(LinkedData::ModrinthModpack {
if let Some(linked_project) = &project_id { project_id: Some(ref linked_project),
modrinth_updatables.push(( ..
profile_path.clone(), }) = &profile.metadata.linked_data
linked_project.clone(), {
)); modrinth_updatables.push((
} profile_path.clone(),
linked_project.clone(),
));
} }
} }
} }
@ -990,7 +989,9 @@ impl Profiles {
}); });
if let Some(recent_version) = recent_version { if let Some(recent_version) = recent_version {
profile.sync_update_version = profile.sync_update_version =
Some(ProfileUpdateData::ModrinthModpack(recent_version.id.clone())); Some(ProfileUpdateData::ModrinthModpack(
recent_version.id.clone(),
));
} else { } else {
profile.sync_update_version = None; profile.sync_update_version = None;
} }
@ -1032,11 +1033,21 @@ impl Profiles {
{ {
let profiles = state.profiles.read().await; let profiles = state.profiles.read().await;
for (profile_path, profile) in profiles.0.iter() { for (profile_path, profile) in profiles.0.iter() {
if let Some(LinkedData::SharedProfile { profile_id, .. }) = &profile.metadata.linked_data { if let Some(LinkedData::SharedProfile {
if let Some(shared_profile) = shared_profiles.iter().find(|x| x.id == *profile_id) { profile_id, ..
}) = &profile.metadata.linked_data
{
if let Some(shared_profile) =
shared_profiles.iter().find(|x| x.id == *profile_id)
{
// Check for update // Check for update
let update = shared_profile::check_updated(profile_path, shared_profile).await?; let update = shared_profile::check_updated(
update_profiles.insert(profile_path.clone(), update); profile_path,
shared_profile,
)
.await?;
update_profiles
.insert(profile_path.clone(), update);
} }
} }
} }
@ -1044,8 +1055,11 @@ impl Profiles {
{ {
let mut new_profiles = state.profiles.write().await; let mut new_profiles = state.profiles.write().await;
for (profile_path, update) in update_profiles.iter() { for (profile_path, update) in update_profiles.iter() {
if let Some(profile) = new_profiles.0.get_mut(&profile_path) { if let Some(profile) = new_profiles.0.get_mut(profile_path)
profile.sync_update_version = Some(ProfileUpdateData::SharedProfile(update.clone())); {
profile.sync_update_version = Some(
ProfileUpdateData::SharedProfile(update.clone()),
);
} }
} }
} }
@ -1054,10 +1068,12 @@ impl Profiles {
profiles.sync().await?; profiles.sync().await?;
for (profile_path, _) in update_profiles.iter() { for (profile_path, _) in update_profiles.iter() {
let Some(profile) = profiles.0.get(&profile_path) else { continue; }; let Some(profile) = profiles.0.get(profile_path) else {
continue;
};
emit_profile( emit_profile(
profile.uuid, profile.uuid,
&profile_path, profile_path,
&profile.metadata.name, &profile.metadata.name,
ProfilePayloadType::Edited, ProfilePayloadType::Edited,
) )
@ -1065,7 +1081,8 @@ impl Profiles {
} }
} }
Ok::<(), crate::Error>(()) Ok::<(), crate::Error>(())
}.await; }
.await;
match res { match res {
Ok(()) => {} Ok(()) => {}
Err(err) => { Err(err) => {

View File

@ -1,10 +1,10 @@
// IO error // IO error
// A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error. // A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error.
use std::{path::Path, io::Write}; use std::{io::Write, path::Path};
use tempfile::NamedTempFile;
use tauri::async_runtime::spawn_blocking; use tauri::async_runtime::spawn_blocking;
use tempfile::NamedTempFile;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum IOError { pub enum IOError {
@ -137,12 +137,13 @@ fn sync_write(
data: impl AsRef<[u8]>, data: impl AsRef<[u8]>,
path: impl AsRef<Path>, path: impl AsRef<Path>,
) -> Result<(), std::io::Error> { ) -> Result<(), std::io::Error> {
let mut tempfile = NamedTempFile::new_in(path.as_ref().parent().ok_or_else(|| { let mut tempfile =
std::io::Error::new( NamedTempFile::new_in(path.as_ref().parent().ok_or_else(|| {
std::io::ErrorKind::Other, std::io::Error::new(
"could not get parent directory for temporary file", std::io::ErrorKind::Other,
) "could not get parent directory for temporary file",
})?)?; )
})?)?;
tempfile.write_all(data.as_ref())?; tempfile.write_all(data.as_ref())?;
let tmp_path = tempfile.into_temp_path(); let tmp_path = tempfile.into_temp_path();
let path = path.as_ref(); let path = path.as_ref();

View File

@ -12,10 +12,10 @@ pub mod pack;
pub mod process; pub mod process;
pub mod profile; pub mod profile;
pub mod profile_create; pub mod profile_create;
pub mod profile_share;
pub mod settings; pub mod settings;
pub mod tags; pub mod tags;
pub mod utils; pub mod utils;
pub mod profile_share;
pub type Result<T> = std::result::Result<T, TheseusSerializableError>; pub type Result<T> = std::result::Result<T, TheseusSerializableError>;

View File

@ -19,10 +19,8 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
// invoke('plugin:profile_share|profile_share_get_all',profile) // invoke('plugin:profile_share|profile_share_get_all',profile)
#[tauri::command] #[tauri::command]
pub async fn profile_share_get_all( pub async fn profile_share_get_all() -> Result<Vec<SharedProfile>> {
) -> Result<Vec<SharedProfile>> { let res = shared_profile::get_all().await?;
let res = shared_profile::get_all()
.await?;
Ok(res) Ok(res)
} }
@ -30,57 +28,46 @@ pub async fn profile_share_get_all(
pub async fn profile_share_install( pub async fn profile_share_install(
profile: SharedProfile, profile: SharedProfile,
) -> Result<ProfilePathId> { ) -> Result<ProfilePathId> {
let res = shared_profile::install(profile) let res = shared_profile::install(profile).await?;
.await?;
Ok(res) Ok(res)
} }
#[tauri::command] #[tauri::command]
pub async fn profile_share_create( pub async fn profile_share_create(path: ProfilePathId) -> Result<()> {
path: ProfilePathId shared_profile::create(path).await?;
) -> Result<()> {
shared_profile::create(path)
.await?;
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn profile_share_inbound_sync( pub async fn profile_share_inbound_sync(path: ProfilePathId) -> Result<()> {
path: ProfilePathId shared_profile::inbound_sync(path).await?;
) -> Result<()> {
shared_profile::inbound_sync(path)
.await?;
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn profile_share_outbound_sync( pub async fn profile_share_outbound_sync(path: ProfilePathId) -> Result<()> {
path : ProfilePathId
) -> Result<()> {
shared_profile::outbound_sync(path).await?; shared_profile::outbound_sync(path).await?;
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn profile_share_generate_share_link( pub async fn profile_share_generate_share_link(
path : ProfilePathId path: ProfilePathId,
) -> Result<String> { ) -> Result<String> {
let res = shared_profile::generate_share_link(path).await?; let res = shared_profile::generate_share_link(path).await?;
Ok(res) Ok(res)
} }
#[tauri::command] #[tauri::command]
pub async fn profile_share_accept_share_link( pub async fn profile_share_accept_share_link(link: String) -> Result<()> {
link : String
) -> Result<()> {
shared_profile::accept_share_link(link).await?; shared_profile::accept_share_link(link).await?;
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn profile_share_remove_users( pub async fn profile_share_remove_users(
path : ProfilePathId, path: ProfilePathId,
users: Vec<String> users: Vec<String>,
) -> Result<()> { ) -> Result<()> {
shared_profile::remove_shared_profile_users(path, users).await?; shared_profile::remove_shared_profile_users(path, users).await?;
Ok(()) Ok(())
@ -88,8 +75,8 @@ pub async fn profile_share_remove_users(
#[tauri::command] #[tauri::command]
pub async fn profile_share_remove_links( pub async fn profile_share_remove_links(
path : ProfilePathId, path: ProfilePathId,
links : Vec<String> links: Vec<String>,
) -> Result<()> { ) -> Result<()> {
shared_profile::remove_shared_profile_links(path, links).await?; shared_profile::remove_shared_profile_links(path, links).await?;
Ok(()) Ok(())

View File

@ -14,9 +14,9 @@ defineExpose({
async show(event) { async show(event) {
linkId.value = event.id linkId.value = event.id
sharedProfile.value = await useFetch( sharedProfile.value = await useFetch(
`https://staging-api.modrinth.com/_internal/share/${encodeURIComponent(event.id)}`, `https://staging-api.modrinth.com/_internal/share/${encodeURIComponent(event.id)}`,
'shared profile' 'shared profile'
) )
confirmModal.value.show() confirmModal.value.show()
}, },

View File

@ -82,7 +82,9 @@ const install = async (e) => {
packs.length === 0 || packs.length === 0 ||
!packs !packs
.map((value) => value.metadata) .map((value) => value.metadata)
.find((pack) => pack.linked_data?.modrinth_modpack?.project_id === props.instance.project_id) .find(
(pack) => pack.linked_data?.modrinth_modpack?.project_id === props.instance.project_id
)
) { ) {
modLoading.value = true modLoading.value = true
await pack_install( await pack_install(

View File

@ -247,9 +247,9 @@ const check_valid = computed(() => {
</Button> </Button>
<div <div
v-tooltip=" v-tooltip="
(profile.metadata.linked_data?.modrinth_modpack.locked (profile.metadata.linked_data?.modrinth_modpack.locked ||
|| profile.metadata.linked_data?.shared_profile profile.metadata.linked_data?.shared_profile) &&
) && !profile.installedMod !profile.installedMod
? 'Unpair or unlock an instance to add mods.' ? 'Unpair or unlock an instance to add mods.'
: '' : ''
" "
@ -267,7 +267,8 @@ const check_valid = computed(() => {
? 'Installing...' ? 'Installing...'
: profile.installedMod : profile.installedMod
? 'Installed' ? 'Installed'
: profile.metadata.linked_data?.modrinth_modpack.locked || profile.metadata.linked_data?.shared_profile : profile.metadata.linked_data?.modrinth_modpack.locked ||
profile.metadata.linked_data?.shared_profile
? 'Paired' ? 'Paired'
: 'Install' : 'Install'
}} }}

View File

@ -28,7 +28,9 @@ const filteredVersions = computed(() => {
}) })
const modpackVersionModal = ref(null) const modpackVersionModal = ref(null)
const installedVersion = computed(() => props.instance?.metadata?.linked_data?.modrinth_modpack?.version_id) const installedVersion = computed(
() => props.instance?.metadata?.linked_data?.modrinth_modpack?.version_id
)
const installing = computed(() => props.instance.install_stage !== 'installed') const installing = computed(() => props.instance.install_stage !== 'installed')
const inProgress = ref(false) const inProgress = ref(false)

View File

@ -24,7 +24,9 @@ defineExpose({
'version' 'version'
) )
project.value = await useFetch( project.value = await useFetch(
`https://staging-api.modrinth.com/v2/project/${encodeURIComponent(version.value.project_id)}`, `https://staging-api.modrinth.com/v2/project/${encodeURIComponent(
version.value.project_id
)}`,
'project' 'project'
) )
} else { } else {
@ -33,7 +35,9 @@ defineExpose({
'project' 'project'
) )
version.value = await useFetch( version.value = await useFetch(
`https://staging-api.modrinth.com/v2/version/${encodeURIComponent(project.value.versions[0])}`, `https://staging-api.modrinth.com/v2/version/${encodeURIComponent(
project.value.versions[0]
)}`,
'version' 'version'
) )
} }

View File

@ -32,7 +32,9 @@ const getInstances = async () => {
let filters = [] let filters = []
for (const instance of recentInstances.value) { for (const instance of recentInstances.value) {
if (instance.metadata.linked_data?.modrinth_modpack?.project_id) { if (instance.metadata.linked_data?.modrinth_modpack?.project_id) {
filters.push(`NOT"project_id"="${instance.metadata.linked_data?.modrinth_modpack?.project_id}"`) filters.push(
`NOT"project_id"="${instance.metadata.linked_data?.modrinth_modpack?.project_id}"`
)
} }
} }
filter.value = filters.join(' AND ') filter.value = filters.join(' AND ')

View File

@ -413,7 +413,10 @@
<XIcon /> Unpair <XIcon /> Unpair
</Button> </Button>
</div> </div>
<div v-if="props.instance.metadata.linked_data?.modrinth_modpack?.project_id" class="adjacent-input"> <div
v-if="props.instance.metadata.linked_data?.modrinth_modpack?.project_id"
class="adjacent-input"
>
<label for="change-modpack-version"> <label for="change-modpack-version">
<span class="label__title">Change modpack version</span> <span class="label__title">Change modpack version</span>
<span class="label__description"> <span class="label__description">
@ -452,14 +455,13 @@
</div> </div>
<div v-if="installedSharedProfileData.is_owned" class="adjacent-input"> <div v-if="installedSharedProfileData.is_owned" class="adjacent-input">
<label for="share-links"> <label for="share-links">
<span class="label__title">Generate share link</span> <span class="label__title">Generate share link</span>
<span class="label__description"> <span class="label__description">
Creates a share link to share this modpack with others. This allows them to install your instance, as well as stay up to date with any changes you make. Creates a share link to share this modpack with others. This allows them to install your
</span> instance, as well as stay up to date with any changes you make.
</span>
</label> </label>
<Button id="share-links" @click="generateShareLink"> <Button id="share-links" @click="generateShareLink"> <GlobeIcon /> Share </Button>
<GlobeIcon /> Share
</Button>
</div> </div>
<div v-if="shareLink" class="adjacent-input"> <div v-if="shareLink" class="adjacent-input">
Generated link: <code>{{ shareLink }}</code> Generated link: <code>{{ shareLink }}</code>
@ -467,16 +469,10 @@
<div v-if="installedSharedProfileData.is_owned" class="table"> <div v-if="installedSharedProfileData.is_owned" class="table">
<div class="table-row table-head"> <div class="table-row table-head">
<div class="table-cell table-text name-cell actions-cell"> <div class="table-cell table-text name-cell actions-cell">
<Button class="transparent"> <Button class="transparent"> Name </Button>
Name
</Button>
</div> </div>
</div> </div>
<div <div v-for="user in installedSharedProfileData.users" :key="user" class="table-row">
v-for="user in installedSharedProfileData.users"
:key="user"
class="table-row"
>
<div class="table-cell table-text name-cell"> <div class="table-cell table-text name-cell">
<div class="user-content"> <div class="user-content">
<span v-tooltip="`${user}`" class="title">{{ user }}</span> <span v-tooltip="`${user}`" class="title">{{ user }}</span>
@ -484,7 +480,11 @@
</div> </div>
<div class="table-cell table-text manage"> <div class="table-cell table-text manage">
<div v-tooltip="'Remove user'"> <div v-tooltip="'Remove user'">
<Button icon-only @click="removeSharedPackUser(user)" :disabled="user === installedSharedProfileData.owner_id"> <Button
icon-only
@click="removeSharedPackUser(user)"
:disabled="user === installedSharedProfileData.owner_id"
>
<TrashIcon /> <TrashIcon />
</Button> </Button>
</div> </div>
@ -496,36 +496,43 @@
{{ props.instance.sync_update_version }} {{ props.instance.sync_update_version }}
:) :)
<label for="share-sync"> <label for="share-sync">
<span class="label__title">Sync shared profile</span> <span class="label__title">Sync shared profile</span>
<span class="label__description" v-if="props.instance.sync_update_version?.is_synced"> <span class="label__description" v-if="props.instance.sync_update_version?.is_synced">
You are up to date with the shared profile. You are up to date with the shared profile.
</span> </span>
<span class="label__description" v-else> <span class="label__description" v-else>
You have changes that have not been synced to the shared profile. Click the button to upload your changes. You have changes that have not been synced to the shared profile. Click the button to
</span> upload your changes.
</span>
</label> </label>
<Button id="share-sync-sync" @click="outboundSyncSharedProfile" :disabled="props.instance.sync_update_version?.is_synced"> <Button
<GlobeIcon /> Sync id="share-sync-sync"
</Button> @click="outboundSyncSharedProfile"
<Button id="share-sync-revert" @click="inboundSyncSharedProfile" :disabled="props.instance.sync_update_version?.is_synced"> :disabled="props.instance.sync_update_version?.is_synced"
<GlobeIcon /> Revert >
</Button> <GlobeIcon /> Sync
</Button>
<Button
id="share-sync-revert"
@click="inboundSyncSharedProfile"
:disabled="props.instance.sync_update_version?.is_synced"
>
<GlobeIcon /> Revert
</Button>
</div> </div>
<div v-else> <div v-else>
not yours not yours
{{ props.instance.sync_update_version }} {{ props.instance.sync_update_version }}
<label for="share-sync"> <label for="share-sync">
<span class="label__title">Sync shared profile</span> <span class="label__title">Sync shared profile</span>
<span class="label__description" v-if="props.instance.sync_update_version?.is_synced"> <span class="label__description" v-if="props.instance.sync_update_version?.is_synced">
You are up to date with the shared profile. You are up to date with the shared profile.
</span> </span>
<span class="label__description" v-else> <span class="label__description" v-else>
You are not up to date with the shared profile. Click the button to update your instance. You are not up to date with the shared profile. Click the button to update your instance.
</span> </span>
</label> </label>
<Button id="share-sync-sync" @click="inboundSyncSharedProfile"> <Button id="share-sync-sync" @click="inboundSyncSharedProfile"> <GlobeIcon /> Sync </Button>
<GlobeIcon /> Sync
</Button>
</div> </div>
</Card> </Card>
<Card> <Card>
@ -750,16 +757,20 @@ const unlinkModpack = ref(false)
const inProgress = ref(false) const inProgress = ref(false)
const installing = computed(() => props.instance.install_stage !== 'installed') const installing = computed(() => props.instance.install_stage !== 'installed')
const installedVersion = computed(() => props.instance?.metadata?.linked_data?.modrinth_modpack?.version_id) const installedVersion = computed(
() => props.instance?.metadata?.linked_data?.modrinth_modpack?.version_id
)
const installedVersionData = computed(() => { const installedVersionData = computed(() => {
if (!installedVersion.value) return null if (!installedVersion.value) return null
return props.versions.find((version) => version.id === installedVersion.value) return props.versions.find((version) => version.id === installedVersion.value)
}) })
const sharedProfiles = await get_all(); const sharedProfiles = await get_all()
const installedSharedProfileData = computed(() => { const installedSharedProfileData = computed(() => {
if (!props.instance.metadata.linked_data?.shared_profile) return null if (!props.instance.metadata.linked_data?.shared_profile) return null
return sharedProfiles.find((profile) => profile.id === props.instance.metadata.linked_data?.shared_profile?.profile_id) return sharedProfiles.find(
(profile) => profile.id === props.instance.metadata.linked_data?.shared_profile?.profile_id
)
}) })
watch( watch(
@ -1046,7 +1057,6 @@ async function generateShareLink() {
async function removeSharedPackUser(user) { async function removeSharedPackUser(user) {
await remove_users(props.instance.path, [user]).catch(handleError) await remove_users(props.instance.path, [user]).catch(handleError)
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -1146,13 +1156,10 @@ async function removeSharedPackUser(user) {
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
.title { .title {
color: var(--color-contrast); color: var(--color-contrast);
font-weight: bolder; font-weight: bolder;
margin-left: 1rem; margin-left: 1rem;
} }
} }
</style> </style>

View File

@ -317,7 +317,10 @@ async function fetchProjectData() {
useFetch(`https://staging-api.modrinth.com/v2/project/${route.params.id}`, 'project'), useFetch(`https://staging-api.modrinth.com/v2/project/${route.params.id}`, 'project'),
useFetch(`https://staging-api.modrinth.com/v2/project/${route.params.id}/version`, 'project'), useFetch(`https://staging-api.modrinth.com/v2/project/${route.params.id}/version`, 'project'),
useFetch(`https://staging-api.modrinth.com/v2/project/${route.params.id}/members`, 'project'), useFetch(`https://staging-api.modrinth.com/v2/project/${route.params.id}/members`, 'project'),
useFetch(`https://staging-api.modrinth.com/v2/project/${route.params.id}/dependencies`, 'project'), useFetch(
`https://staging-api.modrinth.com/v2/project/${route.params.id}/dependencies`,
'project'
),
get_categories().catch(handleError), get_categories().catch(handleError),
route.query.i ? getInstance(route.query.i, false).catch(handleError) : Promise.resolve(), route.query.i ? getInstance(route.query.i, false).catch(handleError) : Promise.resolve(),
]) ])