From b0c830119b74c9448b97289d62900014baf5e436 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Mon, 3 Apr 2023 16:08:53 -0700 Subject: [PATCH] Tag fetching and caching (#59) * basic framework. still has errors * added functionality for main endpoints + some structuring * formatting * unused code * mimicked CLI function with wait_for process * added basic auth bindings * made PR changes, added playground * cargo fmt * removed missed println * misc tests fixes * cargo fmt * added windows support * cargo fmt * all OS use dunce * restructured profile slightly; fixed mac bug * profile changes, new main.rs * fixed requested pr + canonicaliation bug * fixed regressed bug in ui * fixed regressed bugs * fixed git error * typo * ran prettier * clippy * playground clippy * ported profile loading fix * profile change for real, url println and clippy * PR changes * auth bindings + semisynch flow * fixed dropping task error * prettier, eslint, clippy * removed debugging modifications * removed unused function that eslinter missed :( * initial errored push * working draft * added tag system! * fixed merge issue --------- Co-authored-by: Wyatt --- theseus/src/api/mod.rs | 1 + theseus/src/api/tags.rs | 71 ++++++++ theseus/src/state/mod.rs | 15 ++ theseus/src/state/tags.rs | 253 ++++++++++++++++++++++++++ theseus_gui/src-tauri/src/api/mod.rs | 2 +- theseus_gui/src-tauri/src/api/tags.rs | 47 +++++ theseus_gui/src-tauri/src/main.rs | 7 + theseus_gui/src/helpers/tags.js | 41 +++++ theseus_playground/src/main.rs | 1 - 9 files changed, 436 insertions(+), 2 deletions(-) create mode 100644 theseus/src/api/tags.rs create mode 100644 theseus/src/state/tags.rs create mode 100644 theseus_gui/src-tauri/src/api/tags.rs create mode 100644 theseus_gui/src/helpers/tags.js diff --git a/theseus/src/api/mod.rs b/theseus/src/api/mod.rs index 6e983c8f7..cad1ffe74 100644 --- a/theseus/src/api/mod.rs +++ b/theseus/src/api/mod.rs @@ -2,6 +2,7 @@ pub mod auth; pub mod profile; pub mod profile_create; +pub mod tags; pub mod settings; pub mod data { diff --git a/theseus/src/api/tags.rs b/theseus/src/api/tags.rs new file mode 100644 index 000000000..595344984 --- /dev/null +++ b/theseus/src/api/tags.rs @@ -0,0 +1,71 @@ +//! Theseus tag management interface +pub use crate::{ + state::{ + Category, DonationPlatform, GameVersion, License, Loader, TagBundle, + }, + State, +}; + +// Get bundled set of tags +#[tracing::instrument] +pub async fn get_tag_bundle() -> crate::Result { + let state = State::get().await?; + let tags = state.tags.read().await; + + tags.get_tag_bundle() +} + +/// Get category tags +#[tracing::instrument] +pub async fn get_category_tags() -> crate::Result> { + let state = State::get().await?; + let tags = state.tags.read().await; + + tags.get_categories() +} + +/// Get report type tags +#[tracing::instrument] +pub async fn get_report_type_tags() -> crate::Result> { + let state = State::get().await?; + let tags = state.tags.read().await; + + tags.get_report_types() +} + +/// Get loader tags +#[tracing::instrument] +pub async fn get_loader_tags() -> crate::Result> { + let state = State::get().await?; + let tags = state.tags.read().await; + + tags.get_loaders() +} + +/// Get game version tags +#[tracing::instrument] +pub async fn get_game_version_tags() -> crate::Result> { + let state = State::get().await?; + let tags = state.tags.read().await; + + tags.get_game_versions() +} + +/// Get license tags +#[tracing::instrument] +pub async fn get_license_tags() -> crate::Result> { + let state = State::get().await?; + let tags = state.tags.read().await; + + tags.get_licenses() +} + +/// Get donation platform tags +#[tracing::instrument] +pub async fn get_donation_platform_tags() -> crate::Result> +{ + let state = State::get().await?; + let tags = state.tags.read().await; + + tags.get_donation_platforms() +} diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index 114b659a7..b14fa3537 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -28,6 +28,9 @@ pub use self::children::*; mod auth_task; pub use self::auth_task::*; +mod tags; +pub use self::tags::*; + // Global state static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); pub struct State { @@ -50,6 +53,8 @@ pub struct State { pub(crate) profiles: RwLock, /// Launcher user account info pub(crate) users: RwLock, + /// Launcher tags + pub(crate) tags: RwLock, } impl State { @@ -87,6 +92,15 @@ impl State { let auth_flow = AuthTask::new(); + // On launcher initialization, attempt a tag fetch after tags init + let mut tags = Tags::init(&database)?; + if let Err(tag_fetch_err) = tags.fetch_update().await { + tracing::error!( + "Failed to fetch tags on launcher init: {}", + tag_fetch_err + ); + }; + Ok(Arc::new(Self { database, directories, @@ -97,6 +111,7 @@ impl State { users: RwLock::new(users), children: RwLock::new(children), auth_flow: RwLock::new(auth_flow), + tags: RwLock::new(tags), })) } }) diff --git a/theseus/src/state/tags.rs b/theseus/src/state/tags.rs new file mode 100644 index 000000000..5643b387d --- /dev/null +++ b/theseus/src/state/tags.rs @@ -0,0 +1,253 @@ +use std::path::PathBuf; + +use bincode::{Decode, Encode}; +use serde::{Deserialize, Serialize}; + +use crate::config::{BINCODE_CONFIG, MODRINTH_API_URL, REQWEST_CLIENT}; + +const CATEGORIES_DB_TREE: &[u8] = b"categories"; +const LOADERS_DB_TREE: &[u8] = b"loaders"; +const GAME_VERSIONS_DB_TREE: &[u8] = b"game_versions"; +const LICENSES_DB_TREE: &[u8] = b"licenses"; +const DONATION_PLATFORMS_DB_TREE: &[u8] = b"donation_platforms"; +const REPORT_TYPES_DB_TREE: &[u8] = b"report_types"; + +#[derive(Clone)] +pub(crate) struct Tags(pub(crate) TagsInner); + +#[derive(Debug, Clone)] +pub struct TagsInner { + pub categories: sled::Tree, + pub loaders: sled::Tree, + pub game_versions: sled::Tree, + pub licenses: sled::Tree, + pub donation_platforms: sled::Tree, + pub report_types: sled::Tree, +} + +impl Tags { + #[tracing::instrument(skip(db))] + pub fn init(db: &sled::Db) -> crate::Result { + Ok(Tags(TagsInner { + categories: db.open_tree(CATEGORIES_DB_TREE)?, + loaders: db.open_tree(LOADERS_DB_TREE)?, + game_versions: db.open_tree(GAME_VERSIONS_DB_TREE)?, + licenses: db.open_tree(LICENSES_DB_TREE)?, + donation_platforms: db.open_tree(DONATION_PLATFORMS_DB_TREE)?, + report_types: db.open_tree(REPORT_TYPES_DB_TREE)?, + })) + } + + // Checks the database for categories tag, returns a Vec::new() if it doesnt exist, otherwise returns the categories + #[tracing::instrument(skip(self))] + pub fn get_categories(&self) -> crate::Result> { + self.0.categories.get("categories")?.map_or( + Ok(Vec::new()), + |categories| { + bincode::decode_from_slice(&categories, *BINCODE_CONFIG) + .map_err(crate::Error::from) + .map(|it| it.0) + }, + ) + } + + // Checks the database for loaders tag, returns a Vec::new() if it doesnt exist, otherwise returns the loaders + #[tracing::instrument(skip(self))] + pub fn get_loaders(&self) -> crate::Result> { + self.0 + .loaders + .get("loaders")? + .map_or(Ok(Vec::new()), |loaders| { + bincode::decode_from_slice(&loaders, *BINCODE_CONFIG) + .map_err(crate::Error::from) + .map(|it| it.0) + }) + } + + // Checks the database for game_versions tag, returns a Vec::new() if it doesnt exist, otherwise returns the game_versions + #[tracing::instrument(skip(self))] + pub fn get_game_versions(&self) -> crate::Result> { + self.0.game_versions.get("game_versions")?.map_or( + Ok(Vec::new()), + |game_versions| { + bincode::decode_from_slice(&game_versions, *BINCODE_CONFIG) + .map_err(crate::Error::from) + .map(|it| it.0) + }, + ) + } + + // Checks the database for licenses tag, returns a Vec::new() if it doesnt exist, otherwise returns the licenses + #[tracing::instrument(skip(self))] + pub fn get_licenses(&self) -> crate::Result> { + self.0 + .licenses + .get("licenses")? + .map_or(Ok(Vec::new()), |licenses| { + bincode::decode_from_slice(&licenses, *BINCODE_CONFIG) + .map_err(crate::Error::from) + .map(|it| it.0) + }) + } + + // Checks the database for donation_platforms tag, returns a Vec::new() if it doesnt exist, otherwise returns the donation_platforms + #[tracing::instrument(skip(self))] + pub fn get_donation_platforms( + &self, + ) -> crate::Result> { + self.0.donation_platforms.get("donation_platforms")?.map_or( + Ok(Vec::new()), + |donation_platforms| { + bincode::decode_from_slice(&donation_platforms, *BINCODE_CONFIG) + .map_err(crate::Error::from) + .map(|it| it.0) + }, + ) + } + + // Checks the database for report_types tag, returns a Vec::new() if it doesnt exist, otherwise returns the report_types + #[tracing::instrument(skip(self))] + pub fn get_report_types(&self) -> crate::Result> { + self.0.report_types.get("report_types")?.map_or( + Ok(Vec::new()), + |report_types| { + bincode::decode_from_slice(&report_types, *BINCODE_CONFIG) + .map_err(crate::Error::from) + .map(|it| it.0) + }, + ) + } + + // Gets all tags together as a serializable bundle + #[tracing::instrument(skip(self))] + pub fn get_tag_bundle(&self) -> crate::Result { + Ok(TagBundle { + categories: self.get_categories()?, + loaders: self.get_loaders()?, + game_versions: self.get_game_versions()?, + licenses: self.get_licenses()?, + donation_platforms: self.get_donation_platforms()?, + report_types: self.get_report_types()?, + }) + } + + // Fetches the tags from the Modrinth API and stores them in the database + #[tracing::instrument(skip(self))] + pub async fn fetch_update(&mut self) -> crate::Result<()> { + let categories = self.fetch_tag::("category"); + let loaders = self.fetch_tag::("loader"); + let game_versions = self.fetch_tag::("game_version"); + let licenses = self.fetch_tag::("license"); + let donation_platforms = + self.fetch_tag::("donation_platform"); + let report_types = self.fetch_tag::("report_type"); + + let ( + categories, + loaders, + game_versions, + licenses, + donation_platforms, + report_types, + ) = futures::join!( + categories, + loaders, + game_versions, + licenses, + donation_platforms, + report_types + ); + + // Store the tags in the database + self.0.categories.insert( + "categories", + bincode::encode_to_vec(categories?, *BINCODE_CONFIG)?, + )?; + self.0.loaders.insert( + "loaders", + bincode::encode_to_vec(loaders?, *BINCODE_CONFIG)?, + )?; + self.0.game_versions.insert( + "game_versions", + bincode::encode_to_vec(game_versions?, *BINCODE_CONFIG)?, + )?; + self.0.licenses.insert( + "licenses", + bincode::encode_to_vec(licenses?, *BINCODE_CONFIG)?, + )?; + self.0.donation_platforms.insert( + "donation_platforms", + bincode::encode_to_vec(donation_platforms?, *BINCODE_CONFIG)?, + )?; + self.0.report_types.insert( + "report_types", + bincode::encode_to_vec(report_types?, *BINCODE_CONFIG)?, + )?; + + Ok(()) + } + + #[tracing::instrument(skip(self))] + pub async fn fetch_tag( + &self, + tag_type: &str, + ) -> Result, reqwest::Error> + where + T: serde::de::DeserializeOwned, + { + let url = &format!("{MODRINTH_API_URL}tag/{}", tag_type); + let content = REQWEST_CLIENT + .get(url) + .send() + .await? + .json::>() + .await?; + Ok(content) + } +} + +// Serializeable struct for all tags to be fetched together by the frontend +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TagBundle { + pub categories: Vec, + pub loaders: Vec, + pub game_versions: Vec, + pub licenses: Vec, + pub donation_platforms: Vec, + pub report_types: Vec, +} + +#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)] +pub struct Category { + pub name: String, + pub project_type: String, + pub header: String, + pub icon: PathBuf, +} + +#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)] +pub struct Loader { + pub name: String, + pub icon: PathBuf, + pub supported_project_types: Vec, +} + +#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)] +pub struct GameVersion { + pub version: String, + pub version_type: String, + pub date: String, + pub major: bool, +} + +#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)] +pub struct License { + pub short: String, + pub name: String, +} + +#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)] +pub struct DonationPlatform { + pub short: String, + pub name: String, +} diff --git a/theseus_gui/src-tauri/src/api/mod.rs b/theseus_gui/src-tauri/src/api/mod.rs index 422cb5b7e..6e26cc51e 100644 --- a/theseus_gui/src-tauri/src/api/mod.rs +++ b/theseus_gui/src-tauri/src/api/mod.rs @@ -3,9 +3,9 @@ use serde::{Serialize, Serializer}; use thiserror::Error; pub mod auth; - pub mod profile; pub mod profile_create; +pub mod tags; pub mod settings; pub type Result = std::result::Result; diff --git a/theseus_gui/src-tauri/src/api/tags.rs b/theseus_gui/src-tauri/src/api/tags.rs new file mode 100644 index 000000000..e33b53aef --- /dev/null +++ b/theseus_gui/src-tauri/src/api/tags.rs @@ -0,0 +1,47 @@ +use crate::api::Result; +use theseus::tags::{ + Category, DonationPlatform, GameVersion, License, Loader, TagBundle, +}; + +/// Gets cached category tags from the database +#[tauri::command] +pub async fn tags_get_category_tags() -> Result> { + Ok(theseus::tags::get_category_tags().await?) +} + +/// Gets cached report type tags from the database +#[tauri::command] +pub async fn tags_get_report_type_tags() -> Result> { + Ok(theseus::tags::get_report_type_tags().await?) +} + +/// Gets cached loader tags from the database +#[tauri::command] +pub async fn tags_get_loader_tags() -> Result> { + Ok(theseus::tags::get_loader_tags().await?) +} + +/// Gets cached game version tags from the database +#[tauri::command] +pub async fn tags_get_game_version_tags() -> Result> { + Ok(theseus::tags::get_game_version_tags().await?) +} + +/// Gets cached license tags from the database +#[tauri::command] +pub async fn tags_get_license_tags() -> Result> { + Ok(theseus::tags::get_license_tags().await?) +} + +/// Gets cached donation platform tags from the database +#[tauri::command] +pub async fn tags_get_donation_platform_tags() -> Result> +{ + Ok(theseus::tags::get_donation_platform_tags().await?) +} + +/// Gets cached tag bundle from the database +#[tauri::command] +pub async fn tags_get_tag_bundle() -> Result { + Ok(theseus::tags::get_tag_bundle().await?) +} diff --git a/theseus_gui/src-tauri/src/main.rs b/theseus_gui/src-tauri/src/main.rs index 0a00734ab..14c24a8a5 100644 --- a/theseus_gui/src-tauri/src/main.rs +++ b/theseus_gui/src-tauri/src/main.rs @@ -38,6 +38,13 @@ fn main() { api::auth::auth_has_user, api::auth::auth_users, api::auth::auth_get_user, + api::tags::tags_get_category_tags, + api::tags::tags_get_donation_platform_tags, + api::tags::tags_get_game_version_tags, + api::tags::tags_get_loader_tags, + api::tags::tags_get_license_tags, + api::tags::tags_get_report_type_tags, + api::tags::tags_get_tag_bundle, api::settings::settings_get, api::settings::settings_set, ]) diff --git a/theseus_gui/src/helpers/tags.js b/theseus_gui/src/helpers/tags.js new file mode 100644 index 000000000..6bdd36e82 --- /dev/null +++ b/theseus_gui/src/helpers/tags.js @@ -0,0 +1,41 @@ +/** + * All theseus API calls return serialized values (both return values and errors); + * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, + * and deserialized into a usable JS object. + */ +import { invoke } from '@tauri-apps/api/tauri' + +// Gets tag bundle of all tags +export async function get_tag_bundle() { + return await invoke('tags_get_tag_bundle') +} + +// Gets cached category tags +export async function get_categories() { + return await invoke('tags_get_categories') +} + +// Gets cached loaders tags +export async function get_loaders() { + return await invoke('tags_get_loaders') +} + +// Gets cached game_versions tags +export async function get_game_versions() { + return await invoke('tags_get_game_versions') +} + +// Gets cached licenses tags +export async function get_licenses() { + return await invoke('tags_get_licenses') +} + +// Gets cached donation_platforms tags +export async function get_donation_platforms() { + return await invoke('tags_get_donation_platforms') +} + +// Gets cached licenses tags +export async function get_report_types() { + return await invoke('tags_get_report_types') +} diff --git a/theseus_playground/src/main.rs b/theseus_playground/src/main.rs index d9c864741..f479cb496 100644 --- a/theseus_playground/src/main.rs +++ b/theseus_playground/src/main.rs @@ -117,7 +117,6 @@ async fn main() -> theseus::Result<()> { profile::run(&canonicalize(&profile_path)?, credentials).await } }?; - // Spawn a thread and hold the lock to the process until it ends println!("Started Minecraft. Waiting for process to end..."); let mut proc: RwLockWriteGuard = proc_lock.write().await;