feat: Package downloading and linking

This commit is contained in:
LynithDev 2025-04-26 14:54:21 +02:00
parent 17f8b382a7
commit f939169aa6
31 changed files with 999 additions and 272 deletions

1
Cargo.lock generated
View file

@ -3116,6 +3116,7 @@ name = "onelauncher_migration"
version = "0.2.0-alpha.2"
dependencies = [
"async-trait",
"chrono",
"sea-orm-migration",
"tokio",
]

View file

@ -1,22 +1,22 @@
[workspace]
resolver = "2" # virtual workspace resolver so we don't need a root package
members = [
"packages/core",
"packages/entity",
"packages/gamemode",
"packages/macro",
"packages/migration",
"packages/serde_json_path_to_error"
"packages/serde_json_path_to_error",
]
resolver = "2" # virtual workspace resolver so we don't need a root package
[workspace.package]
license = "GPL-3.0-only"
edition = "2024"
repository = "https://github.com/Polyfrost/OneLauncher"
documentation = "https://docs.polyfrost.org/onelauncher"
readme = "README.md"
homepage = "https://polyfrost.org/projects/onelauncher"
authors = [ "Polyfrost", "Polyfrost Contributors" ]
documentation = "https://docs.polyfrost.org/onelauncher"
edition = "2024"
homepage = "https://polyfrost.org/projects/onelauncher"
license = "GPL-3.0-only"
readme = "README.md"
repository = "https://github.com/Polyfrost/OneLauncher"
version = "0.2.0-alpha.2"
[workspace.dependencies]
@ -34,16 +34,16 @@ tauri = { version = "2.0", features = [
"tracing",
"specta",
"uuid",
"protocol-asset"
"protocol-asset",
] }
tauri-build = { version = "2.0" }
tauri-plugin-shell = { version = "2.0" }
tauri-plugin-window-state = { version = "2.0" }
tauri-plugin-single-instance = { version = "2.0" }
tauri-plugin-clipboard-manager = { version = "2.0" }
tauri-plugin-updater = { version = "2.0" }
tauri-plugin-dialog = { version = "2.0" }
tauri-plugin-deep-link = { version = "2.0" }
tauri-plugin-dialog = { version = "2.0" }
tauri-plugin-shell = { version = "2.0" }
tauri-plugin-single-instance = { version = "2.0" }
tauri-plugin-updater = { version = "2.0" }
tauri-plugin-window-state = { version = "2.0" }
# core
interpulse = { version = "=1.2.2", features = [ "specta" ] }
@ -56,10 +56,10 @@ specta = { version = "2.0.0-rc.20", features = [
"serde_json",
"function",
"derive",
"time"
"time",
] }
tauri-specta = { version = "2.0.0-rc.20", features = [ "javascript", "typescript", "derive" ] }
specta-typescript = { version = "=0.0.7" }
tauri-specta = { version = "2.0.0-rc.20", features = [ "javascript", "typescript", "derive" ] }
# security
iota_stronghold = { version = "2.1" }
@ -67,27 +67,28 @@ zeroize = { version = "1.8", features = [ "zeroize_derive" ] }
# logging
tracing = { version = "0.1" }
tracing-subscriber = { version = "0.3", features = [ "chrono", "env-filter" ] }
tracing-error = { version = "0.2" }
tracing-appender = { version = "0.2" }
tracing-error = { version = "0.2" }
tracing-subscriber = { version = "0.3", features = [ "chrono", "env-filter" ] }
# serialization & text
base64 = { version = "0.22" }
byteorder = { version = "1.5.0" }
chrono = { version = "0.4", features = [ "serde" ] }
nom = { version = "7.1" }
p256 = { version = "0.13.2", features = [ "ecdsa" ] }
rand = { version = "0.8.5" }
regex = { version = "1.10" }
serde = { version = "1.0", features = [ "derive" ] }
serde_json = { version = "1.0" }
serde_with = { version = "=3.12.0", features = [ "chrono_0_4", "json", "macros" ] }
regex = { version = "1.10" }
url = { version = "2.5.2" }
strum = { version = "0.26", features = [ "derive" ] }
chrono = { version = "0.4", features = [ "serde" ] }
rand = { version = "0.8.5" }
byteorder = { version = "1.5.0" }
base64 = { version = "0.22" }
p256 = { version = "0.13.2", features = [ "ecdsa" ] }
uuid = { version = "1.10", features = [ "serde", "v4" ] }
toml = { version = "0.8" }
nom = { version = "7.1" }
url = { version = "2.5.2" }
uuid = { version = "1.10", features = [ "serde", "v4" ] }
# network
async-tungstenite = { version = "0.28", features = [ "tokio-runtime", "tokio-rustls-webpki-roots" ] }
reqwest = { version = "0.12", default-features = false, features = [
"json",
"stream",
@ -99,33 +100,32 @@ reqwest = { version = "0.12", default-features = false, features = [
"macos-system-configuration",
"deflate",
"gzip",
"brotli"
"brotli",
] }
async-tungstenite = { version = "0.28", features = [ "tokio-runtime", "tokio-rustls-webpki-roots" ] }
webbrowser = { version = "1.0" }
# io
tokio = { version = "1.40", features = [ "full" ] }
tokio-util = { version = "0.7", features = [ "compat" ] }
tokio-stream = { version = "0.1.16", features = [ "fs" ] }
async-compression = { version = "0.4.12", features = [ "flate2", "tokio" ] }
async_zip = { version = "0.0.17", features = [ "full" ] }
bytes = { version = "1.7" }
dirs = { version = "5.0" }
dunce = { version = "1.0.5" }
flate2 = { version = "1.0" }
notify = { version = "6.1", default-features = false }
notify-debouncer-mini = { version = "0.4", default-features = false }
tempfile = { version = "3.19" }
dunce = { version = "1.0.5" }
zip = { version = "2.2.0" }
dirs = { version = "5.0" }
async_zip = { version = "0.0.17", features = [ "full" ] }
async-compression = { version = "0.4.12", features = [ "flate2", "tokio" ] }
flate2 = { version = "1.0" }
bytes = { version = "1.7" }
sanitize-filename = { version = "0.6" }
tempfile = { version = "3.19" }
tokio = { version = "1.40", features = [ "full" ] }
tokio-stream = { version = "0.1.16", features = [ "fs" ] }
tokio-util = { version = "0.7", features = [ "compat" ] }
zip = { version = "2.2.0" }
# crypto
digest = { version = "0.10" }
sha1 = { version = "0.10" }
sha2 = { version = "0.10" }
md-5 = { version = "0.10" }
murmur2 = { version = "0.1.0" }
sha1 = { version = "0.10" }
sha2 = { version = "0.10" }
# db
sea-orm = { version = "1.1", features = [
@ -134,85 +134,91 @@ sea-orm = { version = "1.1", features = [
"macros",
"with-chrono",
"with-json",
"with-uuid"
"with-uuid",
] }
sea-orm-migration = { version = "1.1", features = [
"sqlx-sqlite",
"runtime-tokio-rustls",
"with-chrono",
"with-json",
"with-uuid",
] }
sea-orm-migration = { version = "1.1", features = [ "sqlx-sqlite", "runtime-tokio-rustls" ] }
# utils
thiserror = { version = "1.0" }
anyhow = { version = "1.0" }
sysinfo = { version = "0.31", features = [ "serde" ] }
async-trait = { version = "0.1" }
cc = { version = "1.1" }
discord-rich-presence = { version = "0.2" }
indicatif = { version = "0.17" }
futures = { version = "0.3" }
futures-lite = { version = "2.6" }
async-trait = { version = "0.1" }
whoami = { version = "1.5" }
cc = { version = "1.1" }
indicatif = { version = "0.17" }
libc = { version = "0.2" }
merge = { version = "0.2" }
sysinfo = { version = "0.31", features = [ "serde" ] }
thiserror = { version = "1.0" }
whoami = { version = "1.5" }
# macro
syn = { version = "2" }
quote = { version = "1" }
paste = { version = "1" }
quote = { version = "1" }
syn = { version = "2" }
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
correctness = { level = "warn", priority = -1 }
perf = { level = "warn", priority = -1 }
style = { level = "warn", priority = -1 }
suspicious = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
unnecessary_cast = "warn"
cast_lossless = "warn"
cast_precision_loss = "allow"
complexity = { level = "warn", priority = -1 }
correctness = { level = "warn", priority = -1 }
dbg_macro = "warn"
deprecated_cfg_attr = "warn"
separated_literal_suffix = "warn"
missing_errors_doc = "allow"
future_not_send = "allow"
module_name_repetitions = "allow"
struct_field_names = "allow"
cast_precision_loss = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
significant_drop_tightening = "allow" # tmp
module_name_repetitions = "allow"
needless_return = "allow"
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
perf = { level = "warn", priority = -1 }
separated_literal_suffix = "warn"
significant_drop_tightening = "allow" # tmp
struct_field_names = "allow"
style = { level = "warn", priority = -1 }
suspicious = { level = "warn", priority = -1 }
unnecessary_cast = "warn"
[workspace.lints.rust]
unused_qualifications = "warn"
deprecated = "warn"
deprecated_in_future = "forbid"
rust_2018_idioms = { level = "warn", priority = -1 }
trivial_casts = "warn"
trivial_numeric_casts = "warn"
unused_allocation = "warn"
unused_import_braces = "warn"
deprecated = "warn"
deprecated_in_future = "forbid"
unused_must_use = "deny"
unused_qualifications = "warn"
[profile.dev]
# add debug symbols/logic and optimize for reduced build time (parallel)
debug = true # add debug info
opt-level = 0 # reduce optimizations
strip = "none" # don't remove debug symbols
incremental = true # enable incremental compilation
split-debuginfo = "unpacked" # unpack debug info
lto = false # disable link time optimizations, reduce compile time
codegen-units = 256 # make parallel incremental builds faster
rpath = false # disable rpath
debug = true # add debug info
incremental = true # enable incremental compilation
lto = false # disable link time optimizations, reduce compile time
opt-level = 0 # reduce optimizations
panic = "unwind" # add unwind panic logic because some deps require it
rpath = false # disable rpath
split-debuginfo = "unpacked" # unpack debug info
strip = "none" # don't remove debug symbols
# add debug symbols/logic and optimize for reduced build time (parallel)
[profile.dev-debug]
inherits = "dev" # extend the dev profile
split-debuginfo = "none" # don't split debug info
opt-level = 0 # reduce optimizations
debug = "full" # add full debug info
strip = "none" # don't remove debug symbols
lto = "off" # disable link time optimizations, reduce compile time
codegen-units = 256 # make parallel incremental builds faster
debug = "full" # add full debug info
incremental = true # enable incremental compilation
inherits = "dev" # extend the dev profile
lto = "off" # disable link time optimizations, reduce compile time
opt-level = 0 # reduce optimizations
split-debuginfo = "none" # don't split debug info
strip = "none" # don't remove debug symbols
# set the settings for build scripts and proc-macros.
[profile.dev.build-override]
@ -220,20 +226,20 @@ opt-level = 3 # optimize build
# set the default for dependencies, except workspace members.
[profile.dev.package."*"]
opt-level = 3 # optimize build
incremental = false # disable incremental compilation
opt-level = 3 # optimize build
# set the default for dependencies, except workspace members.
[profile.dev-debug.package."*"]
inherits = "dev" # extend the dev profile
opt-level = 3 # optimize build
debug = "full" # add full debug info
incremental = false # disable incremental compilation
inherits = "dev" # extend the dev profile
opt-level = 3 # optimize build
# optimize for speed and reduce size for release
[profile.release]
panic = "abort" # strip panic cleanup logic
codegen-units = 1 # compile crate one after another for better optimization
lto = true # enable link time optimization
opt-level = "s" # optimize binary
panic = "abort" # strip panic cleanup logic
strip = true # remove debug symbols

View file

@ -53,7 +53,7 @@ pub async fn main() -> LauncherResult<()> {
let cluster_name = "Launchable";
let path = dirs.clusters_dir().join(cluster_name);
let mut cluster = if let Some(cluster) = api::cluster::dao::get_cluster_by_path(&path).await? {
let mut cluster = if let Some(cluster) = api::cluster::dao::get_cluster_by_folder_name(&path).await? {
cluster
} else {
api::cluster::create_cluster(cluster_name, "1.20.4", GameLoader::Fabric, None, None).await?
@ -66,7 +66,7 @@ pub async fn main() -> LauncherResult<()> {
Ok(cluster)
}).await?;
println!("Using cluster: {} at '{}'", cluster.name, cluster.path);
println!("Using cluster: {} at '{}'", cluster.name, cluster.folder_name);
// Launch the game
let process = api::game::launch::launch_minecraft(&mut cluster, creds, None).await?;

View file

@ -0,0 +1,39 @@
use onelauncher_core::{api::{cluster, packages::{self, download_package, provider::ProviderExt}, proxy::ProxyDynamic}, error::LauncherResult, initialize_core, store::{CoreOptions, Dirs}};
use onelauncher_entity::{loader::GameLoader, package::Provider};
#[tokio::main]
pub async fn main() -> LauncherResult<()> {
initialize_core(CoreOptions::default(), ProxyDynamic::default()).await?;
let dirs = Dirs::get().await?;
// Here we create the cluster
let cluster_name = "Chatting Profile";
let path = dirs.clusters_dir().join(cluster_name);
let cluster = if let Some(cluster) = cluster::dao::get_cluster_by_folder_name(&path).await? {
cluster
} else {
cluster::create_cluster(cluster_name, "1.8.9", GameLoader::Forge, None, None).await?
};
// Fetch our package and its versions
let provider = Provider::Modrinth;
let package = &provider.get("chatting").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");
// We can now download the package
let model = download_package(package, version, 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");
Ok(())
}

View file

@ -10,7 +10,7 @@ pub type ClusterId = i64;
/// Inserts a cluster in the database.
pub async fn insert_cluster(
name: &str,
path: &str,
folder_name: &str,
mc_version: &str,
mc_loader: GameLoader,
mc_loader_version: Option<&str>,
@ -21,7 +21,7 @@ pub async fn insert_cluster(
let model = clusters::ActiveModel {
name: Set(name.to_string()),
path: Set(path.to_string()),
folder_name: Set(folder_name.to_string()),
mc_version: Set(mc_version.to_string()),
mc_loader: Set(mc_loader),
mc_loader_version: Set(mc_loader_version.map(String::from)),
@ -96,12 +96,12 @@ pub async fn get_cluster_by_id(id: ClusterId) -> LauncherResult<Option<clusters:
}
/// Gets a cluster by its path from the database.
pub async fn get_cluster_by_path(path: &Path) -> LauncherResult<Option<clusters::Model>> {
pub async fn get_cluster_by_folder_name(folder_name: &Path) -> LauncherResult<Option<clusters::Model>> {
let state = State::get().await?;
let db = &state.db;
let model = clusters::Entity::find()
.filter(clusters::Column::Path.eq(path.to_string_lossy().to_string()))
.filter(clusters::Column::FolderName.eq(folder_name.to_string_lossy().to_string()))
.one(db)
.await?;

View file

@ -55,15 +55,17 @@ pub async fn create_cluster(
let cluster_dir = Dirs::get_clusters_dir().await?;
// Get the directory for the cluster
let mut path = cluster_dir.join(&name);
let mut folder_name = name.clone();
let mut path = cluster_dir.join(&folder_name);
// Folder name conflict resolution
if path.exists() {
let mut which = 1;
loop {
let new_name = format!("{name} ({which})");
let new_name = format!("{folder_name} ({which})");
path = cluster_dir.join(&new_name);
if !path.exists() {
folder_name = new_name;
break;
}
which += 1;
@ -84,7 +86,7 @@ pub async fn create_cluster(
// Finally add the cluster to the database
dao::insert_cluster(
name.as_str(),
path.to_string_lossy().into_owned().as_str(),
&folder_name,
mc_version,
mc_loader,
mc_loader_version,
@ -253,7 +255,7 @@ async fn run_forge_processors(
client => cluster.mc_version.clone(),
server => "";
"ROOT":
client => cluster.path.clone(),
client => dirs.clusters_dir().join(cluster.folder_name.clone()).to_string_lossy(),
server => "";
"LIBRARY_DIR":
client => libraries.to_string_lossy(),
@ -314,7 +316,7 @@ pub async fn update_playtime(id: ClusterId, duration: i64) -> LauncherResult<clu
let overall_played = cluster.overall_played.take().unwrap_or_default().unwrap_or_default();
cluster.overall_played = Set(Some(overall_played + duration));
cluster.last_played = Set(Some(Utc::now().timestamp()));
cluster.last_played = Set(Some(Utc::now()));
Ok(cluster)
}).await

View file

@ -30,7 +30,8 @@ pub async fn sync_clusters() -> LauncherResult<Vec<ClusterId>> {
let clusters = dao::get_all_clusters().await?;
let mut missing_ids = Vec::new();
let mut checked_paths = HashSet::new();
let mut checked_folders = HashSet::new();
let dirs = Dirs::get().await?;
for cluster in clusters {
if sync_cluster(&cluster)
@ -40,14 +41,14 @@ pub async fn sync_clusters() -> LauncherResult<Vec<ClusterId>> {
{
missing_ids.push(cluster.id);
let path = cluster.path.clone();
let path = cluster.folder_name.clone();
send_error!(
"cluster {} is missing from the filesystem",
path.clone()
);
}
checked_paths.insert(PathBuf::from(cluster.path));
checked_folders.insert(dirs.clusters_dir().join(cluster.folder_name.clone()));
}
// Check for clusters in the filesystem that are not in the database
@ -58,7 +59,7 @@ pub async fn sync_clusters() -> LauncherResult<Vec<ClusterId>> {
let mut stream = io::read_dir(cluster_dir).await?;
while let Ok(Some(entry)) = stream.next_entry().await {
let path = entry.path();
if !path.is_dir() || checked_paths.contains(&path) {
if !path.is_dir() || checked_folders.contains(&path) {
// Skip if it's not a directory or if we've already checked it
continue;
}
@ -79,7 +80,7 @@ pub async fn sync_clusters() -> LauncherResult<Vec<ClusterId>> {
/// Checks if the cluster in the database exists and if the directory exists.
#[tracing::instrument]
pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult<SyncAction> {
let path = PathBuf::from(cluster.path.clone());
let path = Dirs::get_clusters_dir().await?.join(cluster.folder_name.clone());
if !path.exists() {
return Ok(SyncAction::MissingFs);
}
@ -104,7 +105,7 @@ pub async fn sync_cluster_by_id(id: ClusterId) -> LauncherResult<SyncAction> {
/// Checks if the cluster in the database exists and if the directory exists.
#[tracing::instrument]
pub async fn sync_cluster_by_path(path: &PathBuf) -> LauncherResult<SyncAction> {
let Some(cluster) = dao::get_cluster_by_path(path).await? else {
let Some(cluster) = dao::get_cluster_by_folder_name(path).await? else {
tracing::warn!("cluster with path {path:?} not found in database");
return Ok(SyncAction::MissingDb);
};

View file

@ -5,12 +5,10 @@ use chrono::Utc;
use discord_rich_presence::activity::Timestamps;
use merge::Merge;
use onelauncher_entity::prelude::model::*;
use sea_orm::ActiveValue::Set;
use tokio::process::Command;
use tokio::sync::RwLock;
use crate::api::cluster::dao::update_cluster;
use crate::api::cluster::{ClusterError, prepare_cluster};
use crate::api::cluster::{prepare_cluster, update_playtime, ClusterError};
use crate::api::game::{arguments, log, metadata};
use crate::api::java::{self, JavaError};
use crate::api::setting_profiles::dao::get_profile_by_name;
@ -41,6 +39,7 @@ pub async fn launch_minecraft(
}
let state = State::get().await?;
let dirs = Dirs::get().await?;
if !state.settings.read().await.allow_parallel_running_clusters {
let running = state.processes.has_running(cluster.id).await;
@ -49,7 +48,7 @@ pub async fn launch_minecraft(
}
}
let cwd = &io::canonicalize(&cluster.path)?;
let cwd = &io::canonicalize(dirs.clusters_dir().join(cluster.folder_name.clone()))?;
let metadata = state.metadata.read().await;
let versions = &metadata.get_vanilla()?.versions;
@ -85,7 +84,6 @@ pub async fn launch_minecraft(
let java_path = PathBuf::from(&java.absolute_path);
java::check_java_runtime(&java_path, false).await?;
let dirs = Dirs::get().await?;
let client_jar_path = dirs
.versions_dir()
.join(&version_name)
@ -187,19 +185,14 @@ pub async fn launch_minecraft(
io::write(&options_path, options_string).await?;
}
let start_time = Utc::now().timestamp();
update_cluster(cluster, async |mut cluster| {
cluster.last_played = Set(Some(start_time));
Ok(cluster)
})
.await?;
update_playtime(cluster.id, 0).await?;
let censors = log::create_censors(&creds);
if let Some(discord) = &state.rpc {
discord.set_message(
&format!("Playing {}", cluster.name),
Some(Timestamps::new().start(start_time)),
Some(Timestamps::new().start(Utc::now().timestamp())),
).await;
}

View file

@ -0,0 +1,165 @@
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 crate::error::{DaoError, LauncherResult};
use crate::store::State;
use super::PackageError;
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;
Ok(package.insert(db).await?)
}
/// Links a package to a cluster in database.
pub async fn link_package_to_cluster(
package: &packages::Model,
cluster: &clusters::Model,
) -> LauncherResult<()> {
let state = State::get().await?;
let db = &state.db;
let cluster_package = cluster_packages::Model {
cluster_id: cluster.id,
package_hash: package.hash.clone(),
}
.into_active_model();
cluster_packages::Entity::insert(cluster_package)
.exec(db)
.await?;
Ok(())
}
/// Retrieves a clusters linked packages from the database.
pub async fn get_linked_packages(
cluster: &clusters::Model,
) -> LauncherResult<Vec<packages::Model>> {
let state = State::get().await?;
let db = &state.db;
let linked_packages = cluster_packages::Entity::find()
.filter(cluster_packages::Column::ClusterId.eq(cluster.id))
.find_with_related(packages::Entity)
.all(db)
.await?
.into_iter()
.flat_map(|(_cluster_packages, packages)| packages)
.collect::<Vec<_>>();
Ok(linked_packages)
}
/// Is the package linked to the cluster?
pub async fn is_package_linked_to_cluster(
package: &packages::Model,
cluster: &clusters::Model,
) -> LauncherResult<bool> {
let state = State::get().await?;
let db = &state.db;
let linked_packages = cluster_packages::Entity::find()
.filter(cluster_packages::Column::ClusterId.eq(cluster.id))
.filter(cluster_packages::Column::PackageHash.eq(package.hash.clone()))
.one(db)
.await?;
Ok(linked_packages.is_some())
}
/// Retrieves a package by its hash from the database.
pub async fn get_package_by_hash(hash: PackageId) -> LauncherResult<Option<packages::Model>> {
let state = State::get().await?;
let db = &state.db;
Ok(packages::Entity::find_by_id(hash).one(db).await?)
}
/// Retrieves all packages that match the package type, loader and version(s) in the database.
pub async fn get_packages_for(
package_type: PackageType,
loader: GameLoader,
versions: Vec<String>,
) -> LauncherResult<Vec<packages::Model>> {
let state = State::get().await?;
let db = &state.db;
Ok(packages::Entity::find()
.filter(packages::Column::PackageType.eq(package_type))
.filter(packages::Column::McLoader.eq(loader))
.all(db)
.await?
.into_iter()
.filter(|p| {
let mc_versions = p.mc_versions.clone();
versions.iter().any(|v| mc_versions.0.contains(v))
})
.collect())
}
/// Updates an existing package in the database.
pub async fn update_package_by_hash<B>(id: PackageId, block: B) -> LauncherResult<packages::Model>
where
B: AsyncFnOnce(packages::ActiveModel) -> LauncherResult<packages::ActiveModel>,
{
let state = State::get().await?;
let db = &state.db;
let model = get_package_by_hash(id).await?.ok_or(DaoError::NotFound)?;
let model = block(model.into_active_model()).await?;
let model = model.update(db).await?;
Ok(model)
}
/// Updates an existing package in the database.
pub async fn update_package<B>(
package: &mut packages::Model,
block: B,
) -> LauncherResult<&mut packages::Model>
where
B: AsyncFnOnce(packages::ActiveModel) -> LauncherResult<packages::ActiveModel>,
{
let state = State::get().await?;
let db = &state.db;
let model = package.clone().into_active_model();
let model = block(model).await?;
let model = model.update(db).await?;
*package = model;
Ok(package)
}
/// Deletes a package by its hash from the **database**.
pub async fn delete_package_by_id(id: PackageId) -> LauncherResult<()> {
let state = State::get().await?;
let db = &state.db;
let deleted = packages::Entity::delete_by_id(id).exec(db).await?;
if deleted.rows_affected == 0 {
return Err(DaoError::NotFound.into());
}
Ok(())
}

View file

@ -40,8 +40,8 @@ pub struct SearchQuery {
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManagedPackage {
pub id: String,
pub provider: Provider,
pub provider_id: String,
pub package_type: PackageType,
pub name: String,
pub short_desc: String,

View file

@ -1,5 +1,180 @@
use data::{ManagedPackage, ManagedVersion};
use onelauncher_entity::icon::Icon;
use onelauncher_entity::package::PackageType;
use onelauncher_entity::{clusters, packages};
use reqwest::Method;
use crate::error::{LauncherError, LauncherResult};
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};
pub mod dao;
pub mod data;
pub mod provider;
#[cfg(test)]
mod tests;
mod tests;
#[derive(Debug, thiserror::Error)]
pub enum PackageError {
#[error("no file marked for download")]
NoPrimaryFile,
#[error("package is a modpack, it must be handled differently")]
IsModPack,
#[error(transparent)]
Incompatible(#[from] IncompatiblePackageType),
}
#[derive(Debug, thiserror::Error)]
pub enum IncompatiblePackageType {
#[error("package is not compatible with the current version")]
McVersion,
#[error("package is not compatible with the current loader")]
Loader,
}
/// Downloads a package and registers it in the database from the given provider.
#[tracing::instrument(level = "debug", skip_all)]
pub async fn download_package(
package: &ManagedPackage,
version: &ManagedVersion,
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,
version.version_id,
package.provider
);
// Check if the package is already downloaded
let primary_file = version
.files
.iter()
.find(|f| f.primary)
.ok_or(PackageError::NoPrimaryFile)?;
if 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 {
hash: primary_file.sha1.clone(),
display_name: package.name.clone(),
display_version: version.display_version.clone(),
file_name: primary_file.file_name.clone(),
version_id: version.version_id.clone(),
published_at: version.published,
provider: package.provider.clone(),
icon,
package_id: package.id.clone(),
mc_loader: version.loaders.clone().into(),
mc_versions: version.mc_versions.clone().into(),
package_type: package.package_type.clone(),
};
dao::insert_package(model.into()).await
}
/// 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)]
pub async fn link_package(
package: &packages::Model,
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,
package.display_version,
cluster.name
);
tracing::trace!("checking if package is already linked to cluster");
if dao::is_package_linked_to_cluster(package, cluster).await? {
tracing::debug!("package is already linked to cluster");
return Ok(());
}
tracing::trace!("checking compatibility of package with cluster");
if !skip_compatibility.unwrap_or(false) {
if !package.mc_loader.iter().any(|v| cluster.mc_loader.compatible_with(v)) {
return Err(LauncherError::from(PackageError::from(IncompatiblePackageType::Loader)));
}
if !package.mc_versions.iter().any(|v| cluster.mc_version.contains(v)) {
return Err(LauncherError::from(PackageError::from(IncompatiblePackageType::McVersion)));
}
}
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)?;
dao::link_package_to_cluster(package, cluster).await?;
tracing::debug!(
"linked package '{}' version '{}' to cluster '{}'",
package.display_name,
package.display_version,
cluster.name
);
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())
}
fn join_package_file(path: impl AsRef<std::path::Path>, version_id: &str, file_name: &str) -> std::path::PathBuf {
path.as_ref().join(format!("{version_id}-{file_name}"))
}

View file

@ -308,7 +308,7 @@ impl From<ModrinthPackage> for ManagedPackage {
fn from(value: ModrinthPackage) -> Self {
Self {
provider: Provider::Modrinth,
provider_id: value.id,
id: value.id,
package_type: value.project_type,
name: value.title,
short_desc: value.description,

View file

@ -1,6 +1,7 @@
use onelauncher_entity::package::Provider;
use crate::{api::packages::provider::ProviderExt, error::LauncherResult, store::{Core, CoreOptions}};
use crate::{api::packages::{self, provider::ProviderExt}, error::LauncherResult, store::{Core, CoreOptions, Dirs}};
#[tokio::test]
pub async fn test_get_provider() -> LauncherResult<()> {
@ -10,7 +11,7 @@ pub async fn test_get_provider() -> LauncherResult<()> {
let res = provider.get("oneconfig").await?;
assert_eq!(res.provider_id, "AibBIVmj");
assert_eq!(res.id, "AibBIVmj");
Ok(())
}
@ -41,4 +42,26 @@ pub async fn test_get_versions() -> LauncherResult<()> {
Ok(())
}
#[tokio::test]
pub async fn test_download_chatting() -> LauncherResult<()> {
Core::initialize(CoreOptions::default()).await?;
let provider = Provider::Modrinth;
let pkg = provider.get("chatting").await?;
let ver_id = pkg.version_ids.first().expect("No version found");
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;
assert!(db_model.is_ok_and(|m| m.display_name == pkg.name));
let dir = Dirs::get_package_dir(&pkg.package_type, &provider, &pkg.id).await?;
let dest = dir.join(&ver.files.first().unwrap().file_name);
assert!(dest.exists(), "File does not exist: {dest:?}");
Ok(())
}
// TODO: Add more tests

View file

@ -21,6 +21,8 @@ pub enum LauncherError {
#[error(transparent)]
ProcessError(#[from] crate::store::processes::ProcessError),
#[error(transparent)]
PackageError(#[from] crate::api::packages::PackageError),
#[error(transparent)]
DaoError(#[from] DaoError),
#[error(transparent)]

View file

@ -1,5 +1,6 @@
use std::path::PathBuf;
use onelauncher_entity::package::{PackageType, Provider};
use tokio::sync::OnceCell;
use crate::{utils, LauncherResult};
@ -46,19 +47,19 @@ impl Dirs {
}
macro_rules! dirs_impl {
($($name:ident => |$self:ident| $path:expr),*$(,)?) => {
($($name:ident = |$self:ident$(, $param:ident : $type:ty )*| $path:expr),*$(,)?) => {
paste::paste! {
impl Dirs {
$(
#[must_use]
pub fn $name(&self) -> PathBuf {
pub fn $name(&self $(, $param: $type)*) -> PathBuf {
let $self = self;
$path
}
pub async fn [<get _ $name>]() -> LauncherResult<PathBuf> {
pub async fn [<get _ $name>]($($param: $type),*) -> LauncherResult<PathBuf> {
Dirs::get().await.map(|$self| {
$self.$name()
$self.$name($($param),*)
})
}
)*
@ -68,22 +69,30 @@ macro_rules! dirs_impl {
}
dirs_impl! {
db_file => |this| this.base_dir.join("user_data.db"),
settings_file => |this| this.base_dir.join("settings.json"),
auth_file => |this| this.base_dir.join("auth.json"),
db_file = |this| this.base_dir.join("user_data.db"),
settings_file = |this| this.base_dir.join("settings.json"),
auth_file = |this| this.base_dir.join("auth.json"),
launcher_logs_dir => |this| this.base_dir.join("logs"),
clusters_dir => |this| this.base_dir.join("clusters"),
metadata_dir => |this| this.base_dir.join("metadata"),
launcher_logs_dir = |this| this.base_dir.join("logs"),
clusters_dir = |this| this.base_dir.join("clusters"),
metadata_dir = |this| this.base_dir.join("metadata"),
versions_dir => |this| this.metadata_dir().join("versions"),
libraries_dir => |this| this.metadata_dir().join("libraries"),
natives_dir => |this| this.metadata_dir().join("natives"),
java_dir => |this| this.metadata_dir().join("java"),
caches_dir => |this| this.metadata_dir().join("caches"),
versions_dir = |this| this.metadata_dir().join("versions"),
libraries_dir = |this| this.metadata_dir().join("libraries"),
natives_dir = |this| this.metadata_dir().join("natives"),
java_dir = |this| this.metadata_dir().join("java"),
caches_dir = |this| this.metadata_dir().join("caches"),
packages_dir = |this| this.metadata_dir().join("packages"),
assets_dir => |this| this.metadata_dir().join("assets"),
legacy_assets_dir => |this| this.metadata_dir().join("resources"),
assets_index_dir => |this| this.assets_dir().join("indexes"),
assets_object_dir => |this| this.assets_dir().join("objects"),
}
assets_dir = |this| this.metadata_dir().join("assets"),
legacy_assets_dir = |this| this.metadata_dir().join("resources"),
assets_index_dir = |this| this.assets_dir().join("indexes"),
assets_object_dir = |this| this.assets_dir().join("objects"),
package_dir = |this, package_type: &PackageType, provider: &Provider, project_id: &str| {
this.packages_dir()
.join(package_type.folder_name())
.join(provider.name())
.join(project_id)
}
}

View file

@ -139,7 +139,7 @@ pub async fn fetch_advanced(
}
}
tracing::debug!("finished downloading {url}");
tracing::debug!("finished fetching {url}");
Ok(bytes)
}

View file

@ -0,0 +1,40 @@
use onelauncher_entity::icon::{Icon, IconType};
use reqwest::Method;
use crate::{error::LauncherResult, store::Dirs};
use super::{crypto::HashAlgorithm, http, io};
/// Caches a icon on the filesystem. Name is the equivalent to the sha1 hash of the icon.
pub async fn cache_icon(icon: &Icon) -> LauncherResult<Icon> {
Ok(match icon.get_type() {
IconType::Cache => icon.clone(),
IconType::Path => {
let bytes = io::read(&**icon).await?;
cache_icon_bytes(&bytes).await?
},
IconType::Url => {
let bytes = http::fetch(Method::GET, icon).await?;
cache_icon_bytes(&bytes).await?
},
IconType::Unknown => {
return Err(anyhow::anyhow!(
"icon type is unknown, cannot cache icon: {}",
icon
).into());
},
})
}
/// Caches a icon bytes on the filesystem. Name is the equivalent to the sha1 hash of the icon.
pub async fn cache_icon_bytes(bytes: &[u8]) -> LauncherResult<Icon> {
let hash = HashAlgorithm::Sha1.hash(bytes).await?;
let dir = Dirs::get_caches_dir().await?.join(&hash[0..2]);
io::create_dir_all(&dir).await?;
let path = dir.join(&hash);
io::write(&path, bytes).await?;
Ok(Icon::from_hash(hash))
}

View file

@ -1,5 +1,6 @@
pub mod crypto;
pub mod os_ext;
pub mod http;
pub mod icon;
pub mod io;
pub mod pagination;

View file

@ -13,8 +13,25 @@ impl Nullable for Icon {
}
impl Icon {
pub fn new(icon: String) -> Self {
Self(icon)
pub fn from_hash(hash: String) -> Self {
Self(format!("cache://{hash}"))
}
pub fn try_from_path(path: impl AsRef<std::path::Path>) -> Option<Self> {
let path = path.as_ref();
if !path.is_absolute() {
return None;
}
Some(Self(format!("file://{}", path.display())))
}
pub fn try_from_url(url: url::Url) -> Option<Self> {
if url.scheme() != "http" && url.scheme() != "https" {
return None;
}
Some(Self(url.as_str().to_string()))
}
pub fn get_type(&self) -> IconType {
@ -22,29 +39,36 @@ impl Icon {
IconType::Path
} else if self.is_url() {
IconType::Url
} else if self.is_cached() {
IconType::Cache
} else {
IconType::Stored
IconType::Unknown
}
}
/// Returns true if the icon is a path to a file.
pub fn is_path(&self) -> bool {
self.0.starts_with("file://")
}
/// Returns true if the icon is a HTTP URL.
pub fn is_url(&self) -> bool {
self.0.starts_with("http://") || self.0.starts_with("https://")
}
pub fn is_stored(&self) -> bool {
!self.is_path() && !self.is_url()
/// Returns true if the icon is a cached icon. Relative to the cache directory.
pub fn is_cached(&self) -> bool {
self.0.starts_with("cache://")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum IconType {
#[default]
Unknown,
Path,
Url,
Stored,
Cache,
}
impl Display for IconType {
@ -63,7 +87,7 @@ impl Deref for Icon {
impl From<String> for Icon {
fn from(s: String) -> Self {
Self::new(s)
Self(s)
}
}
@ -71,7 +95,7 @@ impl FromStr for Icon {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::new(s.to_string()))
Ok(Self(s.to_string()))
}
}

View file

@ -44,6 +44,17 @@ impl GameLoader {
}
}
pub fn compatible_with(&self, other: &Self) -> bool {
match self {
Self::Vanilla => matches!(other, Self::Vanilla),
Self::Forge => matches!(other, Self::Forge),
Self::NeoForge => matches!(other, Self::NeoForge),
Self::Quilt => matches!(other, Self::Quilt | Self::Fabric),
Self::Fabric => matches!(other, Self::Fabric),
Self::LegacyFabric => matches!(other, Self::LegacyFabric),
}
}
}
impl TryFrom<String> for GameLoader {

View file

@ -4,3 +4,4 @@ pub mod loader;
pub mod package;
pub mod resolution;
pub mod settings;
pub mod utility;

View file

@ -0,0 +1,68 @@
use std::ops::Deref;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(transparent)]
pub struct DbVec<T>(pub Vec<T>);
impl<T> Deref for DbVec<T> {
type Target = Vec<T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> From<Vec<T>> for DbVec<T> {
fn from(v: Vec<T>) -> Self {
Self(v)
}
}
impl<T> sea_orm::TryGetableFromJson for DbVec<T> where for<'de> T: Deserialize<'de> {}
impl<T> std::convert::From<DbVec<T>> for sea_orm::Value
where
DbVec<T>: Serialize,
{
fn from(source: DbVec<T>) -> Self {
sea_orm::Value::Json(
serde_json::to_value(&source)
.ok()
.map(std::boxed::Box::new),
)
}
}
impl<T> sea_orm::sea_query::ValueType for DbVec<T>
where
DbVec<T>: DeserializeOwned,
{
fn try_from(v: sea_orm::Value) -> Result<Self, sea_orm::sea_query::ValueTypeErr> {
match v {
sea_orm::Value::Json(Some(json)) => {
Ok(serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?)
}
_ => Err(sea_orm::sea_query::ValueTypeErr),
}
}
fn type_name() -> String {
stringify!(#ident).to_owned()
}
fn array_type() -> sea_orm::sea_query::ArrayType {
sea_orm::sea_query::ArrayType::Json
}
fn column_type() -> sea_orm::sea_query::ColumnType {
sea_orm::sea_query::ColumnType::Json
}
}
impl<T> sea_orm::sea_query::Nullable for DbVec<T> {
fn null() -> sea_orm::Value {
sea_orm::Value::Json(None)
}
}

View file

@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize};
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
#[sea_orm(column_type = "Text")]
pub name: String,
}

View file

@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub cluster_id: i64,
#[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
#[sea_orm(primary_key, auto_increment = false)]
pub package_hash: String,
}

View file

@ -9,25 +9,22 @@ use crate::{cluster_stage::ClusterStage, icon::Icon, loader::GameLoader};
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
#[sea_orm(column_type = "Text")]
pub path: String,
pub folder_name: String,
pub stage: ClusterStage,
pub created_at: i64,
pub created_at: DateTimeUtc,
pub group_id: Option<i64>,
#[sea_orm(column_type = "Text")]
pub name: String,
#[sea_orm(column_type = "Text")]
pub mc_version: String,
pub mc_loader: GameLoader,
#[sea_orm(column_type = "Text", nullable)]
#[sea_orm(nullable)]
pub mc_loader_version: Option<String>,
pub last_played: Option<i64>,
pub last_played: Option<DateTimeUtc>,
pub overall_played: Option<i64>,
#[sea_orm(column_type = "Text", nullable)]
#[sea_orm(nullable)]
pub icon_url: Option<Icon>,
#[sea_orm(column_type = "Text", nullable)]
#[sea_orm(nullable)]
pub setting_profile_name: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
#[sea_orm(nullable)]
pub linked_pack_id: Option<String>,
pub linked_pack_version: Option<String>,
}

View file

@ -10,13 +10,9 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub major_version: u32,
#[sea_orm(column_type = "Text")]
pub vendor_name: String,
#[sea_orm(column_type = "Text")]
pub absolute_path: String,
#[sea_orm(column_type = "Text")]
pub full_version: String,
#[sea_orm(column_type = "Text")]
pub arch: String,
}

View file

@ -3,30 +3,26 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::{icon::Icon, package::{PackageType, Provider}};
use crate::{icon::Icon, loader::GameLoader, package::{PackageType, Provider}, utility::DbVec};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "packages")]
#[onelauncher_macro::specta]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
#[sea_orm(primary_key, auto_increment = false)]
pub hash: String,
#[sea_orm(column_type = "Text")]
pub file_name: String,
#[sea_orm(column_type = "Text")]
pub version_id: String,
pub published_at: DateTimeUtc,
pub display_name: String,
#[sea_orm(column_type = "Text")]
pub display_version: String,
pub project_type_id: PackageType,
pub provider_id: Provider,
#[sea_orm(column_type = "Text")]
pub provider_version: String,
#[sea_orm(column_type = "Text")]
pub mc_versions: String,
#[sea_orm(column_type = "Text")]
pub mc_loader: String,
#[sea_orm(column_type = "Text", nullable)]
pub icon_url: Option<Icon>,
pub package_type: PackageType,
pub provider: Provider,
pub package_id: String,
pub mc_versions: DbVec<String>,
pub mc_loader: DbVec<GameLoader>,
#[sea_orm(nullable)]
pub icon: Option<Icon>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -15,21 +15,21 @@ pub struct Model {
#[merge(skip)]
pub name: String,
pub java_id: Option<i64>,
#[sea_orm(column_type = "Text", nullable)]
#[sea_orm(nullable)]
pub res: Option<Resolution>,
pub force_fullscreen: Option<bool>,
pub mem_max: Option<u32>,
#[sea_orm(column_type = "Text", nullable)]
#[sea_orm(nullable)]
pub launch_args: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
#[sea_orm(nullable)]
pub launch_env: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
#[sea_orm(nullable)]
pub hook_pre: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
#[sea_orm(nullable)]
pub hook_wrapper: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
#[sea_orm(nullable)]
pub hook_post: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
#[sea_orm(nullable)]
pub os_extra: Option<SettingsOsExtra>
}

View file

@ -19,3 +19,4 @@ path = "src/lib.rs"
tokio = { workspace = true }
async-trait = { workspace = true }
sea-orm-migration = { workspace = true }
chrono = { workspace = true }

View file

@ -1,82 +0,0 @@
create table cluster_groups (
id integer not null,
name text not null,
constraint cluster_groups_pk primary key (id autoincrement)
);
create table java_versions (
id integer not null,
major_version integer not null,
vendor_name text not null,
absolute_path text not null,
full_version text not null,
arch text not null,
constraint java_versions_pk primary key (id autoincrement)
);
create index java_versions_major_version_idx on java_versions (major_version);
create table packages (
hash text not null,
file_name text not null,
display_name text not null,
display_version text not null,
project_type_id integer not null,
provider_id integer not null,
provider_version text not null,
mc_versions text not null,
mc_loader text not null,
icon_url text,
constraint packages_pk primary key (hash)
);
create index packages_project_type_id_idx on packages (project_type_id);
create index packages_provider_id_idx on packages (provider_id);
create index packages_mc_loader_mc_versions_idx on packages (mc_loader, mc_versions);
create table setting_profiles (
name text not null,
java_id integer,
res json_text,
force_fullscreen boolean,
mem_max integer,
launch_args text,
launch_env text,
hook_pre text,
hook_wrapper text,
hook_post text,
os_extra json_text,
constraint setting_profiles_pk primary key (name),
constraint setting_profiles_java_versions_id_fk foreign key (java_id) references java_versions(id)
);
create table clusters (
id integer not null,
path text not null,
stage integer default (0) not null,
created_at integer default (unixepoch()) not null,
group_id integer,
name text not null,
mc_version text not null,
mc_loader integer not null,
mc_loader_version text,
last_played integer,
overall_played integer,
icon_url text,
setting_profile_name text,
linked_pack_id text,
linked_pack_version text,
constraint clusters_pk primary key (id autoincrement),
constraint clusters_cluster_groups_id_fk foreign key (group_id) references cluster_groups(id),
constraint clusters_setting_profiles_name_fk foreign key (setting_profile_name) references setting_profiles(name)
);
create index clusters_path_idx on clusters (path);
create table cluster_packages (
cluster_id integer not null,
package_hash text not null,
constraint cluster_packages_pk primary key (cluster_id, package_hash),
constraint cluster_packages_clusters_id_fk foreign key (cluster_id) references clusters(id),
constraint cluster_packages_packages_hash_fk foreign key (package_hash) references packages(hash)
);

View file

@ -1,3 +1,4 @@
use chrono::Utc;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
@ -6,12 +7,270 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.get_connection().execute_unprepared(include_str!("./init.sql")).await?;
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_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_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(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_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?;
Ok(())
}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
unreachable!("Down migration is not supported for init migration");
Err(DbErr::Migration("no rollback available".to_owned()))
}
}
#[derive(Iden)]
enum ClusterGroups {
Table,
Id,
Name,
}
#[derive(Iden)]
enum JavaVersions {
Table,
Id,
MajorVersion,
VendorName,
AbsolutePath,
FullVersion,
Arch,
}
#[derive(Iden)]
enum Packages {
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,
}
#[derive(Iden)]
enum Clusters {
Table,
Id,
FolderName,
Stage,
CreatedAt,
GroupId,
Name,
McVersion,
McLoader,
McLoaderVersion,
LastPlayed,
OverallPlayed,
IconUrl,
SettingProfileName,
LinkedPackId,
LinkedPackVersion,
}
#[derive(Iden)]
enum ClusterPackages {
Table,
ClusterId,
PackageHash,
}