feat: mrpack support

This commit is contained in:
LynithDev 2025-08-07 18:39:59 +02:00
parent b4746b8ec6
commit 33c865893f
18 changed files with 1173 additions and 371 deletions

View file

@ -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(())
}
}

View file

@ -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))
}
}

View file

@ -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,

View file

@ -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)]

View file

@ -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))
}
}

View 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),
}

View 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(())
}

View 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,
}

View 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(())
}

View file

@ -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;

View file

@ -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:?}");
}

View file

@ -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?;

View file

@ -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()

View file

@ -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>;
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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)]

View file

@ -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,
}