Profile mod management

This commit is contained in:
Jai A
2023-03-29 15:34:46 -07:00
parent 8512b45e2b
commit 59b835d374
10 changed files with 1322 additions and 841 deletions

1465
Cargo.lock generated

File diff suppressed because it is too large Load Diff

103
flake.lock generated
View File

@@ -1,103 +0,0 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1655706580,
"narHash": "sha256-7DshIT1Ya5W9NAW7UdnYCHsGmXfOXJZCEHbbB/cCX7g=",
"owner": "nix-community",
"repo": "fenix",
"rev": "d895003d8e03ac2fc8ffe2aa898299cbef1a7048",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1655042882,
"narHash": "sha256-9BX8Fuez5YJlN7cdPO63InoyBy7dm3VlJkkmTt6fS1A=",
"owner": "nix-community",
"repo": "naersk",
"rev": "cddffb5aa211f50c4b8750adbec0bbbdfb26bb9f",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1655624069,
"narHash": "sha256-7g1zwTdp35GMTERnSzZMWJ7PG3QdDE8VOX3WsnOkAtM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "0d68d7c857fe301d49cdcd56130e0beea4ecd5aa",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"naersk": "naersk",
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1655654433,
"narHash": "sha256-auHQ0XPCiaTPSn+R3Yu4J7oZ5Zq/FS5/Da1ivvdYb/Y=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "427061da19723f2206fe4dcb175c9c43b9a6193d",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"utils": {
"locked": {
"lastModified": 1653893745,
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,72 +0,0 @@
{
description = "The official Modrinth launcher";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
utils.url = "github:numtide/flake-utils";
naersk = {
url = "github:nix-community/naersk";
inputs.nixpkgs.follows = "nixpkgs";
};
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs@{self, ...}:
inputs.utils.lib.eachDefaultSystem (system: let
pkgs = import inputs.nixpkgs { inherit system; };
fenix = inputs.fenix.packages.${system};
utils = inputs.utils.lib;
toolchain = with fenix;
combine [
minimal.rustc minimal.cargo
];
naersk = inputs.naersk.lib.${system}.override {
rustc = toolchain;
cargo = toolchain;
};
deps = with pkgs; {
global = [
openssl pkg-config gcc
];
gui = [
gtk4 gdk-pixbuf atk webkitgtk dbus
];
shell = [
(with fenix; combine [toolchain default.clippy complete.rust-src rust-analyzer])
git
jdk17 jdk8
];
};
in {
packages = {
theseus-cli = naersk.buildPackage {
pname = "theseus_cli";
src = ./.;
buildInputs = deps.global;
cargoBuildOptions = x: x ++ ["-p" "theseus_cli"];
};
};
apps = {
cli = utils.mkApp {
drv = self.packages.${system}.theseus-cli;
};
cli-dev = utils.mkApp {
drv = self.packages.${system}.theseus-cli.overrideAttrs (old: old // {
release = false;
});
};
};
devShell = pkgs.mkShell {
buildInputs = with deps;
global ++ gui ++ shell;
};
});
}

View File

@@ -11,7 +11,9 @@ bytes = "1"
bincode = { version = "2.0.0-rc.1", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha1 = { version = "0.6.0", features = ["std"]}
toml = "0.7.3"
sha1 = { version = "0.6.1", features = ["std"]}
sha2 = "0.9.9"
sled = { version = "0.34.7", features = ["compression"] }
url = "2.2"
uuid = { version = "1.1", features = ["serde", "v4"] }
@@ -29,7 +31,7 @@ tracing = "0.1"
tracing-error = "0.2"
async-tungstenite = { version = "0.17", features = ["tokio-runtime", "tokio-native-tls"] }
async-tungstenite = { version = "0.20.0", features = ["tokio-runtime", "tokio-native-tls"] }
futures = "0.3"
once_cell = "1.9.0"
reqwest = { version = "0.11", features = ["json"] }

View File

@@ -17,6 +17,8 @@ pub static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
.unwrap()
});
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";
pub fn sled_config() -> sled::Config {
sled::Config::default().use_compression(true)
}

View File

