26.1.2 support
This commit is contained in:
parent
6c7d0a1ca8
commit
d593ecb09f
27 changed files with 967 additions and 86 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4599,6 +4599,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -684,6 +684,7 @@ impl CoreBridgeRuntime {
|
||||||
BridgeCommand::SyncBundles => {
|
BridgeCommand::SyncBundles => {
|
||||||
let state = LauncherState::get()?;
|
let state = LauncherState::get()?;
|
||||||
state.bundles.sync(&state.services).await?;
|
state.bundles.sync(&state.services).await?;
|
||||||
|
oneclient_core::clusters::apply_remote_migrations(&state).await?;
|
||||||
oneclient_core::clusters::ensure_from_bundles(&state).await?;
|
oneclient_core::clusters::ensure_from_bundles(&state).await?;
|
||||||
let changed = oneclient_core::bundles::sync_all_cluster_bundles(
|
let changed = oneclient_core::bundles::sync_all_cluster_bundles(
|
||||||
state.bundles.as_ref(),
|
state.bundles.as_ref(),
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ impl Component for ActiveClusterPanel {
|
||||||
.and_then(|c| parse_mc_version(&c.mc_version));
|
.and_then(|c| parse_mc_version(&c.mc_version));
|
||||||
let metadata = use_version_metadata(
|
let metadata = use_version_metadata(
|
||||||
parsed.as_ref().map(|p| p.major),
|
parsed.as_ref().map(|p| p.major),
|
||||||
parsed.and_then(|p| p.minor),
|
parsed.and_then(|p| p.key()),
|
||||||
active.as_ref().map(|c| c.mc_loader),
|
active.as_ref().map(|c| c.mc_loader),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
|
use oneclient_core::VersionKey;
|
||||||
use oneclient_core::packages::domain::GameLoader;
|
use oneclient_core::packages::domain::GameLoader;
|
||||||
|
|
||||||
use crate::components::DynamicArt;
|
use crate::components::DynamicArt;
|
||||||
|
|
@ -14,12 +15,12 @@ pub struct ClusterLandscapeArt {
|
||||||
impl ClusterLandscapeArt {
|
impl ClusterLandscapeArt {
|
||||||
pub fn for_version(
|
pub fn for_version(
|
||||||
major: u32,
|
major: u32,
|
||||||
minor: Option<u32>,
|
key: Option<VersionKey>,
|
||||||
loader: Option<GameLoader>,
|
loader: Option<GameLoader>,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
art: DynamicArt::for_version(major, minor, loader).max_edge(512),
|
art: DynamicArt::for_version(major, key, loader).max_edge(512),
|
||||||
selected,
|
selected,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use freya::query::QueryStateData;
|
||||||
use oneclient_core::clusters::Cluster;
|
use oneclient_core::clusters::Cluster;
|
||||||
use oneclient_core::images::DEFAULT_IMAGE_EDGE;
|
use oneclient_core::images::DEFAULT_IMAGE_EDGE;
|
||||||
use oneclient_core::packages::domain::GameLoader;
|
use oneclient_core::packages::domain::GameLoader;
|
||||||
use oneclient_core::parse_mc_version;
|
use oneclient_core::{VersionKey, parse_mc_version};
|
||||||
|
|
||||||
use crate::AppAssets;
|
use crate::AppAssets;
|
||||||
use crate::hooks::{use_cached_image, use_version_metadata};
|
use crate::hooks::{use_cached_image, use_version_metadata};
|
||||||
|
|
@ -13,16 +13,16 @@ use crate::layout::HOME_BACKGROUND_ASSET;
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
pub struct DynamicArt {
|
pub struct DynamicArt {
|
||||||
major: Option<u32>,
|
major: Option<u32>,
|
||||||
minor: Option<u32>,
|
key: Option<VersionKey>,
|
||||||
loader: Option<GameLoader>,
|
loader: Option<GameLoader>,
|
||||||
max_edge: u32,
|
max_edge: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DynamicArt {
|
impl DynamicArt {
|
||||||
pub fn for_version(major: u32, minor: Option<u32>, loader: Option<GameLoader>) -> Self {
|
pub fn for_version(major: u32, key: Option<VersionKey>, loader: Option<GameLoader>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
major: Some(major),
|
major: Some(major),
|
||||||
minor,
|
key,
|
||||||
loader,
|
loader,
|
||||||
max_edge: DEFAULT_IMAGE_EDGE,
|
max_edge: DEFAULT_IMAGE_EDGE,
|
||||||
}
|
}
|
||||||
|
|
@ -31,7 +31,7 @@ impl DynamicArt {
|
||||||
pub fn for_major(major: u32) -> Self {
|
pub fn for_major(major: u32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
major: Some(major),
|
major: Some(major),
|
||||||
minor: None,
|
key: None,
|
||||||
loader: None,
|
loader: None,
|
||||||
max_edge: DEFAULT_IMAGE_EDGE,
|
max_edge: DEFAULT_IMAGE_EDGE,
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +41,7 @@ impl DynamicArt {
|
||||||
let parsed = parse_mc_version(&cluster.mc_version);
|
let parsed = parse_mc_version(&cluster.mc_version);
|
||||||
Self {
|
Self {
|
||||||
major: parsed.as_ref().map(|p| p.major),
|
major: parsed.as_ref().map(|p| p.major),
|
||||||
minor: parsed.and_then(|p| p.minor),
|
key: parsed.and_then(|p| p.key()),
|
||||||
loader: Some(cluster.mc_loader),
|
loader: Some(cluster.mc_loader),
|
||||||
max_edge: DEFAULT_IMAGE_EDGE,
|
max_edge: DEFAULT_IMAGE_EDGE,
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ impl DynamicArt {
|
||||||
pub fn fallback() -> Self {
|
pub fn fallback() -> Self {
|
||||||
Self {
|
Self {
|
||||||
major: None,
|
major: None,
|
||||||
minor: None,
|
key: None,
|
||||||
loader: None,
|
loader: None,
|
||||||
max_edge: DEFAULT_IMAGE_EDGE,
|
max_edge: DEFAULT_IMAGE_EDGE,
|
||||||
}
|
}
|
||||||
|
|
@ -63,19 +63,19 @@ impl DynamicArt {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_bytes(&self) -> (String, Bytes) {
|
pub fn use_bytes(&self) -> (String, Bytes) {
|
||||||
use_art_bytes(self.major, self.minor, self.loader, self.max_edge)
|
use_art_bytes(self.major, self.key, self.loader, self.max_edge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_art_bytes(
|
pub fn use_art_bytes(
|
||||||
major: Option<u32>,
|
major: Option<u32>,
|
||||||
minor: Option<u32>,
|
key: Option<VersionKey>,
|
||||||
loader: Option<GameLoader>,
|
loader: Option<GameLoader>,
|
||||||
max_edge: u32,
|
max_edge: u32,
|
||||||
) -> (String, Bytes) {
|
) -> (String, Bytes) {
|
||||||
let fallback = use_memo(|| AppAssets::get_bytes(HOME_BACKGROUND_ASSET).unwrap_or_default());
|
let fallback = use_memo(|| AppAssets::get_bytes(HOME_BACKGROUND_ASSET).unwrap_or_default());
|
||||||
|
|
||||||
let art_url = use_version_metadata(major, minor, loader).and_then(|m| m.art_url);
|
let art_url = use_version_metadata(major, key, loader).and_then(|m| m.art_url);
|
||||||
|
|
||||||
let image_query = use_cached_image(art_url.clone(), max_edge);
|
let image_query = use_cached_image(art_url.clone(), max_edge);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use freya::query::QueryStateData;
|
use freya::query::QueryStateData;
|
||||||
|
use oneclient_core::VersionKey;
|
||||||
use oneclient_core::VersionMetadata;
|
use oneclient_core::VersionMetadata;
|
||||||
use oneclient_core::packages::domain::GameLoader;
|
use oneclient_core::packages::domain::GameLoader;
|
||||||
|
|
||||||
|
|
@ -7,7 +8,7 @@ use super::use_versions;
|
||||||
pub fn pick_version_metadata(
|
pub fn pick_version_metadata(
|
||||||
list: &[VersionMetadata],
|
list: &[VersionMetadata],
|
||||||
major: u32,
|
major: u32,
|
||||||
minor: Option<u32>,
|
key: Option<VersionKey>,
|
||||||
loader: Option<GameLoader>,
|
loader: Option<GameLoader>,
|
||||||
) -> Option<VersionMetadata> {
|
) -> Option<VersionMetadata> {
|
||||||
let loader_matches = |m: &VersionMetadata| match (&m.loader, loader) {
|
let loader_matches = |m: &VersionMetadata| match (&m.loader, loader) {
|
||||||
|
|
@ -15,12 +16,20 @@ pub fn pick_version_metadata(
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(minor) = minor {
|
if let Some(key) = key {
|
||||||
if let Some(hit) = list.iter().find(|m| {
|
if let Some(hit) = list
|
||||||
m.major_version == major && m.minor_version == Some(minor) && loader_matches(m)
|
.iter()
|
||||||
}) {
|
.find(|m| m.major_version == major && m.key() == Some(key) && loader_matches(m))
|
||||||
|
{
|
||||||
return Some(hit.clone());
|
return Some(hit.clone());
|
||||||
}
|
}
|
||||||
|
if let Some(hit) = list
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.major_version == major && m.key() == Some(key))
|
||||||
|
{
|
||||||
|
return Some(hit.clone());
|
||||||
|
}
|
||||||
|
let (minor, _) = key;
|
||||||
if let Some(hit) = list
|
if let Some(hit) = list
|
||||||
.iter()
|
.iter()
|
||||||
.find(|m| m.major_version == major && m.minor_version == Some(minor))
|
.find(|m| m.major_version == major && m.minor_version == Some(minor))
|
||||||
|
|
@ -36,7 +45,7 @@ pub fn pick_version_metadata(
|
||||||
|
|
||||||
pub fn use_version_metadata(
|
pub fn use_version_metadata(
|
||||||
major: Option<u32>,
|
major: Option<u32>,
|
||||||
minor: Option<u32>,
|
key: Option<VersionKey>,
|
||||||
loader: Option<GameLoader>,
|
loader: Option<GameLoader>,
|
||||||
) -> Option<VersionMetadata> {
|
) -> Option<VersionMetadata> {
|
||||||
let versions_query = use_versions();
|
let versions_query = use_versions();
|
||||||
|
|
@ -52,5 +61,59 @@ pub fn use_version_metadata(
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
pick_version_metadata(list, major, minor, loader)
|
pick_version_metadata(list, major, key, loader)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn entry(
|
||||||
|
major: u32,
|
||||||
|
minor: Option<u32>,
|
||||||
|
patch: Option<u32>,
|
||||||
|
loader: Option<&str>,
|
||||||
|
name: &str,
|
||||||
|
) -> VersionMetadata {
|
||||||
|
VersionMetadata {
|
||||||
|
major_version: major,
|
||||||
|
minor_version: minor,
|
||||||
|
patch_version: patch,
|
||||||
|
loader: loader.map(str::to_string),
|
||||||
|
name: name.to_string(),
|
||||||
|
art_url: None,
|
||||||
|
long_description: None,
|
||||||
|
tags: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list() -> Vec<VersionMetadata> {
|
||||||
|
vec![
|
||||||
|
entry(26, None, None, None, "major"),
|
||||||
|
entry(26, Some(1), None, Some("fabric"), "bare minor"),
|
||||||
|
entry(26, Some(1), Some(2), Some("fabric"), "patched"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_and_bare_minor_are_distinct() {
|
||||||
|
let bare = pick_version_metadata(&list(), 26, Some((1, None)), Some(GameLoader::Fabric));
|
||||||
|
assert_eq!(bare.unwrap().name, "bare minor");
|
||||||
|
|
||||||
|
let patched =
|
||||||
|
pick_version_metadata(&list(), 26, Some((1, Some(2))), Some(GameLoader::Fabric));
|
||||||
|
assert_eq!(patched.unwrap().name, "patched");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_patch_falls_back_to_its_minor_line() {
|
||||||
|
let hit = pick_version_metadata(&list(), 26, Some((1, Some(9))), Some(GameLoader::Fabric));
|
||||||
|
assert_eq!(hit.unwrap().name, "bare minor");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_minor_falls_back_to_major() {
|
||||||
|
let hit = pick_version_metadata(&list(), 26, Some((7, None)), Some(GameLoader::Fabric));
|
||||||
|
assert_eq!(hit.unwrap().name, "major");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ impl Component for ClusterShell {
|
||||||
let parsed = parse_mc_version(&cluster.mc_version);
|
let parsed = parse_mc_version(&cluster.mc_version);
|
||||||
let metadata = use_version_metadata(
|
let metadata = use_version_metadata(
|
||||||
parsed.as_ref().map(|p| p.major),
|
parsed.as_ref().map(|p| p.major),
|
||||||
parsed.and_then(|p| p.minor),
|
parsed.and_then(|p| p.key()),
|
||||||
Some(cluster.mc_loader),
|
Some(cluster.mc_loader),
|
||||||
);
|
);
|
||||||
let subtitle = metadata
|
let subtitle = metadata
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,14 @@ use std::sync::OnceLock;
|
||||||
use chrono::{Datelike, NaiveDate};
|
use chrono::{Datelike, NaiveDate};
|
||||||
use oneclient_core::clusters::Cluster;
|
use oneclient_core::clusters::Cluster;
|
||||||
use oneclient_core::packages::domain::GameLoader;
|
use oneclient_core::packages::domain::GameLoader;
|
||||||
use oneclient_core::{format_mc_version, parse_mc_version};
|
use oneclient_core::{VersionKey, format_mc_version, parse_mc_version};
|
||||||
|
|
||||||
pub type ClusterGroups = BTreeMap<u32, Vec<Cluster>>;
|
pub type ClusterGroups = BTreeMap<u32, Vec<Cluster>>;
|
||||||
|
|
||||||
|
fn cluster_key(cluster: &Cluster) -> Option<VersionKey> {
|
||||||
|
parse_mc_version(&cluster.mc_version).and_then(|parsed| parsed.key())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn group_clusters_by_major(clusters: &[Cluster]) -> ClusterGroups {
|
pub fn group_clusters_by_major(clusters: &[Cluster]) -> ClusterGroups {
|
||||||
let mut groups: ClusterGroups = BTreeMap::new();
|
let mut groups: ClusterGroups = BTreeMap::new();
|
||||||
|
|
||||||
|
|
@ -23,7 +27,7 @@ pub fn group_clusters_by_major(clusters: &[Cluster]) -> ClusterGroups {
|
||||||
}
|
}
|
||||||
|
|
||||||
for list in groups.values_mut() {
|
for list in groups.values_mut() {
|
||||||
list.sort_by(|a, b| a.mc_version.cmp(&b.mc_version).reverse());
|
list.sort_by(|a, b| cluster_key(b).cmp(&cluster_key(a)));
|
||||||
}
|
}
|
||||||
|
|
||||||
groups
|
groups
|
||||||
|
|
@ -51,18 +55,17 @@ pub fn loader_tags(clusters: &[Cluster]) -> Vec<String> {
|
||||||
tags
|
tags
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn minor_versions(clusters: &[Cluster]) -> Vec<u32> {
|
pub fn version_keys(clusters: &[Cluster]) -> Vec<VersionKey> {
|
||||||
let mut minors = Vec::new();
|
let mut keys = Vec::new();
|
||||||
for cluster in clusters {
|
for cluster in clusters {
|
||||||
if let Some(parsed) = parse_mc_version(&cluster.mc_version)
|
if let Some(key) = cluster_key(cluster)
|
||||||
&& let Some(minor) = parsed.minor
|
&& !keys.contains(&key)
|
||||||
&& !minors.contains(&minor)
|
|
||||||
{
|
{
|
||||||
minors.push(minor);
|
keys.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
minors.sort();
|
keys.sort();
|
||||||
minors
|
keys
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn loaders_for_major(clusters: &[Cluster]) -> Vec<GameLoader> {
|
pub fn loaders_for_major(clusters: &[Cluster]) -> Vec<GameLoader> {
|
||||||
|
|
@ -78,15 +81,13 @@ pub fn loaders_for_major(clusters: &[Cluster]) -> Vec<GameLoader> {
|
||||||
|
|
||||||
pub fn resolve_cluster(
|
pub fn resolve_cluster(
|
||||||
clusters: &[Cluster],
|
clusters: &[Cluster],
|
||||||
minor: Option<u32>,
|
key: Option<VersionKey>,
|
||||||
loader: Option<GameLoader>,
|
loader: Option<GameLoader>,
|
||||||
) -> Option<Cluster> {
|
) -> Option<Cluster> {
|
||||||
clusters
|
clusters
|
||||||
.iter()
|
.iter()
|
||||||
.find(|cluster| {
|
.find(|cluster| {
|
||||||
let version_ok = minor.is_none_or(|minor| {
|
let version_ok = key.is_none_or(|key| cluster_key(cluster) == Some(key));
|
||||||
parse_mc_version(&cluster.mc_version).and_then(|parsed| parsed.minor) == Some(minor)
|
|
||||||
});
|
|
||||||
let loader_ok = loader.is_none_or(|loader| cluster.mc_loader == loader);
|
let loader_ok = loader.is_none_or(|loader| cluster.mc_loader == loader);
|
||||||
version_ok && loader_ok
|
version_ok && loader_ok
|
||||||
})
|
})
|
||||||
|
|
@ -102,17 +103,20 @@ pub fn default_major(groups: &ClusterGroups, active: Option<Cluster>) -> Option<
|
||||||
groups.keys().next().copied()
|
groups.keys().next().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_minor(clusters: &[Cluster], preferred: Option<u32>) -> Option<u32> {
|
pub fn default_version_key(
|
||||||
let minors = minor_versions(clusters);
|
clusters: &[Cluster],
|
||||||
if minors.is_empty() {
|
preferred: Option<VersionKey>,
|
||||||
|
) -> Option<VersionKey> {
|
||||||
|
let keys = version_keys(clusters);
|
||||||
|
if keys.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if let Some(minor) = preferred
|
if let Some(key) = preferred
|
||||||
&& minors.contains(&minor)
|
&& keys.contains(&key)
|
||||||
{
|
{
|
||||||
return Some(minor);
|
return Some(key);
|
||||||
}
|
}
|
||||||
minors.last().copied()
|
keys.last().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_loader(clusters: &[Cluster], preferred: Option<GameLoader>) -> Option<GameLoader> {
|
pub fn default_loader(clusters: &[Cluster], preferred: Option<GameLoader>) -> Option<GameLoader> {
|
||||||
|
|
@ -128,8 +132,8 @@ pub fn default_loader(clusters: &[Cluster], preferred: Option<GameLoader>) -> Op
|
||||||
Some(loaders[0])
|
Some(loaders[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn minor_label(major: u32, minor: u32) -> String {
|
pub fn version_label(major: u32, (minor, patch): VersionKey) -> String {
|
||||||
format_mc_version(major, minor)
|
format_mc_version(major, minor, patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Human-readable byte size (B / KB / MB).
|
/// Human-readable byte size (B / KB / MB).
|
||||||
|
|
@ -236,10 +240,10 @@ fn compare_last_played(a: &Cluster, b: &Cluster) -> Ordering {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn version_sort_key(cluster: &Cluster) -> (u32, u32) {
|
fn version_sort_key(cluster: &Cluster) -> (u32, u32, u32) {
|
||||||
parse_mc_version(&cluster.mc_version)
|
parse_mc_version(&cluster.mc_version)
|
||||||
.map(|v| (v.major, v.minor.unwrap_or(0)))
|
.map(|v| (v.major, v.minor.unwrap_or(0), v.patch.unwrap_or(0)))
|
||||||
.unwrap_or((0, 0))
|
.unwrap_or((0, 0, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use freya::query::QueryStateData;
|
use freya::query::QueryStateData;
|
||||||
use freya::router::RouterContext;
|
use freya::router::RouterContext;
|
||||||
|
use oneclient_core::VersionKey;
|
||||||
use oneclient_core::clusters::Cluster;
|
use oneclient_core::clusters::Cluster;
|
||||||
use oneclient_core::packages::domain::GameLoader;
|
use oneclient_core::packages::domain::GameLoader;
|
||||||
|
|
||||||
|
|
@ -12,8 +13,8 @@ use crate::routes::Route;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::ui::border_all_color;
|
use crate::ui::border_all_color;
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
default_loader, default_major, default_minor, group_clusters_by_major, loaders_for_major,
|
default_loader, default_major, default_version_key, group_clusters_by_major, loaders_for_major,
|
||||||
major_pretty_name, minor_label, minor_versions, resolve_cluster,
|
major_pretty_name, resolve_cluster, version_keys, version_label,
|
||||||
};
|
};
|
||||||
use crate::view::app::launch_button_state;
|
use crate::view::app::launch_button_state;
|
||||||
|
|
||||||
|
|
@ -31,7 +32,7 @@ impl Component for Clusters {
|
||||||
let clusters_query = use_clusters();
|
let clusters_query = use_clusters();
|
||||||
let active_id = use_active_cluster_id();
|
let active_id = use_active_cluster_id();
|
||||||
let mut selected_major = use_state(|| None::<u32>);
|
let mut selected_major = use_state(|| None::<u32>);
|
||||||
let mut selected_minor = use_state(|| None::<u32>);
|
let mut selected_version = use_state(|| None::<VersionKey>);
|
||||||
let mut selected_loader = use_state(|| None::<GameLoader>);
|
let mut selected_loader = use_state(|| None::<GameLoader>);
|
||||||
let mut grid_columns = use_state(|| 2_usize);
|
let mut grid_columns = use_state(|| 2_usize);
|
||||||
|
|
||||||
|
|
@ -90,12 +91,12 @@ impl Component for Clusters {
|
||||||
|
|
||||||
let clusters_for_major = groups.get(&major).cloned().unwrap_or_default();
|
let clusters_for_major = groups.get(&major).cloned().unwrap_or_default();
|
||||||
|
|
||||||
if selected_minor.read().is_none() {
|
if selected_version.read().is_none() {
|
||||||
let preferred = active_cluster
|
let preferred = active_cluster
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|c| oneclient_core::parse_mc_version(&c.mc_version))
|
.and_then(|c| oneclient_core::parse_mc_version(&c.mc_version))
|
||||||
.and_then(|p| p.minor);
|
.and_then(|p| p.key());
|
||||||
*selected_minor.write() = default_minor(&clusters_for_major, preferred);
|
*selected_version.write() = default_version_key(&clusters_for_major, preferred);
|
||||||
}
|
}
|
||||||
|
|
||||||
if selected_loader.read().is_none() {
|
if selected_loader.read().is_none() {
|
||||||
|
|
@ -105,7 +106,7 @@ impl Component for Clusters {
|
||||||
|
|
||||||
let cluster = resolve_cluster(
|
let cluster = resolve_cluster(
|
||||||
&clusters_for_major,
|
&clusters_for_major,
|
||||||
*selected_minor.read(),
|
*selected_version.read(),
|
||||||
*selected_loader.read(),
|
*selected_loader.read(),
|
||||||
)
|
)
|
||||||
.or_else(|| clusters_for_major.first().cloned());
|
.or_else(|| clusters_for_major.first().cloned());
|
||||||
|
|
@ -152,12 +153,12 @@ impl Component for Clusters {
|
||||||
let list = groups.get(&major).cloned().unwrap_or_default();
|
let list = groups.get(&major).cloned().unwrap_or_default();
|
||||||
let selected = *selected_major.read() == Some(major);
|
let selected = *selected_major.read() == Some(major);
|
||||||
let mut selected_major = selected_major;
|
let mut selected_major = selected_major;
|
||||||
let mut selected_minor = selected_minor;
|
let mut selected_version = selected_version;
|
||||||
let mut selected_loader = selected_loader;
|
let mut selected_loader = selected_loader;
|
||||||
|
|
||||||
MajorVersionCard::new(major, &list, selected, move |_| {
|
MajorVersionCard::new(major, &list, selected, move |_| {
|
||||||
*selected_major.write() = Some(major);
|
*selected_major.write() = Some(major);
|
||||||
*selected_minor.write() = None;
|
*selected_version.write() = None;
|
||||||
*selected_loader.write() = None;
|
*selected_loader.write() = None;
|
||||||
})
|
})
|
||||||
.into_element()
|
.into_element()
|
||||||
|
|
@ -176,7 +177,7 @@ impl Component for Clusters {
|
||||||
major,
|
major,
|
||||||
cluster,
|
cluster,
|
||||||
&clusters_for_major,
|
&clusters_for_major,
|
||||||
selected_minor,
|
selected_version,
|
||||||
selected_loader,
|
selected_loader,
|
||||||
),
|
),
|
||||||
None => sidebar_error(),
|
None => sidebar_error(),
|
||||||
|
|
@ -189,20 +190,20 @@ fn detail_sidebar(
|
||||||
major: u32,
|
major: u32,
|
||||||
cluster: Cluster,
|
cluster: Cluster,
|
||||||
clusters_for_major: &[Cluster],
|
clusters_for_major: &[Cluster],
|
||||||
selected_minor: State<Option<u32>>,
|
selected_version: State<Option<VersionKey>>,
|
||||||
selected_loader: State<Option<GameLoader>>,
|
selected_loader: State<Option<GameLoader>>,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let active_id = use_active_cluster_id();
|
let active_id = use_active_cluster_id();
|
||||||
let dispatch = use_dispatch();
|
let dispatch = use_dispatch();
|
||||||
let game = use_game_snapshot();
|
let game = use_game_snapshot();
|
||||||
let minors = minor_versions(clusters_for_major);
|
let keys = version_keys(clusters_for_major);
|
||||||
let loaders = loaders_for_major(clusters_for_major);
|
let loaders = loaders_for_major(clusters_for_major);
|
||||||
let cluster_id = cluster.id;
|
let cluster_id = cluster.id;
|
||||||
let version_title = major_pretty_name(major);
|
let version_title = major_pretty_name(major);
|
||||||
let minor_value = *selected_minor.read();
|
let version_value = *selected_version.read();
|
||||||
let loader_value = *selected_loader.read();
|
let loader_value = *selected_loader.read();
|
||||||
|
|
||||||
let metadata = use_version_metadata(Some(major), minor_value, loader_value);
|
let metadata = use_version_metadata(Some(major), version_value, loader_value);
|
||||||
|
|
||||||
let heading = metadata
|
let heading = metadata
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -229,7 +230,7 @@ fn detail_sidebar(
|
||||||
.border(border_all_color(1., colors::component_border()))
|
.border(border_all_color(1., colors::component_border()))
|
||||||
.overflow(Overflow::Clip)
|
.overflow(Overflow::Clip)
|
||||||
.child(rect().width(Size::fill()).max_height(Size::px(140.)).child(
|
.child(rect().width(Size::fill()).max_height(Size::px(140.)).child(
|
||||||
ClusterLandscapeArt::for_version(major, minor_value, loader_value, false),
|
ClusterLandscapeArt::for_version(major, version_value, loader_value, false),
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -259,7 +260,7 @@ fn detail_sidebar(
|
||||||
.font_size(12.)
|
.font_size(12.)
|
||||||
.color(colors::fg_secondary()),
|
.color(colors::fg_secondary()),
|
||||||
)
|
)
|
||||||
.children(minor_rows(major, &minors, minor_value, selected_minor))
|
.children(version_rows(major, &keys, version_value, selected_version))
|
||||||
.children(loader_rows(&loaders, loader_value, selected_loader)),
|
.children(loader_rows(&loaders, loader_value, selected_loader)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|
@ -308,28 +309,28 @@ fn tags_row(tags: &[String]) -> Option<Element> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn minor_rows(
|
fn version_rows(
|
||||||
major: u32,
|
major: u32,
|
||||||
minors: &[u32],
|
keys: &[VersionKey],
|
||||||
selected: Option<u32>,
|
selected: Option<VersionKey>,
|
||||||
mut selected_minor: State<Option<u32>>,
|
mut selected_version: State<Option<VersionKey>>,
|
||||||
) -> Option<Element> {
|
) -> Option<Element> {
|
||||||
if minors.len() <= 1 {
|
if keys.len() <= 1 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let options: Vec<String> = minors.iter().map(|m| minor_label(major, *m)).collect();
|
let options: Vec<String> = keys.iter().map(|k| version_label(major, *k)).collect();
|
||||||
let current = selected
|
let current = selected
|
||||||
.and_then(|s| minors.iter().position(|m| *m == s))
|
.and_then(|s| keys.iter().position(|k| *k == s))
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let minors = minors.to_vec();
|
let keys = keys.to_vec();
|
||||||
|
|
||||||
Some(info_row(
|
Some(info_row(
|
||||||
"Minor Version",
|
"Minor Version",
|
||||||
Dropdown::new(options[current].clone(), options)
|
Dropdown::new(options[current].clone(), options)
|
||||||
.on_select(move |idx: usize| {
|
.on_select(move |idx: usize| {
|
||||||
if let Some(minor) = minors.get(idx).copied() {
|
if let Some(key) = keys.get(idx).copied() {
|
||||||
*selected_minor.write() = Some(minor);
|
*selected_version.write() = Some(key);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.into_element(),
|
.into_element(),
|
||||||
|
|
|
||||||
272
packages/oneclient_core/src/clusters/migrate.rs
Normal file
272
packages/oneclient_core/src/clusters/migrate.rs
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use oneclient_db::dao::{applied_migration as migration_dao, cluster as cluster_dao};
|
||||||
|
use oneclient_db::models::ClusterRow;
|
||||||
|
|
||||||
|
use crate::LauncherResult;
|
||||||
|
use crate::packages::domain::GameLoader;
|
||||||
|
use crate::state::LauncherState;
|
||||||
|
use crate::versions::RemoteMigration;
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state))]
|
||||||
|
pub async fn apply_remote_migrations(state: &LauncherState) -> LauncherResult<usize> {
|
||||||
|
let rules = state.versions.migrations().await;
|
||||||
|
if rules.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut migrated = 0;
|
||||||
|
|
||||||
|
for rule in rules {
|
||||||
|
match apply_rule(state, &rule).await {
|
||||||
|
Ok(true) => migrated += 1,
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!(
|
||||||
|
migration_id = %rule.id,
|
||||||
|
error = %err,
|
||||||
|
"cluster migration failed; leaving install untouched"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if migrated > 0 {
|
||||||
|
tracing::info!(migrated, "applied cluster migrations");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(migrated)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply_rule(state: &LauncherState, rule: &RemoteMigration) -> LauncherResult<bool> {
|
||||||
|
let db = &state.services.db;
|
||||||
|
|
||||||
|
if migration_dao::is_applied(db, &rule.id).await? {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(loader) = GameLoader::from_str(&rule.from.loader) else {
|
||||||
|
tracing::warn!(
|
||||||
|
migration_id = %rule.id,
|
||||||
|
loader = %rule.from.loader,
|
||||||
|
"unknown loader in migration rule; retiring rule"
|
||||||
|
);
|
||||||
|
migration_dao::mark_applied(db, &rule.id).await?;
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if rule.from.mc_version == rule.to.mc_version {
|
||||||
|
tracing::warn!(migration_id = %rule.id, "migration is a no-op; retiring rule");
|
||||||
|
migration_dao::mark_applied(db, &rule.id).await?;
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(source) =
|
||||||
|
cluster_dao::find_by_version_loader(db, &rule.from.mc_version, loader as i64).await?
|
||||||
|
else {
|
||||||
|
migration_dao::mark_applied(db, &rule.id).await?;
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if cluster_dao::find_by_version_loader(db, &rule.to.mc_version, loader as i64)
|
||||||
|
.await?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
migration_id = %rule.id,
|
||||||
|
from = %rule.from.mc_version,
|
||||||
|
to = %rule.to.mc_version,
|
||||||
|
"target cluster already exists; skipping migration"
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if crate::game::is_running(state, source.id) {
|
||||||
|
tracing::info!(
|
||||||
|
migration_id = %rule.id,
|
||||||
|
cluster_id = source.id,
|
||||||
|
"cluster is running; deferring migration"
|
||||||
|
);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate_cluster(state, rule, &source).await?;
|
||||||
|
|
||||||
|
migration_dao::mark_applied(db, &rule.id).await?;
|
||||||
|
tracing::info!(
|
||||||
|
migration_id = %rule.id,
|
||||||
|
cluster_id = source.id,
|
||||||
|
from = %rule.from.mc_version,
|
||||||
|
to = %rule.to.mc_version,
|
||||||
|
"migrated cluster"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn migrate_cluster(
|
||||||
|
state: &LauncherState,
|
||||||
|
rule: &RemoteMigration,
|
||||||
|
source: &ClusterRow,
|
||||||
|
) -> LauncherResult<()> {
|
||||||
|
let from = &rule.from.mc_version;
|
||||||
|
let to = &rule.to.mc_version;
|
||||||
|
|
||||||
|
let clusters_dir = crate::paths::clusters_dir()?;
|
||||||
|
let old_dir = clusters_dir.join(&source.folder_name);
|
||||||
|
|
||||||
|
let new_folder = resolve_new_folder(&clusters_dir, &source.folder_name, from, to).await?;
|
||||||
|
let new_name = retarget_version_prefix(&source.name, from, to);
|
||||||
|
|
||||||
|
let mut renamed: Option<(PathBuf, PathBuf)> = None;
|
||||||
|
if let Some(folder) = &new_folder {
|
||||||
|
let new_dir = clusters_dir.join(folder);
|
||||||
|
if polyio::try_exists(&old_dir).await? {
|
||||||
|
polyio::rename(&old_dir, &new_dir).await?;
|
||||||
|
renamed = Some((old_dir.clone(), new_dir));
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
folder = %source.folder_name,
|
||||||
|
"cluster directory is missing on disk; migrating the row only"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let folder_for_db = folder_for_db(
|
||||||
|
&source.folder_name,
|
||||||
|
new_folder.as_deref(),
|
||||||
|
renamed.is_some(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match cluster_dao::migrate_version(
|
||||||
|
&state.services.db,
|
||||||
|
source.id,
|
||||||
|
to,
|
||||||
|
new_name.as_deref(),
|
||||||
|
folder_for_db,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
if let Some((old_dir, new_dir)) = renamed
|
||||||
|
&& let Err(rollback) = polyio::rename(&new_dir, &old_dir).await
|
||||||
|
{
|
||||||
|
tracing::error!(
|
||||||
|
migration_id = %rule.id,
|
||||||
|
cluster_id = source.id,
|
||||||
|
from = ?new_dir,
|
||||||
|
to = ?old_dir,
|
||||||
|
error = %rollback,
|
||||||
|
"failed to roll back cluster directory rename after database error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn folder_for_db<'a>(current: &'a str, new_folder: Option<&'a str>, renamed: bool) -> &'a str {
|
||||||
|
match (renamed, new_folder) {
|
||||||
|
(true, Some(folder)) => folder,
|
||||||
|
_ => current,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_new_folder(
|
||||||
|
clusters_dir: &Path,
|
||||||
|
folder_name: &str,
|
||||||
|
from: &str,
|
||||||
|
to: &str,
|
||||||
|
) -> LauncherResult<Option<String>> {
|
||||||
|
let Some(candidate) = retarget_version_prefix(folder_name, from, to) else {
|
||||||
|
tracing::debug!(
|
||||||
|
folder = %folder_name,
|
||||||
|
"folder name is not in generated form; leaving it in place"
|
||||||
|
);
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
if polyio::try_exists(&clusters_dir.join(&candidate)).await? {
|
||||||
|
tracing::warn!(
|
||||||
|
folder = %folder_name,
|
||||||
|
candidate = %candidate,
|
||||||
|
"target folder already exists; leaving directory in place"
|
||||||
|
);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(candidate))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn retarget_version_prefix(value: &str, from: &str, to: &str) -> Option<String> {
|
||||||
|
let rest = value.strip_prefix(from)?;
|
||||||
|
if !rest.starts_with(' ') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(format!("{to}{rest}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retargets_generated_folder() {
|
||||||
|
assert_eq!(
|
||||||
|
retarget_version_prefix("26.1 fabric", "26.1", "26.1.2").as_deref(),
|
||||||
|
Some("26.1.2 fabric")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retargets_deduplicated_folder() {
|
||||||
|
assert_eq!(
|
||||||
|
retarget_version_prefix("26.1 fabric (1)", "26.1", "26.1.2").as_deref(),
|
||||||
|
Some("26.1.2 fabric (1)")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_match_adjacent_minor() {
|
||||||
|
assert_eq!(
|
||||||
|
retarget_version_prefix("26.10 fabric", "26.1", "26.1.2"),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaves_custom_names_alone() {
|
||||||
|
assert_eq!(
|
||||||
|
retarget_version_prefix("my cool pack", "26.1", "26.1.2"),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
retarget_version_prefix("26.1fabric", "26.1", "26.1.2"),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_version_with_no_suffix_is_not_retargeted() {
|
||||||
|
assert_eq!(retarget_version_prefix("26.1", "26.1", "26.1.2"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn row_claims_new_folder_only_when_the_directory_moved() {
|
||||||
|
assert_eq!(
|
||||||
|
folder_for_db("26.1 fabric", Some("26.1.2 fabric"), true),
|
||||||
|
"26.1.2 fabric"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn row_keeps_its_folder_when_the_rename_was_skipped() {
|
||||||
|
assert_eq!(
|
||||||
|
folder_for_db("26.1 fabric", Some("26.1.2 fabric"), false),
|
||||||
|
"26.1 fabric"
|
||||||
|
);
|
||||||
|
assert_eq!(folder_for_db("my cool pack", None, false), "my cool pack");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
mod cluster;
|
mod cluster;
|
||||||
mod error;
|
mod error;
|
||||||
mod manager;
|
mod manager;
|
||||||
|
mod migrate;
|
||||||
mod options;
|
mod options;
|
||||||
mod prepare;
|
mod prepare;
|
||||||
mod provision;
|
mod provision;
|
||||||
|
|
@ -9,6 +10,7 @@ mod stage;
|
||||||
pub use cluster::{Cluster, ClusterLinkTarget};
|
pub use cluster::{Cluster, ClusterLinkTarget};
|
||||||
pub use error::ClusterError;
|
pub use error::ClusterError;
|
||||||
pub use manager::ClusterManager;
|
pub use manager::ClusterManager;
|
||||||
|
pub use migrate::apply_remote_migrations;
|
||||||
pub use options::{ClusterUpdate, CreateClusterOptions};
|
pub use options::{ClusterUpdate, CreateClusterOptions};
|
||||||
pub use prepare::{estimate_cluster_download, prepare_cluster};
|
pub use prepare::{estimate_cluster_download, prepare_cluster};
|
||||||
pub use provision::{ensure_from_bundles, ensure_from_versions};
|
pub use provision::{ensure_from_bundles, ensure_from_versions};
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ pub async fn ensure_from_versions(state: &LauncherState) -> LauncherResult<Vec<C
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mc_version = format_mc_version(entry.major_version, minor);
|
let mc_version = format_mc_version(entry.major_version, minor, entry.patch_version);
|
||||||
|
|
||||||
if cluster_dao::find_by_version_loader(&state.services.db, &mc_version, loader as i64)
|
if cluster_dao::find_by_version_loader(&state.services.db, &mc_version, loader as i64)
|
||||||
.await?
|
.await?
|
||||||
|
|
|
||||||
|
|
@ -75,5 +75,5 @@ pub use patch::Patch;
|
||||||
pub use settings::ProfileUpdate;
|
pub use settings::ProfileUpdate;
|
||||||
pub use state::LauncherServices;
|
pub use state::LauncherServices;
|
||||||
pub use state::LauncherState;
|
pub use state::LauncherState;
|
||||||
pub use version::{ParsedMcVersion, format_mc_version, parse_mc_version};
|
pub use version::{ParsedMcVersion, VersionKey, format_mc_version, parse_mc_version};
|
||||||
pub use versions::{VersionMetadata, VersionsManager, VersionsManifest};
|
pub use versions::{RemoteMigration, VersionMetadata, VersionsManager, VersionsManifest};
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,10 @@ impl LauncherState {
|
||||||
tracing::warn!("bundle tracking restore failed: {err:#}");
|
tracing::warn!("bundle tracking restore failed: {err:#}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Err(err) = crate::clusters::apply_remote_migrations(&background).await {
|
||||||
|
tracing::error!("cluster migrations failed: {err:#}");
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(err) = crate::clusters::ensure_from_versions(&background).await {
|
if let Err(err) = crate::clusters::ensure_from_versions(&background).await {
|
||||||
tracing::error!("versions cluster provisioning failed: {err:#}");
|
tracing::error!("versions cluster provisioning failed: {err:#}");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,18 @@
|
||||||
pub struct ParsedMcVersion {
|
pub struct ParsedMcVersion {
|
||||||
pub major: u32,
|
pub major: u32,
|
||||||
pub minor: Option<u32>,
|
pub minor: Option<u32>,
|
||||||
|
pub patch: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ParsedMcVersion {
|
||||||
|
#[must_use]
|
||||||
|
pub fn key(&self) -> Option<VersionKey> {
|
||||||
|
Some((self.minor?, self.patch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type VersionKey = (u32, Option<u32>);
|
||||||
|
|
||||||
pub fn parse_mc_version(version: &str) -> Option<ParsedMcVersion> {
|
pub fn parse_mc_version(version: &str) -> Option<ParsedMcVersion> {
|
||||||
let parts: Vec<&str> = version.split('.').collect();
|
let parts: Vec<&str> = version.split('.').collect();
|
||||||
|
|
||||||
|
|
@ -14,9 +24,14 @@ pub fn parse_mc_version(version: &str) -> Option<ParsedMcVersion> {
|
||||||
}
|
}
|
||||||
let major = parts[0].parse().ok()?;
|
let major = parts[0].parse().ok()?;
|
||||||
let minor = parts[1].parse().ok()?;
|
let minor = parts[1].parse().ok()?;
|
||||||
|
let patch = match parts.get(2) {
|
||||||
|
Some(p) => Some(p.parse().ok()?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
return Some(ParsedMcVersion {
|
return Some(ParsedMcVersion {
|
||||||
major,
|
major,
|
||||||
minor: Some(minor),
|
minor: Some(minor),
|
||||||
|
patch,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,19 +40,26 @@ pub fn parse_mc_version(version: &str) -> Option<ParsedMcVersion> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let major = parts[1].parse().ok()?;
|
let major = parts[1].parse().ok()?;
|
||||||
let minor = parts
|
let minor = parts.get(2).and_then(|p| p.parse().ok());
|
||||||
.get(2)
|
|
||||||
.and_then(|p| p.parse().ok());
|
|
||||||
|
|
||||||
Some(ParsedMcVersion { major, minor })
|
Some(ParsedMcVersion {
|
||||||
|
major,
|
||||||
|
minor,
|
||||||
|
patch: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn format_mc_version(major: u32, minor: u32) -> String {
|
pub fn format_mc_version(major: u32, minor: u32, patch: Option<u32>) -> String {
|
||||||
if major >= 26 {
|
let base = if major >= 26 {
|
||||||
format!("{major}.{minor}")
|
format!("{major}.{minor}")
|
||||||
} else {
|
} else {
|
||||||
format!("1.{major}.{minor}")
|
format!("1.{major}.{minor}")
|
||||||
|
};
|
||||||
|
|
||||||
|
match patch {
|
||||||
|
Some(patch) => format!("{base}.{patch}"),
|
||||||
|
None => base,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,10 +77,20 @@ mod tests {
|
||||||
let v = parse_mc_version("26.1").unwrap();
|
let v = parse_mc_version("26.1").unwrap();
|
||||||
assert_eq!(v.major, 26);
|
assert_eq!(v.major, 26);
|
||||||
assert_eq!(v.minor, Some(1));
|
assert_eq!(v.minor, Some(1));
|
||||||
|
assert_eq!(v.patch, None);
|
||||||
|
|
||||||
let v = parse_mc_version("26.2").unwrap();
|
let v = parse_mc_version("26.2").unwrap();
|
||||||
assert_eq!(v.major, 26);
|
assert_eq!(v.major, 26);
|
||||||
assert_eq!(v.minor, Some(2));
|
assert_eq!(v.minor, Some(2));
|
||||||
|
assert_eq!(v.patch, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_modern_patch_versions() {
|
||||||
|
let v = parse_mc_version("26.1.2").unwrap();
|
||||||
|
assert_eq!(v.major, 26);
|
||||||
|
assert_eq!(v.minor, Some(1));
|
||||||
|
assert_eq!(v.patch, Some(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -66,11 +98,36 @@ mod tests {
|
||||||
let v = parse_mc_version("1.21.5").unwrap();
|
let v = parse_mc_version("1.21.5").unwrap();
|
||||||
assert_eq!(v.major, 21);
|
assert_eq!(v.major, 21);
|
||||||
assert_eq!(v.minor, Some(5));
|
assert_eq!(v.minor, Some(5));
|
||||||
|
assert_eq!(v.patch, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reject_garbage_patch_component() {
|
||||||
|
assert!(parse_mc_version("26.1.x").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_versions() {
|
fn format_versions() {
|
||||||
assert_eq!(format_mc_version(26, 1), "26.1");
|
assert_eq!(format_mc_version(26, 1, None), "26.1");
|
||||||
assert_eq!(format_mc_version(21, 5), "1.21.5");
|
assert_eq!(format_mc_version(26, 1, Some(2)), "26.1.2");
|
||||||
|
assert_eq!(format_mc_version(21, 5, None), "1.21.5");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_round_trips_through_parse() {
|
||||||
|
for (major, minor, patch) in [(26, 1, None), (26, 1, Some(2)), (21, 5, None)] {
|
||||||
|
let text = format_mc_version(major, minor, patch);
|
||||||
|
let parsed = parse_mc_version(&text).unwrap();
|
||||||
|
assert_eq!(parsed.major, major);
|
||||||
|
assert_eq!(parsed.minor, Some(minor));
|
||||||
|
assert_eq!(parsed.patch, patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_sorts_after_bare_minor() {
|
||||||
|
let bare = parse_mc_version("26.1").unwrap().key().unwrap();
|
||||||
|
let patched = parse_mc_version("26.1.2").unwrap().key().unwrap();
|
||||||
|
assert!(bare < patched);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use tokio::sync::RwLock;
|
||||||
use crate::api_config::meta_url_base;
|
use crate::api_config::meta_url_base;
|
||||||
use crate::paths;
|
use crate::paths;
|
||||||
use crate::state::LauncherServices;
|
use crate::state::LauncherServices;
|
||||||
use crate::versions::manifest::{VersionMetadata, VersionsManifest};
|
use crate::versions::manifest::{RemoteMigration, VersionMetadata, VersionsManifest};
|
||||||
use crate::{LauncherError, LauncherResult};
|
use crate::{LauncherError, LauncherResult};
|
||||||
|
|
||||||
pub struct VersionsManager {
|
pub struct VersionsManager {
|
||||||
|
|
@ -36,6 +36,10 @@ impl VersionsManager {
|
||||||
self.manifest.read().await.metadata()
|
self.manifest.read().await.metadata()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn migrations(&self) -> Vec<RemoteMigration> {
|
||||||
|
self.manifest.read().await.migrations.clone()
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(services))]
|
#[tracing::instrument(level = "debug", skip(services))]
|
||||||
async fn fetch_manifest(
|
async fn fetch_manifest(
|
||||||
services: &LauncherServices,
|
services: &LauncherServices,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::api_config::meta_url_base;
|
use crate::api_config::meta_url_base;
|
||||||
|
use crate::version::VersionKey;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct VersionsManifest {
|
pub struct VersionsManifest {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub clusters: Vec<RemoteCluster>,
|
pub clusters: Vec<RemoteCluster>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub migrations: Vec<RemoteMigration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
|
@ -27,6 +30,8 @@ pub struct RemoteCluster {
|
||||||
pub struct RemoteEntry {
|
pub struct RemoteEntry {
|
||||||
pub minor_version: u32,
|
pub minor_version: u32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub patch_version: Option<u32>,
|
||||||
|
#[serde(default)]
|
||||||
pub loader: Option<String>,
|
pub loader: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
|
@ -38,10 +43,29 @@ pub struct RemoteEntry {
|
||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct RemoteMigration {
|
||||||
|
pub id: String,
|
||||||
|
pub from: MigrationSource,
|
||||||
|
pub to: MigrationTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct MigrationSource {
|
||||||
|
pub mc_version: String,
|
||||||
|
pub loader: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct MigrationTarget {
|
||||||
|
pub mc_version: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct VersionMetadata {
|
pub struct VersionMetadata {
|
||||||
pub major_version: u32,
|
pub major_version: u32,
|
||||||
pub minor_version: Option<u32>,
|
pub minor_version: Option<u32>,
|
||||||
|
pub patch_version: Option<u32>,
|
||||||
pub loader: Option<String>,
|
pub loader: Option<String>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub art_url: Option<String>,
|
pub art_url: Option<String>,
|
||||||
|
|
@ -49,6 +73,13 @@ pub struct VersionMetadata {
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl VersionMetadata {
|
||||||
|
#[must_use]
|
||||||
|
pub fn key(&self) -> Option<VersionKey> {
|
||||||
|
Some((self.minor_version?, self.patch_version))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn art_url(path: &Option<String>) -> Option<String> {
|
fn art_url(path: &Option<String>) -> Option<String> {
|
||||||
path.as_ref()
|
path.as_ref()
|
||||||
.map(|p| format!("{}{p}", meta_url_base()))
|
.map(|p| format!("{}{p}", meta_url_base()))
|
||||||
|
|
@ -67,6 +98,7 @@ impl VersionsManifest {
|
||||||
out.push(VersionMetadata {
|
out.push(VersionMetadata {
|
||||||
major_version: cluster.major_version,
|
major_version: cluster.major_version,
|
||||||
minor_version: None,
|
minor_version: None,
|
||||||
|
patch_version: None,
|
||||||
loader: None,
|
loader: None,
|
||||||
name: cluster_name.clone(),
|
name: cluster_name.clone(),
|
||||||
art_url: art_url(&cluster.art),
|
art_url: art_url(&cluster.art),
|
||||||
|
|
@ -78,6 +110,7 @@ impl VersionsManifest {
|
||||||
out.push(VersionMetadata {
|
out.push(VersionMetadata {
|
||||||
major_version: cluster.major_version,
|
major_version: cluster.major_version,
|
||||||
minor_version: Some(entry.minor_version),
|
minor_version: Some(entry.minor_version),
|
||||||
|
patch_version: entry.patch_version,
|
||||||
loader: entry.loader.clone(),
|
loader: entry.loader.clone(),
|
||||||
name: entry.name.clone().unwrap_or_else(|| cluster_name.clone()),
|
name: entry.name.clone().unwrap_or_else(|| cluster_name.clone()),
|
||||||
art_url: art_url(&entry.art).or_else(|| art_url(&cluster.art)),
|
art_url: art_url(&entry.art).or_else(|| art_url(&cluster.art)),
|
||||||
|
|
@ -93,3 +126,57 @@ impl VersionsManifest {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn manifest_without_migrations_key_parses() {
|
||||||
|
let manifest: VersionsManifest =
|
||||||
|
serde_json::from_str(r#"{"clusters": []}"#).expect("should parse");
|
||||||
|
assert!(manifest.migrations.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn entry_without_patch_key_parses() {
|
||||||
|
let manifest: VersionsManifest = serde_json::from_str(
|
||||||
|
r#"{"clusters":[{"major_version":26,"entries":[{"minor_version":1}]}]}"#,
|
||||||
|
)
|
||||||
|
.expect("should parse");
|
||||||
|
assert_eq!(manifest.clusters[0].entries[0].patch_version, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_version_reaches_metadata() {
|
||||||
|
let manifest: VersionsManifest = serde_json::from_str(
|
||||||
|
r#"{"clusters":[{"major_version":26,"entries":[
|
||||||
|
{"minor_version":1,"patch_version":2,"loader":"fabric"}
|
||||||
|
]}]}"#,
|
||||||
|
)
|
||||||
|
.expect("should parse");
|
||||||
|
|
||||||
|
let metadata = manifest.metadata();
|
||||||
|
let entry = metadata
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.minor_version == Some(1))
|
||||||
|
.expect("entry present");
|
||||||
|
assert_eq!(entry.patch_version, Some(2));
|
||||||
|
assert_eq!(entry.key(), Some((1, Some(2))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn migrations_parse() {
|
||||||
|
let manifest: VersionsManifest = serde_json::from_str(
|
||||||
|
r#"{"clusters":[],"migrations":[
|
||||||
|
{"id":"x","from":{"mc_version":"26.1","loader":"fabric"},
|
||||||
|
"to":{"mc_version":"26.1.2"}}
|
||||||
|
]}"#,
|
||||||
|
)
|
||||||
|
.expect("should parse");
|
||||||
|
|
||||||
|
assert_eq!(manifest.migrations[0].id, "x");
|
||||||
|
assert_eq!(manifest.migrations[0].from.mc_version, "26.1");
|
||||||
|
assert_eq!(manifest.migrations[0].to.mc_version, "26.1.2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,6 @@ mod manager;
|
||||||
mod manifest;
|
mod manifest;
|
||||||
|
|
||||||
pub use manager::VersionsManager;
|
pub use manager::VersionsManager;
|
||||||
pub use manifest::{VersionMetadata, VersionsManifest};
|
pub use manifest::{
|
||||||
|
MigrationSource, MigrationTarget, RemoteMigration, VersionMetadata, VersionsManifest,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT id FROM applied_migrations WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "applied_migrations",
|
||||||
|
"name": "id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "07450861d48b3bff106b7c06e958665f6be44e7937deffb921ddb6a5450805ab"
|
||||||
|
}
|
||||||
158
packages/oneclient_db/.sqlx/query-1a83654d854901219ca4be57d793ddef96835a67aa8affe3453845a646e5ba61.json
generated
Normal file
158
packages/oneclient_db/.sqlx/query-1a83654d854901219ca4be57d793ddef96835a67aa8affe3453845a646e5ba61.json
generated
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tUPDATE clusters\n\t\tSET mc_version = ?,\n\t\t name = ?,\n\t\t folder_name = ?\n\t\tWHERE id = ?\n\t\tRETURNING\n\t\t\tid, name, folder_name, setting_profile_name, mc_version, mc_loader,\n\t\t\tstage, mc_loader_version, created_at, last_played, overall_played, linked_modpack_hash\n\t\t",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "folder_name",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "folder_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "setting_profile_name",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "setting_profile_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mc_version",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "mc_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mc_loader",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "mc_loader"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stage",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "stage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mc_loader_version",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "mc_loader_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "created_at"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "last_played",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "last_played"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "overall_played",
|
||||||
|
"ordinal": 10,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "overall_played"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linked_modpack_hash",
|
||||||
|
"ordinal": 11,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "clusters",
|
||||||
|
"name": "linked_modpack_hash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 4
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "1a83654d854901219ca4be57d793ddef96835a67aa8affe3453845a646e5ba61"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT OR IGNORE INTO applied_migrations (id, applied_at) VALUES (?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "836429d5d4054d1605d7de4cbcf3703ba78d72f39b3090734bb3e69c2b72d50c"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT 1 FROM cluster_artifacts\n WHERE cluster_id = ? AND bundle_name = ? AND package_id = ?\n LIMIT 1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "1",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": "Expression"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 3
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "dc93cdd57e23e48f8e3c333db4ec396b0ee319adec619e5a7d147805573ae2b2"
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,9 @@ authors.workspace = true
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
CREATE TABLE applied_migrations (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
applied_at TEXT NOT NULL
|
||||||
|
);
|
||||||
24
packages/oneclient_db/src/dao/applied_migration.rs
Normal file
24
packages/oneclient_db/src/dao/applied_migration.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
use chrono::Utc;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
pub async fn is_applied(pool: &SqlitePool, id: &str) -> Result<bool, sqlx::Error> {
|
||||||
|
let row = sqlx::query!("SELECT id FROM applied_migrations WHERE id = ?", id)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(row.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mark_applied(pool: &SqlitePool, id: &str) -> Result<(), sqlx::Error> {
|
||||||
|
let applied_at = Utc::now().to_rfc3339();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT OR IGNORE INTO applied_migrations (id, applied_at) VALUES (?, ?)",
|
||||||
|
id,
|
||||||
|
applied_at
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -149,6 +149,40 @@ pub async fn update(
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn migrate_version(
|
||||||
|
pool: &SqlitePool,
|
||||||
|
id: i64,
|
||||||
|
mc_version: &str,
|
||||||
|
name: Option<&str>,
|
||||||
|
folder_name: &str,
|
||||||
|
) -> Result<ClusterRow, sqlx::Error> {
|
||||||
|
let existing = get_by_id(pool, id)
|
||||||
|
.await?
|
||||||
|
.ok_or(sqlx::Error::RowNotFound)?;
|
||||||
|
|
||||||
|
let name = name.unwrap_or(&existing.name);
|
||||||
|
|
||||||
|
sqlx::query_as!(
|
||||||
|
ClusterRow,
|
||||||
|
r#"
|
||||||
|
UPDATE clusters
|
||||||
|
SET mc_version = ?,
|
||||||
|
name = ?,
|
||||||
|
folder_name = ?
|
||||||
|
WHERE id = ?
|
||||||
|
RETURNING
|
||||||
|
id, name, folder_name, setting_profile_name, mc_version, mc_loader,
|
||||||
|
stage, mc_loader_version, created_at, last_played, overall_played, linked_modpack_hash
|
||||||
|
"#,
|
||||||
|
mc_version,
|
||||||
|
name,
|
||||||
|
folder_name,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn set_stage(pool: &SqlitePool, id: i64, stage: i64) -> Result<ClusterRow, sqlx::Error> {
|
pub async fn set_stage(pool: &SqlitePool, id: i64, stage: i64) -> Result<ClusterRow, sqlx::Error> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
ClusterRow,
|
ClusterRow,
|
||||||
|
|
@ -200,3 +234,102 @@ pub async fn delete_by_id(pool: &SqlitePool, id: i64) -> Result<bool, sqlx::Erro
|
||||||
|
|
||||||
Ok(result.rows_affected() > 0)
|
Ok(result.rows_affected() > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::dao::applied_migration;
|
||||||
|
|
||||||
|
async fn pool() -> SqlitePool {
|
||||||
|
let pool = SqlitePool::connect("sqlite::memory:")
|
||||||
|
.await
|
||||||
|
.expect("in-memory sqlite");
|
||||||
|
sqlx::migrate!().run(&pool).await.expect("migrations run");
|
||||||
|
pool
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn seed(pool: &SqlitePool, mc_version: &str, folder: &str) -> ClusterRow {
|
||||||
|
insert(
|
||||||
|
pool,
|
||||||
|
&NewCluster {
|
||||||
|
name: &format!("{mc_version} fabric"),
|
||||||
|
folder_name: folder,
|
||||||
|
mc_version,
|
||||||
|
mc_loader: 1,
|
||||||
|
mc_loader_version: None,
|
||||||
|
setting_profile_name: None,
|
||||||
|
stage: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("insert cluster")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn migrate_version_rewrites_identity_and_keeps_history() {
|
||||||
|
let pool = pool().await;
|
||||||
|
let cluster = seed(&pool, "26.1", "26.1 fabric").await;
|
||||||
|
add_playtime(&pool, cluster.id, 3600)
|
||||||
|
.await
|
||||||
|
.expect("record playtime");
|
||||||
|
|
||||||
|
let migrated = migrate_version(
|
||||||
|
&pool,
|
||||||
|
cluster.id,
|
||||||
|
"26.1.2",
|
||||||
|
Some("26.1.2 fabric"),
|
||||||
|
"26.1.2 fabric",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("migrate");
|
||||||
|
|
||||||
|
assert_eq!(migrated.id, cluster.id, "must move the row, not replace it");
|
||||||
|
assert_eq!(migrated.mc_version, "26.1.2");
|
||||||
|
assert_eq!(migrated.folder_name, "26.1.2 fabric");
|
||||||
|
assert_eq!(migrated.name, "26.1.2 fabric");
|
||||||
|
assert_eq!(migrated.overall_played, Some(3600));
|
||||||
|
assert_eq!(migrated.created_at, cluster.created_at);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
find_by_version_loader(&pool, "26.1", 1)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
find_by_version_loader(&pool, "26.1.2", 1)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.id,
|
||||||
|
cluster.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn migrate_version_can_leave_name_and_folder_untouched() {
|
||||||
|
let pool = pool().await;
|
||||||
|
let cluster = seed(&pool, "26.1", "my cool pack").await;
|
||||||
|
|
||||||
|
let migrated = migrate_version(&pool, cluster.id, "26.1.2", None, "my cool pack")
|
||||||
|
.await
|
||||||
|
.expect("migrate");
|
||||||
|
|
||||||
|
assert_eq!(migrated.mc_version, "26.1.2");
|
||||||
|
assert_eq!(migrated.name, "26.1 fabric", "custom name must survive");
|
||||||
|
assert_eq!(migrated.folder_name, "my cool pack");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn applied_migrations_ledger_is_idempotent() {
|
||||||
|
let pool = pool().await;
|
||||||
|
|
||||||
|
assert!(!applied_migration::is_applied(&pool, "rule-a").await.unwrap());
|
||||||
|
applied_migration::mark_applied(&pool, "rule-a").await.unwrap();
|
||||||
|
assert!(applied_migration::is_applied(&pool, "rule-a").await.unwrap());
|
||||||
|
|
||||||
|
applied_migration::mark_applied(&pool, "rule-a").await.unwrap();
|
||||||
|
assert!(applied_migration::is_applied(&pool, "rule-a").await.unwrap());
|
||||||
|
assert!(!applied_migration::is_applied(&pool, "rule-b").await.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod applied_migration;
|
||||||
pub mod artifact;
|
pub mod artifact;
|
||||||
pub mod bundle;
|
pub mod bundle;
|
||||||
pub mod cluster;
|
pub mod cluster;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue