Retroactive fix for 1ab294f2
This commit is contained in:
parent
1b633adcf2
commit
5a8728ef04
9 changed files with 268 additions and 32 deletions
5
.changeset/reask_seeded_bundle_choices.md
Normal file
5
.changeset/reask_seeded_bundle_choices.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
default: patch
|
||||
---
|
||||
|
||||
# SkyBlock content installed without asking is removed from 26.3 clusters once, and turning an optional bundle down removes its mods
|
||||
|
|
@ -53,10 +53,19 @@ impl Component for BundleChoicesPopup {
|
|||
.width(Size::flex(1.0))
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(toggle_controlled(chosen.read().contains(name), on_toggle)),
|
||||
.child(toggle_controlled(
|
||||
choices.held.contains(name) != chosen.read().contains(name),
|
||||
on_toggle,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
let names: Vec<String> = choices
|
||||
.bundles
|
||||
.iter()
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect();
|
||||
let held = choices.held.clone();
|
||||
let later = dispatch.clone();
|
||||
let close = dispatch.clone();
|
||||
|
||||
|
|
@ -92,7 +101,7 @@ impl Component for BundleChoicesPopup {
|
|||
.child(
|
||||
label()
|
||||
.text(format!(
|
||||
"{} has optional bundles you have not chosen yet. Turn on the ones you want.",
|
||||
"{} has optional bundles you have not chosen yet. Turn on the ones you want, turning one off removes its mods.",
|
||||
choices.cluster_name
|
||||
))
|
||||
.font_size(12.5)
|
||||
|
|
@ -125,7 +134,15 @@ impl Component for BundleChoicesPopup {
|
|||
Button::new()
|
||||
.primary()
|
||||
.on_press(move |_| {
|
||||
let picked = chosen.read().clone();
|
||||
let flipped = chosen.read().clone();
|
||||
let picked = names
|
||||
.iter()
|
||||
.filter(|name| {
|
||||
held.contains(*name)
|
||||
!= flipped.contains(*name)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
chosen.set(HashSet::new());
|
||||
dispatch.close_bundle_choices(Some(picked));
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1827,7 +1827,23 @@ impl Actions {
|
|||
};
|
||||
|
||||
let choices_for: Vec<String> = bundles.iter().map(|a| a.manifest.name.clone()).collect();
|
||||
let held = match oneclient_db::dao::cluster_bundle::list_bundle_tracked(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tracked) => tracked
|
||||
.into_iter()
|
||||
.filter_map(|row| row.bundle_name)
|
||||
.collect(),
|
||||
Err(err) => {
|
||||
tracing::warn!(cluster_id, error = %err, "could not read what the cluster holds, launching without asking about bundles");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let choices = BundleChoices {
|
||||
held,
|
||||
cluster_name: crate::install::cluster_display_name(cluster_id, &state.services).await,
|
||||
bundles: bundles
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ impl BlockedDownloads {
|
|||
pub struct BundleChoices {
|
||||
pub cluster_name: String,
|
||||
pub bundles: Vec<(String, String)>,
|
||||
pub held: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -98,6 +98,52 @@ pub async fn set_bundle_choices(
|
|||
ctx: &ContentCtx,
|
||||
) -> ContentResult<()> {
|
||||
bundle_dao::save_bundle_choices(&ctx.db, cluster_id, choices).await?;
|
||||
|
||||
for row in bundle_dao::list_bundle_tracked(&ctx.db, cluster_id).await? {
|
||||
let declined = row.bundle_name.as_ref().is_some_and(|name| {
|
||||
choices
|
||||
.iter()
|
||||
.any(|(choice, accepted)| choice == name && !accepted)
|
||||
});
|
||||
if declined {
|
||||
detach_artifact(cluster_id, &row.hash, ctx).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const UNASKED_SKYBLOCK: &str = "decline_unasked_skyblock";
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(cluster_id = cluster.id))]
|
||||
pub(crate) async fn decline_unasked_skyblock(
|
||||
cluster: &ClusterRow,
|
||||
archives: &[BundleArchive],
|
||||
ctx: &ContentCtx,
|
||||
) -> ContentResult<()> {
|
||||
let marker = format!("{UNASKED_SKYBLOCK}:{}", cluster.id);
|
||||
if migration_dao::is_applied(&ctx.db, &marker).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let unasked = cluster.mc_version == "26.3"
|
||||
&& bundle_dao::predates_bundle_choices(&ctx.db, cluster.id).await?;
|
||||
if unasked {
|
||||
let declined: Vec<(String, bool)> = archives
|
||||
.iter()
|
||||
.filter(|archive| {
|
||||
!archive.manifest.enabled
|
||||
&& archive
|
||||
.manifest
|
||||
.category
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("SkyBlock")
|
||||
})
|
||||
.map(|archive| (archive.manifest.name.clone(), false))
|
||||
.collect();
|
||||
set_bundle_choices(cluster.id, &declined, ctx).await?;
|
||||
}
|
||||
|
||||
migration_dao::mark_applied(&ctx.db, &marker).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -971,6 +1017,35 @@ pub async fn remove_artifact_from_cluster(
|
|||
) -> ContentResult<()> {
|
||||
let bundle_data = bundle_dao::get_bundle_tracked(&ctx.db, cluster_id, hash).await?;
|
||||
|
||||
detach_artifact(cluster_id, hash, ctx).await?;
|
||||
|
||||
if let Some(tracked) = bundle_data
|
||||
&& let (Some(bundle_name), Some(package_id)) =
|
||||
(tracked.bundle_name.clone(), tracked.package_id.clone())
|
||||
{
|
||||
if record_override {
|
||||
bundle_dao::save_override(
|
||||
&ctx.db,
|
||||
cluster_id,
|
||||
&bundle_name,
|
||||
&package_id,
|
||||
OverrideType::Removed,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
let replacement_exists =
|
||||
bundle_dao::has_bundle_mapping_for_package(&ctx.db, cluster_id, &package_id)
|
||||
.await?;
|
||||
if !replacement_exists {
|
||||
clear_suppressing_overrides(cluster_id, &package_id, ctx).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn detach_artifact(cluster_id: i64, hash: &str, ctx: &ContentCtx) -> ContentResult<()> {
|
||||
// Looked up first but never allowed to block removal
|
||||
// a package whose artifact row has gone missing must still be removable
|
||||
let cluster = PackageStore::get_cluster(cluster_id, ctx).await?;
|
||||
|
|
@ -1006,29 +1081,6 @@ pub async fn remove_artifact_from_cluster(
|
|||
tracing::warn!(hash, error = %err, "failed to evict unused artifact from the cache");
|
||||
}
|
||||
|
||||
if let Some(tracked) = bundle_data
|
||||
&& let (Some(bundle_name), Some(package_id)) =
|
||||
(tracked.bundle_name.clone(), tracked.package_id.clone())
|
||||
{
|
||||
if record_override {
|
||||
bundle_dao::save_override(
|
||||
&ctx.db,
|
||||
cluster_id,
|
||||
&bundle_name,
|
||||
&package_id,
|
||||
OverrideType::Removed,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
let replacement_exists =
|
||||
bundle_dao::has_bundle_mapping_for_package(&ctx.db, cluster_id, &package_id)
|
||||
.await?;
|
||||
if !replacement_exists {
|
||||
clear_suppressing_overrides(cluster_id, &package_id, ctx).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ use tokio::sync::Mutex as AsyncMutex;
|
|||
use futures_util::StreamExt;
|
||||
|
||||
use crate::bundles::install::{
|
||||
BUNDLE_INSTALL_CONCURRENCY, accepted_bundles, bundle_cluster, disable_was_deliberate,
|
||||
external_ids_by_sha1, find_override, find_user_suppression, heal_bundle_activity,
|
||||
install_package_from_bundle, remove_artifact_from_cluster, set_artifact_enabled_to,
|
||||
takes_bundle,
|
||||
BUNDLE_INSTALL_CONCURRENCY, accepted_bundles, bundle_cluster, decline_unasked_skyblock,
|
||||
disable_was_deliberate, external_ids_by_sha1, find_override, find_user_suppression,
|
||||
heal_bundle_activity, install_package_from_bundle, remove_artifact_from_cluster,
|
||||
set_artifact_enabled_to, takes_bundle,
|
||||
};
|
||||
use crate::bundles::manager::BundlesManager;
|
||||
use crate::bundles::overrides;
|
||||
|
|
@ -486,6 +486,7 @@ pub async fn apply_bundle_updates_with(
|
|||
let loader = GameLoader::from_repr(cluster.mc_loader as u8).unwrap_or(GameLoader::Fabric);
|
||||
if let Ok(archives) = bundles.archives_for(ctx, &cluster.mc_version, loader).await {
|
||||
heal_bundle_activity(cluster_id, &archives, ctx).await?;
|
||||
decline_unasked_skyblock(&cluster, &archives, ctx).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1390,6 +1390,132 @@ async fn taking_a_bundle_at_the_prompt_records_the_choice() {
|
|||
assert!(pending().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turning_a_bundle_down_removes_what_it_installed() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
seed_opt_in_bundle(&state, BUNDLE).await;
|
||||
let cluster_id = cluster_with_tracked_mod(&state).await;
|
||||
let ctx = state.services.content();
|
||||
|
||||
oneclient_content::bundles::set_bundle_choices(cluster_id, &[(BUNDLE.to_string(), true)], &ctx)
|
||||
.await
|
||||
.unwrap();
|
||||
let tracked = bundle_dao::list_bundle_tracked(&state.services.db, cluster_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(tracked.len(), 1, "taking the bundle keeps its mods");
|
||||
bundle_dao::save_override(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
BUNDLE,
|
||||
PROJECT_ID,
|
||||
OverrideType::Disabled,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
oneclient_content::bundles::set_bundle_choices(
|
||||
cluster_id,
|
||||
&[(BUNDLE.to_string(), false)],
|
||||
&ctx,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let tracked = bundle_dao::list_bundle_tracked(&state.services.db, cluster_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(tracked.is_empty());
|
||||
let overrides = bundle_dao::list_overrides(&state.services.db, cluster_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(overrides.len(), 1, "a mod the user switched off stays off");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skyblock_nobody_asked_for_is_taken_out_of_an_old_26_3_cluster_once() {
|
||||
const SKYBLOCK: &str = "Test [SkyBlock]";
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
let db = &state.services.db;
|
||||
let mut m = named_manifest(SKYBLOCK, vec![managed_file(true)]);
|
||||
m.enabled = false;
|
||||
m.category = "SkyBlock".to_string();
|
||||
m.mc_version = "26.3".to_string();
|
||||
seed_bundle_with_pack(&state, m).await;
|
||||
let cluster_id = cluster_with_tracked_mod(&state).await;
|
||||
let ctx = state.services.content();
|
||||
let track = || {
|
||||
bundle_dao::track_bundle_artifact(db, cluster_id, HASH, SKYBLOCK, VERSION_ID, PROJECT_ID)
|
||||
};
|
||||
let apply = || {
|
||||
oneclient_content::bundles::apply_bundle_updates(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&ctx,
|
||||
None,
|
||||
)
|
||||
};
|
||||
track().await.unwrap();
|
||||
bundle_dao::save_bundle_choices(db, cluster_id, &[(SKYBLOCK.to_string(), true)])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query("UPDATE clusters SET mc_version = '26.3' WHERE id = ?")
|
||||
.bind(cluster_id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
apply().await.unwrap();
|
||||
assert_eq!(
|
||||
bundle_dao::list_bundle_tracked(db, cluster_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1,
|
||||
"a cluster made after bundles started asking is left alone"
|
||||
);
|
||||
|
||||
sqlx::query("DELETE FROM applied_migrations")
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query("UPDATE clusters SET created_at = '2020-01-01T00:00:00+00:00' WHERE id = ?")
|
||||
.bind(cluster_id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
apply().await.unwrap();
|
||||
assert!(
|
||||
bundle_dao::list_bundle_tracked(db, cluster_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
artifact_dao::insert_artifact(
|
||||
db,
|
||||
HASH,
|
||||
ContentType::Mod as i64,
|
||||
"artifacts/sodium.jar",
|
||||
"sodium.jar",
|
||||
Some(1),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
artifact_dao::link_cluster_artifact(db, cluster_id, HASH, "sodium.jar")
|
||||
.await
|
||||
.unwrap();
|
||||
track().await.unwrap();
|
||||
apply().await.unwrap();
|
||||
assert_eq!(
|
||||
bundle_dao::list_bundle_tracked(db, cluster_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1,
|
||||
"taking SkyBlock again later is not undone"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_opt_in_bundle_switched_off_by_accident_is_repaired_at_install() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::models::{
|
||||
ArtifactRow, ClusterArtifactRow, ClusterKind, LinkedArtifactRow, ProviderReleaseRow,
|
||||
SeenStatus,
|
||||
ArtifactRow, ClusterArtifactRow, ClusterKind, LinkedArtifactRow, ProviderReleaseRow, SeenStatus,
|
||||
};
|
||||
|
||||
pub async fn get_artifact_by_hash(
|
||||
|
|
|
|||
|
|
@ -341,6 +341,25 @@ pub async fn save_bundle_choices(
|
|||
tx.commit().await
|
||||
}
|
||||
|
||||
pub async fn predates_bundle_choices(
|
||||
pool: &SqlitePool,
|
||||
cluster_id: i64,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM clusters c, _sqlx_migrations m
|
||||
WHERE c.id = ? AND m.version = 20261005120000
|
||||
AND (c.created_at IS NULL OR datetime(c.created_at) < m.installed_on)
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(cluster_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn copy_bundle_choices(
|
||||
pool: &SqlitePool,
|
||||
source_cluster_id: i64,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue