diff --git a/Cargo.lock b/Cargo.lock index 2b70e98..3e23001 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2734,9 +2734,9 @@ dependencies = [ [[package]] name = "native_db" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed54dc7c04d201260247cbd1de99d459297fe80f0a79daa4598ef9c6ce4d7782" +checksum = "89733f755069940958e2b63f1d70d614f8b2773e3ff8575f3ab797122b14b4c3" dependencies = [ "native_db_macro", "native_model", @@ -2749,9 +2749,9 @@ dependencies = [ [[package]] name = "native_db_macro" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "163533e9cf510dfdc1f417462beb3651fccdcb9dd027533d85f780081ee212e8" +checksum = "83c478e633e022e48b6700b066959609fb7c637807bc3c1f8193393e3fbfcdf3" dependencies = [ "proc-macro2", "quote", diff --git a/crates/resolute/Cargo.toml b/crates/resolute/Cargo.toml index 13abe92..d337a98 100644 --- a/crates/resolute/Cargo.toml +++ b/crates/resolute/Cargo.toml @@ -21,10 +21,12 @@ futures-util = "0.3" path-clean = "1.0" sha2 = "0.10" steamlocate = "2.0.0-beta.2" -native_db = { version = "0.7", optional = true } +native_db = { version = "0.7.1", optional = true, default-features = false } native_model = { version = "0.4", optional = true } -redb = { version = "2.0", optional = true } +redb = { version = "2.1", optional = true } [features] -default = ["db"] +default = ["db", "migrate"] db = ["dep:native_db", "dep:native_model", "dep:redb"] +migrate = ["models_v1", "native_db/upgrade_0_5_x"] +models_v1 = [] diff --git a/crates/resolute/src/db.rs b/crates/resolute/src/db.rs index 7b0df39..791e068 100644 --- a/crates/resolute/src/db.rs +++ b/crates/resolute/src/db.rs @@ -1,3 +1,4 @@ +use core::result; use std::{io::ErrorKind, path::Path}; use log::{info, warn}; @@ -5,7 +6,7 @@ use native_db::{db_type::Error as NativeDbError, Builder, Database, Models}; use once_cell::sync::Lazy; use redb::{DatabaseError, StorageError}; -use crate::{mods::ResoluteMod, Error, Result as ResoluteResult}; +use crate::{models::ResoluteMod, Error, Result}; /// Wrapper for interacting with a Resolute database #[allow(missing_debug_implementations)] @@ -16,7 +17,7 @@ pub struct ResoluteDatabase<'a> { impl ResoluteDatabase<'_> { /// Opens a database using a provided builder. /// If the database doesn't already exist at the given path, it will be created. - pub fn open(db_path: impl AsRef) -> ResoluteResult { + pub fn open(db_path: impl AsRef) -> Result { info!("Opening database at {}", db_path.as_ref().display()); // Try to open an already-existing database @@ -35,25 +36,34 @@ impl ResoluteDatabase<'_> { Err(err) => return Err(err.into()), }; + // Run migrations + #[cfg(feature = "migrate")] + { + info!("Running migrations on database"); + let rw = db.rw_transaction()?; + rw.migrate::()?; + rw.commit()?; + } + info!("Database initialized"); Ok(Self { db }) } /// Retrieves all mods stored in the database - pub fn get_mods(&self) -> ResoluteResult> { + pub fn get_mods(&self) -> Result> { let read = self.db.r_transaction()?; - let mods = read.scan().primary()?.all().collect::>()?; + let mods = read.scan().primary()?.all().collect::>()?; Ok(mods) } /// Retrieves all mods from the database that have an installed version - pub fn get_installed_mods(&self) -> ResoluteResult> { + pub fn get_installed_mods(&self) -> Result> { let read = self.db.r_transaction()?; let mods = read .scan() .primary()? .all() - .collect::, _>>()? + .collect::, _>>()? .into_iter() .filter(|rmod: &ResoluteMod| rmod.installed_version.is_some()) .collect(); @@ -61,14 +71,14 @@ impl ResoluteDatabase<'_> { } /// Retrieves a single mod from the database by its ID - pub fn get_mod(&self, id: impl AsRef) -> ResoluteResult> { + pub fn get_mod(&self, id: impl AsRef) -> Result> { let read = self.db.r_transaction()?; let rmod = read.get().primary(id.as_ref())?; Ok(rmod) } /// Stores a mod in the database (overwrites any existing entry for the same mod) - pub fn store_mod(&self, rmod: ResoluteMod) -> ResoluteResult<()> { + pub fn store_mod(&self, rmod: ResoluteMod) -> Result<()> { let mod_name = rmod.to_string(); let rw = self.db.rw_transaction()?; @@ -80,7 +90,7 @@ impl ResoluteDatabase<'_> { } /// Removes a mod from the database - pub fn remove_mod(&self, rmod: ResoluteMod) -> ResoluteResult<()> { + pub fn remove_mod(&self, rmod: ResoluteMod) -> Result<()> { let mod_name = rmod.to_string(); // Remove the mod @@ -93,7 +103,7 @@ impl ResoluteDatabase<'_> { } /// Removes a mod from the database by its ID - pub fn remove_mod_by_id(&self, id: impl AsRef) -> ResoluteResult<()> { + pub fn remove_mod_by_id(&self, id: impl AsRef) -> Result<()> { // Find the item in the database let id = id.as_ref(); let read = self.db.r_transaction()?; @@ -108,10 +118,20 @@ impl ResoluteDatabase<'_> { } /// Models that a [`ResoluteDatabase`] interacts with +#[allow(clippy::absolute_paths)] pub static MODELS: Lazy = Lazy::new(|| { let mut models = Models::new(); + + // Current models models .define::() - .expect("Unable to define ResoluteMod model"); + .expect("Unable to define current ResoluteMod model"); + + // Legacy models + #[cfg(feature = "models_v1")] + models + .define::() + .expect("Unable to define v1 ResoluteMod model"); + models }); diff --git a/crates/resolute/src/discover.rs b/crates/resolute/src/discover.rs index 7c4f89c..3d2bd3b 100644 --- a/crates/resolute/src/discover.rs +++ b/crates/resolute/src/discover.rs @@ -10,7 +10,7 @@ use sha2::{Digest, Sha256}; use steamlocate::SteamDir; use crate::{ - mods::{ModVersion, ResoluteMod, ResoluteModMap}, + models::{ModVersion, ResoluteMod, ResoluteModMap}, Result, }; diff --git a/crates/resolute/src/error.rs b/crates/resolute/src/error.rs index 1b72322..4d0e310 100644 --- a/crates/resolute/src/error.rs +++ b/crates/resolute/src/error.rs @@ -9,7 +9,7 @@ use tokio::task; use crate::{ manager::artifacts::{ArtifactError, ArtifactErrorVec}, - mods::ResoluteMod, + models::ResoluteMod, }; /// Error returned from a Downloader diff --git a/crates/resolute/src/lib.rs b/crates/resolute/src/lib.rs index 28199b5..5c211f3 100644 --- a/crates/resolute/src/lib.rs +++ b/crates/resolute/src/lib.rs @@ -77,7 +77,7 @@ pub mod discover; mod error; pub mod manager; pub mod manifest; -pub mod mods; +pub mod models; pub use error::Error; pub use error::Result; diff --git a/crates/resolute/src/manager/delete.rs b/crates/resolute/src/manager/delete.rs index 4dd94d3..de08585 100644 --- a/crates/resolute/src/manager/delete.rs +++ b/crates/resolute/src/manager/delete.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use log::info; -use crate::mods::{ModArtifact, ModVersion}; +use crate::models::{ModArtifact, ModVersion}; use crate::Error; use super::artifacts::{self, ArtifactAction, ArtifactError, ArtifactErrorVec, MappableToArtifactError}; diff --git a/crates/resolute/src/manager/download.rs b/crates/resolute/src/manager/download.rs index 5738391..f24038c 100644 --- a/crates/resolute/src/manager/download.rs +++ b/crates/resolute/src/manager/download.rs @@ -13,7 +13,7 @@ use tokio::{ io::{AsyncWriteExt, BufWriter}, }; -use crate::mods::{ModArtifact, ModVersion}; +use crate::models::{ModArtifact, ModVersion}; use crate::{Error, Result}; use super::artifacts::{self, ArtifactAction, ArtifactError, MappableToArtifactError}; diff --git a/crates/resolute/src/manager/mod.rs b/crates/resolute/src/manager/mod.rs index d0883c9..bc99ef8 100644 --- a/crates/resolute/src/manager/mod.rs +++ b/crates/resolute/src/manager/mod.rs @@ -11,7 +11,7 @@ use tokio::task; #[cfg(feature = "db")] use crate::db::ResoluteDatabase; -use crate::mods::{self, ResoluteMod, ResoluteModMap}; +use crate::models::{self, ResoluteMod, ResoluteModMap}; use crate::{discover, manifest, Error, Result}; pub use self::delete::Deleter; @@ -93,7 +93,7 @@ impl_ModManager_with_without_db! { // Parse the JSON into raw manifest data, load that into a mod map let mut mods = task::spawn_blocking(move || -> Result { let data = manifest.parse(&json)?; - let mods = mods::load_manifest(data); + let mods = models::load_manifest(data); Ok(mods) }) .await??; @@ -113,7 +113,7 @@ impl_ModManager_with_without_db! { pub async fn mark_installed_mods(&self, mods: &mut ResoluteModMap) -> Result> { use std::{collections::HashSet, ffi::OsString}; - use crate::mods::{ModArtifact, ModVersion}; + use crate::models::{ModArtifact, ModVersion}; // Load the installed mods let LoadedMods { diff --git a/crates/resolute/src/manifest.rs b/crates/resolute/src/manifest.rs index 5c0223a..d0dab75 100644 --- a/crates/resolute/src/manifest.rs +++ b/crates/resolute/src/manifest.rs @@ -282,6 +282,7 @@ pub struct ManifestEntryVersion { pub conflicts: Option, #[serde(rename = "releaseUrl")] pub release_url: Option, + pub changelog: Option, } /// Represents a single "artifacts" entry in the manifest JSON diff --git a/crates/resolute/src/models/mod.rs b/crates/resolute/src/models/mod.rs new file mode 100644 index 0000000..08fb1ab --- /dev/null +++ b/crates/resolute/src/models/mod.rs @@ -0,0 +1,5 @@ +#[cfg(all(feature = "db", feature = "models_v1"))] +pub mod v1; +pub mod v2; + +pub use v2::*; diff --git a/crates/resolute/src/models/v1.rs b/crates/resolute/src/models/v1.rs new file mode 100644 index 0000000..09824a7 --- /dev/null +++ b/crates/resolute/src/models/v1.rs @@ -0,0 +1,154 @@ +#![allow(clippy::exhaustive_structs, clippy::absolute_paths)] + +use std::collections::HashMap; + +#[cfg(feature = "db")] +use native_db::{native_db, ToKey}; +#[cfg(feature = "db")] +use native_model::{native_model, Model}; +use semver::Version; +use serde::{Deserialize, Serialize}; +use url::Url; + +use super::{ModAuthor, ModDependencyMap}; + +/// First version of the [`super::ResoluteMod`] struct, kept around for database migration purposes +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "db", native_model(id = 1, version = 1))] +#[cfg_attr(feature = "db", native_db)] +pub struct ResoluteMod { + // The primary_key and secondary_key macros don't work with cfg_attr for whatever reason + #[cfg(feature = "db")] + #[primary_key] + pub id: String, + #[cfg(not(feature = "db"))] + pub id: String, + + // The primary_key and secondary_key macros don't work with cfg_attr for whatever reason + #[cfg(feature = "db")] + #[secondary_key] + pub name: String, + #[cfg(not(feature = "db"))] + pub name: String, + + pub description: String, + pub category: String, + pub authors: Vec, + #[serde(rename = "sourceLocation")] + pub source_location: Option, + pub website: Option, + pub tags: Option>, + pub flags: Option>, + pub platforms: Option>, + pub versions: HashMap, + #[serde(rename = "installedVersion")] + pub installed_version: Option, +} + +impl From for super::v2::ResoluteMod { + fn from(value: ResoluteMod) -> Self { + Self { + id: value.id, + name: value.name, + description: value.description, + category: value.category, + authors: value.authors, + source_location: value.source_location, + website: value.website, + tags: value.tags, + flags: value.flags, + platforms: value.platforms, + versions: value.versions.into_iter().map(|(svr, ver)| (svr, ver.into())).collect(), + active: value.installed_version.is_some(), + installed_version: value.installed_version, + } + } +} + +impl From for ResoluteMod { + fn from(value: super::v2::ResoluteMod) -> Self { + Self { + id: value.id, + name: value.name, + description: value.description, + category: value.category, + authors: value.authors, + source_location: value.source_location, + website: value.website, + tags: value.tags, + flags: value.flags, + platforms: value.platforms, + versions: value.versions.into_iter().map(|(svr, ver)| (svr, ver.into())).collect(), + installed_version: value.installed_version, + } + } +} + +/// First version of the [`super::ModVersion`] struct, kept around for database migration purposes +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModVersion { + pub semver: Version, + pub artifacts: Vec, + pub dependencies: ModDependencyMap, + pub conflicts: ModDependencyMap, + #[serde(rename = "releaseUrl")] + pub release_url: Option, +} + +impl From for super::v2::ModVersion { + fn from(value: ModVersion) -> Self { + Self { + semver: value.semver, + artifacts: value.artifacts.into_iter().map(Into::into).collect(), + dependencies: value.dependencies, + conflicts: value.conflicts, + release_url: value.release_url, + changelog: None, + } + } +} + +impl From for ModVersion { + fn from(value: super::v2::ModVersion) -> Self { + Self { + semver: value.semver, + artifacts: value.artifacts.into_iter().map(Into::into).collect(), + dependencies: value.dependencies, + conflicts: value.conflicts, + release_url: value.release_url, + } + } +} + +/// First version of the [`super::ModArtifact`] struct, kept around for database migration purposes +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModArtifact { + pub url: Url, + pub sha256: String, + pub filename: Option, + #[serde(rename = "installLocation")] + pub install_location: Option, +} + +impl From for super::v2::ModArtifact { + fn from(value: ModArtifact) -> Self { + Self { + url: value.url, + sha256: value.sha256, + filename: value.filename, + install_location: value.install_location, + override_filename: None, + } + } +} + +impl From for ModArtifact { + fn from(value: super::v2::ModArtifact) -> Self { + Self { + url: value.url, + sha256: value.sha256, + filename: value.filename, + install_location: value.install_location, + } + } +} diff --git a/crates/resolute/src/mods.rs b/crates/resolute/src/models/v2.rs similarity index 95% rename from crates/resolute/src/mods.rs rename to crates/resolute/src/models/v2.rs index 123bea3..9c8272c 100644 --- a/crates/resolute/src/mods.rs +++ b/crates/resolute/src/models/v2.rs @@ -5,17 +5,16 @@ use std::{ path::{Path, PathBuf}, }; +#[cfg(feature = "db")] +use native_db::{native_db, ToKey}; +#[cfg(feature = "db")] +use native_model::{native_model, Model}; use once_cell::sync::Lazy; use path_clean::PathClean; use semver::{BuildMetadata, Prerelease, Version, VersionReq}; use serde::{Deserialize, Serialize}; use url::Url; -#[cfg(feature = "db")] -use native_db::{native_db, ToKey}; -#[cfg(feature = "db")] -use native_model::{native_model, Model}; - use crate::{ manifest::{ ManifestAuthors, ManifestData, ManifestEntryArtifact, ManifestEntryDependencies, ManifestEntryVersions, @@ -71,6 +70,7 @@ pub fn load_manifest(manifest: ManifestData) -> ResoluteModMap { flags: entry.flags, platforms: entry.platforms, installed_version: None, + active: false, } }) }) @@ -101,6 +101,7 @@ fn build_mod_versions_map(versions: ManifestEntryVersions, category: &str) -> Ha conflicts: build_mod_version_dependencies(version.conflicts), artifacts: build_mod_version_artifacts(version.artifacts, category), release_url: version.release_url, + changelog: version.changelog, }) .map(|version| (version.semver.clone(), version)) .collect() @@ -131,7 +132,8 @@ pub type ResoluteModMap = HashMap; /// A single Resonite mod with all information relevant to it #[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "db", native_model(id = 1, version = 1))] +#[cfg_attr(all(feature = "db", feature = "models_v1"), native_model(id = 1, version = 2, from = super::v1::ResoluteMod))] +#[cfg_attr(all(feature = "db", not(feature = "models_v1")), native_model(id = 1, version = 2))] #[cfg_attr(feature = "db", native_db)] #[non_exhaustive] pub struct ResoluteMod { @@ -161,6 +163,7 @@ pub struct ResoluteMod { pub versions: HashMap, #[serde(rename = "installedVersion")] pub installed_version: Option, + pub active: bool, } impl ResoluteMod { @@ -217,6 +220,7 @@ impl ResoluteMod { platforms: None, versions, installed_version: Some(semver), + active: true, } } } @@ -266,6 +270,7 @@ pub struct ModVersion { pub conflicts: ModDependencyMap, #[serde(rename = "releaseUrl")] pub release_url: Option, + pub changelog: Option, } impl ModVersion { @@ -293,6 +298,7 @@ impl ModVersion { dependencies: ModDependencyMap::new(), conflicts: ModDependencyMap::new(), release_url: None, + changelog: None, } } @@ -304,6 +310,7 @@ impl ModVersion { dependencies: ModDependencyMap::new(), conflicts: ModDependencyMap::new(), release_url: None, + changelog: None, } } } @@ -323,6 +330,8 @@ pub struct ModArtifact { pub filename: Option, #[serde(rename = "installLocation")] pub install_location: Option, + #[serde(rename = "overrideFilename")] + pub override_filename: Option, } impl ModArtifact { @@ -419,11 +428,18 @@ impl ModArtifact { install_location }; + let (filename, override_filename) = if let Some(stripped) = filename.strip_suffix(".disabled") { + (stripped.to_owned(), Some(filename.to_owned())) + } else { + (filename.to_owned(), None) + }; + ModArtifact { url, sha256: sha256.to_owned(), - filename: Some(filename.to_owned()), + filename: Some(filename), install_location: Some(install_location), + override_filename, } } @@ -436,6 +452,7 @@ impl ModArtifact { "Plugins" => Some("/Libraries".to_owned()), _ => None, }), + override_filename: None, } } @@ -499,6 +516,7 @@ impl From for ModArtifact { sha256: value.sha256, filename: value.filename, install_location: value.install_location, + override_filename: None, } } } diff --git a/crates/tauri-app/src/commands/discover.rs b/crates/tauri-app/src/commands/discover.rs index dde8f1b..f63c48c 100644 --- a/crates/tauri-app/src/commands/discover.rs +++ b/crates/tauri-app/src/commands/discover.rs @@ -1,5 +1,5 @@ use log::{error, info}; -use resolute::{discover, manager::ModManager, mods::ResoluteModMap}; +use resolute::{discover, manager::ModManager, models::ResoluteModMap}; use tauri::{async_runtime, AppHandle, State}; use tokio::sync::Mutex; diff --git a/crates/tauri-app/src/commands/manager.rs b/crates/tauri-app/src/commands/manager.rs index f5c9adf..47132c3 100644 --- a/crates/tauri-app/src/commands/manager.rs +++ b/crates/tauri-app/src/commands/manager.rs @@ -1,7 +1,7 @@ use log::{error, info}; use resolute::{ manager::{LoadedMods, ModManager}, - mods::{ModVersion, ResoluteMod}, + models::{ModVersion, ResoluteMod}, }; use tauri::{AppHandle, State}; use tokio::sync::Mutex;