Migrate to SQLite for Internal Launcher Data (#1300)

* initial migration

* barebones profiles

* Finish profiles

* Add back file watcher

* UI support progress

* Finish most of cache

* Fix options page

* Fix forge, finish modrinth auth

* Accounts, process cache

* Run SQLX prepare

* Finish

* Run lint + actions

* Fix version to be compat with windows

* fix lint

* actually fix lint

* actually fix lint again
This commit is contained in:
Geometrically
2024-07-24 11:03:19 -07:00
committed by GitHub
parent 90f74427d9
commit 49a20a303a
156 changed files with 9208 additions and 8547 deletions

View File

@@ -8,7 +8,7 @@ use crate::{
import::{self, copy_dotminecraft},
install_from::CreatePackDescription,
},
prelude::{ModLoader, Profile, ProfilePathId},
prelude::ModLoader,
state::{LinkedData, ProfileInstallStage},
util::io,
State,
@@ -116,11 +116,11 @@ pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
}
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn import_atlauncher(
atlauncher_base_path: PathBuf, // path to base atlauncher folder
instance_folder: String, // instance folder in atlauncher_base_path
profile_path: ProfilePathId, // path to profile
profile_path: &str, // path to profile
) -> crate::Result<()> {
let atlauncher_instance_path = atlauncher_base_path
.join("instances")
@@ -159,7 +159,7 @@ pub async fn import_atlauncher(
project_id: None,
version_id: None,
existing_loading_bar: None,
profile_path: profile_path.clone(),
profile_path: profile_path.to_string(),
};
let backup_name = format!("ATLauncher-{}", instance_folder);
@@ -177,7 +177,7 @@ pub async fn import_atlauncher(
}
async fn import_atlauncher_unmanaged(
profile_path: ProfilePathId,
profile_path: &str,
minecraft_folder: PathBuf,
backup_name: String,
description: CreatePackDescription,
@@ -198,10 +198,10 @@ async fn import_atlauncher_unmanaged(
let game_version = atinstance.id;
let loader_version = if mod_loader != ModLoader::Vanilla {
crate::profile::create::get_loader_version_from_loader(
game_version.clone(),
crate::launcher::get_loader_version_from_profile(
&game_version,
mod_loader,
Some(atinstance.launcher.loader_version.version.clone()),
Some(&atinstance.launcher.loader_version.version),
)
.await?
} else {
@@ -209,24 +209,30 @@ async fn import_atlauncher_unmanaged(
};
// Set profile data to created default profile
crate::api::profile::edit(&profile_path, |prof| {
prof.metadata.name = description
crate::api::profile::edit(profile_path, |prof| {
prof.name = description
.override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
prof.install_stage = ProfileInstallStage::PackInstalling;
prof.metadata.linked_data = Some(LinkedData {
project_id: description.project_id.clone(),
version_id: description.version_id.clone(),
locked: Some(
description.project_id.is_some()
&& description.version_id.is_some(),
),
});
prof.metadata.icon.clone_from(&description.icon);
prof.metadata.game_version.clone_from(&game_version);
prof.metadata.loader_version.clone_from(&loader_version);
prof.metadata.loader = mod_loader;
if let Some(ref project_id) = description.project_id {
if let Some(ref version_id) = description.version_id {
prof.linked_data = Some(LinkedData {
project_id: project_id.clone(),
version_id: version_id.clone(),
locked: true,
})
}
}
prof.icon_path = description
.icon
.clone()
.map(|x| x.to_string_lossy().to_string());
prof.game_version.clone_from(&game_version);
prof.loader_version = loader_version.clone().map(|x| x.id);
prof.loader = mod_loader;
async { Ok(()) }
})
@@ -235,32 +241,20 @@ async fn import_atlauncher_unmanaged(
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
let state = State::get().await?;
let loading_bar = copy_dotminecraft(
profile_path.clone(),
profile_path,
minecraft_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await?
{
if let Some(profile_val) = crate::api::profile::get(profile_path).await? {
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
false,
)
.await?;
{
let state = State::get().await?;
let mut file_watcher = state.file_watcher.write().await;
Profile::watch_fs(
&profile_val.get_profile_full_path().await?,
&mut file_watcher,
)
.await?;
}
State::sync().await?;
}
Ok(())
}

View File

@@ -2,10 +2,8 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::prelude::Profile;
use crate::state::CredentialsStore;
use crate::{
prelude::{ModLoader, ProfilePathId},
prelude::ModLoader,
state::ProfileInstallStage,
util::{
fetch::{fetch, write_cached_icon},
@@ -49,7 +47,7 @@ pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
pub async fn import_curseforge(
curseforge_instance_folder: PathBuf, // instance's folder
profile_path: ProfilePathId, // path to profile
profile_path: &str, // path to profile
) -> crate::Result<()> {
// Load minecraftinstance.json
let minecraft_instance: String = io::read_to_string(
@@ -77,13 +75,9 @@ pub async fn import_curseforge(
thumbnail_url: Some(thumbnail_url),
}) = minecraft_instance.installed_modpack.clone()
{
let icon_bytes = fetch(
&thumbnail_url,
None,
&state.fetch_semaphore,
&CredentialsStore(None),
)
.await?;
let icon_bytes =
fetch(&thumbnail_url, None, &state.fetch_semaphore, &state.pool)
.await?;
let filename = thumbnail_url.rsplit('/').last();
if let Some(filename) = filename {
icon = Some(
@@ -121,10 +115,10 @@ pub async fn import_curseforge(
let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla);
let loader_version = if mod_loader != ModLoader::Vanilla {
crate::profile::create::get_loader_version_from_loader(
game_version.clone(),
crate::launcher::get_loader_version_from_profile(
&game_version,
mod_loader,
loader_version,
loader_version.as_deref(),
)
.await?
} else {
@@ -132,31 +126,32 @@ pub async fn import_curseforge(
};
// Set profile data to created default profile
crate::api::profile::edit(&profile_path, |prof| {
prof.metadata.name = override_title
crate::api::profile::edit(profile_path, |prof| {
prof.name = override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
prof.install_stage = ProfileInstallStage::PackInstalling;
prof.metadata.icon.clone_from(&icon);
prof.metadata.game_version.clone_from(&game_version);
prof.metadata.loader_version.clone_from(&loader_version);
prof.metadata.loader = mod_loader;
prof.icon_path =
icon.clone().map(|x| x.to_string_lossy().to_string());
prof.game_version.clone_from(&game_version);
prof.loader_version = loader_version.clone().map(|x| x.id);
prof.loader = mod_loader;
async { Ok(()) }
})
.await?;
} else {
// create a vanilla profile
crate::api::profile::edit(&profile_path, |prof| {
prof.metadata.name = override_title
crate::api::profile::edit(profile_path, |prof| {
prof.name = override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
prof.metadata.icon.clone_from(&icon);
prof.metadata
.game_version
prof.icon_path =
icon.clone().map(|x| x.to_string_lossy().to_string());
prof.game_version
.clone_from(&minecraft_instance.game_version);
prof.metadata.loader_version = None;
prof.metadata.loader = ModLoader::Vanilla;
prof.loader_version = None;
prof.loader = ModLoader::Vanilla;
async { Ok(()) }
})
@@ -166,33 +161,20 @@ pub async fn import_curseforge(
// Copy in contained folders as overrides
let state = State::get().await?;
let loading_bar = copy_dotminecraft(
profile_path.clone(),
profile_path,
curseforge_instance_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await?
{
if let Some(profile_val) = crate::api::profile::get(profile_path).await? {
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
false,
)
.await?;
{
let state = State::get().await?;
let mut file_watcher = state.file_watcher.write().await;
Profile::watch_fs(
&profile_val.get_profile_full_path().await?,
&mut file_watcher,
)
.await?;
}
State::sync().await?;
}
Ok(())

View File

@@ -2,12 +2,7 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::{
prelude::{ModLoader, Profile, ProfilePathId},
state::ProfileInstallStage,
util::io,
State,
};
use crate::{prelude::ModLoader, state::ProfileInstallStage, util::io, State};
use super::{copy_dotminecraft, recache_icon};
@@ -41,7 +36,7 @@ pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool {
pub async fn import_gdlauncher(
gdlauncher_instance_folder: PathBuf, // instance's folder
profile_path: ProfilePathId, // path to profile
profile_path: &str, // path to profile
) -> crate::Result<()> {
// Load config.json
let config: String =
@@ -74,10 +69,10 @@ pub async fn import_gdlauncher(
let loader_version = config.loader.loader_version;
let loader_version = if mod_loader != ModLoader::Vanilla {
crate::profile::create::get_loader_version_from_loader(
game_version.clone(),
crate::launcher::get_loader_version_from_profile(
&game_version,
mod_loader,
loader_version,
loader_version.as_deref(),
)
.await?
} else {
@@ -85,15 +80,15 @@ pub async fn import_gdlauncher(
};
// Set profile data to created default profile
crate::api::profile::edit(&profile_path, |prof| {
prof.metadata.name = override_title
crate::api::profile::edit(profile_path, |prof| {
prof.name = override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
prof.install_stage = ProfileInstallStage::PackInstalling;
prof.metadata.icon.clone_from(&icon);
prof.metadata.game_version.clone_from(&game_version);
prof.metadata.loader_version.clone_from(&loader_version);
prof.metadata.loader = mod_loader;
prof.icon_path = icon.clone().map(|x| x.to_string_lossy().to_string());
prof.game_version.clone_from(&game_version);
prof.loader_version = loader_version.clone().map(|x| x.id);
prof.loader = mod_loader;
async { Ok(()) }
})
@@ -102,32 +97,20 @@ pub async fn import_gdlauncher(
// Copy in contained folders as overrides
let state = State::get().await?;
let loading_bar = copy_dotminecraft(
profile_path.clone(),
profile_path,
gdlauncher_instance_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await?
{
if let Some(profile_val) = crate::api::profile::get(profile_path).await? {
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
false,
)
.await?;
{
let state = State::get().await?;
let mut file_watcher = state.file_watcher.write().await;
Profile::watch_fs(
&profile_val.get_profile_full_path().await?,
&mut file_watcher,
)
.await?;
}
State::sync().await?;
}
Ok(())

View File

@@ -7,7 +7,6 @@ use crate::{
import::{self, copy_dotminecraft},
install_from::{self, CreatePackDescription, PackDependency},
},
prelude::{Profile, ProfilePathId},
util::io,
State,
};
@@ -176,11 +175,11 @@ async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> {
}
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn import_mmc(
mmc_base_path: PathBuf, // path to base mmc folder
instance_folder: String, // instance folder in mmc_base_path
profile_path: ProfilePathId, // path to profile
mmc_base_path: PathBuf, // path to base mmc folder
instance_folder: String, // instance folder in mmc_base_path
profile_path: &str, // path to profile
) -> crate::Result<()> {
let mmc_instance_path = mmc_base_path
.join("instances")
@@ -202,13 +201,13 @@ pub async fn import_mmc(
};
// Create description from instance.cfg
let description = CreatePackDescription {
let mut description = CreatePackDescription {
icon,
override_title: instance_cfg.name,
project_id: instance_cfg.managed_pack_id,
version_id: instance_cfg.managed_pack_version_id,
project_id: None,
version_id: None,
existing_loading_bar: None,
profile_path: profile_path.clone(),
profile_path: profile_path.to_string(),
};
// Managed pack
@@ -217,6 +216,9 @@ pub async fn import_mmc(
if instance_cfg.managed_pack.unwrap_or(false) {
match instance_cfg.managed_pack_type {
Some(MMCManagedPackType::Modrinth) => {
description.project_id = instance_cfg.managed_pack_id;
description.version_id = instance_cfg.managed_pack_version_id;
// Modrinth Managed Pack
// Kept separate as we may in the future want to add special handling for modrinth managed packs
let backup_name = "Imported Modrinth Modpack".to_string();
@@ -260,7 +262,7 @@ pub async fn import_mmc(
}
async fn import_mmc_unmanaged(
profile_path: ProfilePathId,
profile_path: &str,
minecraft_folder: PathBuf,
backup_name: String,
description: CreatePackDescription,
@@ -302,7 +304,7 @@ async fn import_mmc_unmanaged(
// Sets profile information to be that loaded from mmc-pack.json and instance.cfg
install_from::set_profile_information(
profile_path.clone(),
profile_path.to_string(),
&description,
&backup_name,
&dependencies,
@@ -313,32 +315,20 @@ async fn import_mmc_unmanaged(
// Moves .minecraft folder over (ie: overrides such as resourcepacks, mods, etc)
let state = State::get().await?;
let loading_bar = copy_dotminecraft(
profile_path.clone(),
profile_path,
minecraft_folder,
&state.io_semaphore,
None,
)
.await?;
if let Some(profile_val) =
crate::api::profile::get(&profile_path, None).await?
{
if let Some(profile_val) = crate::api::profile::get(profile_path).await? {
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
false,
)
.await?;
{
let state = State::get().await?;
let mut file_watcher = state.file_watcher.write().await;
Profile::watch_fs(
&profile_val.get_profile_full_path().await?,
&mut file_watcher,
)
.await?;
}
State::sync().await?;
}
Ok(())
}

View File

@@ -11,8 +11,6 @@ use crate::{
emit::{emit_loading, init_or_edit_loading},
LoadingBarId,
},
prelude::ProfilePathId,
state::Profiles,
util::{
fetch::{self, IoSemaphore},
io,
@@ -105,10 +103,10 @@ pub async fn get_importable_instances(
// Import an instance from a launcher type and base path
// Note: this *deletes* the submitted empty profile
#[theseus_macros::debug_pin]
#[tracing::instrument]
pub async fn import_instance(
profile_path: ProfilePathId, // This should be a blank profile
profile_path: &str, // This should be a blank profile
launcher_type: ImportLauncherType,
base_path: PathBuf,
instance_folder: String,
@@ -117,31 +115,31 @@ pub async fn import_instance(
let res = match launcher_type {
ImportLauncherType::MultiMC | ImportLauncherType::PrismLauncher => {
mmc::import_mmc(
base_path, // path to base mmc folder
instance_folder, // instance folder in mmc_base_path
profile_path.clone(), // path to profile
base_path, // path to base mmc folder
instance_folder, // instance folder in mmc_base_path
profile_path, // path to profile
)
.await
}
ImportLauncherType::ATLauncher => {
atlauncher::import_atlauncher(
base_path, // path to atlauncher folder
instance_folder, // instance folder in atlauncher
profile_path.clone(), // path to profile
base_path, // path to atlauncher folder
instance_folder, // instance folder in atlauncher
profile_path, // path to profile
)
.await
}
ImportLauncherType::GDLauncher => {
gdlauncher::import_gdlauncher(
base_path.join("instances").join(instance_folder), // path to gdlauncher folder
profile_path.clone(), // path to profile
profile_path, // path to profile
)
.await
}
ImportLauncherType::Curseforge => {
curseforge::import_curseforge(
base_path.join("Instances").join(instance_folder), // path to curseforge folder
profile_path.clone(), // path to profile
profile_path, // path to profile
)
.await
}
@@ -158,14 +156,11 @@ pub async fn import_instance(
Ok(_) => {}
Err(e) => {
tracing::warn!("Import failed: {:?}", e);
let _ = crate::api::profile::remove(&profile_path).await;
let _ = crate::api::profile::remove(profile_path).await;
return Err(e);
}
}
// Check existing managed packs for potential updates
tokio::task::spawn(Profiles::update_modrinth_versions());
tracing::debug!("Completed import.");
Ok(())
}
@@ -200,7 +195,7 @@ pub fn get_default_launcher_path(
}
/// Checks if this PathBuf is a valid instance for the given launcher type
#[theseus_macros::debug_pin]
#[tracing::instrument]
pub async fn is_valid_importable_instance(
instance_path: PathBuf,
@@ -224,7 +219,7 @@ pub async fn is_valid_importable_instance(
}
/// Caches an image file in the filesystem into the cache directory, and returns the path to the cached file.
#[theseus_macros::debug_pin]
#[tracing::instrument]
pub async fn recache_icon(
icon_path: PathBuf,
@@ -252,13 +247,14 @@ pub async fn recache_icon(
}
pub async fn copy_dotminecraft(
profile_path_id: ProfilePathId,
profile_path_id: &str,
dotminecraft: PathBuf,
io_semaphore: &IoSemaphore,
existing_loading_bar: Option<LoadingBarId>,
) -> crate::Result<LoadingBarId> {
// Get full path to profile
let profile_path = profile_path_id.get_full_path().await?;
let profile_path =
crate::api::profile::get_full_path(profile_path_id).await?;
// Gets all subfiles recursively in src
let subfiles = get_all_subfiles(&dotminecraft).await?;
@@ -298,7 +294,7 @@ pub async fn copy_dotminecraft(
/// Recursively get a list of all subfiles in src
/// uses async recursion
#[theseus_macros::debug_pin]
#[async_recursion::async_recursion]
#[tracing::instrument]
pub async fn get_all_subfiles(src: &Path) -> crate::Result<Vec<PathBuf>> {

View File

@@ -1,16 +1,10 @@
use crate::config::MODRINTH_API_URL;
use crate::data::ModLoader;
use crate::event::emit::{emit_loading, init_loading};
use crate::event::{LoadingBarId, LoadingBarType};
use crate::prelude::ProfilePathId;
use crate::state::{
LinkedData, ModrinthProject, ModrinthVersion, ProfileInstallStage, SideType,
};
use crate::util::fetch::{
fetch, fetch_advanced, fetch_json, write_cached_icon,
};
use crate::state::{CachedEntry, LinkedData, ProfileInstallStage, SideType};
use crate::util::fetch::{fetch, fetch_advanced, write_cached_icon};
use crate::util::io;
use crate::{InnerProjectPathUnix, State};
use crate::State;
use reqwest::Method;
use serde::{Deserialize, Serialize};
@@ -33,7 +27,7 @@ pub struct PackFormat {
#[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PackFile {
pub path: InnerProjectPathUnix,
pub path: String,
pub hashes: HashMap<PackFileHash, String>,
pub env: Option<HashMap<EnvType, SideType>>,
pub downloads: Vec<String>,
@@ -84,7 +78,7 @@ pub enum PackDependency {
Minecraft,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum CreatePackLocation {
// Create a pack from a modrinth version ID (such as a modpack)
@@ -144,7 +138,7 @@ pub struct CreatePackDescription {
pub project_id: Option<String>,
pub version_id: Option<String>,
pub existing_loading_bar: Option<LoadingBarId>,
pub profile_path: ProfilePathId,
pub profile_path: String,
}
pub fn get_profile_from_pack(
@@ -160,9 +154,9 @@ pub fn get_profile_from_pack(
name: title,
icon_url,
linked_data: Some(LinkedData {
project_id: Some(project_id),
version_id: Some(version_id),
locked: Some(true),
project_id,
version_id,
locked: true,
}),
..Default::default()
},
@@ -182,13 +176,13 @@ pub fn get_profile_from_pack(
}
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn generate_pack_from_version_id(
project_id: String,
version_id: String,
title: String,
icon_url: Option<String>,
profile_path: ProfilePathId,
profile_path: String,
// Existing loading bar. Unlike when existing_loading_bar is used, this one is pre-initialized with PackFileDownload
// For example, you might use this if multiple packs are being downloaded at once and you want to use the same loading bar
@@ -202,7 +196,7 @@ pub async fn generate_pack_from_version_id(
} else {
init_loading(
LoadingBarType::PackFileDownload {
profile_path: profile_path.get_full_path().await?,
profile_path: profile_path.clone(),
pack_name: title,
icon: icon_url,
pack_version: version_id.clone(),
@@ -214,16 +208,18 @@ pub async fn generate_pack_from_version_id(
};
emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?;
let creds = state.credentials.read().await;
let version: ModrinthVersion = fetch_json(
Method::GET,
&format!("{}version/{}", MODRINTH_API_URL, version_id),
let version = CachedEntry::get_version(
&version_id,
None,
None,
&state.fetch_semaphore,
&creds,
&state.pool,
&state.api_semaphore,
)
.await?;
.await?
.ok_or_else(|| {
crate::ErrorKind::InputError(
"Invalid version ID specified!".to_string(),
)
})?;
emit_loading(&loading_bar, 10.0, None).await?;
let (url, hash) =
@@ -249,27 +245,29 @@ pub async fn generate_pack_from_version_id(
None,
Some((&loading_bar, 70.0)),
&state.fetch_semaphore,
&creds,
&state.pool,
)
.await?;
emit_loading(&loading_bar, 0.0, Some("Fetching project metadata")).await?;
let project: ModrinthProject = fetch_json(
Method::GET,
&format!("{}project/{}", MODRINTH_API_URL, version.project_id),
let project = CachedEntry::get_project(
&version.project_id,
None,
None,
&state.fetch_semaphore,
&creds,
&state.pool,
&state.api_semaphore,
)
.await?;
.await?
.ok_or_else(|| {
crate::ErrorKind::InputError(
"Invalid project ID specified!".to_string(),
)
})?;
emit_loading(&loading_bar, 10.0, Some("Retrieving icon")).await?;
let icon = if let Some(icon_url) = project.icon_url {
let state = State::get().await?;
let icon_bytes =
fetch(&icon_url, None, &state.fetch_semaphore, &creds).await?;
drop(creds);
fetch(&icon_url, None, &state.fetch_semaphore, &state.pool).await?;
let filename = icon_url.rsplit('/').next();
@@ -305,10 +303,10 @@ pub async fn generate_pack_from_version_id(
}
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn generate_pack_from_file(
path: PathBuf,
profile_path: ProfilePathId,
profile_path: String,
) -> crate::Result<CreatePack> {
let file = io::read(&path).await?;
Ok(CreatePack {
@@ -326,9 +324,9 @@ pub async fn generate_pack_from_file(
/// Sets generated profile attributes to the pack ones (using profile::edit)
/// This includes the pack name, icon, game version, loader version, and loader
#[theseus_macros::debug_pin]
pub async fn set_profile_information(
profile_path: ProfilePathId,
profile_path: String,
description: &CreatePackDescription,
backup_name: &str,
dependencies: &HashMap<PackDependency, String>,
@@ -371,10 +369,10 @@ pub async fn set_profile_information(
let mod_loader = mod_loader.unwrap_or(ModLoader::Vanilla);
let loader_version = if mod_loader != ModLoader::Vanilla {
crate::profile::create::get_loader_version_from_loader(
game_version.clone(),
crate::launcher::get_loader_version_from_profile(
game_version,
mod_loader,
loader_version.cloned(),
loader_version.cloned().as_deref(),
)
.await?
} else {
@@ -382,35 +380,36 @@ pub async fn set_profile_information(
};
// Sets values in profile
crate::api::profile::edit(&profile_path, |prof| {
prof.metadata.name = description
prof.name = description
.override_title
.clone()
.unwrap_or_else(|| backup_name.to_string());
prof.install_stage = ProfileInstallStage::PackInstalling;
let project_id = description.project_id.clone();
let version_id = description.version_id.clone();
if let Some(ref project_id) = description.project_id {
if let Some(ref version_id) = description.version_id {
prof.linked_data = Some(LinkedData {
project_id: project_id.clone(),
version_id: version_id.clone(),
locked: if !ignore_lock {
true
} else {
prof.linked_data
.as_ref()
.map(|x| x.locked)
.unwrap_or(true)
},
})
}
}
prof.metadata.linked_data = if project_id.is_some()
&& version_id.is_some()
{
Some(LinkedData {
project_id,
version_id,
locked: if !ignore_lock {
Some(true)
} else {
prof.metadata.linked_data.as_ref().and_then(|x| x.locked)
},
})
} else {
None
};
prof.metadata.icon.clone_from(&description.icon);
prof.metadata.game_version.clone_from(game_version);
prof.metadata.loader_version.clone_from(&loader_version);
prof.metadata.loader = mod_loader;
prof.icon_path = description
.icon
.clone()
.map(|x| x.to_string_lossy().to_string());
prof.game_version.clone_from(game_version);
prof.loader_version = loader_version.clone().map(|x| x.id);
prof.loader = mod_loader;
async { Ok(()) }
})

View File

@@ -1,4 +1,3 @@
use crate::config::MODRINTH_API_URL;
use crate::event::emit::{
emit_loading, init_or_edit_loading, loading_try_for_each_concurrent,
};
@@ -6,16 +5,14 @@ use crate::event::LoadingBarType;
use crate::pack::install_from::{
set_profile_information, EnvType, PackFile, PackFileHash,
};
use crate::prelude::{ModrinthVersion, ProfilePathId, ProjectMetadata};
use crate::state::{ProfileInstallStage, Profiles, SideType};
use crate::util::fetch::{fetch_json, fetch_mirrors, write};
use crate::state::{
cache_file_hash, CacheBehaviour, CachedEntry, ProfileInstallStage, SideType,
};
use crate::util::fetch::{fetch_mirrors, write};
use crate::util::io;
use crate::{profile, State};
use async_zip::base::read::seek::ZipFileReader;
use reqwest::Method;
use serde_json::json;
use std::collections::HashMap;
use std::io::Cursor;
use std::path::{Component, PathBuf};
@@ -28,11 +25,11 @@ use super::install_from::{
/// Wrapper around install_pack_files that generates a pack creation description, and
/// attempts to install the pack files. If it fails, it will remove the profile (fail safely)
/// Install a modpack from a mrpack file (a modrinth .zip format)
#[theseus_macros::debug_pin]
pub async fn install_zipped_mrpack(
location: CreatePackLocation,
profile_path: ProfilePathId,
) -> crate::Result<ProfilePathId> {
profile_path: String,
) -> crate::Result<String> {
// Get file from description
let create_pack: CreatePack = match location {
CreatePackLocation::FromVersionId {
@@ -59,9 +56,6 @@ pub async fn install_zipped_mrpack(
// Install pack files, and if it fails, fail safely by removing the profile
let result = install_zipped_mrpack_files(create_pack, false).await;
// Check existing managed packs for potential updates
tokio::task::spawn(Profiles::update_modrinth_versions());
match result {
Ok(profile) => Ok(profile),
Err(err) => {
@@ -74,11 +68,11 @@ pub async fn install_zipped_mrpack(
/// Install all pack files from a description
/// Does not remove the profile if it fails
#[theseus_macros::debug_pin]
pub async fn install_zipped_mrpack_files(
create_pack: CreatePack,
ignore_lock: bool,
) -> crate::Result<ProfilePathId> {
) -> crate::Result<String> {
let state = &State::get().await?;
let file = create_pack.file;
@@ -132,7 +126,7 @@ pub async fn install_zipped_mrpack_files(
let loading_bar = init_or_edit_loading(
existing_loading_bar,
LoadingBarType::PackDownload {
profile_path: profile_path.get_full_path().await?.clone(),
profile_path: profile_path.clone(),
pack_name: pack.name.clone(),
icon,
pack_id: project_id,
@@ -167,7 +161,6 @@ pub async fn install_zipped_mrpack_files(
}
}
let creds = state.credentials.read().await;
let file = fetch_mirrors(
&project
.downloads
@@ -176,10 +169,9 @@ pub async fn install_zipped_mrpack_files(
.collect::<Vec<&str>>(),
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
&state.fetch_semaphore,
&creds,
&state.pool,
)
.await?;
drop(creds);
let project_path = project.path.to_string();
@@ -188,10 +180,23 @@ pub async fn install_zipped_mrpack_files(
if let Some(path) = path {
match path {
Component::CurDir | Component::Normal(_) => {
let path = profile_path
.get_full_path()
.await?
.join(&project_path);
let path =
profile::get_full_path(&profile_path)
.await?
.join(&project_path);
cache_file_hash(
file.clone(),
&profile_path,
&project_path,
project
.hashes
.get(&PackFileHash::Sha1)
.map(|x| &**x),
&state.pool,
)
.await?;
write(&path, &file, &state.io_semaphore)
.await?;
}
@@ -243,9 +248,22 @@ pub async fn install_zipped_mrpack_files(
}
if new_path.file_name().is_some() {
let bytes = bytes::Bytes::from(content);
cache_file_hash(
bytes.clone(),
&profile_path,
&new_path.to_string_lossy(),
None,
&state.pool,
)
.await?;
write(
&profile_path.get_full_path().await?.join(new_path),
&content,
&profile::get_full_path(&profile_path)
.await?
.join(new_path),
&bytes,
&state.io_semaphore,
)
.await?;
@@ -265,24 +283,23 @@ pub async fn install_zipped_mrpack_files(
// If the icon doesn't exist, we expect icon.png to be a potential icon.
// If it doesn't exist, and an override to icon.png exists, cache and use that
let potential_icon =
profile_path.get_full_path().await?.join("icon.png");
let potential_icon = profile::get_full_path(&profile_path)
.await?
.join("icon.png");
if !icon_exists && potential_icon.exists() {
profile::edit_icon(&profile_path, Some(&potential_icon)).await?;
}
if let Some(profile_val) = profile::get(&profile_path, None).await? {
if let Some(profile_val) = profile::get(&profile_path).await? {
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
false,
)
.await?;
State::sync().await?;
}
Ok::<ProfilePathId, crate::Error>(profile_path.clone())
Ok::<String, crate::Error>(profile_path.clone())
} else {
Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
@@ -291,9 +308,9 @@ pub async fn install_zipped_mrpack_files(
}
#[tracing::instrument(skip(mrpack_file))]
#[theseus_macros::debug_pin]
pub async fn remove_all_related_files(
profile_path: ProfilePathId,
profile_path: String,
mrpack_file: bytes::Bytes,
) -> crate::Result<()> {
let reader: Cursor<&bytes::Bytes> = Cursor::new(&mrpack_file);
@@ -339,43 +356,39 @@ pub async fn remove_all_related_files(
let all_hashes = pack
.files
.iter()
.filter_map(|f| Some(f.hashes.get(&PackFileHash::Sha512)?.clone()))
.filter_map(|f| Some(f.hashes.get(&PackFileHash::Sha1)?.clone()))
.collect::<Vec<_>>();
let creds = state.credentials.read().await;
// First, get project info by hash
let files_url = format!("{}version_files", MODRINTH_API_URL);
let hash_projects = fetch_json::<HashMap<String, ModrinthVersion>>(
Method::POST,
&files_url,
let file_infos = CachedEntry::get_file_many(
&all_hashes.iter().map(|x| &**x).collect::<Vec<_>>(),
None,
Some(json!({
"hashes": all_hashes,
"algorithm": "sha512",
})),
&state.fetch_semaphore,
&creds,
&state.pool,
&state.api_semaphore,
)
.await?;
let to_remove = hash_projects
.into_values()
let to_remove = file_infos
.into_iter()
.map(|p| p.project_id)
.collect::<Vec<_>>();
let profile =
profile::get(&profile_path, None).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(
profile_path.to_string(),
)
})?;
for (project_id, project) in &profile.projects {
if let ProjectMetadata::Modrinth { project, .. } = &project.metadata
{
if to_remove.contains(&project.id) {
let path = profile
.get_profile_full_path()
.await?
.join(project_id.0.clone());
let profile = profile::get(&profile_path).await?.ok_or_else(|| {
crate::ErrorKind::UnmanagedProfileError(profile_path.to_string())
})?;
let profile_full_path = profile::get_full_path(&profile_path).await?;
for (file_path, project) in profile
.get_projects(
Some(CacheBehaviour::MustRevalidate),
&state.pool,
&state.api_semaphore,
)
.await?
{
if let Some(metadata) = &project.metadata {
if to_remove.contains(&metadata.project_id) {
let path = profile_full_path.join(file_path);
if path.exists() {
io::remove_file(&path).await?;
}
@@ -386,10 +399,7 @@ pub async fn remove_all_related_files(
// Iterate over all Modrinth project file paths in the json, and remove them
// (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized)
for file in pack.files {
let path: PathBuf = profile_path
.get_full_path()
.await?
.join(file.path.to_string());
let path: PathBuf = profile_full_path.join(file.path);
if path.exists() {
io::remove_file(&path).await?;
}
@@ -414,8 +424,9 @@ pub async fn remove_all_related_files(
}
// Remove this file if a corresponding one exists in the filesystem
let existing_file =
profile_path.get_full_path().await?.join(&new_path);
let existing_file = profile::get_full_path(&profile_path)
.await?
.join(&new_path);
if existing_file.exists() {
io::remove_file(&existing_file).await?;
}