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 <wyatt@modrinth.com>
This commit is contained in:
Wyatt Verchere
2023-04-03 16:08:53 -07:00
committed by GitHub
parent a13b7a2566
commit b0c830119b
9 changed files with 436 additions and 2 deletions

View File

@@ -2,6 +2,7 @@
pub mod auth;
pub mod profile;
pub mod profile_create;
pub mod tags;
pub mod settings;
pub mod data {

71
theseus/src/api/tags.rs Normal file
View File

@@ -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<TagBundle> {
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<Vec<Category>> {
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<Vec<String>> {
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<Vec<Loader>> {
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<Vec<GameVersion>> {
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<Vec<License>> {
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<Vec<DonationPlatform>>
{
let state = State::get().await?;
let tags = state.tags.read().await;
tags.get_donation_platforms()
}

View File

@@ -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<Arc<State>> = OnceCell::const_new();
pub struct State {
@@ -50,6 +53,8 @@ pub struct State {
pub(crate) profiles: RwLock<Profiles>,
/// Launcher user account info
pub(crate) users: RwLock<Users>,
/// Launcher tags
pub(crate) tags: RwLock<Tags>,
}
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),
}))
}
})

253
theseus/src/state/tags.rs Normal file
View File

@@ -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<Self> {
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<Vec<Category>> {
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<Vec<Loader>> {
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<Vec<GameVersion>> {
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<Vec<License>> {
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<Vec<DonationPlatform>> {
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<Vec<String>> {
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<TagBundle> {
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>("category");
let loaders = self.fetch_tag::<Loader>("loader");
let game_versions = self.fetch_tag::<GameVersion>("game_version");
let licenses = self.fetch_tag::<License>("license");
let donation_platforms =
self.fetch_tag::<DonationPlatform>("donation_platform");
let report_types = self.fetch_tag::<String>("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<T>(
&self,
tag_type: &str,
) -> Result<Vec<T>, 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::<Vec<T>>()
.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<Category>,
pub loaders: Vec<Loader>,
pub game_versions: Vec<GameVersion>,
pub licenses: Vec<License>,
pub donation_platforms: Vec<DonationPlatform>,
pub report_types: Vec<String>,
}
#[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<String>,
}
#[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,
}

View File

@@ -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<T> = std::result::Result<T, TheseusGuiError>;

View File

@@ -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<Vec<Category>> {
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<Vec<String>> {
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<Vec<Loader>> {
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<Vec<GameVersion>> {
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<Vec<License>> {
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<Vec<DonationPlatform>>
{
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<TagBundle> {
Ok(theseus::tags::get_tag_bundle().await?)
}

View File

@@ -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,
])

View File

@@ -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')
}

View File

@@ -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<Child> = proc_lock.write().await;