diff --git a/packages/oneclient_app/src/main.rs b/packages/oneclient_app/src/main.rs index fee2dff9..f9d5b2c6 100644 --- a/packages/oneclient_app/src/main.rs +++ b/packages/oneclient_app/src/main.rs @@ -110,6 +110,8 @@ fn main() { let rt = builder.build().unwrap(); let _tokio_guard = rt.enter(); + let adopted = rt.block_on(oneclient_core::relocate::adopt_legacy_dir()); + // no settings file is the sign of a fresh install, but not proof of one let never_set_up = oneclient_common::paths::settings_file() .map(|path| !path.exists()) @@ -151,6 +153,14 @@ fn main() { } .expect("Failed to initialize logger"); + match adopted { + Ok(Some(from)) => { + tracing::info!(from = %from.display(), "moved out of the old launcher folder") + } + Ok(None) => {} + Err(err) => tracing::error!("{err}"), + } + if let Some(reason) = unprotected { tracing::warn!("no single-instance endpoint, a second launcher can start: {reason}"); } diff --git a/packages/oneclient_common/src/paths.rs b/packages/oneclient_common/src/paths.rs index d29209de..0a725a34 100644 --- a/packages/oneclient_common/src/paths.rs +++ b/packages/oneclient_common/src/paths.rs @@ -14,7 +14,8 @@ const APPLICATION: &str = "OneClient"; #[cfg(debug_assertions)] const APPLICATION: &str = "OneClient-dev"; -const SETTINGS_FILE: &str = "settings.json"; +pub const SETTINGS_FILE: &str = "settings.json"; +pub const DATABASE_FILE: &str = "user_data.db"; static DEFAULT_DIR: OnceLock = OnceLock::new(); static CONFIG_DIR_OVERRIDE: OnceLock = OnceLock::new(); @@ -35,7 +36,8 @@ fn organization_dir() -> PathsResult { .ok_or(PathsError::DataDirUnavailable) } -fn legacy_dir() -> Option { +#[must_use] +pub fn legacy_dir() -> Option { ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) .map(|dirs| dirs.data_local_dir().to_path_buf()) } @@ -47,6 +49,10 @@ fn resolve_default_dir() -> PathsResult { return Ok(legacy); } + standard_dir() +} + +pub fn standard_dir() -> PathsResult { Ok(organization_dir()?.join(APPLICATION)) } @@ -87,7 +93,7 @@ pub fn picker_start_dir() -> Option { } pub fn database_file() -> PathsResult { - Ok(data_dir()?.join("user_data.db")) + Ok(data_dir()?.join(DATABASE_FILE)) } pub fn settings_file() -> PathsResult { diff --git a/packages/oneclient_core/src/relocate.rs b/packages/oneclient_core/src/relocate.rs index d12303a4..ff1b095c 100644 --- a/packages/oneclient_core/src/relocate.rs +++ b/packages/oneclient_core/src/relocate.rs @@ -285,6 +285,118 @@ pub fn incomplete(dir: &Path) -> bool { dir.join(IN_PROGRESS_MARKER).exists() } +pub async fn adopt_legacy_dir() -> Result, String> { + let (Some(from), Ok(to)) = (paths::legacy_dir(), paths::standard_dir()) else { + return Ok(None); + }; + + let Ok(raw) = polyio::read(from.join(paths::SETTINGS_FILE)).await else { + return Ok(None); + }; + + let chosen_folder = serde_json::from_slice::(&raw) + .map_or(true, |settings| !settings["data_dir"].is_null()); + if chosen_folder { + return Ok(None); + } + + #[cfg(unix)] + if std::os::unix::net::UnixStream::connect(from.join("ipc.sock")).is_ok() { + return Ok(None); + } + + let old = polyio::canonicalize(&from).unwrap_or_else(|_| from.clone()); + + if let Some(parent) = to.parent() { + polyio::create_dir_all(parent) + .await + .map_err(|err| format!("Couldn't create {}: {err}", parent.display()))?; + } + polyio::remove_dir(&to).await.ok(); + + polyio::rename(&from, &to).await.map_err(|err| { + format!( + "Couldn't move {} to {}, staying on the old folder: {err}", + from.display(), + to.display() + ) + })?; + + let database = to.join(paths::DATABASE_FILE); + if database.is_file() + && let Err(err) = rewrite_java_paths(&database, &from, &to).await + { + return match polyio::rename(&to, &from).await { + Ok(()) => Err(format!("{err} Staying on {}.", from.display())), + Err(back) => Err(format!( + "{err} Moving {} back failed too: {back}", + to.display() + )), + }; + } + + for dir in from.ancestors().skip(1).take(2) { + if polyio::remove_dir(dir).await.is_err() { + break; + } + } + + let new = polyio::canonicalize(&to).unwrap_or_else(|_| to.clone()); + repoint_links(&new, &old, &from).await.map_err(|err| { + format!( + "Moved to {} but a link could not be repointed: {err}", + to.display() + ) + })?; + + Ok(Some(from)) +} + +async fn repoint_links(root: &Path, old: &Path, raw_old: &Path) -> Result<(), polyio::IOError> { + let mut stack = vec![root.to_path_buf()]; + + while let Some(dir) = stack.pop() { + let mut entries = polyio::read_dir(&dir).await?; + + while let Ok(Some(entry)) = entries.next_entry().await { + let path = entry.path(); + let Ok(kind) = entry.file_type().await else { + continue; + }; + + if kind.is_dir() { + stack.push(path); + continue; + } + + if !kind.is_symlink() { + continue; + } + + let Ok(target) = polyio::read_link(&path).await else { + continue; + }; + let Ok(rest) = target + .strip_prefix(old) + .or_else(|_| target.strip_prefix(raw_old)) + else { + continue; + }; + + let target = root.join(rest); + if target.is_dir() { + polyio::remove_symlink_dir(&path).await?; + polyio::symlink_dir(&target, &path).await?; + } else { + polyio::remove_file(&path).await?; + polyio::symlink_file(&target, &path).await?; + } + } + } + + Ok(()) +} + fn current_dir() -> Result { paths::data_dir() .map(Path::to_path_buf)