@@ -122,6 +122,12 @@ impl DirectoryInfo {
self.config_dir.join("settings.json")
}
/// Get the cache directory for Theseus
#[inline]
pub fn caches_dir(&self) -> PathBuf {
self.config_dir.join("caches")
}
/// Get path from environment variable
#[inline]
fn env_path(name: &str) -> Option<PathBuf> {

View File

@@ -16,6 +16,9 @@ pub use self::profiles::*;
mod settings;
pub use self::settings::*;
mod projects;
pub use self::projects::*;
mod users;
pub use self::users::*;

View File

@@ -1,5 +1,7 @@
use super::settings::{Hooks, MemorySettings, WindowSize};
use crate::config::BINCODE_CONFIG;
use crate::data::DirectoryInfo;
use crate::state::projects::Project;
use daedalus::modded::LoaderVersion;
use futures::prelude::*;
use serde::{Deserialize, Serialize};
@@ -27,6 +29,7 @@ pub struct Profile {
#[serde(skip)]
pub path: PathBuf,
pub metadata: ProfileMetadata,
pub projects: HashMap<PathBuf, Project>,
#[serde(skip_serializing_if = "Option::is_none")]
pub java: Option<JavaSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -107,6 +110,7 @@ impl Profile {
loader_version: None,
format_version: CURRENT_FORMAT_VERSION,
},
projects: HashMap::new(),
java: None,
memory: None,
resolution: None,
@@ -200,7 +204,10 @@ impl Profile {
impl Profiles {
#[tracing::instrument(skip(db))]
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
pub async fn init(
db: &sled::Db,
dirs: &DirectoryInfo,
) -> crate::Result<Self> {
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
Ok(Default::default()),
|bytes| {
@@ -212,7 +219,7 @@ impl Profiles {
},
)?;
let profiles = stream::iter(profile_db.iter())
let mut profiles = stream::iter(profile_db.iter())
.then(|it| async move {
let path = PathBuf::from(it);
let prof = match Self::read_profile_from_dir(&path).await {
@@ -227,6 +234,15 @@ impl Profiles {
.collect::<HashMap<PathBuf, Option<Profile>>>()
.await;
// {
// for (path, profile_opt) in profiles.iter_mut() {
// if let Some(profile) = profile_opt {
//
// }
// }
// }
// dirs.caches_dir()
Ok(Self(profiles))
}
@@ -296,63 +312,3 @@ impl Profiles {
Ok(profile)
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::{assert_eq, assert_str_eq};
use std::collections::HashSet;
#[test]
fn profile_test() -> Result<(), serde_json::Error> {
let profile = Profile {
path: PathBuf::new(),
metadata: ProfileMetadata {
name: String::from("Example Pack"),
icon: None,
game_version: String::from("1.18.2"),
loader: ModLoader::Vanilla,
loader_version: None,
format_version: CURRENT_FORMAT_VERSION,
},
java: Some(JavaSettings {
install: Some(PathBuf::from("/usr/bin/java")),
extra_arguments: Some(Vec::new()),
}),
memory: Some(MemorySettings {
minimum: None,
maximum: 8192,
}),
resolution: Some(WindowSize(1920, 1080)),
hooks: Some(Hooks {
pre_launch: HashSet::new(),
wrapper: None,
post_exit: HashSet::new(),
}),
};
let json = serde_json::json!({
"metadata": {
"name": "Example Pack",
"game_version": "1.18.2",
"format_version": 1u32,
"loader": "vanilla",
},
"java": {
"extra_arguments": [],
"install": "/usr/bin/java",
},
"memory": {
"maximum": 8192u32,
},
"resolution": (1920u16, 1080u16),
"hooks": {},
});
assert_eq!(serde_json::to_value(profile.clone())?, json.clone());
assert_str_eq!(
format!("{:?}", serde_json::from_value::<Profile>(json)?),
format!("{:?}", profile),
);
Ok(())
}
}

View File

@@ -0,0 +1,424 @@
//! Project management + inference
use crate::config::{MODRINTH_API_URL, REQWEST_CLIENT};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::Digest;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use tokio::io::AsyncReadExt;
use zip::ZipArchive;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Project {
pub sha512: String,
pub disabled: bool,
pub metadata: ProjectMetadata,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ModrinthProject {
pub id: String,
pub slug: Option<String>,
pub project_type: String,
pub team: String,
pub title: String,
pub description: String,
pub body: String,
pub published: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub client_side: String,
pub server_side: String,
pub downloads: u32,
pub followers: u32,
pub categories: Vec<String>,
pub additional_categories: Vec<String>,
pub game_versions: Vec<String>,
pub loaders: Vec<String>,
pub versions: Vec<String>,
pub icon_url: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ProjectMetadata {
Modrinth(ModrinthProject),
Inferred {
title: Option<String>,
description: Option<String>,
authors: Vec<String>,
version: Option<String>,
icon: Option<PathBuf>,
},
Unknown,
}
pub async fn infer_data_from_files(
paths: Vec<PathBuf>,
cache_dir: PathBuf,
) -> 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?;
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());
}
// TODO: add disabled mods
// TODO: add retrying
#[derive(Deserialize)]
pub struct ModrinthVersion {
pub project_id: String,
}
let files: HashMap<String, ModrinthVersion> = REQWEST_CLIENT
.post(format!("{}version_files", MODRINTH_API_URL))
.json(&json!({
"hashes": file_path_hashes.keys().collect::<Vec<_>>(),
"algorithm": "sha512",
}))
.send()
.await?
.json()
.await?;
let projects: Vec<ModrinthProject> = REQWEST_CLIENT
.get(format!(
"{}projects?ids={}",
MODRINTH_API_URL,
serde_json::to_string(
&files
.values()
.map(|x| x.project_id.clone())
.collect::<Vec<_>>()
)?
))
.send()
.await?
.json()
.await?;
let mut return_projects = HashMap::new();
let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new();
for (hash, path) in file_path_hashes {
if let Some(file) = files.get(&hash) {
if let Some(project) =
projects.iter().find(|x| file.project_id == x.id)
{
return_projects.insert(
path,
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Modrinth(project.clone()),
},
);
continue;
}
}
further_analyze_projects.push((hash, path));
}
for (hash, path) in further_analyze_projects {
let file = File::open(path.clone())?;
// TODO: get rid of below unwrap
let mut zip = ZipArchive::new(file).unwrap();
let read_icon_from_file =
|icon_path: Option<String>| -> crate::Result<Option<PathBuf>> {
if let Some(icon_path) = icon_path {
// we have to repoen the zip twice here :(
let zip_file = File::open(path.clone())?;
if let Ok(mut zip) = ZipArchive::new(zip_file) {
if let Ok(mut file) = zip.by_name(&icon_path) {
let mut bytes = Vec::new();
if file.read_to_end(&mut bytes).is_ok() {
let extension = Path::new(&icon_path)
.extension()
.and_then(OsStr::to_str);
let hash = sha1::Sha1::from(&bytes).hexdigest();
let path = cache_dir.join("icons").join(
if let Some(ext) = extension {
format!("{hash}.{ext}")
} else {
hash
},
);
if !path.exists() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = File::create(path.clone())?;
file.write_all(&bytes)?;
}
return Ok(Some(path));
}
};
}
}
Ok(None)
};
if let Ok(mut file) = zip.by_name("META-INF/mods.toml") {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ForgeModInfo {
pub mods: Vec<ForgeMod>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ForgeMod {
mod_id: String,
version: Option<String>,
display_name: Option<String>,
description: Option<String>,
logo_file: Option<String>,
authors: Option<String>,
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if let Ok(pack) = serde_json::from_str::<ForgeMod>(&file_str) {
let icon = read_icon_from_file(pack.logo_file)?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Inferred {
title: Some(
pack.display_name.unwrap_or(pack.mod_id),
),
description: pack.description,
authors: pack
.authors
.map(|x| vec![x])
.unwrap_or_default(),
version: pack.version,
icon,
},
},
);
continue;
}
}
}
if let Ok(mut file) = zip.by_name("mcmod.info") {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ForgeMod {
modid: String,
name: String,
description: Option<String>,
version: Option<String>,
author_list: Option<Vec<String>>,
logo_file: Option<String>,
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if let Ok(pack) = serde_json::from_str::<ForgeMod>(&file_str) {
let icon = read_icon_from_file(pack.logo_file)?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Inferred {
title: Some(if pack.name.is_empty() {
pack.modid
} else {
pack.name
}),
description: pack.description,
authors: pack.author_list.unwrap_or_default(),
version: pack.version,
icon,
},
},
);
continue;
}
}
}
if let Ok(mut file) = zip.by_name("fabric.mod.json") {
#[derive(Deserialize)]
#[serde(untagged)]
enum FabricAuthor {
String(String),
Object { name: String },
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct FabricMod {
id: String,
version: String,
name: Option<String>,
description: Option<String>,
authors: Vec<FabricAuthor>,
icon: Option<String>,
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if let Ok(pack) = serde_json::from_str::<FabricMod>(&file_str) {
let icon = read_icon_from_file(pack.icon)?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Inferred {
title: Some(pack.name.unwrap_or(pack.id)),
description: pack.description,
authors: pack
.authors
.into_iter()
.map(|x| match x {
FabricAuthor::String(name) => name,
FabricAuthor::Object { name } => name,
})
.collect(),
version: Some(pack.version),
icon,
},
},
);
continue;
}
}
}
if let Ok(mut file) = zip.by_name("quilt.mod.json") {
#[derive(Deserialize)]
struct QuiltMetadata {
pub name: Option<String>,
pub description: Option<String>,
pub contributors: Option<HashMap<String, String>>,
pub icon: Option<String>,
}
#[derive(Deserialize)]
struct QuiltMod {
id: String,
version: String,
metadata: Option<QuiltMetadata>,
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if let Ok(pack) = serde_json::from_str::<QuiltMod>(&file_str) {
let icon = read_icon_from_file(
pack.metadata
.as_ref()
.map(|x| x.icon.clone())
.flatten(),
)?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Inferred {
title: Some(
pack.metadata
.as_ref()
.map(|x| x.name.clone())
.flatten()
.unwrap_or(pack.id),
),
description: pack
.metadata
.as_ref()
.map(|x| x.description.clone())
.flatten(),
authors: pack
.metadata
.map(|x| {
x.contributors
.unwrap_or_default()
.keys()
.cloned()
.collect()
})
.unwrap_or_default(),
version: Some(pack.version),
icon,
},
},
);
continue;
}
}
}
if let Ok(mut file) = zip.by_name("pack.mcmeta") {
#[derive(Deserialize)]
struct Pack {
description: Option<String>,
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if let Ok(pack) = serde_json::from_str::<Pack>(&file_str) {
let icon =
read_icon_from_file(Some("pack.png".to_string()))?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Inferred {
title: None,
description: pack.description,
authors: Vec::new(),
version: None,
icon,
},
},
);
continue;
}
}
}
return_projects.insert(
path,
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Unknown,
},
);
}
Ok(return_projects)
}

View File

@@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
tauri_build::build()
}