Updating + Profile Repairs + Performance Improvements (#97)

* repairing

* Main framework for updating

* add jsconfig

* more work

* Improve performance

* Finish updating

* run lint
This commit is contained in:
Geometrically
2023-04-26 10:28:08 -07:00
committed by GitHub
parent c53104c28e
commit f0b8a708a3
48 changed files with 1217 additions and 894 deletions

View File

@@ -134,6 +134,11 @@ impl DirectoryInfo {
self.config_dir.join("caches")
}
#[inline]
pub fn caches_meta_dir(&self) -> PathBuf {
self.config_dir.join("caches").join("metadata")
}
/// Get path from environment variable
#[inline]
fn env_path(name: &str) -> Option<PathBuf> {

View File

@@ -1,21 +1,18 @@
//! Theseus metadata
use crate::config::BINCODE_CONFIG;
use bincode::{Decode, Encode};
use crate::data::DirectoryInfo;
use crate::util::fetch::{read_json, write};
use daedalus::{
minecraft::{fetch_version_manifest, VersionManifest as MinecraftManifest},
modded::{
fetch_manifest as fetch_loader_manifest, Manifest as LoaderManifest,
},
};
use futures::prelude::*;
use std::collections::LinkedList;
use serde::{Deserialize, Serialize};
use tokio::sync::{RwLock, Semaphore};
const METADATA_URL: &str = "https://meta.modrinth.com";
const METADATA_DB_FIELD: &[u8] = b"metadata";
const RETRY_ATTEMPTS: i32 = 3;
// TODO: store as subtree in database
#[derive(Encode, Decode, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Metadata {
pub minecraft: MinecraftManifest,
pub forge: LoaderManifest,
@@ -27,7 +24,7 @@ impl Metadata {
format!("{METADATA_URL}/{name}/v0/manifest.json")
}
async fn fetch() -> crate::Result<Self> {
pub async fn fetch() -> crate::Result<Self> {
let (minecraft, forge, fabric) = tokio::try_join! {
async {
let url = Self::get_manifest("minecraft");
@@ -51,41 +48,42 @@ impl Metadata {
}
// Attempt to fetch metadata and store in sled DB
#[tracing::instrument(skip_all)]
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
pub async fn init(
dirs: &DirectoryInfo,
io_semaphore: &RwLock<Semaphore>,
) -> crate::Result<Self> {
let mut metadata = None;
let metadata_path = dirs.caches_meta_dir().join("metadata.json");
if let Some(ref meta_bin) = db.get(METADATA_DB_FIELD)? {
match bincode::decode_from_slice::<Self, _>(
meta_bin,
*BINCODE_CONFIG,
) {
Ok((meta, _)) => metadata = Some(meta),
if let Ok(metadata_json) =
read_json::<Metadata>(&metadata_path, io_semaphore).await
{
metadata = Some(metadata_json);
} else {
let res = async {
let metadata_fetch = Self::fetch().await?;
write(
&metadata_path,
&serde_json::to_vec(&metadata_fetch).unwrap_or_default(),
io_semaphore,
)
.await?;
metadata = Some(metadata_fetch);
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
log::warn!("Could not read launcher metadata: {err}")
log::warn!("Unable to fetch launcher metadata: {err}")
}
}
}
let mut fetch_futures = LinkedList::new();
for _ in 0..RETRY_ATTEMPTS {
fetch_futures.push_back(Self::fetch().boxed());
}
match future::select_ok(fetch_futures).await {
Ok(meta) => metadata = Some(meta.0),
Err(err) => log::warn!("Unable to fetch launcher metadata: {err}"),
}
if let Some(meta) = metadata {
db.insert(
METADATA_DB_FIELD,
sled::IVec::from(bincode::encode_to_vec(
&meta,
*BINCODE_CONFIG,
)?),
)?;
db.flush_async().await?;
Ok(meta)
} else {
Err(
@@ -94,4 +92,35 @@ impl Metadata {
)
}
}
pub async fn update() {
let res = async {
let metadata_fetch = Metadata::fetch().await?;
let state = crate::State::get().await?;
let metadata_path =
state.directories.caches_meta_dir().join("metadata.json");
write(
&metadata_path,
&serde_json::to_vec(&metadata_fetch)?,
&state.io_semaphore,
)
.await
.unwrap();
let mut old_metadata = state.metadata.write().await;
*old_metadata = metadata_fetch;
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
log::warn!("Unable to update launcher metadata: {err}")
}
};
}
}

View File

@@ -1,12 +1,11 @@
//! Theseus state management system
use crate::config::sled_config;
use crate::event::emit::emit_loading;
use crate::event::emit::init_loading;
use crate::event::LoadingBarType;
use crate::jre;
use crate::loading_join;
use crate::state::users::Users;
use std::sync::Arc;
use tokio::sync::{OnceCell, RwLock, Semaphore};
@@ -27,7 +26,6 @@ mod projects;
pub use self::projects::*;
mod users;
pub use self::users::*;
mod children;
pub use self::children::*;
@@ -51,8 +49,7 @@ pub struct State {
/// Stored maximum number of sempahores of current io_semaphore
pub io_semaphore_max: RwLock<u32>,
/// Launcher metadata
pub metadata: Metadata,
// TODO: settings API
pub metadata: RwLock<Metadata>,
/// Launcher configuration
pub settings: RwLock<Settings>,
/// Reference to minecraft process children
@@ -68,76 +65,55 @@ pub struct State {
}
impl State {
#[tracing::instrument]
/// Get the current launcher state, initializing it if needed
pub async fn get() -> crate::Result<Arc<Self>> {
LAUNCHER_STATE
.get_or_try_init(|| {
async {
let loading_bar = init_loading(
LoadingBarType::StateInit,
100.0,
"Initializing launcher...",
)
.await?;
let loading_bar = init_loading(LoadingBarType::StateInit, 100.0, "Initializing launcher...").await?;
// Directories
let directories = DirectoryInfo::init().await?;
// Database
// TODO: make database versioned
let database = sled_config()
.path(directories.database_file())
.open()?;
emit_loading(&loading_bar, 10.0, None).await?;
// Settings
let mut settings =
let settings =
Settings::init(&directories.settings_file()).await?;
let io_semaphore = RwLock::new(Semaphore::new(
settings.max_concurrent_downloads,
));
emit_loading(&loading_bar, 10.0, None).await?;
// Loose initializations
let io_semaphore_max = settings.max_concurrent_downloads;
let io_semaphore =
RwLock::new(Semaphore::new(io_semaphore_max));
let metadata_fut = Metadata::init(&database);
let metadata_fut =
Metadata::init(&directories, &io_semaphore);
let profiles_fut =
Profiles::init(&directories, &io_semaphore);
let tags_fut = Tags::init(&directories, &io_semaphore);
let users_fut = Users::init(&directories, &io_semaphore);
// Launcher data
let (metadata, profiles) = loading_join! {
Some(&loading_bar), 20.0, Some("Initializing metadata and profiles...");
metadata_fut, profiles_fut
let (metadata, profiles, tags, users) = loading_join! {
Some(&loading_bar), 70.0, Some("Initializing...");
metadata_fut,
profiles_fut,
tags_fut,
users_fut,
}?;
emit_loading(&loading_bar, 10.0, None).await?;
let users = Users::init(&database)?;
let children = Children::new();
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(&io_semaphore,Some(&loading_bar)).await
{
tracing::error!(
"Failed to fetch tags on launcher init: {}",
tag_fetch_err
);
};
emit_loading(&loading_bar, 10.0, None).await?;
// On launcher initialization, if global java variables are unset, try to find and set them
// (they are required for the game to launch)
if settings.java_globals.count() == 0 {
settings.java_globals = jre::autodetect_java_globals().await?;
}
Ok(Arc::new(Self {
directories,
io_semaphore,
io_semaphore_max: RwLock::new(io_semaphore_max as u32),
metadata,
io_semaphore_max: RwLock::new(
settings.max_concurrent_downloads as u32,
),
metadata: RwLock::new(metadata),
settings: RwLock::new(settings),
profiles: RwLock::new(profiles),
users: RwLock::new(users),
@@ -151,6 +127,14 @@ impl State {
.map(Arc::clone)
}
/// Updates state with data from the web
pub fn update() {
tokio::task::spawn(Metadata::update());
tokio::task::spawn(Tags::update());
tokio::task::spawn(Profiles::update_projects());
tokio::task::spawn(Settings::update_java());
}
#[tracing::instrument]
/// Synchronize in-memory state with persistent state
pub async fn sync() -> crate::Result<()> {

View File

@@ -1,10 +1,8 @@
use super::settings::{Hooks, MemorySettings, WindowSize};
use crate::config::MODRINTH_API_URL;
use crate::data::DirectoryInfo;
use crate::event::emit::{
emit_profile, init_loading, loading_try_for_each_concurrent,
};
use crate::event::{LoadingBarType, ProfilePayloadType};
use crate::event::emit::emit_profile;
use crate::event::ProfilePayloadType;
use crate::state::projects::Project;
use crate::state::{ModrinthVersion, ProjectType};
use crate::util::fetch::{fetch, fetch_json, write, write_cached_icon};
@@ -34,6 +32,8 @@ pub const CURRENT_FORMAT_VERSION: u32 = 1;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Profile {
pub uuid: Uuid, // todo: will be used in restructure to refer to profiles
#[serde(default)]
pub installed: bool,
pub path: PathBuf,
pub metadata: ProfileMetadata,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -58,10 +58,15 @@ pub struct ProfileMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub loader_version: Option<LoaderVersion>,
pub format_version: u32,
pub linked_project_id: Option<String>,
pub linked_data: Option<LinkedData>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct LinkedData {
pub project_id: Option<String>,
pub version_id: Option<String>,
}
// TODO: Quilt?
#[derive(
Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize, Default,
)]
@@ -85,6 +90,17 @@ impl std::fmt::Display for ModLoader {
}
}
impl ModLoader {
pub(crate) fn as_api_str(&self) -> &'static str {
match *self {
Self::Vanilla => "vanilla",
Self::Forge => "forge",
Self::Fabric => "fabric",
Self::Quilt => "quilt",
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct JavaSettings {
#[serde(skip_serializing_if = "Option::is_none")]
@@ -110,6 +126,7 @@ impl Profile {
Ok(Self {
uuid,
installed: false,
path: canonicalize(path)?,
metadata: ProfileMetadata {
name,
@@ -118,7 +135,7 @@ impl Profile {
loader: ModLoader::Vanilla,
loader_version: None,
format_version: CURRENT_FORMAT_VERSION,
linked_project_id: None,
linked_data: None,
},
projects: HashMap::new(),
java: None,
@@ -147,7 +164,7 @@ impl Profile {
let paths = self.get_profile_project_paths()?;
let projects = crate::state::infer_data_from_files(
paths,
&[(self.clone(), paths)],
state.directories.caches_dir(),
&state.io_semaphore,
)
@@ -155,6 +172,14 @@ impl Profile {
self.projects = projects;
emit_profile(
self.uuid,
self.path.clone(),
&self.metadata.name,
ProfilePayloadType::Synced,
)
.await?;
Ok(())
}
@@ -266,8 +291,6 @@ impl Profile {
let path = self.path.join(project_type.get_folder()).join(file_name);
write(&path, &bytes, &state.io_semaphore).await?;
self.sync().await?;
Ok(path)
}
@@ -345,36 +368,62 @@ impl Profiles {
}
}
// project path, parent profile path
let mut files: HashMap<PathBuf, PathBuf> = HashMap::new();
{
for (profile_path, profile) in profiles.iter() {
let paths = profile.get_profile_project_paths()?;
for path in paths {
files.insert(path, profile_path.clone());
}
}
}
let inferred = super::projects::infer_data_from_files(
files.keys().cloned().collect(),
dirs.caches_dir(),
io_sempahore,
)
.await?;
for (key, value) in inferred {
if let Some(profile_path) = files.get(&key) {
if let Some(profile) = profiles.get_mut(profile_path) {
profile.projects.insert(key, value);
}
}
}
Ok(Self(profiles))
}
pub async fn update_projects() {
let res = async {
let state = State::get().await?;
// profile, child paths
let mut files: Vec<(Profile, Vec<PathBuf>)> = Vec::new();
{
let profiles = state.profiles.read().await;
for (_profile_path, profile) in profiles.0.iter() {
let paths = profile.get_profile_project_paths()?;
files.push((profile.clone(), paths));
}
}
if !files.is_empty() {
let inferred = super::projects::infer_data_from_files(
&files,
state.directories.caches_dir(),
&state.io_semaphore,
)
.await?;
let mut wipe_profiles = Vec::new();
for (key, value) in inferred {
if let Some((profile, _)) =
files.iter().find(|(_, files)| files.contains(&key))
{
let mut new_profiles = state.profiles.write().await;
if let Some(profile) =
new_profiles.0.get_mut(&profile.path)
{
if !wipe_profiles.contains(&profile.path) {
profile.projects = HashMap::new();
wipe_profiles.push(profile.path.clone());
}
profile.projects.insert(key, value);
}
}
}
}
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
log::warn!("Unable to fetch profile projects: {err}")
}
};
}
#[tracing::instrument(skip(self))]
pub async fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
emit_profile(
@@ -406,35 +455,26 @@ impl Profiles {
}
#[tracing::instrument(skip(self))]
pub async fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
pub async fn remove(
&mut self,
path: &Path,
) -> crate::Result<Option<Profile>> {
let path =
PathBuf::from(&canonicalize(path)?.to_string_lossy().to_string());
self.0.remove(&path);
let profile = self.0.remove(&path);
if path.exists() {
fs::remove_dir_all(path).await?;
}
Ok(self)
Ok(profile)
}
#[tracing::instrument(skip_all)]
pub async fn sync(&self) -> crate::Result<&Self> {
let loading_bar = init_loading(
LoadingBarType::ProfileSync,
100.0,
"Syncing profiles...",
)
.await?;
let num_futs = self.0.len();
loading_try_for_each_concurrent(
stream::iter(self.0.iter()).map(Ok::<_, crate::Error>),
None,
Some(&loading_bar),
100.0,
num_futs,
None,
|(path, profile)| async move {
stream::iter(self.0.iter())
.map(Ok::<_, crate::Error>)
.try_for_each_concurrent(None, |(path, profile)| async move {
let json = serde_json::to_vec(&profile)?;
let json_path = Path::new(&path.to_string_lossy().to_string())
@@ -442,9 +482,8 @@ impl Profiles {
fs::write(json_path, json).await?;
Ok::<_, crate::Error>(())
},
)
.await?;
})
.await?;
Ok(self)
}

View File

@@ -1,6 +1,8 @@
//! Project management + inference
use crate::config::MODRINTH_API_URL;
use crate::data::ModLoader;
use crate::state::Profile;
use crate::util::fetch::{fetch_json, write_cached_icon};
use async_zip::tokio::read::fs::ZipFileReader;
use chrono::{DateTime, Utc};
@@ -185,6 +187,8 @@ pub enum ProjectMetadata {
project: Box<ModrinthProject>,
version: Box<ModrinthVersion>,
members: Vec<ModrinthTeamMember>,
update_version: Option<Box<ModrinthVersion>>,
incompatible: bool,
},
Inferred {
title: Option<String>,
@@ -246,21 +250,23 @@ async fn read_icon_from_file(
}
pub async fn infer_data_from_files(
paths: Vec<PathBuf>,
paths: &[(Profile, Vec<PathBuf>)],
cache_dir: PathBuf,
io_semaphore: &RwLock<Semaphore>,
) -> crate::Result<HashMap<PathBuf, Project>> {
let mut file_path_hashes = HashMap::new();
// TODO: Make this concurrent and use progressive hashing to avoid loading each JAR in memory
for path in paths.clone() {
let mut file = tokio::fs::File::open(path.clone()).await?;
for set in paths {
for path in &set.1 {
let mut file = tokio::fs::File::open(path.clone()).await?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await?;
let hash = format!("{:x}", sha2::Sha512::digest(&buffer));
file_path_hashes.insert(hash, path.clone());
let hash = format!("{:x}", sha2::Sha512::digest(&buffer));
file_path_hashes.insert(hash, path.clone());
}
}
let files: HashMap<String, ModrinthVersion> = fetch_json(
@@ -291,7 +297,6 @@ pub async fn infer_data_from_files(
io_semaphore,
)
.await?;
let teams: Vec<ModrinthTeamMember> = fetch_json::<
Vec<Vec<ModrinthTeamMember>>,
>(
@@ -312,6 +317,26 @@ pub async fn infer_data_from_files(
.flatten()
.collect();
let mut update_versions: Vec<ModrinthVersion> = fetch_json(
Method::GET,
&format!(
"{}versions?ids={}",
MODRINTH_API_URL,
serde_json::to_string(
&projects
.iter()
.flat_map(|x| x.versions.clone())
.collect::<Vec<String>>()
)?
),
None,
None,
io_semaphore,
)
.await?;
update_versions.sort_by(|a, b| b.date_published.cmp(&a.date_published));
let mut return_projects = HashMap::new();
let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new();
@@ -320,18 +345,14 @@ pub async fn infer_data_from_files(
if let Some(project) =
projects.iter().find(|x| version.project_id == x.id)
{
let profile = paths.iter().find(|x| x.1.contains(&path));
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let team_members = teams
.iter()
.filter(|x| x.team_id == project.team)
.cloned()
.collect::<Vec<_>>();
return_projects.insert(
path,
Project {
@@ -340,7 +361,52 @@ pub async fn infer_data_from_files(
metadata: ProjectMetadata::Modrinth {
project: Box::new(project.clone()),
version: Box::new(version.clone()),
members: team_members,
members: teams
.iter()
.filter(|x| x.team_id == project.team)
.cloned()
.collect::<Vec<_>>(),
update_version: if let Some((profile, _)) = &profile
{
update_versions
.iter()
.find(|x| {
x.project_id == project.id
&& x.game_versions.contains(
&profile.metadata.game_version,
)
&& if profile.metadata.loader
== ModLoader::Vanilla
{
true
} else {
x.loaders.contains(
&profile
.metadata
.loader
.as_api_str()
.to_string(),
)
}
})
.cloned()
.map(Box::new)
} else {
None
},
incompatible: if let Some((profile, _)) = &profile {
!version.loaders.contains(
&profile
.metadata
.loader
.as_api_str()
.to_string(),
) || version
.game_versions
.contains(&profile.metadata.game_version)
} else {
false
},
},
file_name,
},

View File

@@ -1,4 +1,5 @@
//! Theseus settings file
use crate::{jre, State};
use serde::{Deserialize, Serialize};
use std::path::Path;
use tokio::fs;
@@ -63,6 +64,29 @@ impl Settings {
}
}
pub async fn update_java() {
let res = async {
let state = State::get().await?;
let settings_read = state.settings.write().await;
if settings_read.java_globals.count() == 0 {
drop(settings_read);
let java_globals = jre::autodetect_java_globals().await?;
state.settings.write().await.java_globals = java_globals;
}
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
log::warn!("Unable to update launcher java: {err}")
}
};
}
#[tracing::instrument(skip(self))]
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
fs::write(to, serde_json::to_vec(self)?)

View File

@@ -1,152 +1,125 @@
use std::path::PathBuf;
use bincode::{Decode, Encode};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use tokio::sync::{RwLock, Semaphore};
use crate::config::{BINCODE_CONFIG, MODRINTH_API_URL};
use crate::event::LoadingBarId;
use crate::loading_join;
use crate::util::fetch::fetch_json;
use crate::config::MODRINTH_API_URL;
use crate::data::DirectoryInfo;
use crate::util::fetch::{fetch_json, read_json, write};
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,
// Serializeable struct for all tags to be fetched together by the frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tags {
pub categories: Vec<Category>,
pub loaders: Vec<Loader>,
pub game_versions: Vec<GameVersion>,
pub donation_platforms: Vec<DonationPlatform>,
pub report_types: Vec<String>,
}
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)?,
}))
pub async fn init(
dirs: &DirectoryInfo,
io_semaphore: &RwLock<Semaphore>,
) -> crate::Result<Self> {
let mut tags = None;
let tags_path = dirs.caches_meta_dir().join("tags.json");
if let Ok(tags_json) = read_json::<Self>(&tags_path, io_semaphore).await
{
tags = Some(tags_json);
} else {
match Self::fetch(io_semaphore).await {
Ok(tags_fetch) => tags = Some(tags_fetch),
Err(err) => {
log::warn!("Unable to fetch launcher tags: {err}")
}
}
}
if let Some(tags_data) = tags {
write(&tags_path, &serde_json::to_vec(&tags_data)?, io_semaphore)
.await?;
Ok(tags_data)
} else {
Err(crate::ErrorKind::NoValueFor(String::from("launcher tags"))
.as_error())
}
}
pub async fn update() {
let res = async {
let state = crate::State::get().await?;
let tags_fetch = Tags::fetch(&state.io_semaphore).await?;
let tags_path =
state.directories.caches_meta_dir().join("tags.json");
write(
&tags_path,
&serde_json::to_vec(&tags_fetch)?,
&state.io_semaphore,
)
.await
.unwrap();
let mut old_tags = state.tags.write().await;
*old_tags = tags_fetch;
Ok::<(), crate::Error>(())
}
.await;
match res {
Ok(()) => {}
Err(err) => {
log::warn!("Unable to update launcher tags: {err}")
}
};
}
// 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)
},
)
pub fn get_categories(&self) -> Vec<Category> {
self.categories.clone()
}
// 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)
})
pub fn get_loaders(&self) -> Vec<Loader> {
self.loaders.clone()
}
// 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)
})
pub fn get_game_versions(&self) -> Vec<GameVersion> {
self.game_versions.clone()
}
// 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)
},
)
pub fn get_donation_platforms(&self) -> Vec<DonationPlatform> {
self.donation_platforms.clone()
}
// 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)
},
)
pub fn get_report_types(&self) -> Vec<String> {
self.report_types.clone()
}
// 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()?,
})
pub fn get_tag_bundle(&self) -> Tags {
self.clone()
}
// Fetches the tags from the Modrinth API and stores them in the database
#[tracing::instrument(skip(self))]
pub async fn fetch_update(
&mut self,
semaphore: &RwLock<Semaphore>,
loading_bar: Option<&LoadingBarId>,
) -> crate::Result<()> {
pub async fn fetch(semaphore: &RwLock<Semaphore>) -> crate::Result<Self> {
let categories = format!("{MODRINTH_API_URL}tag/category");
let loaders = format!("{MODRINTH_API_URL}tag/loader");
let game_versions = format!("{MODRINTH_API_URL}tag/game_version");
let licenses = format!("{MODRINTH_API_URL}tag/license");
let donation_platforms =
format!("{MODRINTH_API_URL}tag/donation_platform");
let report_types = format!("{MODRINTH_API_URL}tag/report_type");
@@ -172,13 +145,6 @@ impl Tags {
None,
semaphore,
);
let licenses_fut = fetch_json::<Vec<License>>(
Method::GET,
&licenses,
None,
None,
semaphore,
);
let donation_platforms_fut = fetch_json::<Vec<DonationPlatform>>(
Method::GET,
&donation_platforms,
@@ -198,60 +164,27 @@ impl Tags {
categories,
loaders,
game_versions,
licenses,
donation_platforms,
report_types,
) = loading_join!(loading_bar, 0.5, None;
) = tokio::try_join!(
categories_fut,
loaders_fut,
game_versions_fut,
licenses_fut,
donation_platforms_fut,
report_types_fut
)?;
// 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(())
Ok(Self {
categories,
loaders,
game_versions,
donation_platforms,
report_types,
})
}
}
// 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,
@@ -259,26 +192,20 @@ pub struct Category {
pub icon: PathBuf,
}
#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)]
#[derive(Debug, Clone, 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 License {
pub short: String,
pub name: String,
}
#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DonationPlatform {
pub short: String,
pub name: String,
}
#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameVersion {
pub version: String,
pub version_type: String,

View File

@@ -1,79 +1,70 @@
//! User login info
use crate::{auth::Credentials, config::BINCODE_CONFIG};
use crate::auth::Credentials;
use crate::data::DirectoryInfo;
use crate::util::fetch::{read_json, write};
use crate::State;
use std::collections::HashMap;
use tokio::sync::{RwLock, Semaphore};
use uuid::Uuid;
const USER_DB_TREE: &[u8] = b"users";
const USERS_JSON: &str = "users.json";
/// The set of users stored in the launcher
#[derive(Clone)]
pub(crate) struct Users(pub(crate) sled::Tree);
pub(crate) struct Users(pub(crate) HashMap<Uuid, Credentials>);
impl Users {
#[tracing::instrument(skip(db))]
pub fn init(db: &sled::Db) -> crate::Result<Self> {
Ok(Self(db.open_tree(USER_DB_TREE)?))
pub async fn init(
dirs: &DirectoryInfo,
io_semaphore: &RwLock<Semaphore>,
) -> crate::Result<Self> {
let users_path = dirs.caches_meta_dir().join(USERS_JSON);
let users = read_json(&users_path, io_semaphore).await.ok();
if let Some(users) = users {
Ok(Self(users))
} else {
Ok(Self(HashMap::new()))
}
}
pub async fn save(&self) -> crate::Result<()> {
let state = State::get().await?;
let users_path = state.directories.caches_meta_dir().join(USERS_JSON);
write(
&users_path,
&serde_json::to_vec(&self.0)?,
&state.io_semaphore,
)
.await?;
Ok(())
}
#[tracing::instrument(skip_all)]
pub fn insert(
pub async fn insert(
&mut self,
credentials: &Credentials,
) -> crate::Result<&Self> {
let id = credentials.id.as_bytes();
self.0.insert(
id,
bincode::encode_to_vec(credentials, *BINCODE_CONFIG)?,
)?;
self.0.insert(credentials.id, credentials.clone());
self.save().await?;
Ok(self)
}
#[tracing::instrument(skip(self))]
pub fn contains(&self, id: uuid::Uuid) -> crate::Result<bool> {
Ok(self.0.contains_key(id.as_bytes())?)
pub fn contains(&self, id: Uuid) -> bool {
self.0.contains_key(&id)
}
#[tracing::instrument(skip(self))]
pub fn get(&self, id: uuid::Uuid) -> crate::Result<Option<Credentials>> {
self.0.get(id.as_bytes())?.map_or(Ok(None), |prof| {
bincode::decode_from_slice(&prof, *BINCODE_CONFIG)
.map_err(crate::Error::from)
.map(|it| Some(it.0))
})
pub fn get(&self, id: Uuid) -> Option<Credentials> {
self.0.get(&id).cloned()
}
#[tracing::instrument(skip(self))]
pub fn remove(&mut self, id: uuid::Uuid) -> crate::Result<&Self> {
self.0.remove(id.as_bytes())?;
pub async fn remove(&mut self, id: Uuid) -> crate::Result<&Self> {
self.0.remove(&id);
self.save().await?;
Ok(self)
}
pub fn iter(&self) -> UserIter<impl UserInnerIter> {
UserIter(self.0.iter().values(), false)
}
}
alias_trait! {
pub UserInnerIter: Iterator<Item = sled::Result<sled::IVec>>, Send, Sync
}
/// An iterator over the set of users
#[derive(Debug)]
pub struct UserIter<I: UserInnerIter>(I, bool);
impl<I: UserInnerIter> Iterator for UserIter<I> {
type Item = crate::Result<Credentials>;
#[tracing::instrument(skip(self))]
fn next(&mut self) -> Option<Self::Item> {
if self.1 {
return None;
}
let it = self.0.next()?;
let res = it.map_err(crate::Error::from).and_then(|it| {
Ok(bincode::decode_from_slice(&it, *BINCODE_CONFIG)?.0)
});
self.1 = res.is_err();
Some(res)
}
}