feat: mrpack support
This commit is contained in:
parent
b4746b8ec6
commit
33c865893f
18 changed files with 1173 additions and 371 deletions
|
|
@ -1,5 +1,13 @@
|
|||
use onelauncher_core::{api::{cluster, packages::{self, data::{SearchQuery, Sort}, download_package, provider::ProviderExt}, proxy::ProxyDynamic}, error::LauncherResult, initialize_core, store::{CoreOptions, Dirs}};
|
||||
use onelauncher_entity::{loader::GameLoader, package::Provider};
|
||||
use onelauncher_core::api::cluster;
|
||||
use onelauncher_core::api::packages::data::{SearchQuery, Sort};
|
||||
use onelauncher_core::api::packages::provider::ProviderExt;
|
||||
use onelauncher_core::api::packages::{self, download_package};
|
||||
use onelauncher_core::api::proxy::ProxyDynamic;
|
||||
use onelauncher_core::error::LauncherResult;
|
||||
use onelauncher_core::initialize_core;
|
||||
use onelauncher_core::store::{CoreOptions, Dirs};
|
||||
use onelauncher_entity::loader::GameLoader;
|
||||
use onelauncher_entity::package::Provider;
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() -> LauncherResult<()> {
|
||||
|
|
@ -20,29 +28,44 @@ pub async fn main() -> LauncherResult<()> {
|
|||
// Fetch our package and its versions
|
||||
let provider = Provider::Modrinth;
|
||||
|
||||
println!("search results: {:#?}", provider.search(&SearchQuery {
|
||||
query: Some("chatting".to_string()),
|
||||
offset: Some(0),
|
||||
limit: Some(10),
|
||||
sort: Some(Sort::Newest),
|
||||
filters: None,
|
||||
}).await?);
|
||||
println!(
|
||||
"search results: {:#?}",
|
||||
provider
|
||||
.search(&SearchQuery {
|
||||
query: Some("chatting".to_string()),
|
||||
offset: Some(0),
|
||||
limit: Some(10),
|
||||
sort: Some(Sort::Newest),
|
||||
filters: None,
|
||||
})
|
||||
.await?
|
||||
);
|
||||
|
||||
let package = &provider.get("chatting").await?;
|
||||
let versions = provider.get_versions(package.version_ids.as_slice()).await?;
|
||||
let versions = provider
|
||||
.get_versions(package.version_ids.as_slice())
|
||||
.await?;
|
||||
|
||||
let version = versions.iter().find(|v| v.mc_versions.contains(&"1.8.9".to_string())).expect("Version supporting 1.8.9 not found");
|
||||
let version = versions
|
||||
.iter()
|
||||
.find(|v| v.mc_versions.contains(&"1.8.9".to_string()))
|
||||
.expect("Version supporting 1.8.9 not found");
|
||||
|
||||
// We can now download the package
|
||||
let model = download_package(package, version, None).await?;
|
||||
let model = download_package(package, version, Some(true), None).await?;
|
||||
|
||||
// Link the package to the cluster
|
||||
packages::link_package(&model, &cluster, None).await?;
|
||||
|
||||
// Check if the package is hard linked
|
||||
let cluster_dir = dirs.clusters_dir().join(cluster.folder_name.clone());
|
||||
let pkg_path = cluster_dir.join(model.package_type.folder_name()).join(model.file_name.as_str());
|
||||
assert!(tokio::fs::metadata(&pkg_path).await.is_ok(), "Package not found in cluster directory");
|
||||
let pkg_path = cluster_dir
|
||||
.join(model.package_type.folder_name())
|
||||
.join(model.file_name.as_str());
|
||||
assert!(
|
||||
tokio::fs::metadata(&pkg_path).await.is_ok(),
|
||||
"Package not found in cluster directory"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use crate::api::{java, setting_profiles};
|
|||
use crate::error::LauncherResult;
|
||||
use crate::store::ingress::{IngressType, SubIngress};
|
||||
use crate::store::{Dirs, State};
|
||||
use crate::utils::DatabaseModelExt;
|
||||
use crate::utils::io::{self, IOError};
|
||||
|
||||
pub mod content;
|
||||
|
|
@ -30,6 +31,10 @@ use super::ingress::init_ingress;
|
|||
pub enum ClusterError {
|
||||
#[error("version '{0}' was not found")]
|
||||
InvalidVersion(String),
|
||||
#[error("version '{0}' is not compatible with version '{1}'")]
|
||||
MismatchedVersion(String, String),
|
||||
#[error("loader '{0}' is not compatible with loader '{1}'")]
|
||||
MismatchedLoader(GameLoader, GameLoader),
|
||||
#[error("failed to imply java version")]
|
||||
MissingJavaVersion,
|
||||
#[error("cluster is in downloading stage")]
|
||||
|
|
@ -347,3 +352,10 @@ pub async fn update_playtime(id: ClusterId, duration: i64) -> LauncherResult<clu
|
|||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl DatabaseModelExt for clusters::Model {
|
||||
async fn path(&self) -> LauncherResult<std::path::PathBuf> {
|
||||
Ok(Dirs::get_clusters_dir().await?.join(&self.folder_name))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use onelauncher_entity::loader::GameLoader;
|
||||
use onelauncher_entity::package::PackageType;
|
||||
use onelauncher_entity::{cluster_packages, clusters, packages};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, TryInsertResult,
|
||||
};
|
||||
|
||||
use crate::error::{DaoError, LauncherResult};
|
||||
use crate::store::State;
|
||||
|
|
@ -12,14 +14,6 @@ pub type PackageId = String;
|
|||
|
||||
/// Inserts a new package into the database.
|
||||
pub async fn insert_package(package: packages::ActiveModel) -> LauncherResult<packages::Model> {
|
||||
if package
|
||||
.package_type
|
||||
.try_as_ref()
|
||||
.is_some_and(|p| p == &PackageType::ModPack)
|
||||
{
|
||||
return Err(PackageError::IsModPack.into());
|
||||
}
|
||||
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
|
|
@ -47,6 +41,36 @@ pub async fn link_package_to_cluster(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Links multiple packages to a cluster in database. Returns the number of packages linked.
|
||||
pub async fn link_many_packages_to_cluster(
|
||||
packages: &[packages::Model],
|
||||
cluster: &clusters::Model,
|
||||
) -> LauncherResult<u64> {
|
||||
let state = State::get().await?;
|
||||
let db = &state.db;
|
||||
|
||||
let cluster_packages = packages
|
||||
.iter()
|
||||
.map(|package| {
|
||||
cluster_packages::Model {
|
||||
cluster_id: cluster.id,
|
||||
package_hash: package.hash.clone(),
|
||||
}
|
||||
.into_active_model()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let inserted = cluster_packages::Entity::insert_many(cluster_packages)
|
||||
.on_conflict_do_nothing()
|
||||
.exec_without_returning(db)
|
||||
.await?;
|
||||
|
||||
Ok(match inserted {
|
||||
TryInsertResult::Inserted(rows) => rows,
|
||||
_ => 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves a clusters linked packages from the database.
|
||||
pub async fn get_linked_packages(
|
||||
cluster: &clusters::Model,
|
||||
|
|
|
|||
|
|
@ -77,6 +77,16 @@ pub struct SearchResult {
|
|||
pub gallery: Vec<String>,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ExternalPackage {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub sha1: String,
|
||||
pub size: usize,
|
||||
pub package_type: PackageType,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use data::{ManagedPackage, ManagedVersion};
|
||||
use onelauncher_entity::icon::Icon;
|
||||
use onelauncher_entity::package::{PackageType, Provider};
|
||||
use onelauncher_entity::package::Provider;
|
||||
use onelauncher_entity::{clusters, packages};
|
||||
use reqwest::Method;
|
||||
|
||||
|
|
@ -10,11 +10,12 @@ use crate::store::Dirs;
|
|||
use crate::store::ingress::SubIngress;
|
||||
use crate::utils::crypto::HashAlgorithm;
|
||||
use crate::utils::io::IOError;
|
||||
use crate::utils::{http, icon, io};
|
||||
use crate::utils::{DatabaseModelExt, http, icon, io};
|
||||
|
||||
pub mod categories;
|
||||
pub mod dao;
|
||||
pub mod data;
|
||||
pub mod modpack;
|
||||
pub mod provider;
|
||||
|
||||
#[onelauncher_macro::error]
|
||||
|
|
@ -22,8 +23,8 @@ pub mod provider;
|
|||
pub enum PackageError {
|
||||
#[error("no file marked for download")]
|
||||
NoPrimaryFile,
|
||||
#[error("package is a modpack, it must be handled differently")]
|
||||
IsModPack,
|
||||
#[error("package is not a modpack")]
|
||||
IsNotModPack,
|
||||
#[error(transparent)]
|
||||
Incompatible(#[from] IncompatiblePackageType),
|
||||
#[error("missing API key for provider '{0}'")]
|
||||
|
|
@ -32,6 +33,8 @@ pub enum PackageError {
|
|||
UnsupportedBodyType(ManagedPackageBody),
|
||||
#[error("unsupported author type '{0}'")]
|
||||
UnsupportedAuthorType(PackageAuthor),
|
||||
#[error("unsupported modpack format")]
|
||||
UnsupportedModpackFormat,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::error]
|
||||
|
|
@ -48,13 +51,9 @@ pub enum IncompatiblePackageType {
|
|||
pub async fn download_package(
|
||||
package: &ManagedPackage,
|
||||
version: &ManagedVersion,
|
||||
force: Option<bool>,
|
||||
ingress: Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<packages::Model> {
|
||||
// Modpacks are downloaded and managed differently.
|
||||
if package.package_type == PackageType::ModPack {
|
||||
return Err(PackageError::IsModPack.into());
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"downloading package '{}' version '{}' from '{}'",
|
||||
package.name,
|
||||
|
|
@ -70,39 +69,14 @@ pub async fn download_package(
|
|||
.find(|f| f.primary)
|
||||
.ok_or(PackageError::NoPrimaryFile)?;
|
||||
|
||||
if let Some(model) = dao::get_package_by_hash(primary_file.sha1.clone()).await? {
|
||||
if !force.unwrap_or(false)
|
||||
&& let Some(model) = dao::get_package_by_hash(primary_file.sha1.clone()).await?
|
||||
{
|
||||
tracing::debug!("package is already downloaded");
|
||||
return Ok(model);
|
||||
}
|
||||
|
||||
let dir = Dirs::get_package_dir(&package.package_type, &package.provider, &package.id).await?;
|
||||
|
||||
io::create_dir_all(&dir).await?;
|
||||
|
||||
let dest = join_package_file(dir, &version.version_id, primary_file.file_name.as_str());
|
||||
|
||||
http::download_advanced(
|
||||
Method::GET,
|
||||
&primary_file.url,
|
||||
dest,
|
||||
None,
|
||||
None,
|
||||
Some((HashAlgorithm::Sha1, primary_file.sha1.as_str())),
|
||||
ingress.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let icon = if let Some(icon_url) = &package.icon_url {
|
||||
if let Some(icon) = Icon::try_from_url(&url::Url::parse(icon_url)?) {
|
||||
Some(icon::cache_icon(&icon).await?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let model = packages::Model {
|
||||
let mut model = packages::Model {
|
||||
hash: primary_file.sha1.clone(),
|
||||
display_name: package.name.clone(),
|
||||
display_version: version.display_version.clone(),
|
||||
|
|
@ -110,16 +84,83 @@ pub async fn download_package(
|
|||
version_id: version.version_id.clone(),
|
||||
published_at: version.published,
|
||||
provider: package.provider.clone(),
|
||||
icon,
|
||||
icon: None,
|
||||
package_id: package.id.clone(),
|
||||
mc_loader: version.loaders.clone().into(),
|
||||
mc_versions: version.mc_versions.clone().into(),
|
||||
package_type: package.package_type.clone(),
|
||||
};
|
||||
|
||||
http::download_advanced(
|
||||
Method::GET,
|
||||
&primary_file.url,
|
||||
model.path().await?,
|
||||
None,
|
||||
None,
|
||||
Some((HashAlgorithm::Sha1, primary_file.sha1.as_str())),
|
||||
ingress.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(icon_url) = &package.icon_url {
|
||||
if let Some(icon) = Icon::try_from_url(&url::Url::parse(icon_url)?) {
|
||||
model.icon = Some(icon::cache_icon(&icon).await?);
|
||||
}
|
||||
}
|
||||
|
||||
dao::insert_package(model.into()).await
|
||||
}
|
||||
|
||||
/// Downloads a package and conditionally returns the database entry if it's hash was already registered.
|
||||
pub async fn download_external_package(
|
||||
package: &data::ExternalPackage,
|
||||
cluster: &clusters::Model,
|
||||
force: Option<bool>,
|
||||
skip_compatibility: Option<bool>,
|
||||
ingress: Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<Option<packages::Model>> {
|
||||
tracing::debug!(
|
||||
"downloading external package '{}' from '{}'",
|
||||
package.name,
|
||||
package.url
|
||||
);
|
||||
|
||||
// check if already downloaded
|
||||
if !force.unwrap_or(false)
|
||||
&& let Some(model) = dao::get_package_by_hash(package.sha1.clone()).await?
|
||||
{
|
||||
tracing::debug!(
|
||||
"external package is already downloaded as '{}'",
|
||||
model.display_name
|
||||
);
|
||||
|
||||
// considering that this function downloads packages to a cluster,
|
||||
// we'll hard-link the already downloaded package to the cluster
|
||||
// as to keep functionality consistent
|
||||
link_package(&model, cluster, skip_compatibility).await?;
|
||||
|
||||
return Ok(Some(model));
|
||||
}
|
||||
|
||||
let dir = Dirs::get_clusters_dir()
|
||||
.await?
|
||||
.join(cluster.folder_name.clone())
|
||||
.join(package.package_type.folder_name());
|
||||
|
||||
http::download_advanced(
|
||||
Method::GET,
|
||||
&package.url,
|
||||
dir,
|
||||
None,
|
||||
None,
|
||||
Some((HashAlgorithm::Sha1, &package.sha1)),
|
||||
ingress.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Links a package to a cluster on the file system and in database.
|
||||
/// * `skip_compatibility` - Checks whether the `mc_loader` and `mc_version` is compatible. Default is false
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
|
|
@ -128,10 +169,6 @@ pub async fn link_package(
|
|||
cluster: &clusters::Model,
|
||||
skip_compatibility: Option<bool>,
|
||||
) -> LauncherResult<()> {
|
||||
if package.package_type == PackageType::ModPack {
|
||||
return Err(PackageError::IsModPack.into());
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"linking package '{}' version '{}' to cluster '{}'",
|
||||
package.display_name,
|
||||
|
|
@ -168,18 +205,7 @@ pub async fn link_package(
|
|||
}
|
||||
}
|
||||
|
||||
let dirs = Dirs::get().await?;
|
||||
let cluster_dir = dirs.clusters_dir().join(cluster.folder_name.clone());
|
||||
|
||||
tracing::trace!("hard linking package to cluster");
|
||||
let src_pkg = path_from_model(dirs, package);
|
||||
let dest_dir = cluster_dir.join(package.package_type.folder_name());
|
||||
io::create_dir_all(&dest_dir).await?;
|
||||
|
||||
let dest_pkg = dest_dir.join(package.file_name.as_str());
|
||||
tokio::fs::hard_link(src_pkg, dest_pkg)
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
hard_link(package, cluster).await?;
|
||||
|
||||
dao::link_package_to_cluster(package, cluster).await?;
|
||||
|
||||
|
|
@ -192,18 +218,74 @@ pub async fn link_package(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn path_from_model(dirs: &Dirs, model: &packages::Model) -> std::path::PathBuf {
|
||||
join_package_file(
|
||||
dirs.package_dir(&model.package_type, &model.provider, &model.package_id),
|
||||
&model.version_id,
|
||||
model.file_name.as_str(),
|
||||
)
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub async fn link_many_packages_to_cluster(
|
||||
packages: &[packages::Model],
|
||||
cluster: &clusters::Model,
|
||||
skip_compatibility: Option<bool>,
|
||||
) -> LauncherResult<u64> {
|
||||
tracing::debug!(
|
||||
"linking {} packages to cluster '{}'",
|
||||
packages.len(),
|
||||
cluster.name
|
||||
);
|
||||
|
||||
let compatible_packages = if skip_compatibility.unwrap_or(false) {
|
||||
packages.to_vec()
|
||||
} else {
|
||||
packages
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.mc_loader
|
||||
.iter()
|
||||
.any(|v| cluster.mc_loader.compatible_with(v))
|
||||
&& p.mc_versions.iter().any(|v| cluster.mc_version.contains(v))
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
for package in compatible_packages.iter() {
|
||||
hard_link(package, cluster).await?;
|
||||
}
|
||||
|
||||
dao::link_many_packages_to_cluster(packages, cluster).await?;
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn join_package_file(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
version_id: &str,
|
||||
file_name: &str,
|
||||
) -> std::path::PathBuf {
|
||||
path.as_ref().join(version_id).join(file_name)
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn hard_link(package: &packages::Model, cluster: &clusters::Model) -> LauncherResult<()> {
|
||||
let cluster_dir = Dirs::get_clusters_dir()
|
||||
.await?
|
||||
.join(cluster.folder_name.clone());
|
||||
|
||||
tracing::trace!(
|
||||
"hard linking package '{}' to cluster '{}'",
|
||||
package.display_name,
|
||||
cluster.name
|
||||
);
|
||||
let src_pkg = package.path().await?;
|
||||
let dest_dir = cluster_dir.join(package.package_type.folder_name());
|
||||
io::create_dir_all(&dest_dir).await?;
|
||||
|
||||
let dest_pkg = dest_dir.join(package.file_name.as_str());
|
||||
tokio::fs::hard_link(src_pkg, dest_pkg)
|
||||
.await
|
||||
.map_err(IOError::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl DatabaseModelExt for packages::Model {
|
||||
async fn path(&self) -> LauncherResult<std::path::PathBuf> {
|
||||
Ok(Dirs::get_packages_dir()
|
||||
.await?
|
||||
.join(&self.package_type.folder_name())
|
||||
.join(&self.provider.name())
|
||||
.join(&self.package_id)
|
||||
.join(&self.version_id)
|
||||
.join(&self.file_name))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
packages/core/src/api/packages/modpack/data.rs
Normal file
22
packages/core/src/api/packages/modpack/data.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use onelauncher_entity::loader::GameLoader;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::packages::data::{ExternalPackage, ManagedPackage, ManagedVersion};
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ModpackManifest {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub loader: GameLoader,
|
||||
pub loader_version: String,
|
||||
pub mc_version: String,
|
||||
pub files: Vec<ModpackFile>,
|
||||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ModpackFile {
|
||||
Managed((ManagedPackage, ManagedVersion)),
|
||||
External(ExternalPackage),
|
||||
}
|
||||
112
packages/core/src/api/packages/modpack/mod.rs
Normal file
112
packages/core/src/api/packages/modpack/mod.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use onelauncher_entity::package::PackageType;
|
||||
use onelauncher_entity::{clusters, packages};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
|
||||
use crate::api::packages::PackageError;
|
||||
use crate::api::packages::modpack::mrpack::MrPackFormatImpl;
|
||||
use crate::api::{self};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::ingress::SubIngress;
|
||||
|
||||
pub mod data;
|
||||
mod mrpack;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModpackFormat {
|
||||
CurseForge,
|
||||
MrPack,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ModpackFormatExt {
|
||||
async fn from_file(
|
||||
path: PathBuf,
|
||||
) -> LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
async fn from_bytes(
|
||||
bytes: Arc<Vec<u8>>,
|
||||
) -> LauncherResult<Option<Box<dyn InstallableModpackFormatExt>>>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait InstallableModpackFormatExt: Send + Sync + std::any::Any {
|
||||
async fn manifest(&self) -> LauncherResult<&data::ModpackManifest>;
|
||||
async fn install_to(
|
||||
&self,
|
||||
cluster: &clusters::Model,
|
||||
skip_compatibility: Option<bool>,
|
||||
ingress: Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<()>;
|
||||
|
||||
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 {
|
||||
if let Some(format) = stage(path.clone()).await? {
|
||||
return Ok(format);
|
||||
}
|
||||
}
|
||||
|
||||
Err(PackageError::UnsupportedModpackFormat.into())
|
||||
}
|
||||
|
||||
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 {
|
||||
if let Some(format) = stage(Arc::clone(&bytes)).await? {
|
||||
return Ok(format);
|
||||
}
|
||||
}
|
||||
|
||||
Err(PackageError::UnsupportedModpackFormat.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs a modpack to a cluster.
|
||||
pub async fn install_managed_modpack(
|
||||
package_model: &packages::Model,
|
||||
modpack: &Box<dyn InstallableModpackFormatExt>,
|
||||
cluster: &mut clusters::Model,
|
||||
skip_compatibility: Option<bool>,
|
||||
ingress: Option<SubIngress<'_>>,
|
||||
) -> LauncherResult<()> {
|
||||
if package_model.package_type != PackageType::ModPack {
|
||||
return Err(PackageError::IsNotModPack.into());
|
||||
}
|
||||
|
||||
modpack
|
||||
.install_to(cluster, skip_compatibility, ingress)
|
||||
.await?;
|
||||
|
||||
api::cluster::dao::update_cluster(cluster, async |mut c| {
|
||||
c.linked_modpack_hash = Set(Some(package_model.hash.clone()));
|
||||
Ok(c)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
343
packages/core/src/api/packages/modpack/mrpack.rs
Normal file
343
packages/core/src/api/packages/modpack/mrpack.rs
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
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::cluster::ClusterError;
|
||||
use crate::api::packages::data::{ExternalPackage, PackageSide};
|
||||
use crate::api::packages::modpack::data::{ModpackFile, ModpackManifest};
|
||||
use crate::api::packages::modpack::{InstallableModpackFormatExt, ModpackFormatExt};
|
||||
use crate::api::packages::provider::ProviderExt;
|
||||
use crate::api::{self};
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::ingress::SubIngress;
|
||||
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,
|
||||
}
|
||||
|
||||
const MODRINTH_URL_PREFIX: &str = "https://cdn.modrinth.com/";
|
||||
|
||||
// TODO: move from_file fetching to manifest function
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ModpackFormatExt for MrPackFormatImpl {
|
||||
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: MrPackManifest = match serde_json::from_slice(&bytes) {
|
||||
Ok(manifest) => manifest,
|
||||
Err(e) => {
|
||||
tracing::debug!("failed to deserialize modpack as mrpack: {}", 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!("mrpack manifest does not contain a minecraft version");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if loader.is_none() {
|
||||
tracing::error!("mrpack manifest does not contain a valid game loader");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if loader_version.is_none() {
|
||||
tracing::error!("mrpack 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_trait::async_trait]
|
||||
impl InstallableModpackFormatExt for MrPackFormatImpl {
|
||||
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 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 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?;
|
||||
|
||||
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 {
|
||||
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?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MrPackManifest {
|
||||
pub format_version: usize,
|
||||
pub version_id: String,
|
||||
pub name: String,
|
||||
pub files: Vec<MrPackFile>,
|
||||
pub dependencies: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MrPackFile {
|
||||
pub path: String,
|
||||
pub hashes: MrPackFileHash,
|
||||
#[serde(default)]
|
||||
pub env: MrPackFileEnv,
|
||||
pub downloads: Vec<String>,
|
||||
pub file_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MrPackFileHash {
|
||||
pub sha1: String,
|
||||
// pub sha512: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MrPackFileEnv {
|
||||
pub client: PackageSide,
|
||||
pub server: PackageSide,
|
||||
}
|
||||
88
packages/core/src/api/packages/modpack/tests.rs
Normal file
88
packages/core/src/api/packages/modpack/tests.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
use std::sync::LazyLock;
|
||||
|
||||
use onelauncher_entity::package::{PackageType, Provider};
|
||||
|
||||
use crate::api::packages::DatabaseModelExt;
|
||||
use crate::api::packages::modpack::ModpackFormat;
|
||||
use crate::api::packages::provider::ProviderExt;
|
||||
use crate::api::{self};
|
||||
use crate::error::{LauncherError, LauncherResult};
|
||||
|
||||
use super::super::provider::tests::init;
|
||||
|
||||
const DATA: LazyLock<Vec<(ModpackFormat, Vec<Entry>)>> = LazyLock::new(|| {
|
||||
vec![(
|
||||
ModpackFormat::MrPack,
|
||||
vec![Entry {
|
||||
slug: "sop",
|
||||
id: "BYfVnHa7",
|
||||
version_id: "nciq4FNy",
|
||||
provider: Provider::Modrinth,
|
||||
}],
|
||||
)]
|
||||
});
|
||||
|
||||
struct Entry {
|
||||
pub id: &'static str,
|
||||
pub slug: &'static str,
|
||||
pub version_id: &'static str,
|
||||
pub provider: Provider,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_install_modpack() -> LauncherResult<()> {
|
||||
let cf = init!();
|
||||
|
||||
for (format_kind, entries) in DATA.iter() {
|
||||
if format_kind == &ModpackFormat::CurseForge && !cf {
|
||||
continue;
|
||||
}
|
||||
|
||||
for entry in entries {
|
||||
let provider = &entry.provider;
|
||||
|
||||
let modpack_pkg = provider.get(entry.id).await?;
|
||||
|
||||
assert_eq!(modpack_pkg.slug, entry.slug);
|
||||
assert_eq!(modpack_pkg.id, entry.id);
|
||||
assert_eq!(modpack_pkg.package_type, PackageType::ModPack);
|
||||
|
||||
let modpack_version = provider
|
||||
.get_versions(&[entry.version_id.to_string()])
|
||||
.await?;
|
||||
let modpack_version = modpack_version.first().ok_or_else(|| {
|
||||
LauncherError::from(anyhow::anyhow!("no version found for {}", entry.version_id))
|
||||
})?;
|
||||
|
||||
assert_eq!(modpack_version.version_id, entry.version_id);
|
||||
|
||||
let modpack_model =
|
||||
api::packages::download_package(&modpack_pkg, &modpack_version, None, None).await?;
|
||||
let mut cluster = api::cluster::create_cluster(
|
||||
"Modpack Format Test",
|
||||
modpack_version.mc_versions.first().unwrap(),
|
||||
*modpack_version.loaders.first().unwrap(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let modpack_format = ModpackFormat::from_file(modpack_model.path().await?).await?;
|
||||
api::packages::modpack::install_managed_modpack(
|
||||
&modpack_model,
|
||||
&modpack_format,
|
||||
&mut cluster,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
cluster.linked_modpack_hash,
|
||||
Some(modpack_model.hash.clone())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ use crate::utils::pagination::Paginated;
|
|||
use super::data::{ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub(crate) mod tests;
|
||||
|
||||
mod curseforge;
|
||||
mod modrinth;
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ use onelauncher_entity::package::{PackageType, Provider};
|
|||
use crate::api::packages;
|
||||
use crate::api::packages::provider::ProviderExt;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::{Core, CoreOptions, Dirs};
|
||||
use crate::utils::DatabaseModelExt;
|
||||
|
||||
macro_rules! init {
|
||||
() => {{
|
||||
Core::initialize(CoreOptions::default()).await?;
|
||||
if Core::get().curseforge_api_key.is_none() {
|
||||
crate::store::Core::initialize(crate::store::CoreOptions::default()).await?;
|
||||
if crate::store::Core::get().curseforge_api_key.is_none() {
|
||||
eprintln!("CURSEFORGE_API_KEY is not set, skipping tests that require it.");
|
||||
false
|
||||
} else {
|
||||
|
|
@ -19,6 +19,8 @@ macro_rules! init {
|
|||
}};
|
||||
}
|
||||
|
||||
pub(crate) use init;
|
||||
|
||||
struct Entry {
|
||||
slug: &'static str,
|
||||
id: &'static str,
|
||||
|
|
@ -186,20 +188,10 @@ pub async fn test_download_packages() -> LauncherResult<()> {
|
|||
let versions = provider.get_versions(&[ver_id.clone()]).await?;
|
||||
let ver = versions.first().expect("No version found");
|
||||
|
||||
let db_model = packages::download_package(&pkg, ver, None).await?;
|
||||
let db_model = packages::download_package(&pkg, ver, None, None).await?;
|
||||
assert!(db_model.hash == ver.files.first().unwrap().sha1);
|
||||
|
||||
let dir = Dirs::get_package_dir(
|
||||
&db_model.package_type,
|
||||
&db_model.provider,
|
||||
&db_model.package_id,
|
||||
)
|
||||
.await?;
|
||||
let dest = dir.join(packages::join_package_file(
|
||||
&dir,
|
||||
&db_model.version_id,
|
||||
&db_model.file_name,
|
||||
));
|
||||
let dest = db_model.path().await?;
|
||||
|
||||
assert!(dest.exists(), "File does not exist: {dest:?}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -515,7 +515,7 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
|
|||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Version not found"))?;
|
||||
|
||||
let model = api::packages::download_package(&package, &version, None).await?;
|
||||
let model = api::packages::download_package(&package, &version, None, None).await?;
|
||||
|
||||
api::packages::link_package(&model, &cluster, skip_compatibility).await?;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ use futures::{Stream, TryStreamExt, pin_mut};
|
|||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tempfile::TempDir;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use tokio_util::compat::TokioAsyncWriteCompatExt;
|
||||
|
||||
/// A wrapper around generic and unhelpful [`std::io::Error`] messages.
|
||||
|
|
@ -19,6 +17,8 @@ use tokio_util::compat::TokioAsyncWriteCompatExt;
|
|||
pub enum IOError {
|
||||
#[error("invalid absolute path '{0}'")]
|
||||
InvalidAbsolutePath(PathBuf),
|
||||
#[error("couldn't find file '{0}' in zip")]
|
||||
FileNotFoundInZip(String),
|
||||
|
||||
#[error("error acessing path: {source}, path: {path}")]
|
||||
IOErrorWrapper {
|
||||
|
|
@ -126,11 +126,11 @@ pub async fn remove_dir_all(path: impl AsRef<std::path::Path>) -> Result<(), IOE
|
|||
pub async fn read_gz_to_string(path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
|
||||
let mut f = tokio::fs::File::open(path).await?;
|
||||
let mut buf = vec![];
|
||||
f.read_to_end(&mut buf).await?;
|
||||
tokio::io::AsyncReadExt::read_to_end(&mut f, &mut buf).await?;
|
||||
|
||||
let mut decoder = GzipDecoder::new(buf.as_slice());
|
||||
let mut dst = String::new();
|
||||
decoder.read_to_string(&mut dst).await?;
|
||||
tokio::io::AsyncReadExt::read_to_string(&mut decoder, &mut dst).await?;
|
||||
|
||||
Ok(dst)
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ where
|
|||
|
||||
f(&mut writer).await?;
|
||||
|
||||
writer.flush().await?;
|
||||
tokio::io::AsyncWriteExt::flush(&mut writer).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -367,10 +367,25 @@ pub async fn unzip_bytes_filtered(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Unzips a zip archive from a file
|
||||
pub async fn unzip_file(
|
||||
pub async fn extract_zip(
|
||||
zip_path: impl AsRef<std::path::Path>,
|
||||
dest_path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
extract_zip_filtered(
|
||||
zip_path,
|
||||
dest_path,
|
||||
None::<fn(&StoredZipEntry) -> bool>,
|
||||
None::<fn(&str) -> String>,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Unzips a zip archive from a file
|
||||
pub async fn extract_zip_filtered(
|
||||
zip_path: impl AsRef<std::path::Path>,
|
||||
dest_path: impl AsRef<std::path::Path>,
|
||||
filter_entries: Option<impl Fn(&StoredZipEntry) -> bool + Send + Sync>,
|
||||
modify_entry_name: Option<impl Fn(&str) -> String>,
|
||||
) -> Result<(), IOError> {
|
||||
let zip_path = zip_path.as_ref();
|
||||
let dest_path = dest_path.as_ref();
|
||||
|
|
@ -381,7 +396,22 @@ pub async fn unzip_file(
|
|||
for index in 0..entries.len() {
|
||||
let entry = entries.get(index).expect("expected more zip entries");
|
||||
|
||||
let path = dest_path.join(sanitize_path(entry.filename().as_str()?));
|
||||
if let Some(filter) = &filter_entries
|
||||
&& !filter(entry)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let old_name = entry.filename().as_str()?;
|
||||
let name: String = if let Some(modify) = &modify_entry_name {
|
||||
modify(&old_name)
|
||||
} else {
|
||||
old_name.to_string()
|
||||
};
|
||||
|
||||
let name = sanitize_path(name);
|
||||
|
||||
let path = dest_path.join(name);
|
||||
let is_dir = entry.dir()?;
|
||||
|
||||
if is_dir {
|
||||
|
|
@ -402,6 +432,32 @@ pub async fn unzip_file(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a zip file entry's bytes without reading the entire file into memory.
|
||||
pub async fn try_read_zip_entry_bytes<R>(reader: R, file_name: &str) -> Result<Vec<u8>, IOError>
|
||||
where
|
||||
R: tokio::io::AsyncRead + tokio::io::AsyncBufRead + tokio::io::AsyncSeek + Unpin,
|
||||
{
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
let compat = reader.compat();
|
||||
|
||||
let mut zip_reader = async_zip::base::read::seek::ZipFileReader::new(compat).await?;
|
||||
|
||||
let index = zip_reader
|
||||
.file()
|
||||
.entries()
|
||||
.iter()
|
||||
.position(|entry| entry.filename().as_str().is_ok_and(|n| n == file_name))
|
||||
.ok_or_else(|| IOError::FileNotFoundInZip(file_name.to_string()))?;
|
||||
|
||||
let mut entry_reader = zip_reader.reader_without_entry(index).await?;
|
||||
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
|
||||
futures::AsyncReadExt::read_to_end(&mut entry_reader, &mut data).await?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub fn sanitize_path(path: impl AsRef<std::path::Path>) -> PathBuf {
|
||||
path.as_ref()
|
||||
.to_string_lossy()
|
||||
|
|
|
|||
|
|
@ -4,3 +4,8 @@ pub mod icon;
|
|||
pub mod io;
|
||||
pub mod os_ext;
|
||||
pub mod pagination;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait DatabaseModelExt {
|
||||
async fn path(&self) -> crate::LauncherResult<std::path::PathBuf>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,17 +82,17 @@ impl GameLoader {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for GameLoader {
|
||||
type Error = String;
|
||||
impl FromStr for GameLoader {
|
||||
type Err = String;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, String> {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(
|
||||
match s.to_lowercase().replace(['_', '.', ' '], "").as_str() {
|
||||
"vanilla" | "minecraft" => Self::Vanilla,
|
||||
"forge" => Self::Forge,
|
||||
"neoforge" | "neo" => Self::NeoForge,
|
||||
"quilt" => Self::Quilt,
|
||||
"fabric" => Self::Fabric,
|
||||
"quilt" | "quilt-loader" => Self::Quilt,
|
||||
"fabric" | "fabric-loader" => Self::Fabric,
|
||||
"legacyfabric" => Self::LegacyFabric,
|
||||
_ => return Err(format!("'{s}' is not a valid game loader")),
|
||||
},
|
||||
|
|
@ -100,14 +100,6 @@ impl TryFrom<String> for GameLoader {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromStr for GameLoader {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for GameLoader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
|
|
|
|||
|
|
@ -32,19 +32,25 @@ impl PackageType {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<String> for PackageType {
|
||||
fn from(value: String) -> Self {
|
||||
impl From<&str> for PackageType {
|
||||
fn from(value: &str) -> Self {
|
||||
match value.to_lowercase().as_str() {
|
||||
"mod" => Self::Mod,
|
||||
"resourcepack" => Self::ResourcePack,
|
||||
"shader" => Self::Shader,
|
||||
"datapack" => Self::DataPack,
|
||||
"mod" | "mods" => Self::Mod,
|
||||
"resourcepack" | "resourcepacks" => Self::ResourcePack,
|
||||
"shader" | "shaders" => Self::Shader,
|
||||
"datapack" | "datapacks" => Self::DataPack,
|
||||
"modpack" => Self::ModPack,
|
||||
_ => Self::Mod, // default case
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for PackageType {
|
||||
fn from(value: String) -> Self {
|
||||
Self::from(value.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for PackageType {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{cluster_stage::ClusterStage, icon::Icon, loader::GameLoader};
|
||||
use crate::cluster_stage::ClusterStage;
|
||||
use crate::icon::Icon;
|
||||
use crate::loader::GameLoader;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "clusters")]
|
||||
|
|
@ -26,8 +28,7 @@ pub struct Model {
|
|||
#[sea_orm(nullable)]
|
||||
pub setting_profile_name: Option<String>,
|
||||
#[sea_orm(nullable)]
|
||||
pub linked_pack_id: Option<String>,
|
||||
pub linked_pack_version: Option<String>,
|
||||
pub linked_modpack_hash: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
|
|||
|
|
@ -6,271 +6,305 @@ pub struct Migration;
|
|||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ClusterGroups::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(ClusterGroups::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(ClusterGroups::Name).text().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ClusterGroups::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(ClusterGroups::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(ClusterGroups::Name).text().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(JavaVersions::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(JavaVersions::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(JavaVersions::MajorVersion).integer().not_null())
|
||||
.col(ColumnDef::new(JavaVersions::VendorName).text().not_null())
|
||||
.col(ColumnDef::new(JavaVersions::AbsolutePath).text().not_null())
|
||||
.col(ColumnDef::new(JavaVersions::FullVersion).text().not_null())
|
||||
.col(ColumnDef::new(JavaVersions::Arch).text().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(JavaVersions::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(JavaVersions::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(JavaVersions::MajorVersion)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(JavaVersions::VendorName).text().not_null())
|
||||
.col(ColumnDef::new(JavaVersions::AbsolutePath).text().not_null())
|
||||
.col(ColumnDef::new(JavaVersions::FullVersion).text().not_null())
|
||||
.col(ColumnDef::new(JavaVersions::Arch).text().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("java_versions_major_version_idx")
|
||||
.table(JavaVersions::Table)
|
||||
.col(JavaVersions::MajorVersion)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("java_versions_major_version_idx")
|
||||
.table(JavaVersions::Table)
|
||||
.col(JavaVersions::MajorVersion)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Packages::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(Packages::Hash).text().not_null().primary_key())
|
||||
.col(ColumnDef::new(Packages::FileName).text().not_null())
|
||||
.col(ColumnDef::new(Packages::VersionId).text().not_null())
|
||||
.col(ColumnDef::new(Packages::PublishedAt).date_time().not_null())
|
||||
.col(ColumnDef::new(Packages::DisplayName).text().not_null())
|
||||
.col(ColumnDef::new(Packages::DisplayVersion).text().not_null())
|
||||
.col(ColumnDef::new(Packages::PackageType).integer().not_null())
|
||||
.col(ColumnDef::new(Packages::Provider).integer().not_null())
|
||||
.col(ColumnDef::new(Packages::PackageId).text().not_null())
|
||||
.col(ColumnDef::new(Packages::McVersions).json().not_null())
|
||||
.col(ColumnDef::new(Packages::McLoader).integer().not_null())
|
||||
.col(ColumnDef::new(Packages::Icon).text().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Packages::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Packages::Hash)
|
||||
.text()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Packages::FileName).text().not_null())
|
||||
.col(ColumnDef::new(Packages::VersionId).text().not_null())
|
||||
.col(ColumnDef::new(Packages::PublishedAt).date_time().not_null())
|
||||
.col(ColumnDef::new(Packages::DisplayName).text().not_null())
|
||||
.col(ColumnDef::new(Packages::DisplayVersion).text().not_null())
|
||||
.col(ColumnDef::new(Packages::PackageType).integer().not_null())
|
||||
.col(ColumnDef::new(Packages::Provider).integer().not_null())
|
||||
.col(ColumnDef::new(Packages::PackageId).text().not_null())
|
||||
.col(ColumnDef::new(Packages::McVersions).json().not_null())
|
||||
.col(ColumnDef::new(Packages::McLoader).integer().not_null())
|
||||
.col(ColumnDef::new(Packages::Icon).text().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("packages_mc_loader_mc_versions_idx")
|
||||
.table(Packages::Table)
|
||||
.col(Packages::PackageType)
|
||||
.col(Packages::McLoader)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("packages_mc_loader_mc_versions_idx")
|
||||
.table(Packages::Table)
|
||||
.col(Packages::PackageType)
|
||||
.col(Packages::McLoader)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(SettingProfiles::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(SettingProfiles::Name).text().not_null().primary_key())
|
||||
.col(ColumnDef::new(SettingProfiles::JavaId).integer().null())
|
||||
.col(ColumnDef::new(SettingProfiles::Res).json().null())
|
||||
.col(ColumnDef::new(SettingProfiles::ForceFullscreen).boolean().null())
|
||||
.col(ColumnDef::new(SettingProfiles::MemMax).integer().null())
|
||||
.col(ColumnDef::new(SettingProfiles::LaunchArgs).text().null())
|
||||
.col(ColumnDef::new(SettingProfiles::LaunchEnv).text().null())
|
||||
.col(ColumnDef::new(SettingProfiles::HookPre).text().null())
|
||||
.col(ColumnDef::new(SettingProfiles::HookWrapper).text().null())
|
||||
.col(ColumnDef::new(SettingProfiles::HookPost).text().null())
|
||||
.col(ColumnDef::new(SettingProfiles::OsExtra).json().null())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(SettingProfiles::Table, SettingProfiles::JavaId)
|
||||
.to(JavaVersions::Table, JavaVersions::Id),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(SettingProfiles::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(SettingProfiles::Name)
|
||||
.text()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(SettingProfiles::JavaId).integer().null())
|
||||
.col(ColumnDef::new(SettingProfiles::Res).json().null())
|
||||
.col(
|
||||
ColumnDef::new(SettingProfiles::ForceFullscreen)
|
||||
.boolean()
|
||||
.null(),
|
||||
)
|
||||
.col(ColumnDef::new(SettingProfiles::MemMax).integer().null())
|
||||
.col(ColumnDef::new(SettingProfiles::LaunchArgs).text().null())
|
||||
.col(ColumnDef::new(SettingProfiles::LaunchEnv).text().null())
|
||||
.col(ColumnDef::new(SettingProfiles::HookPre).text().null())
|
||||
.col(ColumnDef::new(SettingProfiles::HookWrapper).text().null())
|
||||
.col(ColumnDef::new(SettingProfiles::HookPost).text().null())
|
||||
.col(ColumnDef::new(SettingProfiles::OsExtra).json().null())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(SettingProfiles::Table, SettingProfiles::JavaId)
|
||||
.to(JavaVersions::Table, JavaVersions::Id),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Clusters::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Clusters::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Clusters::FolderName).text().not_null())
|
||||
.col(ColumnDef::new(Clusters::Stage).integer().not_null().default(0))
|
||||
.col(ColumnDef::new(Clusters::CreatedAt).date_time().not_null().default(Utc::now()))
|
||||
.col(ColumnDef::new(Clusters::GroupId).integer().null())
|
||||
.col(ColumnDef::new(Clusters::Name).text().not_null())
|
||||
.col(ColumnDef::new(Clusters::McVersion).text().not_null())
|
||||
.col(ColumnDef::new(Clusters::McLoader).integer().not_null())
|
||||
.col(ColumnDef::new(Clusters::McLoaderVersion).text().null())
|
||||
.col(ColumnDef::new(Clusters::LastPlayed).date_time().null())
|
||||
.col(ColumnDef::new(Clusters::OverallPlayed).integer().null())
|
||||
.col(ColumnDef::new(Clusters::IconUrl).text().null())
|
||||
.col(ColumnDef::new(Clusters::SettingProfileName).text().null())
|
||||
.col(ColumnDef::new(Clusters::LinkedPackId).text().null())
|
||||
.col(ColumnDef::new(Clusters::LinkedPackVersion).text().null())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(Clusters::Table, Clusters::GroupId)
|
||||
.to(ClusterGroups::Table, ClusterGroups::Id),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(Clusters::Table, Clusters::SettingProfileName)
|
||||
.to(SettingProfiles::Table, SettingProfiles::Name),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Clusters::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Clusters::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Clusters::FolderName).text().not_null())
|
||||
.col(
|
||||
ColumnDef::new(Clusters::Stage)
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Clusters::CreatedAt)
|
||||
.date_time()
|
||||
.not_null()
|
||||
.default(Utc::now()),
|
||||
)
|
||||
.col(ColumnDef::new(Clusters::GroupId).integer().null())
|
||||
.col(ColumnDef::new(Clusters::Name).text().not_null())
|
||||
.col(ColumnDef::new(Clusters::McVersion).text().not_null())
|
||||
.col(ColumnDef::new(Clusters::McLoader).integer().not_null())
|
||||
.col(ColumnDef::new(Clusters::McLoaderVersion).text().null())
|
||||
.col(ColumnDef::new(Clusters::LastPlayed).date_time().null())
|
||||
.col(ColumnDef::new(Clusters::OverallPlayed).integer().null())
|
||||
.col(ColumnDef::new(Clusters::IconUrl).text().null())
|
||||
.col(ColumnDef::new(Clusters::SettingProfileName).text().null())
|
||||
.col(ColumnDef::new(Clusters::LinkedModpackHash).text().null())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(Clusters::Table, Clusters::GroupId)
|
||||
.to(ClusterGroups::Table, ClusterGroups::Id),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(Clusters::Table, Clusters::SettingProfileName)
|
||||
.to(SettingProfiles::Table, SettingProfiles::Name),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("clusters_path_idx")
|
||||
.table(Clusters::Table)
|
||||
.col(Clusters::FolderName)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("clusters_path_idx")
|
||||
.table(Clusters::Table)
|
||||
.col(Clusters::FolderName)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ClusterPackages::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(ClusterPackages::ClusterId).integer().not_null())
|
||||
.col(ColumnDef::new(ClusterPackages::PackageHash).text().not_null())
|
||||
.primary_key(
|
||||
Index::create()
|
||||
.col(ClusterPackages::ClusterId)
|
||||
.col(ClusterPackages::PackageHash)
|
||||
.primary(),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(ClusterPackages::Table, ClusterPackages::ClusterId)
|
||||
.to(Clusters::Table, Clusters::Id),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(ClusterPackages::Table, ClusterPackages::PackageHash)
|
||||
.to(Packages::Table, Packages::Hash),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(ClusterPackages::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(ClusterPackages::ClusterId)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(ClusterPackages::PackageHash)
|
||||
.text()
|
||||
.not_null(),
|
||||
)
|
||||
.primary_key(
|
||||
Index::create()
|
||||
.col(ClusterPackages::ClusterId)
|
||||
.col(ClusterPackages::PackageHash)
|
||||
.primary(),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(ClusterPackages::Table, ClusterPackages::ClusterId)
|
||||
.to(Clusters::Table, Clusters::Id),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(ClusterPackages::Table, ClusterPackages::PackageHash)
|
||||
.to(Packages::Table, Packages::Hash),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
Err(DbErr::Migration("no rollback available".to_owned()))
|
||||
}
|
||||
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
Err(DbErr::Migration("no rollback available".to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
enum ClusterGroups {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
enum JavaVersions {
|
||||
Table,
|
||||
Id,
|
||||
MajorVersion,
|
||||
VendorName,
|
||||
AbsolutePath,
|
||||
FullVersion,
|
||||
Arch,
|
||||
Table,
|
||||
Id,
|
||||
MajorVersion,
|
||||
VendorName,
|
||||
AbsolutePath,
|
||||
FullVersion,
|
||||
Arch,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
enum Packages {
|
||||
Table,
|
||||
Hash,
|
||||
FileName,
|
||||
VersionId,
|
||||
PublishedAt,
|
||||
DisplayName,
|
||||
DisplayVersion,
|
||||
PackageType,
|
||||
Provider,
|
||||
PackageId,
|
||||
McVersions,
|
||||
McLoader,
|
||||
Icon,
|
||||
Table,
|
||||
Hash,
|
||||
FileName,
|
||||
VersionId,
|
||||
PublishedAt,
|
||||
DisplayName,
|
||||
DisplayVersion,
|
||||
PackageType,
|
||||
Provider,
|
||||
PackageId,
|
||||
McVersions,
|
||||
McLoader,
|
||||
Icon,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
enum SettingProfiles {
|
||||
Table,
|
||||
Name,
|
||||
JavaId,
|
||||
Res,
|
||||
ForceFullscreen,
|
||||
MemMax,
|
||||
LaunchArgs,
|
||||
LaunchEnv,
|
||||
HookPre,
|
||||
HookWrapper,
|
||||
HookPost,
|
||||
OsExtra,
|
||||
Table,
|
||||
Name,
|
||||
JavaId,
|
||||
Res,
|
||||
ForceFullscreen,
|
||||
MemMax,
|
||||
LaunchArgs,
|
||||
LaunchEnv,
|
||||
HookPre,
|
||||
HookWrapper,
|
||||
HookPost,
|
||||
OsExtra,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
enum Clusters {
|
||||
Table,
|
||||
Id,
|
||||
FolderName,
|
||||
Stage,
|
||||
CreatedAt,
|
||||
GroupId,
|
||||
Name,
|
||||
McVersion,
|
||||
McLoader,
|
||||
McLoaderVersion,
|
||||
LastPlayed,
|
||||
OverallPlayed,
|
||||
IconUrl,
|
||||
SettingProfileName,
|
||||
LinkedPackId,
|
||||
LinkedPackVersion,
|
||||
Table,
|
||||
Id,
|
||||
FolderName,
|
||||
Stage,
|
||||
CreatedAt,
|
||||
GroupId,
|
||||
Name,
|
||||
McVersion,
|
||||
McLoader,
|
||||
McLoaderVersion,
|
||||
LastPlayed,
|
||||
OverallPlayed,
|
||||
IconUrl,
|
||||
SettingProfileName,
|
||||
LinkedModpackHash,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
enum ClusterPackages {
|
||||
Table,
|
||||
ClusterId,
|
||||
PackageHash,
|
||||
}
|
||||
Table,
|
||||
ClusterId,
|
||||
PackageHash,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue