Files
AstralRinth/packages/app-lib/src/state/legacy_converter.rs
Geometrically 49a20a303a Migrate to SQLite for Internal Launcher Data (#1300)
* initial migration

* barebones profiles

* Finish profiles

* Add back file watcher

* UI support progress

* Finish most of cache

* Fix options page

* Fix forge, finish modrinth auth

* Accounts, process cache

* Run SQLX prepare

* Finish

* Run lint + actions

* Fix version to be compat with windows

* fix lint

* actually fix lint

* actually fix lint again
2024-07-24 18:03:19 +00:00

514 lines
18 KiB
Rust

use crate::data::DirectoryInfo;
use crate::jre::check_jre;
use crate::prelude::ModLoader;
use crate::state;
use crate::state::{
Credentials, DefaultPage, DeviceToken, DeviceTokenKey, DeviceTokenPair,
Hooks, LinkedData, MemorySettings, ModrinthCredentials, Profile,
ProfileInstallStage, Theme, WindowSize,
};
use crate::util::fetch::{read_json, IoSemaphore};
use chrono::{DateTime, Utc};
use p256::ecdsa::SigningKey;
use p256::pkcs8::DecodePrivateKey;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
use tokio::sync::Semaphore;
use uuid::Uuid;
pub async fn migrate_legacy_data<'a, E>(exec: E) -> crate::Result<()>
where
E: sqlx::Executor<'a, Database = sqlx::Sqlite> + Copy,
{
let mut settings = state::Settings::get(exec).await?;
if settings.migrated {
return Ok(());
};
let old_launcher_root = if let Some(dir) = default_settings_dir() {
dir
} else {
return Ok(());
};
let old_launcher_root_str = old_launcher_root.to_string_lossy().to_string();
let new_launcher_root = DirectoryInfo::get_initial_settings_dir().ok_or(
crate::ErrorKind::FSError(
"Could not find valid config dir".to_string(),
),
)?;
let new_launcher_root_str = new_launcher_root
.to_string_lossy()
.to_string()
.trim_end_matches('/')
.trim_end_matches('\\')
.to_string();
let io_semaphore = IoSemaphore(Semaphore::new(10));
let settings_path = old_launcher_root.join("settings.json");
if let Ok(legacy_settings) =
read_json::<LegacySettings>(&settings_path, &io_semaphore).await
{
settings.max_concurrent_writes = legacy_settings.max_concurrent_writes;
settings.max_concurrent_downloads =
legacy_settings.max_concurrent_downloads;
settings.theme = match legacy_settings.theme {
LegacyTheme::Dark => Theme::Dark,
LegacyTheme::Light => Theme::Light,
LegacyTheme::Oled => Theme::Oled,
};
settings.default_page = match legacy_settings.default_page {
LegacyDefaultPage::Home => DefaultPage::Home,
LegacyDefaultPage::Library => DefaultPage::Library,
};
settings.collapsed_navigation = legacy_settings.collapsed_navigation;
settings.advanced_rendering = legacy_settings.advanced_rendering;
settings.native_decorations = legacy_settings.native_decorations;
settings.telemetry = !legacy_settings.opt_out_analytics;
settings.discord_rpc = !legacy_settings.disable_discord_rpc;
settings.developer_mode = legacy_settings.developer_mode;
settings.onboarded = legacy_settings.fully_onboarded;
settings.extra_launch_args = legacy_settings.custom_java_args;
settings.custom_env_vars = legacy_settings.custom_env_args;
settings.memory.maximum = legacy_settings.memory.maximum;
settings.force_fullscreen = legacy_settings.force_fullscreen;
settings.game_resolution.0 = legacy_settings.game_resolution.0;
settings.game_resolution.1 = legacy_settings.game_resolution.1;
settings.hide_on_process_start = legacy_settings.hide_on_process;
settings.hooks.pre_launch = legacy_settings.hooks.pre_launch;
settings.hooks.wrapper = legacy_settings.hooks.wrapper;
settings.hooks.post_exit = legacy_settings.hooks.post_exit;
if let Some(path) = legacy_settings
.loaded_config_dir
.clone()
.and_then(|x| x.to_str().map(|x| x.to_string()))
{
if path != old_launcher_root_str {
settings.custom_dir = Some(path);
}
}
settings.prev_custom_dir = Some(old_launcher_root_str.clone());
for (_, legacy_version) in legacy_settings.java_globals.0 {
if let Ok(Some(mut java_version)) =
check_jre(PathBuf::from(legacy_version.path)).await
{
java_version.path = java_version
.path
.replace(&old_launcher_root_str, &new_launcher_root_str);
java_version.upsert(exec).await?;
}
}
let modrinth_auth_path =
old_launcher_root.join("caches/metadata/auth.json");
if let Ok(creds) = read_json::<LegacyModrinthCredentials>(
&modrinth_auth_path,
&io_semaphore,
)
.await
{
ModrinthCredentials {
session: creds.session,
expires: creds.expires_at,
user_id: creds.user.id,
active: true,
}
.upsert(exec)
.await?;
}
let minecraft_auth_path =
old_launcher_root.join("caches/metadata/minecraft_auth.json");
if let Ok(minecraft_auth) = read_json::<LegacyMinecraftAuthStore>(
&minecraft_auth_path,
&io_semaphore,
)
.await
{
let minecraft_users_len = minecraft_auth.users.len();
for (uuid, credential) in minecraft_auth.users {
Credentials {
id: credential.id,
username: credential.username,
access_token: credential.access_token,
refresh_token: credential.refresh_token,
expires: credential.expires,
active: minecraft_auth.default_user == Some(uuid)
|| minecraft_users_len == 1,
}
.upsert(exec)
.await?;
}
if let Some(device_token) = minecraft_auth.token {
if let Ok(private_key) =
SigningKey::from_pkcs8_pem(&device_token.private_key)
{
if let Ok(uuid) = Uuid::parse_str(&device_token.id) {
DeviceTokenPair {
token: DeviceToken {
issue_instant: device_token.token.issue_instant,
not_after: device_token.token.not_after,
token: device_token.token.token,
display_claims: device_token
.token
.display_claims,
},
key: DeviceTokenKey {
id: uuid,
key: private_key,
x: device_token.x,
y: device_token.y,
},
}
.upsert(exec)
.await?;
}
}
}
}
if let Ok(profiles_dir) = std::fs::read_dir(
&legacy_settings
.loaded_config_dir
.unwrap_or(old_launcher_root)
.join("profiles"),
) {
for entry in profiles_dir.flatten() {
if entry.path().is_dir() {
let profile_path = entry.path().join("profile.json");
if let Ok(profile) =
read_json::<LegacyProfile>(&profile_path, &io_semaphore)
.await
{
Profile {
path: profile.path,
install_stage: match profile.install_stage {
LegacyProfileInstallStage::Installed => {
ProfileInstallStage::Installed
}
LegacyProfileInstallStage::Installing => {
ProfileInstallStage::Installing
}
LegacyProfileInstallStage::PackInstalling => {
ProfileInstallStage::PackInstalling
}
LegacyProfileInstallStage::NotInstalled => {
ProfileInstallStage::NotInstalled
}
},
name: profile.metadata.name,
icon_path: profile.metadata.icon.map(|x| {
x.replace(
&old_launcher_root_str,
&new_launcher_root_str,
)
}),
game_version: profile.metadata.game_version,
loader: match profile.metadata.loader {
LegacyModLoader::Vanilla => ModLoader::Vanilla,
LegacyModLoader::Forge => ModLoader::Forge,
LegacyModLoader::Fabric => ModLoader::Fabric,
LegacyModLoader::Quilt => ModLoader::Quilt,
LegacyModLoader::NeoForge => {
ModLoader::NeoForge
}
},
loader_version: profile
.metadata
.loader_version
.map(|x| x.id),
groups: profile.metadata.groups,
linked_data: profile.metadata.linked_data.and_then(
|x| {
if let Some(project_id) = x.project_id {
if let Some(version_id) = x.version_id {
if let Some(locked) = x.locked {
return Some(LinkedData {
project_id,
version_id,
locked,
});
}
}
}
None
},
),
created: profile.metadata.date_created,
modified: profile.metadata.date_modified,
last_played: profile.metadata.last_played,
submitted_time_played: profile
.metadata
.submitted_time_played,
recent_time_played: profile
.metadata
.recent_time_played,
java_path: profile.java.as_ref().and_then(|x| {
x.override_version.clone().map(|x| {
x.path.replace(
&old_launcher_root_str,
&new_launcher_root_str,
)
})
}),
extra_launch_args: profile
.java
.as_ref()
.and_then(|x| x.extra_arguments.clone()),
custom_env_vars: profile
.java
.and_then(|x| x.custom_env_args),
memory: profile
.memory
.map(|x| MemorySettings { maximum: x.maximum }),
force_fullscreen: profile.fullscreen,
game_resolution: profile
.resolution
.map(|x| WindowSize(x.0, x.1)),
hooks: Hooks {
pre_launch: profile
.hooks
.as_ref()
.and_then(|x| x.pre_launch.clone()),
wrapper: profile
.hooks
.as_ref()
.and_then(|x| x.wrapper.clone()),
post_exit: profile
.hooks
.and_then(|x| x.post_exit),
},
}
.upsert(exec)
.await?;
}
}
}
}
settings.migrated = true;
settings.update(exec).await?;
}
Ok(())
}
#[derive(Deserialize, Debug, Clone)]
struct LegacySettings {
pub theme: LegacyTheme,
pub memory: LegacyMemorySettings,
#[serde(default)]
pub force_fullscreen: bool,
pub game_resolution: LegacyWindowSize,
pub custom_java_args: Vec<String>,
pub custom_env_args: Vec<(String, String)>,
pub java_globals: LegacyJavaGlobals,
pub hooks: LegacyHooks,
pub max_concurrent_downloads: usize,
pub max_concurrent_writes: usize,
pub collapsed_navigation: bool,
#[serde(default)]
pub disable_discord_rpc: bool,
#[serde(default)]
pub hide_on_process: bool,
#[serde(default)]
pub native_decorations: bool,
#[serde(default)]
pub default_page: LegacyDefaultPage,
#[serde(default)]
pub developer_mode: bool,
#[serde(default)]
pub opt_out_analytics: bool,
#[serde(default)]
pub advanced_rendering: bool,
#[serde(default)]
pub fully_onboarded: bool,
#[serde(default = "default_settings_dir")]
pub loaded_config_dir: Option<PathBuf>,
}
fn default_settings_dir() -> Option<PathBuf> {
Some(dirs::config_dir()?.join("com.modrinth.theseus"))
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LegacyTheme {
Dark,
Light,
Oled,
}
#[derive(Deserialize, Default, Debug, Clone, Copy)]
enum LegacyDefaultPage {
#[default]
Home,
Library,
}
#[derive(Deserialize, Debug, Clone)]
struct LegacyHooks {
pub pre_launch: Option<String>,
pub wrapper: Option<String>,
pub post_exit: Option<String>,
}
#[derive(Deserialize, Debug, Clone, Copy)]
struct LegacyMemorySettings {
pub maximum: u32,
}
#[derive(Deserialize, Debug, Clone, Copy)]
struct LegacyWindowSize(pub u16, pub u16);
#[derive(Debug, Deserialize, Clone)]
struct LegacyJavaGlobals(HashMap<String, LegacyJavaVersion>);
#[derive(Debug, PartialEq, Eq, Hash, Deserialize, Clone)]
struct LegacyJavaVersion {
pub path: String,
pub version: String,
pub architecture: String,
}
#[derive(Deserialize, Clone, Debug)]
struct LegacyModrinthUser {
pub id: String,
}
#[derive(Deserialize, Clone, Debug)]
struct LegacyModrinthCredentials {
pub session: String,
pub expires_at: DateTime<Utc>,
pub user: LegacyModrinthUser,
}
#[derive(Deserialize, Debug)]
struct LegacyMinecraftAuthStore {
pub users: HashMap<Uuid, LegacyCredentials>,
pub token: Option<LegacySaveDeviceToken>,
pub default_user: Option<Uuid>,
}
#[derive(Deserialize, Clone, Debug)]
struct LegacyCredentials {
pub id: Uuid,
pub username: String,
pub access_token: String,
pub refresh_token: String,
pub expires: DateTime<Utc>,
}
#[derive(Deserialize, Debug)]
struct LegacySaveDeviceToken {
pub id: String,
pub private_key: String,
pub x: String,
pub y: String,
pub token: LegacyDeviceToken,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "PascalCase")]
struct LegacyDeviceToken {
pub issue_instant: DateTime<Utc>,
pub not_after: DateTime<Utc>,
pub token: String,
pub display_claims: HashMap<String, serde_json::Value>,
}
#[derive(Deserialize, Clone, Debug)]
struct LegacyProfile {
#[serde(default)]
pub install_stage: LegacyProfileInstallStage,
#[serde(default)]
pub path: String,
pub metadata: LegacyProfileMetadata,
pub java: Option<LegacyJavaSettings>,
pub memory: Option<LegacyMemorySettings>,
pub resolution: Option<LegacyWindowSize>,
pub fullscreen: Option<bool>,
pub hooks: Option<LegacyHooks>,
}
#[derive(Deserialize, Clone, Debug)]
struct LegacyProfileMetadata {
pub name: String,
pub icon: Option<String>,
#[serde(default)]
pub groups: Vec<String>,
pub game_version: String,
#[serde(default)]
pub loader: LegacyModLoader,
pub loader_version: Option<LegacyLoaderVersion>,
pub linked_data: Option<LegacyLinkedData>,
#[serde(default)]
pub date_created: DateTime<Utc>,
#[serde(default)]
pub date_modified: DateTime<Utc>,
pub last_played: Option<DateTime<Utc>>,
#[serde(default)]
pub submitted_time_played: u64,
#[serde(default)]
pub recent_time_played: u64,
}
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
enum LegacyModLoader {
#[default]
Vanilla,
Forge,
Fabric,
Quilt,
NeoForge,
}
#[derive(Deserialize, Clone, Debug)]
struct LegacyLinkedData {
pub project_id: Option<String>,
pub version_id: Option<String>,
#[serde(default = "default_locked")]
pub locked: Option<bool>,
}
fn default_locked() -> Option<bool> {
Some(true)
}
#[derive(Deserialize, Clone, Debug)]
struct LegacyJavaSettings {
pub override_version: Option<LegacyJavaVersion>,
pub extra_arguments: Option<Vec<String>>,
pub custom_env_args: Option<Vec<(String, String)>>,
}
#[derive(Deserialize, Clone, Debug)]
struct LegacyLoaderVersion {
pub id: String,
}
#[derive(Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
enum LegacyProfileInstallStage {
Installed,
Installing,
PackInstalling,
#[default]
NotInstalled,
}