feat: oneclient bundles
This commit is contained in:
parent
0a77a31846
commit
1eb6505c0c
12 changed files with 167 additions and 61 deletions
|
|
@ -3,10 +3,11 @@ 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;
|
||||
|
||||
use crate::oneclient::bundles::BundlesManager;
|
||||
|
||||
#[taurpc::procedures(path = "oneclient", export_to = "../frontend/src/bindings.gen.ts")]
|
||||
pub trait OneClientApi {
|
||||
#[taurpc(alias = "openDevTools")]
|
||||
|
|
@ -15,8 +16,8 @@ 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(alias = "getBundlesFor")]
|
||||
async fn get_bundles_for(cluster_id: ClusterId) -> LauncherResult<Vec<ModpackArchive>>;
|
||||
}
|
||||
|
||||
#[taurpc::ipc_type]
|
||||
|
|
@ -54,26 +55,21 @@ 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,
|
||||
}
|
||||
async fn get_bundles_for(self, cluster_id: ClusterId) -> LauncherResult<Vec<ModpackArchive>> {
|
||||
let cluster = onelauncher_core::api::cluster::dao::get_cluster_by_id(cluster_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
onelauncher_core::error::LauncherError::from(anyhow::anyhow!(
|
||||
"cluster with id {} not found",
|
||||
cluster_id
|
||||
))
|
||||
})?;
|
||||
|
||||
// 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,
|
||||
// },
|
||||
// ];
|
||||
let bundles = BundlesManager::get()
|
||||
.await
|
||||
.get_bundles_for(&cluster.mc_version, cluster.mc_loader)
|
||||
.await?;
|
||||
|
||||
Ok(vec![])
|
||||
Ok(bundles)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ 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";
|
||||
"https://raw.githubusercontent.com/PolyFrost/DataStorage/refs/heads/main/oneclient";
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use onelauncher_core::api::packages::modpack::data::ModpackArchive;
|
||||
use onelauncher_core::api::packages::modpack::{InstallableModpackFormatExt, ModpackFormat};
|
||||
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 reqwest::{Method, header};
|
||||
use tokio::sync::{OnceCell, RwLock};
|
||||
|
||||
/// e.g.
|
||||
|
|
@ -54,7 +57,7 @@ impl BundlesManager {
|
|||
loader: onelauncher_core::entity::loader::GameLoader,
|
||||
) -> LauncherResult<Vec<ModpackArchive>> {
|
||||
let manifest = self.manifest.read().await;
|
||||
let mut bundles_lock = self.bundles.write().await;
|
||||
let bundles_lock = self.bundles.read().await;
|
||||
|
||||
if let Some(entry) = bundles_lock.get(mc_version) {
|
||||
if let Some(bundles) = entry.get(&loader) {
|
||||
|
|
@ -62,28 +65,60 @@ impl BundlesManager {
|
|||
}
|
||||
}
|
||||
|
||||
// drop read lock as we're gonna acquire a write lock this time
|
||||
drop(bundles_lock);
|
||||
|
||||
let mut bundles_lock = self.bundles.write().await;
|
||||
|
||||
let mut found = Vec::new();
|
||||
|
||||
// for (version, loaders) in &manifest.versions {
|
||||
// if version != mc_version {
|
||||
// continue;
|
||||
// }
|
||||
for (version, loaders) in &manifest.versions {
|
||||
if version != mc_version {
|
||||
continue;
|
||||
}
|
||||
|
||||
// let Some(paths) = loaders.get(&loader.get_format_name()) else {
|
||||
// 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?;
|
||||
// we will be first checking the disk cache, if that fails we fetch from remote
|
||||
for path in paths {
|
||||
let Some(file_name) = path.split('/').last() else {
|
||||
tracing::error!("no bundle name was found in path: {path}");
|
||||
continue;
|
||||
};
|
||||
|
||||
// // res.
|
||||
let disk_path = BundlesManager::dir().await.join("bundles").join(file_name);
|
||||
|
||||
// let name = path.split('/').last().unwrap_or(&format!("bundle-{}-{}-{}.mrpack"));
|
||||
// }
|
||||
let modpack = match download_and_load_bundle(path, &disk_path).await {
|
||||
Ok(modpack) => modpack,
|
||||
Err(e) => {
|
||||
tracing::error!("failed to load bundle from {path}: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// }
|
||||
let manifest = match modpack.manifest().await {
|
||||
Ok(manifest) => manifest,
|
||||
Err(e) => {
|
||||
tracing::error!("failed to load modpack manifest from {path}: {e}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
.clone();
|
||||
|
||||
found.push(ModpackArchive {
|
||||
manifest,
|
||||
path: disk_path,
|
||||
format: modpack.kind(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bundles_lock
|
||||
.entry(mc_version.to_string())
|
||||
.or_default()
|
||||
.insert(loader, found.clone());
|
||||
|
||||
Ok(found.clone())
|
||||
}
|
||||
|
|
@ -96,6 +131,12 @@ impl BundlesManager {
|
|||
|
||||
match http::fetch_json::<BundleManifest>(Method::GET, &url, None, None).await {
|
||||
Ok(manifest) => {
|
||||
io::create_dir_all(manifest_path.parent().unwrap())
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::error!("failed to create bundles dir: {e}");
|
||||
});
|
||||
|
||||
if let Err(e) = io::write_json(&manifest_path, &manifest).await {
|
||||
send_error!("failed to cache bundles manifest to disk: {e}");
|
||||
}
|
||||
|
|
@ -133,3 +174,49 @@ impl BundlesManager {
|
|||
.join("bundles")
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn download_and_load_bundle(
|
||||
url_path: &str,
|
||||
disk_path: &PathBuf,
|
||||
) -> LauncherResult<Box<dyn InstallableModpackFormatExt>> {
|
||||
let url = format!("{}{}", crate::constants::META_URL_BASE, url_path);
|
||||
|
||||
if disk_path.exists() {
|
||||
// we check if the remote file is different to the local file
|
||||
let res = http::request(Method::HEAD, &url).await?;
|
||||
|
||||
if !res.status().is_success() {
|
||||
return Err(anyhow::anyhow!("failed to download bundle from remote: {}", url).into());
|
||||
}
|
||||
|
||||
if res.headers().get(reqwest::header::CONTENT_LENGTH).is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"bundle at {url} missing content-length header, skipping..."
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// TODO: check hash if provided in future, for now we check file size :(
|
||||
let content_length = res
|
||||
.headers()
|
||||
.get(header::CONTENT_LENGTH)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let file_size = io::stat(disk_path).await.map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
tracing::debug!("bundle content length: {content_length}, local file size: {file_size}");
|
||||
if content_length == file_size {
|
||||
// file is up to date, load from disk
|
||||
return Ok(ModpackFormat::from_file(disk_path).await?);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("downloading bundle from remote: {url}");
|
||||
// if we are at this point, it means we either need to update or download
|
||||
let bytes = http::download(Method::GET, &url, disk_path, None, None).await?;
|
||||
|
||||
Ok(ModpackFormat::from_bytes(Arc::new(bytes.to_vec())).await?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,5 +2,7 @@ pub mod bundles;
|
|||
pub mod clusters;
|
||||
|
||||
pub async fn initialize_oneclient() {
|
||||
clusters::init_clusters().await;
|
||||
if let Err(err) = clusters::init_clusters().await {
|
||||
tracing::error!("failed to initialize clusters: {err}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ export type VersionType =
|
|||
*/
|
||||
"old_beta"
|
||||
|
||||
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"]}' }
|
||||
const ARGS_MAP = { 'core':'{"open":["input"],"setDefaultUser":["uuid"],"getScreenshots":["id"],"getClusters":[],"getClusterById":["id"],"getDefaultUser":["fallback"],"removeCluster":["id"],"getProfileOrDefault":["name"],"getRunningProcessesByClusterId":["cluster_id"],"getWorlds":["id"],"fetchMinecraftProfile":["uuid"],"getUser":["uuid"],"getLogByName":["id","name"],"writeSettings":["setting"],"installModpack":["modpack","cluster_id"],"killProcess":["pid"],"updateClusterProfile":["name","profile"],"getUsersFromAuthor":["provider","author"],"getUsers":[],"readSettings":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getPackage":["provider","slug"],"updateClusterById":["id","request"],"getLogs":["id"],"getGameVersions":[],"getGlobalProfile":[],"searchPackages":["provider","query"],"getRunningProcesses":[],"isClusterRunning":["cluster_id"],"getMultiplePackages":["provider","slugs"],"getLoadersForVersion":["mc_version"],"openMsaLogin":[],"removeUser":["uuid"],"createCluster":["options"],"getPackageBody":["provider","body"],"launchCluster":["id","uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[],"getBundlesFor":["cluster_id"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'events':'{"ingress":["event"],"process":["event"],"message":["event"]}' }
|
||||
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
getClusterById: (id: number) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: number) => Promise<null>,
|
||||
|
|
@ -282,14 +282,14 @@ getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<Manag
|
|||
installModpack: (modpack: ModpackArchive, clusterId: number) => Promise<null>,
|
||||
fetchMinecraftProfile: (uuid: string) => Promise<MojangPlayerProfile>,
|
||||
open: (input: string) => Promise<null> },
|
||||
'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> },
|
||||
'oneclient': { openDevTools: () => Promise<void>,
|
||||
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>>,
|
||||
getModpacksFor: (clusterId: number) => Promise<ModpackArchive[]> } };
|
||||
getBundlesFor: (clusterId: number) => Promise<ModpackArchive[]> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> } };
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
|
|
@ -1,15 +1,8 @@
|
|||
import { SheetPage } from '@/components/SheetPage';
|
||||
import { bindings } from '@/main';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/app/cluster/overview')({
|
||||
component: RouteComponent,
|
||||
async beforeLoad(ctx) {
|
||||
const cluster = await bindings.core.getClusterById(ctx.search.clusterId);
|
||||
return {
|
||||
cluster,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
|
|
@ -21,7 +14,7 @@ function RouteComponent() {
|
|||
<h1 className="text-2xl font-semibold">Cluster Overview</h1>
|
||||
<p>Welcome to the cluster overview page. Here you can find information about your clusters.</p>
|
||||
<div style={{ height: '1500px' }}>
|
||||
|
||||
Cluster Name: {cluster.name}
|
||||
</div>
|
||||
<p>test</p>
|
||||
{/* Additional content can be added here */}
|
||||
|
|
|
|||
|
|
@ -80,10 +80,12 @@ pub trait InstallableModpackFormatExt: Send + Sync + std::any::Any {
|
|||
) -> LauncherResult<()>;
|
||||
|
||||
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any + Send + Sync>;
|
||||
|
||||
fn kind(&self) -> ModpackFormat;
|
||||
}
|
||||
|
||||
impl ModpackFormat {
|
||||
pub async fn from_file(path: PathBuf) -> LauncherResult<Box<dyn InstallableModpackFormatExt>> {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -140,6 +140,10 @@ impl InstallableModpackFormatExt for MrPackFormatImpl {
|
|||
self
|
||||
}
|
||||
|
||||
fn kind(&self) -> super::ModpackFormat {
|
||||
super::ModpackFormat::MrPack
|
||||
}
|
||||
|
||||
async fn manifest(&self) -> LauncherResult<&ModpackManifest> {
|
||||
if let Some(manifest) = self.manifest.get() {
|
||||
return Ok(manifest);
|
||||
|
|
@ -210,6 +214,10 @@ pub(super) async fn download_and_link_packages(
|
|||
|
||||
// TODO: Ingress
|
||||
for file in manifest.files.iter() {
|
||||
if file.enabled == false {
|
||||
continue;
|
||||
}
|
||||
|
||||
match &file.kind {
|
||||
ModpackFileKind::Managed((package, version)) => {
|
||||
match api::packages::download_package(package, version, None, None).await {
|
||||
|
|
|
|||
|
|
@ -137,6 +137,10 @@ impl InstallableModpackFormatExt for PolyMrPackFormatImpl {
|
|||
self
|
||||
}
|
||||
|
||||
fn kind(&self) -> super::ModpackFormat {
|
||||
super::ModpackFormat::PolyMrPack
|
||||
}
|
||||
|
||||
async fn manifest(&self) -> LauncherResult<&ModpackManifest> {
|
||||
if let Some(manifest) = self.manifest.get() {
|
||||
return Ok(manifest);
|
||||
|
|
@ -301,8 +305,8 @@ pub(super) struct PolyMrPackManifest {
|
|||
pub id: String,
|
||||
pub category: String,
|
||||
pub enabled: bool,
|
||||
#[serde(rename = "polyFormat")]
|
||||
pub poly_format_version: i32,
|
||||
// #[serde(rename = "polyFormat")]
|
||||
// pub poly_format_version: i32,
|
||||
pub update_url: Option<String>,
|
||||
|
||||
pub format_version: usize,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ pub async fn test_install_modpack() -> LauncherResult<()> {
|
|||
)
|
||||
.await?;
|
||||
|
||||
let modpack_format = ModpackFormat::from_file(modpack_model.path().await?).await?;
|
||||
let modpack_format = ModpackFormat::from_file(&modpack_model.path().await?).await?;
|
||||
api::packages::modpack::install_managed_modpack(
|
||||
&modpack_model,
|
||||
&modpack_format,
|
||||
|
|
|
|||
|
|
@ -49,7 +49,16 @@ pub(crate) static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
|||
.expect("failed to build reqwest client!")
|
||||
});
|
||||
|
||||
pub async fn request(
|
||||
pub async fn request(method: Method, url: &str) -> LauncherResult<reqwest::Response> {
|
||||
request_builder(
|
||||
method,
|
||||
url,
|
||||
None::<fn(reqwest::RequestBuilder) -> reqwest::Result<_>>,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn request_builder(
|
||||
method: Method,
|
||||
url: &str,
|
||||
builder: Option<impl Fn(reqwest::RequestBuilder) -> reqwest::Result<reqwest::Request>>,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use std::fs::Metadata;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -466,3 +467,7 @@ pub fn sanitize_path(path: impl AsRef<std::path::Path>) -> PathBuf {
|
|||
.map(sanitize_filename::sanitize)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn stat(path: impl AsRef<std::path::Path>) -> Result<Metadata, IOError> {
|
||||
tokio::fs::metadata(path).await.map_err(IOError::from)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue