feat: oneclient bundles
This commit is contained in:
parent
aafffbacfa
commit
0a77a31846
17 changed files with 1069 additions and 271 deletions
|
|
@ -1,6 +1,9 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use onelauncher_core::api::cluster::dao::ClusterId;
|
||||
use onelauncher_core::api::packages::modpack::data::ModpackArchive;
|
||||
use onelauncher_core::entity::clusters;
|
||||
use onelauncher_core::entity::loader::GameLoader;
|
||||
use onelauncher_core::error::LauncherResult;
|
||||
use tauri::Runtime;
|
||||
|
||||
|
|
@ -11,6 +14,9 @@ pub trait OneClientApi {
|
|||
|
||||
#[taurpc(alias = "getClustersGroupedByMajor")]
|
||||
async fn get_clusters_grouped_by_major() -> LauncherResult<HashMap<u32, Vec<clusters::Model>>>;
|
||||
|
||||
#[taurpc(alias = "getModpacksFor")]
|
||||
async fn get_modpacks_for(cluster_id: ClusterId) -> LauncherResult<Vec<ModpackArchive>>;
|
||||
}
|
||||
|
||||
#[taurpc::ipc_type]
|
||||
|
|
@ -47,4 +53,27 @@ impl OneClientApi for OneClientApiImpl {
|
|||
|
||||
Ok(mapped)
|
||||
}
|
||||
|
||||
async fn get_modpacks_for(self, cluster_id: ClusterId) -> LauncherResult<Vec<ModpackArchive>> {
|
||||
struct ModpackEntry {
|
||||
pub category: String,
|
||||
pub version: String,
|
||||
pub loader: GameLoader,
|
||||
}
|
||||
|
||||
// const ENTRIES: &[ModpackEntry] = &[
|
||||
// ModpackEntry {
|
||||
// category: "Gameplay".into(),
|
||||
// version: "1.0.0".into(),
|
||||
// loader: GameLoader::Fabric,
|
||||
// },
|
||||
// ModpackEntry {
|
||||
// category: "Graphics".into(),
|
||||
// version: "1.1.0".into(),
|
||||
// loader: GameLoader::Forge,
|
||||
// },
|
||||
// ];
|
||||
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
// === API ===
|
||||
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
|
||||
pub const DISCORD_CLIENT_ID: &str = "1274084193193955408";
|
||||
pub const CURSEFORGE_API_KEY: &'static str =
|
||||
"$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
|
||||
pub const DISCORD_CLIENT_ID: &'static str = "1274084193193955408";
|
||||
|
||||
// === Meta ===
|
||||
// Cannot contain trailing slash
|
||||
pub const META_URL_BASE: &'static str =
|
||||
"https://raw.githubusercontent.com/LynithDev/DataStorage/refs/heads/main/oneclient";
|
||||
|
|
|
|||
135
apps/oneclient/desktop/src/oneclient/bundles.rs
Normal file
135
apps/oneclient/desktop/src/oneclient/bundles.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use onelauncher_core::api::packages::modpack::data::ModpackArchive;
|
||||
use onelauncher_core::entity::loader::GameLoader;
|
||||
use onelauncher_core::error::LauncherResult;
|
||||
use onelauncher_core::send_error;
|
||||
use onelauncher_core::store::Dirs;
|
||||
use onelauncher_core::utils::{http, io};
|
||||
use reqwest::Method;
|
||||
use tokio::sync::{OnceCell, RwLock};
|
||||
|
||||
/// e.g.
|
||||
/// ```json
|
||||
/// {
|
||||
/// "versions": {
|
||||
/// "1.21.5": {
|
||||
/// "fabric": ["/generated/hud-fabric-1.21.5.mrpack"],
|
||||
/// "forge": ["/generated/hud-forge-1.21.5.mrpack"]
|
||||
/// }
|
||||
/// },
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
struct BundleManifest {
|
||||
pub versions: HashMap<String, HashMap<String, Vec<String>>>,
|
||||
}
|
||||
|
||||
static BUNDLES_STATE: OnceCell<BundlesManager> = OnceCell::const_new();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BundlesManager {
|
||||
manifest: RwLock<BundleManifest>,
|
||||
bundles: RwLock<HashMap<String, HashMap<GameLoader, Vec<ModpackArchive>>>>,
|
||||
}
|
||||
|
||||
impl BundlesManager {
|
||||
pub async fn get() -> &'static Self {
|
||||
BUNDLES_STATE
|
||||
.get_or_init(|| async {
|
||||
let manifest = Self::fetch_cached().await;
|
||||
|
||||
Self {
|
||||
manifest: RwLock::new(manifest),
|
||||
bundles: RwLock::new(HashMap::new()),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_bundles_for(
|
||||
&self,
|
||||
mc_version: &str,
|
||||
loader: onelauncher_core::entity::loader::GameLoader,
|
||||
) -> LauncherResult<Vec<ModpackArchive>> {
|
||||
let manifest = self.manifest.read().await;
|
||||
let mut bundles_lock = self.bundles.write().await;
|
||||
|
||||
if let Some(entry) = bundles_lock.get(mc_version) {
|
||||
if let Some(bundles) = entry.get(&loader) {
|
||||
return Ok(bundles.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut found = Vec::new();
|
||||
|
||||
// for (version, loaders) in &manifest.versions {
|
||||
// if version != mc_version {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// let Some(paths) = loaders.get(&loader.get_format_name()) else {
|
||||
// continue;
|
||||
// };
|
||||
|
||||
// // we will be first checking the disk cache, if that fails we fetch from remote
|
||||
// for path in paths {
|
||||
// let url = format!("{}{}", crate::constants::META_URL_BASE, path);
|
||||
// let res = http::request(Method::HEAD, &url, None).await?;
|
||||
|
||||
// // res.
|
||||
|
||||
// let name = path.split('/').last().unwrap_or(&format!("bundle-{}-{}-{}.mrpack"));
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
Ok(found.clone())
|
||||
}
|
||||
|
||||
/// Fetches the bundles manifest from remote, falling back to a saved copy on disk if available
|
||||
#[tracing::instrument]
|
||||
pub async fn fetch_cached() -> BundleManifest {
|
||||
let url = format!("{}/bundles.json", crate::constants::META_URL_BASE);
|
||||
let manifest_path = Self::dir().await.join("bundles.json");
|
||||
|
||||
match http::fetch_json::<BundleManifest>(Method::GET, &url, None, None).await {
|
||||
Ok(manifest) => {
|
||||
if let Err(e) = io::write_json(&manifest_path, &manifest).await {
|
||||
send_error!("failed to cache bundles manifest to disk: {e}");
|
||||
}
|
||||
|
||||
manifest
|
||||
}
|
||||
Err(e) if manifest_path.exists() => {
|
||||
tracing::debug!(
|
||||
"falling back to cached bundles manifest, due to error fetching remote: {e}"
|
||||
);
|
||||
|
||||
match io::read_json::<BundleManifest>(&manifest_path).await {
|
||||
Ok(manifest) => manifest,
|
||||
Err(e) => {
|
||||
tracing::error!("failed to read cached bundles manifest: {e}");
|
||||
|
||||
BundleManifest::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("failed to fetch bundles manifest from remote: {e}");
|
||||
|
||||
BundleManifest::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// returns the directory for everything bundle related
|
||||
pub async fn dir() -> std::path::PathBuf {
|
||||
Dirs::get_caches_dir()
|
||||
.await
|
||||
.expect("failed to get caches dir")
|
||||
.join("oneclient")
|
||||
.join("bundles")
|
||||
}
|
||||
}
|
||||
113
apps/oneclient/desktop/src/oneclient/clusters.rs
Normal file
113
apps/oneclient/desktop/src/oneclient/clusters.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use std::path::Path;
|
||||
|
||||
use onelauncher_core::entity::clusters::Model as ClusterModel;
|
||||
use onelauncher_core::entity::loader::GameLoader;
|
||||
use onelauncher_core::error::LauncherResult;
|
||||
use onelauncher_core::send_error;
|
||||
use onelauncher_core::utils::http::fetch_json;
|
||||
use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
|
||||
///
|
||||
/// e.g.
|
||||
/// ```json
|
||||
/// {
|
||||
/// "clusters": [
|
||||
/// {
|
||||
/// "major_version": 21,
|
||||
/// "entries": [
|
||||
/// {
|
||||
/// "minor_version": 5,
|
||||
/// "loader": "fabric"
|
||||
/// },
|
||||
/// {
|
||||
/// "minor_version": 5,
|
||||
/// "loader": "forge"
|
||||
/// }
|
||||
/// ]
|
||||
/// },
|
||||
/// {
|
||||
/// "major_version": 20,
|
||||
/// "entries": [
|
||||
/// {
|
||||
/// "minor_version": 5,
|
||||
/// "loader": "fabric"
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Deserialize)]
|
||||
struct OnlineClusterManifest {
|
||||
clusters: Vec<OnlineCluster>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OnlineCluster {
|
||||
major_version: u8,
|
||||
entries: Vec<OnlineClusterEntry>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OnlineClusterEntry {
|
||||
minor_version: u8,
|
||||
loader: GameLoader,
|
||||
}
|
||||
|
||||
pub async fn init_clusters() -> LauncherResult<()> {
|
||||
let manifest = match fetch_json::<OnlineClusterManifest>(
|
||||
Method::GET,
|
||||
&format!("{}/versions/versions.json", crate::constants::META_URL_BASE),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
send_error!("failed to fetch clusters manifest: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
for cluster in manifest.clusters {
|
||||
for entry in cluster.entries {
|
||||
let mc_version = format!("1.{}.{}", cluster.major_version, entry.minor_version);
|
||||
|
||||
if let Err(e) = create_cluster_if_not_exist(&mc_version, entry.loader, None).await {
|
||||
send_error!("failed to create cluster for {}: {}", mc_version, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cluster_name(mc_version: &str, loader: &GameLoader) -> String {
|
||||
format!("{mc_version} {loader}")
|
||||
}
|
||||
|
||||
async fn create_cluster_if_not_exist(
|
||||
mc_version: &str,
|
||||
mc_loader: GameLoader,
|
||||
mc_loader_version: Option<&str>,
|
||||
) -> LauncherResult<Option<ClusterModel>> {
|
||||
let name = cluster_name(mc_version, &mc_loader);
|
||||
|
||||
let cluster =
|
||||
onelauncher_core::api::cluster::dao::get_cluster_by_folder_name(Path::new(&name)).await?;
|
||||
if cluster.is_some() {
|
||||
return Ok(cluster);
|
||||
}
|
||||
|
||||
onelauncher_core::api::cluster::create_cluster(
|
||||
&name,
|
||||
mc_version,
|
||||
mc_loader,
|
||||
mc_loader_version,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(Some)
|
||||
}
|
||||
|
|
@ -1,74 +1,6 @@
|
|||
use onelauncher_core::entity::clusters::Model as ClusterModel;
|
||||
use onelauncher_core::entity::icon::Icon;
|
||||
use onelauncher_core::entity::loader::GameLoader;
|
||||
use onelauncher_core::error::LauncherResult;
|
||||
use onelauncher_core::send_error;
|
||||
pub mod bundles;
|
||||
pub mod clusters;
|
||||
|
||||
pub async fn initialize_oneclient() {
|
||||
init_clusters().await;
|
||||
}
|
||||
|
||||
struct OnlineCluster {
|
||||
mc_major_version: u8,
|
||||
mc_minor_versions: Vec<u8>,
|
||||
mc_loaders: Vec<GameLoader>,
|
||||
}
|
||||
|
||||
async fn init_clusters() {
|
||||
// TODO: use online metadata stuff
|
||||
let online_clusters: Vec<OnlineCluster> = vec![
|
||||
OnlineCluster {
|
||||
mc_major_version: 21,
|
||||
mc_minor_versions: vec![1, 2, 4, 7],
|
||||
mc_loaders: vec![GameLoader::Fabric, GameLoader::Forge],
|
||||
},
|
||||
OnlineCluster {
|
||||
mc_major_version: 8,
|
||||
mc_minor_versions: vec![9],
|
||||
mc_loaders: vec![GameLoader::Forge],
|
||||
},
|
||||
];
|
||||
|
||||
for cluster in online_clusters {
|
||||
for minor_version in cluster.mc_minor_versions {
|
||||
let mc_version = format!("1.{}.{}", cluster.mc_major_version, minor_version);
|
||||
|
||||
for loader in &cluster.mc_loaders {
|
||||
if let Err(e) = create_cluster_if_not_exist(
|
||||
&format!("{mc_version} {loader}"),
|
||||
&mc_version,
|
||||
*loader,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
send_error!("failed to create cluster for {}: {}", mc_version, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_cluster_if_not_exist(
|
||||
name: &str,
|
||||
mc_version: &str,
|
||||
mc_loader: GameLoader,
|
||||
mc_loader_version: Option<&str>,
|
||||
icon_url: Option<Icon>,
|
||||
) -> LauncherResult<Option<ClusterModel>> {
|
||||
let clusters = onelauncher_core::api::cluster::dao::get_clusters_by_version(mc_version).await?;
|
||||
if clusters.len() > 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
onelauncher_core::api::cluster::create_cluster(
|
||||
name,
|
||||
mc_version,
|
||||
mc_loader,
|
||||
mc_loader_version,
|
||||
icon_url,
|
||||
)
|
||||
.await
|
||||
.map(Some)
|
||||
clusters::init_clusters().await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ export type DirectoryError = { type: "BaseDir"; data: string }
|
|||
|
||||
export type DiscordError = { type: "MissingClientId"; data: string } | { type: "ConnectError"; data: string }
|
||||
|
||||
export type ExternalPackage = { name: string; url: string; sha1: string; size: number; package_type: PackageType }
|
||||
|
||||
export type Filters = { game_versions: string[] | null; loaders: GameLoader[] | null; categories: PackageCategories | null; package_type: PackageType | null }
|
||||
|
||||
export type GameLoader = "vanilla" | "forge" | "neoforge" | "quilt" | "fabric" | "legacyfabric"
|
||||
|
|
@ -94,6 +96,16 @@ refresh_token: string;
|
|||
*/
|
||||
expires: string }
|
||||
|
||||
export type ModpackArchive = { manifest: ModpackManifest; path: string; format: ModpackFormat }
|
||||
|
||||
export type ModpackFile = { enabled: boolean; kind: ModpackFileKind }
|
||||
|
||||
export type ModpackFileKind = { Managed: [ManagedPackage, ManagedVersion] } | { External: ExternalPackage }
|
||||
|
||||
export type ModpackFormat = "CurseForge" | "MrPack" | "PolyMrPack"
|
||||
|
||||
export type ModpackManifest = { name: string; version: string; loader: GameLoader; loader_version: string; mc_version: string; files: ModpackFile[] }
|
||||
|
||||
export type MojangPlayerProfile = { uuid: string; username: string; is_slim: boolean; skin_url: string | null; cape_url: string | null }
|
||||
|
||||
export type PackageAuthor = { Team: { team_id: string; org_id: string | null } } | { Users: ManagedUser[] }
|
||||
|
|
@ -232,11 +244,8 @@ export type VersionType =
|
|||
*/
|
||||
"old_beta"
|
||||
|
||||
const ARGS_MAP = { 'core':'{"getUser":["uuid"],"readSettings":[],"searchPackages":["provider","query"],"writeSettings":["setting"],"createCluster":["options"],"fetchMinecraftProfile":["uuid"],"getLoadersForVersion":["mc_version"],"launchCluster":["id","uuid"],"getGlobalProfile":[],"openMsaLogin":[],"open":["input"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"isClusterRunning":["cluster_id"],"getGameVersions":[],"getMultiplePackages":["provider","slugs"],"setDefaultUser":["uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getLogs":["id"],"getRunningProcesses":[],"getUsersFromAuthor":["provider","author"],"getClusterById":["id"],"getScreenshots":["id"],"getProfileOrDefault":["name"],"getPackageBody":["provider","body"],"getRunningProcessesByClusterId":["cluster_id"],"updateClusterProfile":["name","profile"],"getPackage":["provider","slug"],"removeUser":["uuid"],"updateClusterById":["id","request"],"getWorlds":["id"],"getLogByName":["id","name"],"killProcess":["pid"],"getClusters":[],"removeCluster":["id"],"getDefaultUser":["fallback"],"getUsers":[]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}' }
|
||||
export type Router = { 'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
const ARGS_MAP = { 'oneclient':'{"getModpacksFor":["cluster_id"],"getClustersGroupedByMajor":[],"openDevTools":[]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'core':'{"getUsersFromAuthor":["provider","author"],"getUsers":[],"launchCluster":["id","uuid"],"fetchMinecraftProfile":["uuid"],"searchPackages":["provider","query"],"getLoadersForVersion":["mc_version"],"open":["input"],"removeCluster":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getGlobalProfile":[],"getLogByName":["id","name"],"getClusterById":["id"],"getRunningProcesses":[],"getClusters":[],"getDefaultUser":["fallback"],"readSettings":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"createCluster":["options"],"killProcess":["pid"],"getProfileOrDefault":["name"],"getUser":["uuid"],"getPackage":["provider","slug"],"updateClusterById":["id","request"],"writeSettings":["setting"],"setDefaultUser":["uuid"],"getLogs":["id"],"getWorlds":["id"],"openMsaLogin":[],"getRunningProcessesByClusterId":["cluster_id"],"removeUser":["uuid"],"getMultiplePackages":["provider","slugs"],"updateClusterProfile":["name","profile"],"getPackageBody":["provider","body"],"getScreenshots":["id"],"installModpack":["modpack","cluster_id"],"isClusterRunning":["cluster_id"],"getGameVersions":[]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}' }
|
||||
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
getClusterById: (id: number) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: number) => Promise<null>,
|
||||
createCluster: (options: CreateCluster) => Promise<ClusterModel>,
|
||||
|
|
@ -270,12 +279,17 @@ getMultiplePackages: (provider: Provider, slugs: string[]) => Promise<ManagedPac
|
|||
getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, loader: GameLoader | null, offset: number, limit: number) => Promise<Paginated<ManagedVersion>>,
|
||||
downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise<PackageModel>,
|
||||
getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<ManagedUser[]>,
|
||||
installModpack: (modpack: ModpackArchive, clusterId: number) => Promise<null>,
|
||||
fetchMinecraftProfile: (uuid: string) => Promise<MojangPlayerProfile>,
|
||||
open: (input: string) => Promise<null> },
|
||||
'oneclient': { openDevTools: () => Promise<void>,
|
||||
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> } };
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'oneclient': { openDevTools: () => Promise<void>,
|
||||
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
|
||||
getModpacksFor: (clusterId: number) => Promise<ModpackArchive[]> } };
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * from './AccountAvatar';
|
||||
export * from './BrowserPackageItem';
|
||||
export * from './GameBackground';
|
||||
export * from './Loader';
|
||||
export * from './LogViewer';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import type { OverlayProps } from './Overlay';
|
||||
import { Overlay } from './Overlay';
|
||||
|
||||
export interface PolyBundleModalProps extends OverlayProps {
|
||||
|
||||
}
|
||||
|
||||
export function PolyBundleModal() {
|
||||
return (
|
||||
<Overlay.Dialog>
|
||||
<Overlay.Title>test</Overlay.Title>
|
||||
</Overlay.Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import type { Filters, PackageCategories, PackageType, Provider, Sort } from '@/bindings.gen';
|
||||
import { LoaderSuspense, SheetPage } from '@/components';
|
||||
import { PackageGrid } from '@/components/PackageItem';
|
||||
import { LoaderSuspense, PackageGrid } from '@/components';
|
||||
import { bindings } from '@/main';
|
||||
import { browserCategories, categoryNameFromId } from '@/utils/browser';
|
||||
import { useCommandSuspense, usePagination } from '@onelauncher/common';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::packages::data::{ExternalPackage, ManagedPackage, ManagedVersion};
|
||||
use crate::api::packages::modpack::ModpackFormat;
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ModpackArchive {
|
||||
pub manifest: ModpackManifest,
|
||||
pub path: PathBuf,
|
||||
pub format: ModpackFormat,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -16,7 +27,14 @@ pub struct ModpackManifest {
|
|||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ModpackFile {
|
||||
pub struct ModpackFile {
|
||||
pub enabled: bool,
|
||||
pub kind: ModpackFileKind,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ModpackFileKind {
|
||||
Managed((ManagedPackage, ManagedVersion)),
|
||||
External(ExternalPackage),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,26 +4,47 @@ use std::sync::Arc;
|
|||
use onelauncher_entity::package::PackageType;
|
||||
use onelauncher_entity::{clusters, packages};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::packages::PackageError;
|
||||
use crate::api::packages::modpack::data::ModpackArchive;
|
||||
use crate::api::packages::modpack::mrpack::MrPackFormatImpl;
|
||||
use crate::api::packages::modpack::polymrpack::PolyMrPackFormatImpl;
|
||||
use crate::api::{self};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::ingress::SubIngress;
|
||||
|
||||
pub mod data;
|
||||
mod mrpack;
|
||||
mod polymrpack;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ModpackFormat {
|
||||
CurseForge,
|
||||
MrPack,
|
||||
PolyMrPack,
|
||||
}
|
||||
|
||||
const FORMAT_PIPELINE_PATH: &[LoaderFn<PathBuf>] = &[
|
||||
PolyMrPackFormatImpl::from_file, // has to be first because of it being a **VALID** mrpack with extra stuff
|
||||
MrPackFormatImpl::from_file,
|
||||
];
|
||||
|
||||
const FORMAT_PIPELINE_BYTES: &[LoaderFn<Arc<Vec<u8>>>] = &[
|
||||
PolyMrPackFormatImpl::from_bytes, // has to be first because of it being a **VALID** mrpack with extra stuff
|
||||
MrPackFormatImpl::from_bytes,
|
||||
];
|
||||
|
||||
type LoaderFn<Arg> = fn(
|
||||
Arg,
|
||||
) -> std::pin::Pin<
|
||||
Box<dyn Future<Output = LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>> + Send>,
|
||||
>;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ModpackFormatExt {
|
||||
async fn from_file(
|
||||
|
|
@ -37,6 +58,15 @@ pub trait ModpackFormatExt {
|
|||
) -> LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
async fn install_modpack_archive(
|
||||
modpack_archive: &ModpackArchive,
|
||||
cluster: &clusters::Model,
|
||||
skip_compatibility: Option<bool>,
|
||||
ingress: Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<()>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
|
|
@ -52,17 +82,9 @@ pub trait InstallableModpackFormatExt: Send + Sync + std::any::Any {
|
|||
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any + Send + Sync>;
|
||||
}
|
||||
|
||||
type LoaderFn<Arg> = fn(
|
||||
Arg,
|
||||
) -> std::pin::Pin<
|
||||
Box<dyn Future<Output = LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>> + Send>,
|
||||
>;
|
||||
|
||||
impl ModpackFormat {
|
||||
async fn from_file(path: PathBuf) -> LauncherResult<Box<dyn InstallableModpackFormatExt>> {
|
||||
let pipeline: Vec<LoaderFn<PathBuf>> = vec![MrPackFormatImpl::from_file];
|
||||
|
||||
for stage in pipeline {
|
||||
pub async fn from_file(path: PathBuf) -> LauncherResult<Box<dyn InstallableModpackFormatExt>> {
|
||||
for stage in FORMAT_PIPELINE_PATH {
|
||||
if let Some(format) = stage(path.clone()).await? {
|
||||
return Ok(format);
|
||||
}
|
||||
|
|
@ -71,12 +93,10 @@ impl ModpackFormat {
|
|||
Err(PackageError::UnsupportedModpackFormat.into())
|
||||
}
|
||||
|
||||
async fn from_bytes(
|
||||
pub async fn from_bytes(
|
||||
bytes: Arc<Vec<u8>>,
|
||||
) -> LauncherResult<Box<dyn InstallableModpackFormatExt>> {
|
||||
let pipeline: Vec<LoaderFn<Arc<Vec<u8>>>> = vec![MrPackFormatImpl::from_bytes];
|
||||
|
||||
for stage in pipeline {
|
||||
for stage in FORMAT_PIPELINE_BYTES {
|
||||
if let Some(format) = stage(Arc::clone(&bytes)).await? {
|
||||
return Ok(format);
|
||||
}
|
||||
|
|
@ -84,6 +104,31 @@ impl ModpackFormat {
|
|||
|
||||
Err(PackageError::UnsupportedModpackFormat.into())
|
||||
}
|
||||
|
||||
pub async fn install_modpack_archive(
|
||||
&self,
|
||||
modpack_archive: &ModpackArchive,
|
||||
cluster: &clusters::Model,
|
||||
skip_compatibility: Option<bool>,
|
||||
ingress: Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<()> {
|
||||
match self {
|
||||
ModpackFormat::CurseForge => unimplemented!(),
|
||||
ModpackFormat::MrPack => MrPackFormatImpl::install_modpack_archive(
|
||||
modpack_archive,
|
||||
cluster,
|
||||
skip_compatibility,
|
||||
ingress,
|
||||
),
|
||||
ModpackFormat::PolyMrPack => PolyMrPackFormatImpl::install_modpack_archive(
|
||||
modpack_archive,
|
||||
cluster,
|
||||
skip_compatibility,
|
||||
ingress,
|
||||
),
|
||||
}
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs a modpack to a cluster.
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ use serde::Deserialize;
|
|||
use tokio::sync::OnceCell;
|
||||
|
||||
use crate::api::cluster::ClusterError;
|
||||
use crate::api::packages::data::{ExternalPackage, PackageSide};
|
||||
use crate::api::packages::modpack::data::{ModpackFile, ModpackManifest};
|
||||
use crate::api::packages::data::{ExternalPackage, ManagedVersion, PackageSide};
|
||||
use crate::api::packages::modpack::data::{
|
||||
ModpackArchive, ModpackFile, ModpackFileKind, ModpackManifest,
|
||||
};
|
||||
use crate::api::packages::modpack::{InstallableModpackFormatExt, ModpackFormatExt};
|
||||
use crate::api::packages::provider::ProviderExt;
|
||||
use crate::api::{self};
|
||||
|
|
@ -21,17 +23,15 @@ use crate::utils::DatabaseModelExt;
|
|||
use crate::utils::io::{self, IOError};
|
||||
|
||||
pub struct MrPackFormatImpl {
|
||||
archive: Option<PathBuf>,
|
||||
raw_manifest: MrPackManifest,
|
||||
manifest: OnceCell<ModpackManifest>,
|
||||
mc_version: String,
|
||||
loader: GameLoader,
|
||||
loader_version: String,
|
||||
pub(super) archive: Option<PathBuf>,
|
||||
pub(super) raw_manifest: MrPackManifest,
|
||||
pub(super) manifest: OnceCell<ModpackManifest>,
|
||||
pub(super) mc_version: String,
|
||||
pub(super) loader: GameLoader,
|
||||
pub(super) loader_version: String,
|
||||
}
|
||||
|
||||
const MODRINTH_URL_PREFIX: &str = "https://cdn.modrinth.com/";
|
||||
|
||||
// TODO: move from_file fetching to manifest function
|
||||
pub(super) const MODRINTH_URL_PREFIX: &str = "https://cdn.modrinth.com/";
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ModpackFormatExt for MrPackFormatImpl {
|
||||
|
|
@ -112,6 +112,23 @@ impl ModpackFormatExt for MrPackFormatImpl {
|
|||
loader_version: loader_version.unwrap(),
|
||||
})))
|
||||
}
|
||||
|
||||
async fn install_modpack_archive(
|
||||
modpack_archive: &ModpackArchive,
|
||||
cluster: &clusters::Model,
|
||||
skip_compatibility: Option<bool>,
|
||||
ingress: Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<()>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let ModpackArchive { manifest, path, .. } = modpack_archive;
|
||||
|
||||
download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress).await?;
|
||||
copy_overrides_folder(cluster, &path, &ingress).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
|
|
@ -128,79 +145,7 @@ impl InstallableModpackFormatExt for MrPackFormatImpl {
|
|||
return Ok(manifest);
|
||||
}
|
||||
|
||||
let mut project_ids_to_fetch: Vec<String> = Vec::new();
|
||||
let mut version_ids_to_fetch: Vec<String> = Vec::new();
|
||||
let mut files: Vec<ModpackFile> = Vec::new();
|
||||
|
||||
for file in &self.raw_manifest.files {
|
||||
let name = file
|
||||
.path
|
||||
.split('/')
|
||||
.last()
|
||||
.unwrap_or(&file.path)
|
||||
.to_string();
|
||||
|
||||
if let Some(url) = file
|
||||
.downloads
|
||||
.iter()
|
||||
.find(|url| url.starts_with(MODRINTH_URL_PREFIX))
|
||||
{
|
||||
// https://cdn.modrinth.com/data/<project_id>/versions/<version_id>/<file_name>
|
||||
let paths = url[MODRINTH_URL_PREFIX.len()..]
|
||||
.split('/')
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if paths.len() >= 4 {
|
||||
let project_id = paths[1];
|
||||
let version_id = paths[3];
|
||||
|
||||
project_ids_to_fetch.push(project_id.to_string());
|
||||
version_ids_to_fetch.push(version_id.to_string())
|
||||
} else {
|
||||
tracing::error!("invalid modrinth file URL: '{}'", url);
|
||||
}
|
||||
} else {
|
||||
let download_url = file
|
||||
.downloads
|
||||
.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
tracing::warn!("mrpack file '{}' does not contain a download URL", name)
|
||||
})
|
||||
.unwrap_or(String::new());
|
||||
|
||||
// the path usually contains the folder name such as "mods" or "resourcepacks"
|
||||
// so we can use it to determine the package type
|
||||
let package_type = file
|
||||
.path
|
||||
.split('/')
|
||||
.next()
|
||||
.and_then(|s| PackageType::try_from(s).ok())
|
||||
.unwrap_or(PackageType::Mod);
|
||||
|
||||
files.push(ModpackFile::External(ExternalPackage {
|
||||
name,
|
||||
url: download_url,
|
||||
sha1: file.hashes.sha1.clone(),
|
||||
size: file.file_size,
|
||||
package_type,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let managed_packages = Provider::Modrinth
|
||||
.get_multiple(&project_ids_to_fetch)
|
||||
.await?;
|
||||
let managed_versions = Provider::Modrinth
|
||||
.get_versions(&version_ids_to_fetch)
|
||||
.await?;
|
||||
|
||||
files.extend(
|
||||
managed_packages
|
||||
.into_iter()
|
||||
.zip(managed_versions.into_iter())
|
||||
.map(|(package, version)| ModpackFile::Managed((package, version))),
|
||||
);
|
||||
let files = to_modpack_files(&self.raw_manifest.files).await?;
|
||||
|
||||
let manifest = ModpackManifest {
|
||||
name: self.raw_manifest.name.clone(),
|
||||
|
|
@ -225,91 +170,217 @@ impl InstallableModpackFormatExt for MrPackFormatImpl {
|
|||
) -> LauncherResult<()> {
|
||||
let manifest = self.manifest().await?;
|
||||
|
||||
if cluster.mc_version != manifest.mc_version {
|
||||
return Err(ClusterError::MismatchedVersion(
|
||||
manifest.mc_version.clone(),
|
||||
cluster.mc_version.clone(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress).await?;
|
||||
|
||||
if cluster.mc_loader != manifest.loader {
|
||||
return Err(ClusterError::MismatchedLoader(
|
||||
manifest.loader.clone(),
|
||||
cluster.mc_loader.clone(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// TODO: Implement loader version checking
|
||||
// if cluster.mc_loader_version.is_some_and(|v| v )
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut packages_to_link = Vec::new();
|
||||
|
||||
// TODO: Ingress
|
||||
for file in manifest.files.iter() {
|
||||
match file {
|
||||
ModpackFile::Managed((package, version)) => {
|
||||
match api::packages::download_package(package, version, None, None).await {
|
||||
Ok(model) => packages_to_link.push(model),
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
ModpackFile::External(package) => {
|
||||
if let Err(e) = api::packages::download_external_package(
|
||||
package,
|
||||
cluster,
|
||||
None,
|
||||
skip_compatibility,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let linked = api::packages::link_many_packages_to_cluster(
|
||||
&packages_to_link,
|
||||
cluster,
|
||||
skip_compatibility,
|
||||
)
|
||||
.await?;
|
||||
if linked < packages_to_link.len() as u64 {
|
||||
tracing::warn!("not all packages could be linked to the cluster, some errors occurred");
|
||||
}
|
||||
|
||||
if let Some(archive) = &self.archive {
|
||||
tracing::debug!(
|
||||
"extracting overrides from modpack archive: {}",
|
||||
archive.display()
|
||||
);
|
||||
let dest = cluster.path().await?;
|
||||
|
||||
io::extract_zip_filtered(
|
||||
archive,
|
||||
dest,
|
||||
Some(|entry: &async_zip::StoredZipEntry| {
|
||||
entry
|
||||
.filename()
|
||||
.as_str()
|
||||
.is_ok_and(|s| s.starts_with("overrides/"))
|
||||
}),
|
||||
Some(|name: &str| name.trim_start_matches("overrides/").to_string()),
|
||||
)
|
||||
.await?;
|
||||
if let Some(path) = self.archive.as_ref() {
|
||||
copy_overrides_folder(cluster, path, &ingress).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn download_and_link_packages(
|
||||
cluster: &clusters::Model,
|
||||
manifest: &ModpackManifest,
|
||||
skip_compatibility: Option<bool>,
|
||||
_ingress: &Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<()> {
|
||||
if cluster.mc_version != manifest.mc_version {
|
||||
return Err(ClusterError::MismatchedVersion(
|
||||
manifest.mc_version.clone(),
|
||||
cluster.mc_version.clone(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if cluster.mc_loader != manifest.loader {
|
||||
return Err(ClusterError::MismatchedLoader(
|
||||
manifest.loader.clone(),
|
||||
cluster.mc_loader.clone(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// TODO: Implement loader version checking
|
||||
// if cluster.mc_loader_version.is_some_and(|v| v )
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut packages_to_link = Vec::new();
|
||||
|
||||
// TODO: Ingress
|
||||
for file in manifest.files.iter() {
|
||||
match &file.kind {
|
||||
ModpackFileKind::Managed((package, version)) => {
|
||||
match api::packages::download_package(package, version, None, None).await {
|
||||
Ok(model) => packages_to_link.push(model),
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
ModpackFileKind::External(package) => {
|
||||
if let Err(e) = api::packages::download_external_package(
|
||||
package,
|
||||
cluster,
|
||||
None,
|
||||
skip_compatibility,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let linked = api::packages::link_many_packages_to_cluster(
|
||||
&packages_to_link,
|
||||
cluster,
|
||||
skip_compatibility,
|
||||
)
|
||||
.await?;
|
||||
if linked < packages_to_link.len() as u64 {
|
||||
tracing::warn!("not all packages could be linked to the cluster, some errors occurred");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn copy_overrides_folder(
|
||||
cluster: &clusters::Model,
|
||||
archive_path: &PathBuf,
|
||||
_ingress: &Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<()> {
|
||||
tracing::debug!(
|
||||
"extracting overrides from modpack archive: {}",
|
||||
archive_path.display()
|
||||
);
|
||||
let dest = cluster.path().await?;
|
||||
|
||||
io::extract_zip_filtered(
|
||||
archive_path,
|
||||
dest,
|
||||
Some(|entry: &async_zip::StoredZipEntry| {
|
||||
entry
|
||||
.filename()
|
||||
.as_str()
|
||||
.is_ok_and(|s| s.starts_with("overrides/"))
|
||||
}),
|
||||
Some(|name: &str| name.trim_start_matches("overrides/").to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn to_modpack_files(mrpack_files: &Vec<MrPackFile>) -> LauncherResult<Vec<ModpackFile>> {
|
||||
struct ToFetch {
|
||||
project_id: String,
|
||||
version_id: String,
|
||||
}
|
||||
|
||||
let mut to_fetch: Vec<ToFetch> = Vec::new();
|
||||
let mut files: Vec<ModpackFile> = Vec::new();
|
||||
|
||||
for file in mrpack_files {
|
||||
let name = file
|
||||
.path
|
||||
.split('/')
|
||||
.last()
|
||||
.unwrap_or(&file.path)
|
||||
.to_string();
|
||||
|
||||
if let Some(url) = file
|
||||
.downloads
|
||||
.iter()
|
||||
.find(|url| url.starts_with(MODRINTH_URL_PREFIX))
|
||||
{
|
||||
// https://cdn.modrinth.com/data/<project_id>/versions/<version_id>/<file_name>
|
||||
let paths = url[MODRINTH_URL_PREFIX.len()..]
|
||||
.split('/')
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if paths.len() >= 4 {
|
||||
let project_id = paths[1];
|
||||
let version_id = paths[3];
|
||||
|
||||
to_fetch.push(ToFetch {
|
||||
project_id: project_id.to_string(),
|
||||
version_id: version_id.to_string(),
|
||||
});
|
||||
} else {
|
||||
tracing::error!("invalid modrinth file URL: '{}'", url);
|
||||
}
|
||||
} else {
|
||||
let download_url = file
|
||||
.downloads
|
||||
.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
tracing::warn!("mrpack file '{}' does not contain a download URL", name)
|
||||
})
|
||||
.unwrap_or(String::new());
|
||||
|
||||
// the path usually contains the folder name such as "mods" or "resourcepacks"
|
||||
// so we can use it to determine the package type
|
||||
let package_type = file
|
||||
.path
|
||||
.split('/')
|
||||
.next()
|
||||
.and_then(|s| PackageType::try_from(s).ok())
|
||||
.unwrap_or(PackageType::Mod);
|
||||
|
||||
files.push(ModpackFile {
|
||||
kind: ModpackFileKind::External(ExternalPackage {
|
||||
name,
|
||||
url: download_url,
|
||||
sha1: file.hashes.sha1.clone(),
|
||||
size: file.file_size,
|
||||
package_type,
|
||||
}),
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let managed_packages = Provider::Modrinth
|
||||
.get_multiple(
|
||||
&to_fetch
|
||||
.iter()
|
||||
.map(|f| f.project_id.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.await?;
|
||||
let managed_versions = Provider::Modrinth
|
||||
.get_versions(
|
||||
&to_fetch
|
||||
.iter()
|
||||
.map(|f| f.version_id.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut version_map = managed_versions
|
||||
.into_iter()
|
||||
.map(|v| (v.project_id.clone(), v))
|
||||
.collect::<HashMap<String, ManagedVersion>>();
|
||||
|
||||
for fetched_pkg in managed_packages {
|
||||
if let Some(version) = version_map.remove(&fetched_pkg.id) {
|
||||
files.push(ModpackFile {
|
||||
kind: ModpackFileKind::Managed((fetched_pkg, version)),
|
||||
enabled: true,
|
||||
});
|
||||
} else {
|
||||
tracing::error!("no version found for managed package '{}'", fetched_pkg.id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MrPackManifest {
|
||||
pub(super) struct MrPackManifest {
|
||||
pub format_version: usize,
|
||||
pub version_id: String,
|
||||
pub name: String,
|
||||
|
|
@ -319,7 +390,7 @@ struct MrPackManifest {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MrPackFile {
|
||||
pub(super) struct MrPackFile {
|
||||
pub path: String,
|
||||
pub hashes: MrPackFileHash,
|
||||
#[serde(default)]
|
||||
|
|
@ -330,14 +401,14 @@ struct MrPackFile {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MrPackFileHash {
|
||||
pub(super) struct MrPackFileHash {
|
||||
pub sha1: String,
|
||||
// pub sha512: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MrPackFileEnv {
|
||||
pub(super) struct MrPackFileEnv {
|
||||
pub client: PackageSide,
|
||||
pub server: PackageSide,
|
||||
}
|
||||
|
|
|
|||
322
packages/core/src/api/packages/modpack/polymrpack.rs
Normal file
322
packages/core/src/api/packages/modpack/polymrpack.rs
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use onelauncher_entity::clusters;
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
use onelauncher_entity::package::{PackageType, Provider};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use crate::api::packages::data::{ExternalPackage, ManagedVersion};
|
||||
use crate::api::packages::modpack::data::{
|
||||
ModpackArchive, ModpackFile, ModpackFileKind, ModpackManifest,
|
||||
};
|
||||
use crate::api::packages::modpack::mrpack::MrPackFile;
|
||||
use crate::api::packages::modpack::{InstallableModpackFormatExt, ModpackFormatExt};
|
||||
use crate::api::packages::provider::ProviderExt;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::ingress::SubIngress;
|
||||
use crate::utils::io::{self, IOError};
|
||||
|
||||
pub struct PolyMrPackFormatImpl {
|
||||
pub(super) archive: Option<PathBuf>,
|
||||
pub(super) raw_manifest: PolyMrPackManifest,
|
||||
pub(super) manifest: OnceCell<ModpackManifest>,
|
||||
pub(super) mc_version: String,
|
||||
pub(super) loader: GameLoader,
|
||||
pub(super) loader_version: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ModpackFormatExt for PolyMrPackFormatImpl {
|
||||
async fn from_file(
|
||||
path: std::path::PathBuf,
|
||||
) -> LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let zip_file = tokio::fs::File::open(path.clone())
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
let buf_reader = tokio::io::BufReader::new(zip_file);
|
||||
|
||||
let manifest_file = io::try_read_zip_entry_bytes(buf_reader, "modrinth.index.json").await?;
|
||||
|
||||
let Some(this) = Self::from_bytes(Arc::new(manifest_file)).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut this = this
|
||||
.as_any()
|
||||
.downcast::<Self>()
|
||||
.expect("downcast failed for MrPackFormatImpl");
|
||||
|
||||
this.archive = Some(path);
|
||||
Ok(Some(this))
|
||||
}
|
||||
|
||||
async fn from_bytes(
|
||||
bytes: Arc<Vec<u8>>,
|
||||
) -> LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let serialized: PolyMrPackManifest = match serde_json::from_slice(&bytes) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(e) => {
|
||||
tracing::debug!("failed to deserialize modpack as polymrpack: {}", e);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let mut mc_version: Option<String> = None;
|
||||
let mut loader: Option<GameLoader> = None;
|
||||
let mut loader_version: Option<String> = None;
|
||||
|
||||
for (key, value) in serialized.dependencies.iter() {
|
||||
if key == "minecraft" {
|
||||
mc_version = Some(value.clone());
|
||||
} else {
|
||||
loader = GameLoader::from_str(key).ok();
|
||||
loader_version = Some(value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if mc_version.is_none() {
|
||||
tracing::error!("polymrpack manifest does not contain a minecraft version");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if loader.is_none() {
|
||||
tracing::error!("polymrpack manifest does not contain a valid game loader");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if loader_version.is_none() {
|
||||
tracing::error!("polymrpack manifest does not contain a valid loader version");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(Box::new(Self {
|
||||
archive: None,
|
||||
raw_manifest: serialized,
|
||||
manifest: OnceCell::new(),
|
||||
mc_version: mc_version.unwrap(),
|
||||
loader: loader.unwrap(),
|
||||
loader_version: loader_version.unwrap(),
|
||||
})))
|
||||
}
|
||||
|
||||
async fn install_modpack_archive(
|
||||
modpack_archive: &ModpackArchive,
|
||||
cluster: &clusters::Model,
|
||||
skip_compatibility: Option<bool>,
|
||||
ingress: Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<()>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let ModpackArchive { manifest, path, .. } = modpack_archive;
|
||||
|
||||
super::mrpack::download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress)
|
||||
.await?;
|
||||
super::mrpack::copy_overrides_folder(cluster, &path, &ingress).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InstallableModpackFormatExt for PolyMrPackFormatImpl {
|
||||
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any + Send + Sync>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self
|
||||
}
|
||||
|
||||
async fn manifest(&self) -> LauncherResult<&ModpackManifest> {
|
||||
if let Some(manifest) = self.manifest.get() {
|
||||
return Ok(manifest);
|
||||
}
|
||||
|
||||
let files = to_modpack_files(&self.raw_manifest.files).await?;
|
||||
|
||||
let manifest = ModpackManifest {
|
||||
name: self.raw_manifest.name.clone(),
|
||||
version: self.raw_manifest.version_id.clone(),
|
||||
mc_version: self.mc_version.clone(),
|
||||
loader: self.loader.clone(),
|
||||
loader_version: self.loader_version.clone(),
|
||||
files,
|
||||
};
|
||||
|
||||
self.manifest
|
||||
.set(manifest)
|
||||
.expect("failed to cache inner modpack manifest");
|
||||
Ok(self.manifest.get().unwrap())
|
||||
}
|
||||
|
||||
async fn install_to(
|
||||
&self,
|
||||
cluster: &clusters::Model,
|
||||
skip_compatibility: Option<bool>,
|
||||
ingress: Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<()> {
|
||||
let manifest = self.manifest().await?;
|
||||
|
||||
super::mrpack::download_and_link_packages(cluster, &manifest, skip_compatibility, &ingress)
|
||||
.await?;
|
||||
|
||||
if let Some(path) = self.archive.as_ref() {
|
||||
super::mrpack::copy_overrides_folder(cluster, path, &ingress).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn to_modpack_files(mrpack_files: &Vec<PolyMrPackFile>) -> LauncherResult<Vec<ModpackFile>> {
|
||||
struct FetchedPackage {
|
||||
version_id: String,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
let mut to_fetch: HashMap<String, FetchedPackage> = HashMap::new();
|
||||
let mut files: Vec<ModpackFile> = Vec::new();
|
||||
|
||||
for file in mrpack_files {
|
||||
if file.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = file
|
||||
.base
|
||||
.path
|
||||
.split('/')
|
||||
.last()
|
||||
.unwrap_or(&file.base.path)
|
||||
.to_string();
|
||||
|
||||
if let Some(url) = file
|
||||
.base
|
||||
.downloads
|
||||
.iter()
|
||||
.find(|url| url.starts_with(super::mrpack::MODRINTH_URL_PREFIX))
|
||||
{
|
||||
// https://cdn.modrinth.com/data/<project_id>/versions/<version_id>/<file_name>
|
||||
let paths = url[super::mrpack::MODRINTH_URL_PREFIX.len()..]
|
||||
.split('/')
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if paths.len() >= 4 {
|
||||
let project_id = paths[1];
|
||||
let version_id = paths[3];
|
||||
|
||||
to_fetch.insert(
|
||||
project_id.to_string(),
|
||||
FetchedPackage {
|
||||
version_id: version_id.to_string(),
|
||||
enabled: file.enabled,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
tracing::error!("invalid modrinth file URL: '{}'", url);
|
||||
}
|
||||
} else {
|
||||
let download_url = file
|
||||
.base
|
||||
.downloads
|
||||
.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
tracing::warn!("mrpack file '{}' does not contain a download URL", name)
|
||||
})
|
||||
.unwrap_or(String::new());
|
||||
|
||||
// the path usually contains the folder name such as "mods" or "resourcepacks"
|
||||
// so we can use it to determine the package type
|
||||
let package_type = file
|
||||
.base
|
||||
.path
|
||||
.split('/')
|
||||
.next()
|
||||
.and_then(|s| PackageType::try_from(s).ok())
|
||||
.unwrap_or(PackageType::Mod);
|
||||
|
||||
files.push(ModpackFile {
|
||||
enabled: file.enabled,
|
||||
kind: ModpackFileKind::External(ExternalPackage {
|
||||
name,
|
||||
url: download_url,
|
||||
sha1: file.base.hashes.sha1.clone(),
|
||||
size: file.base.file_size,
|
||||
package_type,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let managed_packages = Provider::Modrinth
|
||||
.get_multiple(&to_fetch.keys().cloned().collect::<Vec<_>>())
|
||||
.await?;
|
||||
let managed_versions = Provider::Modrinth
|
||||
.get_versions(
|
||||
&to_fetch
|
||||
.values()
|
||||
.map(|f| f.version_id.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut version_map = managed_versions
|
||||
.into_iter()
|
||||
.map(|v| (v.project_id.clone(), v))
|
||||
.collect::<HashMap<String, ManagedVersion>>();
|
||||
|
||||
for fetched_pkg in managed_packages {
|
||||
if let Some(version) = version_map.remove(&fetched_pkg.id) {
|
||||
let enabled = to_fetch
|
||||
.get(&fetched_pkg.id)
|
||||
.map(|f| f.enabled)
|
||||
.unwrap_or(true);
|
||||
|
||||
files.push(ModpackFile {
|
||||
enabled,
|
||||
kind: ModpackFileKind::Managed((fetched_pkg, version)),
|
||||
});
|
||||
} else {
|
||||
tracing::error!("no version found for managed package '{}'", fetched_pkg.id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct PolyMrPackManifest {
|
||||
pub id: String,
|
||||
pub category: String,
|
||||
pub enabled: bool,
|
||||
#[serde(rename = "polyFormat")]
|
||||
pub poly_format_version: i32,
|
||||
pub update_url: Option<String>,
|
||||
|
||||
pub format_version: usize,
|
||||
pub version_id: String,
|
||||
pub name: String,
|
||||
pub files: Vec<PolyMrPackFile>,
|
||||
pub dependencies: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct PolyMrPackFile {
|
||||
#[serde(flatten)]
|
||||
pub base: MrPackFile,
|
||||
pub enabled: bool,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ use crate::api::packages::data::{
|
|||
ManagedPackage, ManagedPackageBody, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery,
|
||||
SearchResult,
|
||||
};
|
||||
use crate::api::packages::modpack::data::ModpackArchive;
|
||||
use crate::api::packages::provider::ProviderExt;
|
||||
use crate::store::processes::Process;
|
||||
use crate::store::{Settings, State};
|
||||
|
|
@ -26,6 +27,7 @@ use onelauncher_entity::prelude::model::*;
|
|||
#[taurpc::procedures(path = "core")]
|
||||
pub trait TauriLauncherApi {
|
||||
// MARK: API: clusters
|
||||
// region: clusters
|
||||
#[taurpc(alias = "getClusters")]
|
||||
async fn get_clusters() -> LauncherResult<Vec<Cluster>>;
|
||||
|
||||
|
|
@ -55,8 +57,10 @@ pub trait TauriLauncherApi {
|
|||
|
||||
#[taurpc(alias = "getLogByName")]
|
||||
async fn get_log_by_name(id: ClusterId, name: String) -> LauncherResult<Option<String>>;
|
||||
// endregion: clusters
|
||||
|
||||
// MARK: API: processes
|
||||
// region: processes
|
||||
#[taurpc(alias = "getRunningProcesses")]
|
||||
async fn get_running_processes() -> LauncherResult<Vec<Process>>;
|
||||
|
||||
|
|
@ -70,8 +74,10 @@ pub trait TauriLauncherApi {
|
|||
|
||||
#[taurpc(alias = "killProcess")]
|
||||
async fn kill_process(pid: u32) -> LauncherResult<()>;
|
||||
// endregion: processes
|
||||
|
||||
// MARK: API: profiles
|
||||
// region: profiles
|
||||
#[taurpc(alias = "getProfileOrDefault")]
|
||||
async fn get_profile_or_default(name: Option<String>) -> LauncherResult<SettingsProfile>;
|
||||
|
||||
|
|
@ -83,15 +89,19 @@ pub trait TauriLauncherApi {
|
|||
name: String,
|
||||
profile: ProfileUpdate,
|
||||
) -> LauncherResult<SettingsProfile>;
|
||||
// endregion: profiles
|
||||
|
||||
// MARK: API: metadata
|
||||
// region: metadata
|
||||
#[taurpc(alias = "getGameVersions")]
|
||||
async fn get_game_versions() -> LauncherResult<Vec<Version>>;
|
||||
|
||||
#[taurpc(alias = "getLoadersForVersion")]
|
||||
async fn get_loaders_for_version(mc_version: String) -> LauncherResult<Vec<GameLoader>>;
|
||||
// endregion: metadata
|
||||
|
||||
// MARK: API: users
|
||||
// region: users
|
||||
#[taurpc(alias = "getUsers")]
|
||||
async fn get_users() -> LauncherResult<Vec<MinecraftCredentials>>;
|
||||
|
||||
|
|
@ -113,15 +123,19 @@ pub trait TauriLauncherApi {
|
|||
async fn open_msa_login<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
) -> LauncherResult<Option<MinecraftCredentials>>;
|
||||
// endregion: users
|
||||
|
||||
// MARK: API: settings
|
||||
// region: settings
|
||||
#[taurpc(alias = "readSettings")]
|
||||
async fn read_settings() -> LauncherResult<Settings>;
|
||||
|
||||
#[taurpc(alias = "writeSettings")]
|
||||
async fn write_settings(setting: Settings) -> LauncherResult<()>;
|
||||
// endregion: settings
|
||||
|
||||
// MARK: API: packages
|
||||
// MARK: API: package
|
||||
// region: package
|
||||
#[taurpc(alias = "searchPackages")]
|
||||
async fn search_packages(
|
||||
provider: Provider,
|
||||
|
|
@ -170,6 +184,13 @@ pub trait TauriLauncherApi {
|
|||
provider: Provider,
|
||||
author: PackageAuthor,
|
||||
) -> LauncherResult<Vec<ManagedUser>>;
|
||||
// endregion: package
|
||||
|
||||
// MARK: API: modpack
|
||||
// region: modpack
|
||||
#[taurpc(alias = "installModpack")]
|
||||
async fn install_modpack(modpack: ModpackArchive, cluster_id: ClusterId) -> LauncherResult<()>;
|
||||
// endregion: modpack
|
||||
|
||||
// MARK: API: minecraft
|
||||
#[taurpc(alias = "fetchMinecraftProfile")]
|
||||
|
|
@ -214,6 +235,7 @@ pub struct TauriLauncherApiImpl;
|
|||
#[taurpc::resolvers]
|
||||
impl TauriLauncherApi for TauriLauncherApiImpl {
|
||||
// MARK: Impl: clusters
|
||||
// region: clusters
|
||||
async fn get_clusters(self) -> LauncherResult<Vec<Cluster>> {
|
||||
api::cluster::dao::get_all_clusters().await
|
||||
}
|
||||
|
|
@ -340,8 +362,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
|
||||
api::cluster::content::get_log_by_name(&cluster, &name).await
|
||||
}
|
||||
// endregion: clusters
|
||||
|
||||
// MARK: Impl: processes
|
||||
// region: processes
|
||||
async fn get_running_processes(self) -> LauncherResult<Vec<Process>> {
|
||||
api::processes::get_running_processes().await
|
||||
}
|
||||
|
|
@ -360,8 +384,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
async fn kill_process(self, pid: u32) -> LauncherResult<()> {
|
||||
api::processes::kill_process(pid).await
|
||||
}
|
||||
// endregion: processes
|
||||
|
||||
// MARK: Impl: profiles
|
||||
// region: profiles
|
||||
async fn get_global_profile(self) -> SettingsProfile {
|
||||
api::setting_profiles::get_global_profile().await
|
||||
}
|
||||
|
|
@ -418,8 +444,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
|
||||
Ok(profile)
|
||||
}
|
||||
// endregion: profiles
|
||||
|
||||
// MARK: Impl: metadata
|
||||
// region: metadata
|
||||
async fn get_loaders_for_version(self, mc_version: String) -> LauncherResult<Vec<GameLoader>> {
|
||||
api::game::metadata::get_loaders_for_version(&mc_version).await
|
||||
}
|
||||
|
|
@ -427,8 +455,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
async fn get_game_versions(self) -> LauncherResult<Vec<Version>> {
|
||||
api::game::metadata::get_game_versions().await
|
||||
}
|
||||
// endregion: metadata
|
||||
|
||||
// MARK: Impl: users
|
||||
// region: users
|
||||
async fn get_users(self) -> LauncherResult<Vec<MinecraftCredentials>> {
|
||||
api::credentials::get_users().await
|
||||
}
|
||||
|
|
@ -516,7 +546,17 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
async fn get_users_from_author(
|
||||
self,
|
||||
provider: Provider,
|
||||
author: PackageAuthor,
|
||||
) -> LauncherResult<Vec<ManagedUser>> {
|
||||
provider.get_users_from_author(author).await
|
||||
}
|
||||
// endregion: users
|
||||
|
||||
// MARK: Impl: settings
|
||||
// region: settings
|
||||
async fn read_settings(self) -> LauncherResult<Settings> {
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
|
|
@ -534,8 +574,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
// endregion: settings
|
||||
|
||||
// MARK: Impl: packages
|
||||
// MARK: Impl: package
|
||||
// region: package
|
||||
async fn search_packages(
|
||||
self,
|
||||
provider: Provider,
|
||||
|
|
@ -604,26 +646,41 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
|
||||
Ok(model)
|
||||
}
|
||||
// endregion: package
|
||||
|
||||
async fn get_users_from_author(
|
||||
// MARK: Impl: modpack
|
||||
// region: modpack
|
||||
async fn install_modpack(
|
||||
self,
|
||||
provider: Provider,
|
||||
author: PackageAuthor,
|
||||
) -> LauncherResult<Vec<ManagedUser>> {
|
||||
provider.get_users_from_author(author).await
|
||||
modpack: ModpackArchive,
|
||||
cluster_id: ClusterId,
|
||||
) -> LauncherResult<()> {
|
||||
let cluster = api::cluster::dao::get_cluster_by_id(cluster_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", cluster_id))?;
|
||||
|
||||
modpack
|
||||
.format
|
||||
.install_modpack_archive(&modpack, &cluster, None, None)
|
||||
.await
|
||||
}
|
||||
// endregion: modpack
|
||||
|
||||
// MARK: Impl: minecraft
|
||||
// region: minecraft
|
||||
async fn fetch_player_profile(
|
||||
self,
|
||||
uuid: String,
|
||||
) -> LauncherResult<crate::utils::minecraft::MojangPlayerProfile> {
|
||||
crate::utils::minecraft::fetch_player_profile(&uuid).await
|
||||
}
|
||||
// endregion: minecraft
|
||||
|
||||
// MARK: Impl: Other
|
||||
// region: other
|
||||
async fn open(self, input: String) -> LauncherResult<()> {
|
||||
opener::open(input)?;
|
||||
Ok(())
|
||||
}
|
||||
// endregion: other
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use tokio::sync::Semaphore;
|
|||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::api::ingress::IngressSendExt;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::error::{LauncherError, LauncherResult};
|
||||
use crate::store::Core;
|
||||
use crate::store::ingress::{SubIngress, SubIngressExt};
|
||||
use crate::store::semaphore::SemaphoreStore;
|
||||
|
|
@ -49,6 +49,48 @@ pub(crate) static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
|||
.expect("failed to build reqwest client!")
|
||||
});
|
||||
|
||||
pub async fn request(
|
||||
method: Method,
|
||||
url: &str,
|
||||
builder: Option<impl Fn(reqwest::RequestBuilder) -> reqwest::Result<reqwest::Request>>,
|
||||
) -> LauncherResult<reqwest::Response> {
|
||||
let semaphore = SemaphoreStore::fetch().await;
|
||||
let permit = semaphore.acquire().await;
|
||||
|
||||
let client = &REQWEST_CLIENT;
|
||||
|
||||
let mut attempt = 0;
|
||||
let res = loop {
|
||||
attempt += 1;
|
||||
|
||||
let req = client.request(method.clone(), url);
|
||||
let req = match builder {
|
||||
Some(ref builder) => builder(req),
|
||||
None => req.build(),
|
||||
}
|
||||
.map_err(LauncherError::from)?;
|
||||
|
||||
match client.execute(req).await {
|
||||
Err(err) => {
|
||||
if attempt <= Core::get().fetch_attempts {
|
||||
tracing::error!(
|
||||
"error occurred whilst fetching on attempt {attempt}: {err} retrying request..."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
return Err(err.into());
|
||||
}
|
||||
Ok(res) => break res,
|
||||
}
|
||||
};
|
||||
|
||||
// Drop the fetch permit as we are no longer fetching
|
||||
drop(permit);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(ingress))]
|
||||
#[onelauncher_macro::pin]
|
||||
pub async fn fetch_advanced(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[toolchain]
|
||||
channel = "nightly-2025-07-23"
|
||||
channel = "nightly-2025-09-12"
|
||||
profile = "minimal"
|
||||
components = [
|
||||
"cargo",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue