Mod Management API (#81)

* Profile mod management

* remove print statement
This commit is contained in:
Geometrically
2023-04-13 12:03:15 -07:00
committed by GitHub
parent bb126c0545
commit f8173d3b78
22 changed files with 616 additions and 252 deletions

View File

@@ -46,7 +46,7 @@ pub async fn authenticate(
))
})?;
let credentials = flow.extract_credentials().await?;
let credentials = flow.extract_credentials(&state.io_semaphore).await?;
users.insert(&credentials)?;
if state.settings.read().await.default_user.is_none() {
@@ -60,13 +60,11 @@ pub async fn authenticate(
/// Refresh some credentials using Hydra, if needed
/// This is the primary desired way to get credentials, as it will also refresh them.
#[tracing::instrument]
pub async fn refresh(
user: uuid::Uuid,
update_name: bool,
) -> crate::Result<Credentials> {
pub async fn refresh(user: uuid::Uuid) -> crate::Result<Credentials> {
let state = State::get().await?;
let mut users = state.users.write().await;
let io_sempahore = &state.io_semaphore;
futures::future::ready(users.get(user)?.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to refresh nonexistent user with ID {user}"
@@ -75,10 +73,7 @@ pub async fn refresh(
}))
.and_then(|mut credentials| async move {
if chrono::offset::Utc::now() > credentials.expires {
inner::refresh_credentials(&mut credentials).await?;
if update_name {
inner::refresh_username(&mut credentials).await?;
}
inner::refresh_credentials(&mut credentials, io_sempahore).await?;
}
users.insert(&credentials)?;
Ok(credentials)

View File

@@ -79,6 +79,7 @@ pub async fn install_pack_from_version_id(
Method::GET,
&format!("{}version/{}", MODRINTH_API_URL, version_id),
None,
None,
&state.io_semaphore,
)
.await?;
@@ -104,6 +105,7 @@ pub async fn install_pack_from_version_id(
Method::GET,
&format!("{}project/{}", MODRINTH_API_URL, version.project_id),
None,
None,
&state.io_semaphore,
)
.await?;
@@ -230,7 +232,7 @@ async fn install_pack(
let profile = profile.clone();
async move {
// TODO: Future update: prompt user for optional files in a modpack
//TODO: Future update: prompt user for optional files in a modpack
if let Some(env) = project.env {
if env
.get(&EnvType::Client)

View File

@@ -13,7 +13,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use tokio::{process::Command, sync::RwLock};
use tokio::{fs, process::Command, sync::RwLock};
/// Remove a profile
#[tracing::instrument]
@@ -67,23 +67,10 @@ pub async fn list() -> crate::Result<std::collections::HashMap<PathBuf, Profile>
#[tracing::instrument]
pub async fn sync(path: &Path) -> crate::Result<()> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = get(path).await? {
let paths = profile.get_profile_project_paths()?;
let projects = crate::state::infer_data_from_files(
paths,
state.directories.caches_dir(),
&state.io_semaphore,
)
.await?;
{
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(path) {
profile.projects = projects;
}
}
if let Some(profile) = profiles.0.get_mut(path) {
profile.sync().await?;
State::sync().await?;
Ok(())
@@ -95,6 +82,99 @@ pub async fn sync(path: &Path) -> crate::Result<()> {
}
}
/// Add a project from a version
#[tracing::instrument]
pub async fn add_project_from_version(
profile: &Path,
version_id: String,
) -> crate::Result<PathBuf> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile) {
profile.add_project_version(version_id).await
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
profile.display().to_string(),
)
.as_error())
}
}
/// Add a project from an FS path
#[tracing::instrument]
pub async fn add_project_from_path(
profile: &Path,
path: &Path,
project_type: Option<String>,
) -> crate::Result<PathBuf> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile) {
let file = fs::read(path).await?;
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
profile
.add_project_bytes(
&file_name,
bytes::Bytes::from(file),
project_type.and_then(|x| serde_json::from_str(&x).ok()),
)
.await
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
profile.display().to_string(),
)
.as_error())
}
}
/// Toggle whether a project is disabled or not
#[tracing::instrument]
pub async fn toggle_disable_project(
profile: &Path,
project: &Path,
) -> crate::Result<()> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile) {
profile.toggle_disable_project(project).await?;
Ok(())
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
profile.display().to_string(),
)
.as_error())
}
}
/// Remove a project from a profile
#[tracing::instrument]
pub async fn remove_project(
profile: &Path,
project: &Path,
) -> crate::Result<()> {
let state = State::get().await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(profile) {
profile.remove_project(project).await?;
Ok(())
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
profile.display().to_string(),
)
.as_error())
}
}
/// Run Minecraft using a profile and the default credentials, logged in credentials,
/// failing with an error if no credentials are available
#[tracing::instrument(skip_all)]
@@ -104,13 +184,13 @@ pub async fn run(path: &Path) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
// Get default account and refresh credentials (preferred way to log in)
let default_account = state.settings.read().await.default_user;
let credentials = if let Some(default_account) = default_account {
refresh(default_account, false).await?
refresh(default_account).await?
} else {
// If no default account, try to use a logged in account
let users = auth::users().await?;
let last_account = users.iter().next();
if let Some(last_account) = last_account {
refresh(last_account.id, false).await?
refresh(last_account.id).await?
} else {
return Err(crate::ErrorKind::NoCredentialsError.as_error());
}
@@ -123,7 +203,7 @@ pub async fn run(path: &Path) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
#[tracing::instrument(skip_all)]
pub async fn run_credentials(
path: &Path,
credentials: &crate::auth::Credentials,
credentials: &auth::Credentials,
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
let state = State::get().await?;
let settings = state.settings.read().await;

View File

@@ -159,10 +159,10 @@ pub async fn profile_create(
let settings = state.settings.read().await;
let optimal_version_key = jre::get_optimal_jre_key(&profile).await?;
if settings.java_globals.get(&optimal_version_key).is_some() {
profile.set_java_settings(Some(JavaSettings {
profile.java = Some(JavaSettings {
jre_key: Some(optimal_version_key),
extra_arguments: None,
}))?;
});
} else {
println!("Could not detect optimal JRE: {optimal_version_key}, falling back to system default.");
}

View File

@@ -1,29 +1,13 @@
//! Configuration structs
use once_cell::sync::Lazy;
use std::time;
use lazy_static::lazy_static;
pub static BINCODE_CONFIG: Lazy<bincode::config::Configuration> =
Lazy::new(|| {
lazy_static! {
pub static ref BINCODE_CONFIG: bincode::config::Configuration =
bincode::config::standard()
.with_little_endian()
.with_no_limit()
});
pub static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
let mut headers = reqwest::header::HeaderMap::new();
let header = reqwest::header::HeaderValue::from_str(&format!(
"modrinth/daedalus/{} (support@modrinth.com)",
env!("CARGO_PKG_VERSION")
))
.unwrap();
headers.insert(reqwest::header::USER_AGENT, header);
reqwest::Client::builder()
.tcp_keepalive(Some(time::Duration::from_secs(10)))
.default_headers(headers)
.build()
.expect("Reqwest Client Building Failed")
});
.with_no_limit();
}
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";

View File

@@ -1,10 +1,13 @@
//! Authentication flow based on Hydra
use crate::util::fetch::{fetch_advanced, fetch_json};
use async_tungstenite as ws;
use bincode::{Decode, Encode};
use chrono::{prelude::*, Duration};
use futures::prelude::*;
use lazy_static::lazy_static;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use tokio::sync::{RwLock, Semaphore};
use url::Url;
lazy_static! {
@@ -93,7 +96,10 @@ impl HydraAuthFlow<ws::tokio::ConnectStream> {
))
}
pub async fn extract_credentials(&mut self) -> crate::Result<Credentials> {
pub async fn extract_credentials(
&mut self,
semaphore: &RwLock<Semaphore>,
) -> crate::Result<Credentials> {
// Minecraft bearer token
let token_resp = self
.socket
@@ -111,7 +117,7 @@ impl HydraAuthFlow<ws::tokio::ConnectStream> {
Utc::now() + Duration::seconds(token.expires_after.into());
// Get account credentials
let info = fetch_info(&token.token).await?;
let info = fetch_info(&token.token, semaphore).await?;
// Return structure from response
Ok(Credentials {
@@ -127,17 +133,16 @@ impl HydraAuthFlow<ws::tokio::ConnectStream> {
pub async fn refresh_credentials(
credentials: &mut Credentials,
semaphore: &RwLock<Semaphore>,
) -> crate::Result<()> {
let resp = crate::config::REQWEST_CLIENT
.post(HYDRA_URL.join("/refresh")?)
.json(
&serde_json::json!({ "refresh_token": credentials.refresh_token }),
)
.send()
.await?
.error_for_status()?
.json::<TokenJSON>()
.await?;
let resp = fetch_json::<TokenJSON>(
Method::POST,
HYDRA_URL.join("/refresh")?.as_str(),
None,
Some(serde_json::json!({ "refresh_token": credentials.refresh_token })),
semaphore,
)
.await?;
credentials.access_token = resp.token;
credentials.refresh_token = resp.refresh_token;
@@ -147,24 +152,21 @@ pub async fn refresh_credentials(
Ok(())
}
pub async fn refresh_username(
credentials: &mut Credentials,
) -> crate::Result<()> {
let info = fetch_info(&credentials.access_token).await?;
credentials.username = info.name;
Ok(())
}
// Helpers
async fn fetch_info(token: &str) -> crate::Result<ProfileInfoJSON> {
let url =
Url::parse("https://api.minecraftservices.com/minecraft/profile")?;
Ok(crate::config::REQWEST_CLIENT
.get(url)
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
.send()
.await?
.error_for_status()?
.json::<ProfileInfoJSON>()
.await?)
async fn fetch_info(
token: &str,
semaphore: &RwLock<Semaphore>,
) -> crate::Result<ProfileInfoJSON> {
let result = fetch_advanced(
Method::GET,
"https://api.minecraftservices.com/minecraft/profile",
None,
None,
Some(("Authorization", &format!("Bearer {token}"))),
semaphore,
)
.await?;
let value = serde_json::from_slice(&result)?;
Ok(value)
}

View File

@@ -100,13 +100,14 @@ impl State {
// 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 {
if let Err(tag_fetch_err) =
tags.fetch_update(&io_semaphore).await
{
tracing::error!(
"Failed to fetch tags on launcher init: {}",
tag_fetch_err
);
};
// 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 {

View File

@@ -1,11 +1,16 @@
use super::settings::{Hooks, MemorySettings, WindowSize};
use crate::config::MODRINTH_API_URL;
use crate::data::DirectoryInfo;
use crate::state::projects::Project;
use crate::util::fetch::write_cached_icon;
use crate::state::{ModrinthVersion, ProjectType};
use crate::util::fetch::{fetch, fetch_json, write, write_cached_icon};
use crate::State;
use daedalus::modded::LoaderVersion;
use dunce::canonicalize;
use futures::prelude::*;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::io::Cursor;
use std::{
collections::HashMap,
path::{Path, PathBuf},
@@ -129,12 +134,19 @@ impl Profile {
Ok(self)
}
#[tracing::instrument]
pub fn set_java_settings(
&mut self,
java: Option<JavaSettings>,
) -> crate::Result<()> {
self.java = java;
pub async fn sync(&mut self) -> crate::Result<()> {
let state = State::get().await?;
let paths = self.get_profile_project_paths()?;
let projects = crate::state::infer_data_from_files(
paths,
state.directories.caches_dir(),
&state.io_semaphore,
)
.await?;
self.projects = projects;
Ok(())
}
@@ -153,13 +165,150 @@ impl Profile {
Ok::<(), crate::Error>(())
};
read_paths("mods")?;
read_paths("shaders")?;
read_paths("resourcepacks")?;
read_paths("datapacks")?;
read_paths(ProjectType::Mod.get_folder())?;
read_paths(ProjectType::ShaderPack.get_folder())?;
read_paths(ProjectType::ResourcePack.get_folder())?;
read_paths(ProjectType::DataPack.get_folder())?;
Ok(files)
}
pub async fn add_project_version(
&mut self,
version_id: String,
) -> crate::Result<PathBuf> {
let state = State::get().await?;
let version = fetch_json::<ModrinthVersion>(
Method::GET,
&format!("{MODRINTH_API_URL}version/{version_id}"),
None,
None,
&state.io_semaphore,
)
.await?;
let file = if let Some(file) = version.files.iter().find(|x| x.primary)
{
file
} else if let Some(file) = version.files.first() {
file
} else {
return Err(crate::ErrorKind::InputError(
"No files for input version present!".to_string(),
)
.into());
};
let bytes = fetch(
&file.url,
file.hashes.get("sha1").map(|x| &**x),
&state.io_semaphore,
)
.await?;
let path = self
.add_project_bytes(
&file.filename,
bytes,
ProjectType::get_from_loaders(version.loaders),
)
.await?;
Ok(path)
}
pub async fn add_project_bytes(
&mut self,
file_name: &str,
bytes: bytes::Bytes,
project_type: Option<ProjectType>,
) -> crate::Result<PathBuf> {
let project_type = if let Some(project_type) = project_type {
project_type
} else {
let cursor = Cursor::new(&*bytes);
let mut archive = zip::ZipArchive::new(cursor).map_err(|_| {
crate::ErrorKind::InputError(
"Unable to infer project type for input file".to_string(),
)
})?;
if archive.by_name("fabric.mod.json").is_ok()
|| archive.by_name("quilt.mod.json").is_ok()
|| archive.by_name("META-INF/mods.toml").is_ok()
|| archive.by_name("mcmod.info").is_ok()
{
ProjectType::Mod
} else if archive.by_name("pack.mcmeta").is_ok() {
if archive.file_names().any(|x| x.starts_with("data/")) {
ProjectType::DataPack
} else {
ProjectType::ResourcePack
}
} else {
return Err(crate::ErrorKind::InputError(
"Unable to infer project type for input file".to_string(),
)
.into());
}
};
let state = State::get().await?;
let path = self.path.join(project_type.get_folder()).join(file_name);
write(&path, &bytes, &state.io_semaphore).await?;
self.sync().await?;
Ok(path)
}
pub async fn toggle_disable_project(
&mut self,
path: &Path,
) -> crate::Result<()> {
if let Some(mut project) = self.projects.remove(path) {
let path = path.to_path_buf();
let mut new_path = path.clone();
if path.extension().map_or(false, |ext| ext == "disabled") {
project.disabled = false;
} else {
new_path.set_file_name(format!(
"{}.disabled",
path.file_name().unwrap_or_default().to_string_lossy()
));
project.disabled = true;
}
fs::rename(path, &new_path).await?;
self.projects.insert(new_path, project);
} else {
return Err(crate::ErrorKind::InputError(format!(
"Project path does not exist: {:?}",
path
))
.into());
}
Ok(())
}
pub async fn remove_project(&mut self, path: &Path) -> crate::Result<()> {
if self.projects.contains_key(path) {
fs::remove_file(path).await?;
self.projects.remove(path);
} else {
return Err(crate::ErrorKind::InputError(format!(
"Project path does not exist: {:?}",
path
))
.into());
}
Ok(())
}
}
impl Profiles {
@@ -258,7 +407,7 @@ impl Profiles {
stream::iter(self.0.iter())
.map(Ok::<_, crate::Error>)
.try_for_each_concurrent(None, |(path, profile)| async move {
let json = serde_json::to_vec_pretty(&profile)?;
let json = serde_json::to_vec(&profile)?;
let json_path = Path::new(&path.to_string_lossy().to_string())
.join(PROFILE_JSON_PATH);

View File

@@ -1,9 +1,10 @@
//! Project management + inference
use crate::config::{MODRINTH_API_URL, REQWEST_CLIENT};
use crate::util::fetch::write_cached_icon;
use crate::config::MODRINTH_API_URL;
use crate::util::fetch::{fetch_json, write_cached_icon};
use async_zip::tokio::read::fs::ZipFileReader;
use chrono::{DateTime, Utc};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::Digest;
@@ -12,13 +13,52 @@ use std::path::{Path, PathBuf};
use tokio::io::AsyncReadExt;
use tokio::sync::{RwLock, Semaphore};
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum ProjectType {
Mod,
DataPack,
ResourcePack,
ShaderPack,
}
impl ProjectType {
pub fn get_from_loaders(loaders: Vec<String>) -> Option<Self> {
if loaders
.iter()
.any(|x| ["fabric", "forge", "quilt"].contains(&&**x))
{
Some(ProjectType::Mod)
} else if loaders.iter().any(|x| x == "datapack") {
Some(ProjectType::DataPack)
} else if loaders.iter().any(|x| ["iris", "optifine"].contains(&&**x)) {
Some(ProjectType::ShaderPack)
} else if loaders
.iter()
.any(|x| ["vanilla", "canvas", "minecraft"].contains(&&**x))
{
Some(ProjectType::ResourcePack)
} else {
None
}
}
pub fn get_folder(&self) -> &'static str {
match self {
ProjectType::Mod => "mods",
ProjectType::DataPack => "datapacks",
ProjectType::ResourcePack => "resourcepacks",
ProjectType::ShaderPack => "shaderpacks",
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Project {
pub sha512: String,
pub disabled: bool,
pub metadata: ProjectMetadata,
pub file_name: String,
pub update_available: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -223,19 +263,20 @@ pub async fn infer_data_from_files(
file_path_hashes.insert(hash, path.clone());
}
let files: HashMap<String, ModrinthVersion> = REQWEST_CLIENT
.post(format!("{}version_files", MODRINTH_API_URL))
.json(&json!({
let files: HashMap<String, ModrinthVersion> = fetch_json(
Method::POST,
&format!("{}version_files", MODRINTH_API_URL),
None,
Some(json!({
"hashes": file_path_hashes.keys().collect::<Vec<_>>(),
"algorithm": "sha512",
}))
.send()
.await?
.json()
.await?;
let projects: Vec<ModrinthProject> = REQWEST_CLIENT
.get(format!(
})),
io_semaphore,
)
.await?;
let projects: Vec<ModrinthProject> = fetch_json(
Method::GET,
&format!(
"{}projects?ids={}",
MODRINTH_API_URL,
serde_json::to_string(
@@ -244,27 +285,32 @@ pub async fn infer_data_from_files(
.map(|x| x.project_id.clone())
.collect::<Vec<_>>()
)?
))
.send()
.await?
.json()
.await?;
),
None,
None,
io_semaphore,
)
.await?;
let teams: Vec<ModrinthTeamMember> = REQWEST_CLIENT
.get(format!(
let teams: Vec<ModrinthTeamMember> = fetch_json::<
Vec<Vec<ModrinthTeamMember>>,
>(
Method::GET,
&format!(
"{}teams?ids={}",
MODRINTH_API_URL,
serde_json::to_string(
&projects.iter().map(|x| x.team.clone()).collect::<Vec<_>>()
)?
))
.send()
.await?
.json::<Vec<Vec<ModrinthTeamMember>>>()
.await?
.into_iter()
.flatten()
.collect();
),
None,
None,
io_semaphore,
)
.await?
.into_iter()
.flatten()
.collect();
let mut return_projects = HashMap::new();
let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new();
@@ -297,7 +343,6 @@ pub async fn infer_data_from_files(
members: team_members,
},
file_name,
update_available: false,
},
);
continue;
@@ -326,7 +371,6 @@ pub async fn infer_data_from_files(
disabled: path.ends_with(".disabled"),
metadata: ProjectMetadata::Unknown,
file_name,
update_available: false,
},
);
continue;
@@ -380,7 +424,6 @@ pub async fn infer_data_from_files(
sha512: hash,
disabled: path.ends_with(".disabled"),
file_name,
update_available: false,
metadata: ProjectMetadata::Inferred {
title: Some(
pack.display_name
@@ -447,7 +490,6 @@ pub async fn infer_data_from_files(
sha512: hash,
disabled: path.ends_with(".disabled"),
file_name,
update_available: false,
metadata: ProjectMetadata::Inferred {
title: Some(if pack.name.is_empty() {
pack.modid
@@ -513,7 +555,6 @@ pub async fn infer_data_from_files(
sha512: hash,
disabled: path.ends_with(".disabled"),
file_name,
update_available: false,
metadata: ProjectMetadata::Inferred {
title: Some(pack.name.unwrap_or(pack.id)),
description: pack.description,
@@ -579,7 +620,6 @@ pub async fn infer_data_from_files(
sha512: hash,
disabled: path.ends_with(".disabled"),
file_name,
update_available: false,
metadata: ProjectMetadata::Inferred {
title: Some(
pack.metadata
@@ -615,7 +655,7 @@ pub async fn infer_data_from_files(
.file()
.entries()
.iter()
.position(|f| f.entry().filename() == "pack.mcdata");
.position(|f| f.entry().filename() == "pack.mcmeta");
if let Some(index) = zip_index_option {
let file = zip_file_reader.file().entries().get(index).unwrap();
#[derive(Deserialize)]
@@ -645,7 +685,6 @@ pub async fn infer_data_from_files(
sha512: hash,
disabled: path.ends_with(".disabled"),
file_name,
update_available: false,
metadata: ProjectMetadata::Inferred {
title: None,
description: pack.description,
@@ -666,7 +705,6 @@ pub async fn infer_data_from_files(
sha512: hash,
disabled: path.ends_with(".disabled"),
file_name,
update_available: false,
metadata: ProjectMetadata::Unknown,
},
);

View File

@@ -63,7 +63,7 @@ impl Settings {
#[tracing::instrument(skip(self))]
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
fs::write(to, serde_json::to_vec_pretty(self)?)
fs::write(to, serde_json::to_vec(self)?)
.await
.map_err(|err| {
crate::ErrorKind::FSError(format!(

View File

@@ -1,9 +1,12 @@
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, REQWEST_CLIENT};
use crate::config::{BINCODE_CONFIG, MODRINTH_API_URL};
use crate::util::fetch::fetch_json;
const CATEGORIES_DB_TREE: &[u8] = b"categories";
const LOADERS_DB_TREE: &[u8] = b"loaders";
@@ -133,13 +136,17 @@ impl Tags {
// 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");
pub async fn fetch_update(
&mut self,
semaphore: &RwLock<Semaphore>,
) -> crate::Result<()> {
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");
let (
categories,
loaders,
@@ -148,70 +155,78 @@ impl Tags {
donation_platforms,
report_types,
) = tokio::try_join!(
categories,
loaders,
game_versions,
licenses,
donation_platforms,
report_types
fetch_json::<Vec<Category>>(
Method::GET,
&categories,
None,
None,
semaphore
),
fetch_json::<Vec<Loader>>(
Method::GET,
&loaders,
None,
None,
semaphore
),
fetch_json::<Vec<GameVersion>>(
Method::GET,
&game_versions,
None,
None,
semaphore
),
fetch_json::<Vec<License>>(
Method::GET,
&licenses,
None,
None,
semaphore
),
fetch_json::<Vec<DonationPlatform>>(
Method::GET,
&donation_platforms,
None,
None,
semaphore
),
fetch_json::<Vec<String>>(
Method::GET,
&report_types,
None,
None,
semaphore
),
)?;
// Store the tags in the database
self.0.categories.insert(
"categories",
bincode::encode_to_vec(
categories.json::<Vec<Category>>().await?,
*BINCODE_CONFIG,
)?,
bincode::encode_to_vec(categories, *BINCODE_CONFIG)?,
)?;
self.0.loaders.insert(
"loaders",
bincode::encode_to_vec(
loaders.json::<Vec<Loader>>().await?,
*BINCODE_CONFIG,
)?,
bincode::encode_to_vec(loaders, *BINCODE_CONFIG)?,
)?;
self.0.game_versions.insert(
"game_versions",
bincode::encode_to_vec(
game_versions.json::<Vec<GameVersion>>().await?,
*BINCODE_CONFIG,
)?,
bincode::encode_to_vec(game_versions, *BINCODE_CONFIG)?,
)?;
self.0.licenses.insert(
"licenses",
bincode::encode_to_vec(
licenses.json::<Vec<License>>().await?,
*BINCODE_CONFIG,
)?,
bincode::encode_to_vec(licenses, *BINCODE_CONFIG)?,
)?;
self.0.donation_platforms.insert(
"donation_platforms",
bincode::encode_to_vec(
donation_platforms.json::<Vec<DonationPlatform>>().await?,
*BINCODE_CONFIG,
)?,
bincode::encode_to_vec(donation_platforms, *BINCODE_CONFIG)?,
)?;
self.0.report_types.insert(
"report_types",
bincode::encode_to_vec(
report_types.json::<Vec<String>>().await?,
*BINCODE_CONFIG,
)?,
bincode::encode_to_vec(report_types, *BINCODE_CONFIG)?,
)?;
Ok(())
}
#[tracing::instrument(skip(self))]
pub async fn fetch_tag(
&self,
tag_type: &str,
) -> Result<reqwest::Response, reqwest::Error> {
let url = &format!("{MODRINTH_API_URL}tag/{}", tag_type);
let content = REQWEST_CLIENT.get(url).send().await?;
Ok(content)
}
}
// Serializeable struct for all tags to be fetched together by the frontend

View File

@@ -1,53 +1,86 @@
//! Functions for fetching infromation from the Internet
use crate::config::REQWEST_CLIENT;
use bytes::Bytes;
use lazy_static::lazy_static;
use reqwest::Method;
use serde::de::DeserializeOwned;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::time;
use tokio::sync::{RwLock, Semaphore};
use tokio::{
fs::{self, File},
io::AsyncWriteExt,
};
lazy_static! {
static ref REQWEST_CLIENT: reqwest::Client = {
let mut headers = reqwest::header::HeaderMap::new();
let header = reqwest::header::HeaderValue::from_str(&format!(
"modrinth/theseus/{} (support@modrinth.com)",
env!("CARGO_PKG_VERSION")
))
.unwrap();
headers.insert(reqwest::header::USER_AGENT, header);
reqwest::Client::builder()
.tcp_keepalive(Some(time::Duration::from_secs(10)))
.default_headers(headers)
.build()
.expect("Reqwest Client Building Failed")
};
}
const FETCH_ATTEMPTS: usize = 3;
#[tracing::instrument(skip(semaphore))]
pub async fn fetch(
url: &str,
sha1: Option<&str>,
semaphore: &RwLock<Semaphore>,
) -> crate::Result<Bytes> {
fetch_advanced(Method::GET, url, sha1, semaphore).await
fetch_advanced(Method::GET, url, sha1, None, None, semaphore).await
}
#[tracing::instrument(skip(json_body, semaphore))]
pub async fn fetch_json<T>(
method: Method,
url: &str,
sha1: Option<&str>,
json_body: Option<serde_json::Value>,
semaphore: &RwLock<Semaphore>,
) -> crate::Result<T>
where
T: DeserializeOwned,
{
let result = fetch_advanced(method, url, sha1, semaphore).await?;
let result =
fetch_advanced(method, url, sha1, json_body, None, semaphore).await?;
let value = serde_json::from_slice(&result)?;
Ok(value)
}
/// Downloads a file with retry and checksum functionality
#[tracing::instrument(skip(semaphore))]
#[tracing::instrument(skip(json_body, semaphore))]
pub async fn fetch_advanced(
method: Method,
url: &str,
sha1: Option<&str>,
json_body: Option<serde_json::Value>,
header: Option<(&str, &str)>,
semaphore: &RwLock<Semaphore>,
) -> crate::Result<Bytes> {
let io_semaphore = semaphore.read().await;
let _permit = io_semaphore.acquire().await?;
for attempt in 1..=(FETCH_ATTEMPTS + 1) {
let result = REQWEST_CLIENT.request(method.clone(), url).send().await;
for attempt in 1..=(FETCH_ATTEMPTS + 1) {
let mut req = REQWEST_CLIENT.request(method.clone(), url);
if let Some(body) = json_body.clone() {
req = req.json(&body);
}
if let Some(header) = header {
req = req.header(header.0, header.1);
}
let result = req.send().await;
match result {
Ok(x) => {
let bytes = x.bytes().await;