You've already forked AstralRinth
forked from didirus/AstralRinth
Mod Management API (#81)
* Profile mod management * remove print statement
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user