Track external mods properly

This commit is contained in:
Wyvest 2025-12-31 03:51:03 +09:00
parent 18c37f8661
commit 09749ea78b
9 changed files with 209 additions and 31 deletions

View file

@ -6,6 +6,10 @@ pub async fn initialize_oneclient() {
tracing::error!("failed to initialize clusters: {err}");
}
if let Err(err) = onelauncher_core::api::cluster::sync_clusters().await {
tracing::error!("failed to sync clusters: {err}");
}
tokio::spawn(async {
bundles::BundlesManager::get().await;
});

View file

@ -220,7 +220,7 @@ export type ProcessPayloadKind = { type: "Starting"; command: string } | { type:
export type ProfileUpdate = { res: Resolution | null; force_fullscreen: boolean | null; mem_max: number | null; launch_args: string | null; launch_env: string | null; hook_pre: string | null; hook_wrapper: string | null; hook_post: string | null }
export type Provider = "Modrinth" | "CurseForge" | "SkyClient"
export type Provider = "Modrinth" | "CurseForge" | "SkyClient" | "Local"
export type Resolution = { width: number; height: number }
@ -303,19 +303,8 @@ export type VersionType =
*/
"old_beta"
const ARGS_MAP = { 'oneclient':'{"installUpdate":[],"getClustersGroupedByMajor":[],"checkForUpdate":[],"getBundlesFor":["cluster_id"],"getVersions":[]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'core':'{"getLogByName":["id","name"],"getClusterById":["id"],"getLogs":["id"],"setDiscordRPCMessage":["message"],"getClusters":[],"getWorlds":["id"],"createSettingsProfile":["name"],"getUsers":[],"isClusterRunning":["cluster_id"],"fetchMinecraftProfile":["uuid"],"getPackage":["provider","slug"],"getScreenshots":["id"],"getRunningProcessesByClusterId":["cluster_id"],"getUsersFromAuthor":["provider","author"],"getLinkedPackages":["cluster_id"],"openMsaLogin":[],"getDefaultUser":["fallback"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"writeSettings":["setting"],"getUser":["uuid"],"readSettings":[],"fetchLoggedInProfile":["access_token"],"getLoadersForVersion":["mc_version"],"open":["input"],"killProcess":["pid"],"installModpack":["modpack","cluster_id"],"setClusterStage":["id","stage"],"getRunningProcesses":[],"getProfileOrDefault":["name"],"updateClusterProfile":["name","profile"],"removeCluster":["id"],"getGameVersions":[],"getPackageBody":["provider","body"],"getMultiplePackages":["provider","slugs"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"changeSkin":["access_token","skin_url","skin_variant"],"changeCape":["access_token","cape_uuid"],"removeUser":["uuid"],"setDefaultUser":["uuid"],"launchCluster":["id","uuid","search_for_java"],"updateClusterById":["id","request"],"createCluster":["options"],"getGlobalProfile":[],"searchPackages":["provider","query"],"removePackage":["cluster_id","package_hash"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"removeCape":["access_token"],"convertUsernameUUID":["username_uuid"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'debug':'{"getArch":[],"getBuildTimestamp":[],"getPlatform":[],"getType":[],"getLocale":[],"getOsVersion":[],"openDevTools":[],"getGitCommitHash":[],"isInDev":[],"getPackageVersion":[],"getFamily":[]}' }
export type Router = { 'debug': { openDevTools: () => Promise<void>,
isInDev: () => Promise<boolean>,
getArch: () => Promise<string>,
getFamily: () => Promise<string>,
getLocale: () => Promise<string | null>,
getType: () => Promise<string>,
getPlatform: () => Promise<string>,
getOsVersion: () => Promise<string>,
getGitCommitHash: () => Promise<string>,
getBuildTimestamp: () => Promise<string>,
getPackageVersion: () => Promise<string> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
const ARGS_MAP = { 'core':'{"searchPackages":["provider","query"],"getPackage":["provider","slug"],"getRunningProcessesByClusterId":["cluster_id"],"getWorlds":["id"],"getGameVersions":[],"getLogs":["id"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"downloadExternalPackage":["package","cluster_id","force","skip_compatibility"],"installModpack":["modpack","cluster_id"],"getLinkedPackages":["cluster_id"],"createCluster":["options"],"setDefaultUser":["uuid"],"changeSkin":["access_token","skin_url","skin_variant"],"convertUsernameUUID":["username_uuid"],"readSettings":[],"getLogByName":["id","name"],"updateClusterProfile":["name","profile"],"getLoadersForVersion":["mc_version"],"getDefaultUser":["fallback"],"fetchMinecraftProfile":["uuid"],"getScreenshots":["id"],"uploadSkinBytes":["access_token","skin_data","image_format","skin_variant"],"getProfileOrDefault":["name"],"launchCluster":["id","uuid","search_for_java"],"isClusterRunning":["cluster_id"],"removePackage":["cluster_id","package_hash"],"getClusters":[],"getUsers":[],"setClusterStage":["id","stage"],"getClusterById":["id"],"createSettingsProfile":["name"],"getUser":["uuid"],"changeCape":["access_token","cape_uuid"],"fetchLoggedInProfile":["access_token"],"removeCape":["access_token"],"getUsersFromAuthor":["provider","author"],"removeCluster":["id"],"setDiscordRPCMessage":["message"],"getMultiplePackages":["provider","slugs"],"removeUser":["uuid"],"writeSettings":["setting"],"updateClusterById":["id","request"],"getPackageBody":["provider","body"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"open":["input"],"getRunningProcesses":[],"openMsaLogin":[],"killProcess":["pid"],"getGlobalProfile":[]}', 'oneclient':'{"getBundlesFor":["cluster_id"],"installUpdate":[],"getVersions":[],"getClustersGroupedByMajor":[],"checkForUpdate":[]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}', 'debug':'{"openDevTools":[],"getGitCommitHash":[],"getPackageVersion":[],"getType":[],"getPlatform":[],"getLocale":[],"isInDev":[],"getArch":[],"getOsVersion":[],"getBuildTimestamp":[],"getFamily":[]}' }
export type Router = { 'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'folders': { fromCluster: (folderName: string) => Promise<string>,
@ -369,6 +358,17 @@ removeCape: (accessToken: string) => Promise<MojangFullPlayerProfile>,
convertUsernameUUID: (usernameUuid: string) => Promise<MowojangProfile>,
setDiscordRPCMessage: (message: string) => Promise<null>,
open: (input: string) => Promise<null> },
'debug': { openDevTools: () => Promise<void>,
isInDev: () => Promise<boolean>,
getArch: () => Promise<string>,
getFamily: () => Promise<string>,
getLocale: () => Promise<string | null>,
getType: () => Promise<string>,
getPlatform: () => Promise<string>,
getOsVersion: () => Promise<string>,
getGitCommitHash: () => Promise<string>,
getBuildTimestamp: () => Promise<string>,
getPackageVersion: () => Promise<string> },
'oneclient': { getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]>,
getVersions: () => Promise<OnlineClusterManifest>,

View file

@ -1,12 +1,18 @@
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use onelauncher_entity::clusters;
use chrono::Utc;
use futures::future::join_all;
use onelauncher_entity::package::{PackageType, Provider};
use onelauncher_entity::{clusters, packages};
use sea_orm::{ActiveValue, Iterable};
use serde::Serialize;
use crate::api::packages::provider::ProviderExt;
use crate::error::{DaoError, LauncherResult};
use crate::send_error;
use crate::store::Dirs;
use crate::utils::crypto::HashAlgorithm;
use crate::utils::io;
use super::dao::{self, ClusterId};
@ -88,7 +94,151 @@ pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult<SyncActio
return Ok(SyncAction::MissingFs);
}
// TODO: sync packages
let mut missing_hashes = HashMap::new();
let mut found_hashes = HashSet::new();
for package_type in PackageType::iter() {
let dir = path.join(package_type.folder_name());
if !dir.exists() {
continue;
}
let mut stream = io::read_dir(&dir).await?;
while let Ok(Some(entry)) = stream.next_entry().await {
let file_path = entry.path();
if !file_path.is_file() {
continue;
}
// Skip hidden files
if file_path
.file_name()
.map(|s| s.to_string_lossy().starts_with('.'))
.unwrap_or(false)
{
continue;
}
let hash = HashAlgorithm::Sha1.hash_file(&file_path).await?;
if let Some(package) =
crate::api::packages::dao::get_package_by_hash(hash.clone()).await?
{
if !crate::api::packages::dao::is_package_linked_to_cluster(&package, cluster)
.await?
{
crate::api::packages::dao::link_package_to_cluster(&package, cluster).await?;
}
found_hashes.insert(hash);
} else {
missing_hashes.insert(hash, (file_path, package_type.clone()));
}
}
}
if !missing_hashes.is_empty() {
let hashes: Vec<String> = missing_hashes.keys().cloned().collect();
let futures = Provider::get_providers().iter().map(|&provider| {
let hashes = hashes.clone();
async move {
match provider.get_versions_by_hashes(&hashes).await {
Ok(versions) => Some((provider, versions)),
Err(err) => {
tracing::error!(
"failed to get versions from provider {}: {}",
provider,
err
);
None
}
}
}
});
let results = join_all(futures).await;
let mut found_versions = HashMap::new();
for result in results.into_iter().flatten() {
let (provider, versions) = result;
for (hash, version) in versions {
found_versions.entry(hash).or_insert((version, provider));
}
}
for (hash, (file_path, package_type)) in &missing_hashes {
if let Some((version, provider)) = found_versions.get(hash) {
// Find the file in version.files
let file = version
.files
.iter()
.find(|f| f.sha1 == *hash)
.ok_or_else(|| anyhow::anyhow!("File hash mismatch"))?;
let model = packages::ActiveModel {
hash: ActiveValue::Set(hash.clone()),
file_name: ActiveValue::Set(file.file_name.clone()),
version_id: ActiveValue::Set(version.version_id.clone()),
published_at: ActiveValue::Set(version.published),
display_name: ActiveValue::Set(version.display_name.clone()),
display_version: ActiveValue::Set(version.display_version.clone()),
package_type: ActiveValue::Set(package_type.clone()),
provider: ActiveValue::Set(*provider),
package_id: ActiveValue::Set(version.project_id.clone()),
mc_versions: ActiveValue::Set(version.mc_versions.clone().into()),
mc_loader: ActiveValue::Set(version.loaders.clone().into()),
icon: ActiveValue::Set(None), // TODO: Fetch icon
};
let package = match crate::api::packages::dao::insert_package(model).await {
Ok(package) => package,
Err(err) => {
tracing::error!("failed to insert package {}: {}", hash, err);
continue;
}
};
if let Err(err) =
crate::api::packages::dao::link_package_to_cluster(&package, cluster).await
{
tracing::error!("failed to link package {} to cluster: {}", hash, err);
}
} else {
// Save as external package
let file_name = file_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let model = packages::ActiveModel {
hash: ActiveValue::Set(hash.clone()),
file_name: ActiveValue::Set(file_name.clone()),
version_id: ActiveValue::Set(hash.clone()),
published_at: ActiveValue::Set(Utc::now()),
display_name: ActiveValue::Set(file_name.clone()),
display_version: ActiveValue::Set("Unknown".to_string()),
package_type: ActiveValue::Set(package_type.clone()),
provider: ActiveValue::Set(Provider::Local),
package_id: ActiveValue::Set(hash.clone()),
mc_versions: ActiveValue::Set(vec![].into()),
mc_loader: ActiveValue::Set(vec![].into()),
icon: ActiveValue::Set(None),
};
let package = match crate::api::packages::dao::insert_package(model).await {
Ok(package) => package,
Err(err) => {
tracing::error!("failed to insert local package {}: {}", hash, err);
continue;
}
};
if let Err(err) =
crate::api::packages::dao::link_package_to_cluster(&package, cluster).await
{
tracing::error!("failed to link local package {} to cluster: {}", hash, err);
}
}
}
}
Ok(SyncAction::NoAction)
}

View file

@ -34,6 +34,7 @@ pub async fn link_package_to_cluster(
.into_active_model();
cluster_packages::Entity::insert(cluster_package)
.on_conflict_do_nothing()
.exec(db)
.await?;

View file

@ -144,16 +144,25 @@ pub async fn download_external_package(
return Ok(Some(model));
}
let dir = Dirs::get_clusters_dir()
.await?
.join(cluster.folder_name.clone())
.join(package.package_type.folder_name())
.join(package.name.clone());
let model = packages::Model {
hash: package.sha1.clone(),
display_name: package.name.clone(),
display_version: "Unknown".to_string(),
file_name: package.name.clone(),
version_id: package.sha1.clone(),
published_at: chrono::Utc::now(),
provider: Provider::Local,
icon: None,
package_id: package.sha1.clone(),
mc_loader: vec![].into(),
mc_versions: vec![].into(),
package_type: package.package_type.clone(),
};
http::download_advanced(
Method::GET,
&package.url,
dir,
model.path().await?,
None,
None,
Some((HashAlgorithm::Sha1, &package.sha1)),
@ -161,7 +170,9 @@ pub async fn download_external_package(
)
.await?;
Ok(None)
let inserted_model = dao::insert_package(model.into()).await?;
Ok(Some(inserted_model))
}
/// Links a package to a cluster on the file system and in database.
@ -186,7 +197,7 @@ pub async fn link_package(
}
tracing::trace!("checking compatibility of package with cluster");
if !skip_compatibility.unwrap_or(false) {
if !skip_compatibility.unwrap_or(false) && package.provider != Provider::Local {
if !package
.mc_loader
.iter()
@ -239,6 +250,9 @@ pub async fn link_many_packages_to_cluster(
packages
.iter()
.filter(|p| {
if p.provider == Provider::Local {
return true;
}
p.mc_loader
.iter()
.any(|v| cluster.mc_loader.compatible_with(v))

View file

@ -227,7 +227,7 @@ pub(super) async fn download_and_link_packages(
}
}
ModpackFileKind::External(package) => {
if let Err(e) = api::packages::download_external_package(
match api::packages::download_external_package(
package,
cluster,
None,
@ -236,7 +236,9 @@ pub(super) async fn download_and_link_packages(
)
.await
{
errors.push(e);
Ok(Some(model)) => packages_to_link.push(model),
Ok(None) => {}
Err(e) => errors.push(e),
}
}
}

View file

@ -67,7 +67,7 @@ macro_rules! delegate {
Provider::Modrinth => ModrinthProviderImpl.$method($($arg),*).await,
Provider::CurseForge => CurseForgeProviderImpl.$method($($arg),*).await,
// Provider::SkyClient => SkyClientProviderImpl.$method($($arg),*).await,
_ => todo!("skyclient not implement")
_ => Err(anyhow::anyhow!("Provider {} not supported", $self).into()),
}
};
}

View file

@ -748,7 +748,7 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
package: ExternalPackage,
cluster_id: ClusterId,
force: Option<bool>,
skip_compatibility: Option<bool>,
skip_compatibility: Option<bool>, // default: true
) -> LauncherResult<Option<packages::Model>> {
let cluster = api::cluster::dao::get_cluster_by_id(cluster_id)
.await?
@ -758,11 +758,15 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
&package,
&cluster,
force,
skip_compatibility,
skip_compatibility.or(Some(true)),
None,
)
.await?;
if let Some(model) = &model {
api::packages::link_package(model, &cluster, skip_compatibility.or(Some(true))).await?;
}
Ok(model)
}

View file

@ -65,12 +65,13 @@ impl ToString for PackageType {
}
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "u8", db_type = "Integer")]
pub enum Provider {
Modrinth = 0,
CurseForge = 1,
SkyClient = 2,
Local = 3,
}
impl Display for Provider {
@ -86,6 +87,7 @@ impl Provider {
Self::Modrinth => "Modrinth",
Self::CurseForge => "CurseForge",
Self::SkyClient => "SkyClient",
Self::Local => "Local",
}
}
@ -96,6 +98,7 @@ impl Provider {
Self::Modrinth => "https://modrinth.com/",
Self::CurseForge => "https://curseforge.com/",
Self::SkyClient => "https://skyclient.co/",
Self::Local => "",
}
}