feat: optionally copy configs into dedicated-folder targets on migration
This commit is contained in:
parent
0e98be3d5a
commit
190723a4f8
7 changed files with 153 additions and 8 deletions
|
|
@ -11,7 +11,9 @@ use oneclient_content::packages::release_migration::{
|
|||
use oneclient_content::packages::{CachedPackageMeta, ProviderId};
|
||||
use oneclient_core::clusters::Cluster;
|
||||
|
||||
use crate::components::{Button, Dropdown, DynamicArt, Icon, IconType, OverlayPopup, ScrollArea};
|
||||
use crate::components::{
|
||||
Button, Dropdown, DynamicArt, Icon, IconType, OverlayPopup, ScrollArea, checkbox_labeled,
|
||||
};
|
||||
use crate::hooks::{
|
||||
loaded_image, package_meta_batch, use_cached_image, use_dispatch, use_game_snapshot,
|
||||
use_launcher, use_notifications_snapshot, use_package_meta_batch, use_release_migration,
|
||||
|
|
@ -103,6 +105,7 @@ impl Component for ReleaseMigrationPopup {
|
|||
|
||||
let mut excluded = use_state(HashSet::<SelectionKey>::new);
|
||||
let mut tab = use_state(|| PackageTab::Mods);
|
||||
let mut copy_configs = use_state(|| true);
|
||||
|
||||
let settled = launcher.ready && !launcher.fetching && !launcher.syncing_bundles;
|
||||
let checked = use_state(|| false);
|
||||
|
|
@ -159,6 +162,7 @@ impl Component for ReleaseMigrationPopup {
|
|||
tab.set_if_modified(PackageTab::Mods);
|
||||
wont_open.set_if_modified(true);
|
||||
deps_open.set_if_modified(false);
|
||||
copy_configs.set_if_modified(true);
|
||||
return rect().into_element();
|
||||
};
|
||||
|
||||
|
|
@ -189,6 +193,7 @@ impl Component for ReleaseMigrationPopup {
|
|||
tab,
|
||||
wont_open,
|
||||
deps_open,
|
||||
copy_configs,
|
||||
target_running,
|
||||
)),
|
||||
)
|
||||
|
|
@ -205,6 +210,7 @@ fn dialog(
|
|||
tab: State<PackageTab>,
|
||||
wont_open: State<bool>,
|
||||
deps_open: State<bool>,
|
||||
copy_configs: State<bool>,
|
||||
target_running: bool,
|
||||
) -> impl IntoElement {
|
||||
let plan = match prompt.plan() {
|
||||
|
|
@ -240,6 +246,7 @@ fn dialog(
|
|||
tab,
|
||||
wont_open,
|
||||
deps_open,
|
||||
copy_configs,
|
||||
target_running,
|
||||
))
|
||||
}
|
||||
|
|
@ -419,6 +426,7 @@ fn content_panel(
|
|||
tab: State<PackageTab>,
|
||||
wont_open: State<bool>,
|
||||
deps_open: State<bool>,
|
||||
copy_configs: State<bool>,
|
||||
target_running: bool,
|
||||
) -> impl IntoElement {
|
||||
let close = dispatch.clone();
|
||||
|
|
@ -493,7 +501,14 @@ fn content_panel(
|
|||
.height(Size::px(1.))
|
||||
.background(colors::component_border()),
|
||||
)
|
||||
.child(footer(prompt, plan, dispatch, excluded, target_running))
|
||||
.child(footer(
|
||||
prompt,
|
||||
plan,
|
||||
dispatch,
|
||||
excluded,
|
||||
copy_configs,
|
||||
target_running,
|
||||
))
|
||||
}
|
||||
|
||||
fn source_picker(prompt: &ReleaseMigrationPrompt, dispatch: crate::Actions) -> impl IntoElement {
|
||||
|
|
@ -1117,6 +1132,7 @@ fn footer(
|
|||
plan: Option<&ReleaseMigrationPlan>,
|
||||
dispatch: crate::Actions,
|
||||
excluded: State<HashSet<SelectionKey>>,
|
||||
copy_configs: State<bool>,
|
||||
target_running: bool,
|
||||
) -> impl IntoElement {
|
||||
let dismiss = dispatch.clone();
|
||||
|
|
@ -1159,7 +1175,9 @@ fn footer(
|
|||
.height(Size::px(52.))
|
||||
.font_size(15.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.on_press(move |_| dispatch.migrate_release_packages(chosen.clone()))
|
||||
.on_press(move |_| {
|
||||
dispatch.migrate_release_packages(chosen.clone(), *copy_configs.peek());
|
||||
})
|
||||
.text("Migrate");
|
||||
if target_running {
|
||||
migrate = migrate.tooltip(format!("Close {} before migrating", prompt.target.name));
|
||||
|
|
@ -1181,6 +1199,11 @@ fn footer(
|
|||
.max_lines(2)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.maybe_child(
|
||||
prompt
|
||||
.target_dedicated
|
||||
.then(|| checkbox_labeled(copy_configs, "Copy configs")),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.ghost()
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ use oneclient_content::packages::{
|
|||
};
|
||||
use oneclient_core::ReleaseTarget;
|
||||
use oneclient_core::clusters::{
|
||||
Cluster, OfferLookup, ReleaseMigrationOffer, manual_migration_offer, record_new_versions,
|
||||
release_migration_offer,
|
||||
Cluster, OfferLookup, ReleaseMigrationOffer, copy_configs, manual_migration_offer,
|
||||
record_new_versions, release_migration_offer,
|
||||
};
|
||||
use oneclient_events::Level;
|
||||
|
||||
|
|
@ -255,6 +255,7 @@ impl Actions {
|
|||
self.write_release_migration(move |prompt| {
|
||||
*prompt = Some(ReleaseMigrationPrompt {
|
||||
key: offer.release.key(),
|
||||
target_dedicated: offer.target.uses_dedicated_dir(),
|
||||
target: offer.target,
|
||||
java_major,
|
||||
sources,
|
||||
|
|
@ -417,6 +418,7 @@ impl Actions {
|
|||
actions.write_release_migration(move |prompt| {
|
||||
*prompt = Some(ReleaseMigrationPrompt {
|
||||
key: release.key(),
|
||||
target_dedicated: target.uses_dedicated_dir(),
|
||||
target,
|
||||
java_major: None,
|
||||
sources: vec![source],
|
||||
|
|
@ -661,7 +663,11 @@ impl Actions {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn migrate_release_packages(&self, packages: Vec<ReleaseMigrationPackage>) {
|
||||
pub fn migrate_release_packages(
|
||||
&self,
|
||||
packages: Vec<ReleaseMigrationPackage>,
|
||||
copy_configs_too: bool,
|
||||
) {
|
||||
let mut taken = None;
|
||||
self.write_release_migration(|prompt| taken = prompt.take());
|
||||
let Some(prompt) = taken else { return };
|
||||
|
|
@ -708,6 +714,7 @@ impl Actions {
|
|||
spawn_forever(async move {
|
||||
let Ok(state) = launcher::state() else { return };
|
||||
let content = state.services.content();
|
||||
let source = prompt.source().filter(|_| copy_configs_too).cloned();
|
||||
let target = prompt.target;
|
||||
let total = packages.len() + dependencies.len();
|
||||
|
||||
|
|
@ -788,6 +795,13 @@ impl Actions {
|
|||
tracing::warn!(error = %err, "could not add packages without a build to the migration waitlist");
|
||||
}
|
||||
|
||||
if let Some(source) = &source {
|
||||
match copy_configs(&state, source, &target).await {
|
||||
Ok(copied) => tracing::info!(copied, "copied config files into the target"),
|
||||
Err(err) => tracing::warn!(error = %err, "could not copy config files"),
|
||||
}
|
||||
}
|
||||
|
||||
let failed = total - migrated;
|
||||
let session_id = session.detach();
|
||||
let spec = Some(NotificationSpec {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ pub enum ReleasePlanState {
|
|||
pub struct ReleaseMigrationPrompt {
|
||||
pub key: String,
|
||||
pub target: Cluster,
|
||||
pub target_dedicated: bool,
|
||||
pub java_major: Option<u32>,
|
||||
pub sources: Vec<Cluster>,
|
||||
pub selected: i64,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ pub use manifest::BundleManifest as RemoteBundleManifest;
|
|||
pub use optional::{
|
||||
PendingOptionalMod, pending_optional_mods, resolve_optional_mods, skip_optional_mods,
|
||||
};
|
||||
pub use overrides::bundle_override_paths;
|
||||
pub use types::{
|
||||
ApplyBundleUpdatesResult, BundleArchive, BundleFile, BundleFileKind, BundleFileType,
|
||||
BundleManifest, BundleOptionalPackage, BundlePackageAddition, BundlePackageRemoval,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use oneclient_common::domain::GameLoader;
|
||||
use oneclient_db::models::ClusterRow;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::bundles::BundlesManager;
|
||||
use crate::ctx::ContentCtx;
|
||||
use crate::error::ContentResult;
|
||||
use oneclient_events::EventBus;
|
||||
use polyio::{ZipEntryCursor, sha1_bytes, sha1_file};
|
||||
|
|
@ -82,6 +85,20 @@ async fn sync_bundle_overrides_at(
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn bundle_override_paths(
|
||||
bundles: &BundlesManager,
|
||||
ctx: &ContentCtx,
|
||||
mc_version: &str,
|
||||
loader: GameLoader,
|
||||
) -> ContentResult<HashSet<String>> {
|
||||
let mut paths = HashSet::new();
|
||||
for archive in bundles.archives_for(ctx, mc_version, loader).await? {
|
||||
let layers = OverrideLayers::open(&archive.bundle.path, &[OVERRIDES_PREFIX]).await?;
|
||||
paths.extend(layers.entries().iter().map(|(rel, _)| rel.clone()));
|
||||
}
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
pub(crate) struct OverrideLayers {
|
||||
cursor: ZipEntryCursor,
|
||||
entries: Vec<(String, String)>,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ pub use prepare::{
|
|||
};
|
||||
pub use provision::{ensure_from_bundles, ensure_from_versions};
|
||||
pub use release_migration::{
|
||||
OfferLookup, ReleaseMigrationOffer, can_migrate_manually, manual_migration_offer,
|
||||
OfferLookup, ReleaseMigrationOffer, can_migrate_manually, copy_configs, manual_migration_offer,
|
||||
rank_migration_sources, record_new_versions, release_migration_offer,
|
||||
};
|
||||
pub use unlink_legacy::{SweepReport, unlink_legacy_cluster_content};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
use std::cmp::Reverse;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
use oneclient_common::domain::GameLoader;
|
||||
|
||||
use oneclient_cluster::Cluster;
|
||||
use oneclient_common::version::parse_mc_version;
|
||||
use oneclient_content::bundles::bundle_override_paths;
|
||||
use oneclient_content::packages::release_migration::{
|
||||
has_migratable_packages, has_packages_to_migrate,
|
||||
};
|
||||
|
|
@ -220,10 +223,96 @@ async fn offer_for(
|
|||
})))
|
||||
}
|
||||
|
||||
pub async fn copy_configs(
|
||||
state: &LauncherState,
|
||||
source: &Cluster,
|
||||
target: &Cluster,
|
||||
) -> LauncherResult<usize> {
|
||||
if !target.uses_dedicated_dir() {
|
||||
return Ok(0);
|
||||
}
|
||||
let from = source.game_dir()?.join("config");
|
||||
let to = target.game_dir()?.join("config");
|
||||
if from == to || !polyio::try_exists(&from).await? {
|
||||
return Ok(0);
|
||||
}
|
||||
let bundled = bundle_override_paths(
|
||||
&state.bundles,
|
||||
&state.services.content(),
|
||||
&target.mc_version,
|
||||
target.mc_loader,
|
||||
)
|
||||
.await?;
|
||||
copy_missing(&from, &to, "config", &bundled).await
|
||||
}
|
||||
|
||||
async fn copy_missing(
|
||||
from: &Path,
|
||||
to: &Path,
|
||||
rel: &str,
|
||||
skip: &HashSet<String>,
|
||||
) -> LauncherResult<usize> {
|
||||
let mut copied = 0;
|
||||
let mut stack = vec![(from.to_path_buf(), to.to_path_buf(), rel.to_string())];
|
||||
while let Some((src, dst, rel)) = stack.pop() {
|
||||
polyio::create_dir_all(&dst).await?;
|
||||
let mut entries = polyio::read_dir(&src).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let name = entry.file_name();
|
||||
let child = dst.join(&name);
|
||||
let child_rel = format!("{rel}/{}", name.to_string_lossy());
|
||||
let file_type = entry.file_type().await?;
|
||||
if file_type.is_dir() {
|
||||
stack.push((entry.path(), child, child_rel));
|
||||
} else if file_type.is_file()
|
||||
&& !skip.contains(&child_rel)
|
||||
&& !polyio::try_exists(&child).await?
|
||||
{
|
||||
polyio::copy(entry.path(), &child).await?;
|
||||
copied += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(copied)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn copy_missing_keeps_existing_and_bundled_files() {
|
||||
let src = polyio::tempdir().await.expect("src dir");
|
||||
let dst = polyio::tempdir().await.expect("dst dir");
|
||||
let (src, dst) = (src.dir_path(), dst.dir_path());
|
||||
|
||||
polyio::create_dir_all(src.join("sub")).await.unwrap();
|
||||
polyio::write(src.join("a.json"), b"old").await.unwrap();
|
||||
polyio::write(src.join("sub/b.json"), b"b").await.unwrap();
|
||||
polyio::write(src.join("sub/bundled.json"), b"bundled")
|
||||
.await
|
||||
.unwrap();
|
||||
polyio::write(dst.join("a.json"), b"keep").await.unwrap();
|
||||
|
||||
let skip = HashSet::from(["config/sub/bundled.json".to_string()]);
|
||||
assert_eq!(copy_missing(src, dst, "config", &skip).await.unwrap(), 1);
|
||||
assert!(
|
||||
!polyio::try_exists(dst.join("sub/bundled.json"))
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
polyio::read_to_string(dst.join("a.json")).await.unwrap(),
|
||||
"keep"
|
||||
);
|
||||
assert_eq!(
|
||||
polyio::read_to_string(dst.join("sub/b.json"))
|
||||
.await
|
||||
.unwrap(),
|
||||
"b"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newer_minor_orders_above_older_minor() {
|
||||
assert!(version_order("26.2") < version_order("26.3"));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue