You've already forked AstralRinth
forked from didirus/AstralRinth
Modpack support (#60)
* Modpack support * Finish feature * Tauri errors fix (#61) * async impl * working * fmt and redundancy * moved ? to if let Ok block * Finish modpacks support * remove generated file * fix compile err * fix lint * Fix code review comments + forge support --------- Co-authored-by: Wyatt Verchere <wverchere@gmail.com>
This commit is contained in:
60
Cargo.lock
generated
60
Cargo.lock
generated
@@ -84,6 +84,23 @@ version = "0.1.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64cb94155d965e3d37ffbbe7cc5b82c3dd79dd33bd48e536f73d2cfb8d85506f"
|
checksum = "64cb94155d965e3d37ffbbe7cc5b82c3dd79dd33bd48e536f73d2cfb8d85506f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-compression"
|
||||||
|
version = "0.3.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00461f243d703f6999c8e7494f077799f1362720a55ae49a90ffe6214032fc0b"
|
||||||
|
dependencies = [
|
||||||
|
"bzip2",
|
||||||
|
"flate2",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"memchr",
|
||||||
|
"pin-project-lite",
|
||||||
|
"xz2",
|
||||||
|
"zstd",
|
||||||
|
"zstd-safe",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-tungstenite"
|
name = "async-tungstenite"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
@@ -100,6 +117,23 @@ dependencies = [
|
|||||||
"tungstenite",
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async_zip"
|
||||||
|
version = "0.0.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79eaa2b44cfdce59cfff6cb013c96900635085fe7c28fbcbe926c9e5ad0ddfbc"
|
||||||
|
dependencies = [
|
||||||
|
"async-compression",
|
||||||
|
"chrono",
|
||||||
|
"crc32fast",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"pin-project",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atk"
|
name = "atk"
|
||||||
version = "0.15.1"
|
version = "0.15.1"
|
||||||
@@ -668,9 +702,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "daedalus"
|
name = "daedalus"
|
||||||
version = "0.1.17"
|
version = "0.1.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f15581178acf1d6160505d7ca36cde6a29d121fb16af80c7343e6b9052b0548e"
|
checksum = "83c9c34a2d4904bcaa4cfa5f62b38c915c106fdc92a6a66276ae2bd5ba1b2527"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1818,6 +1852,17 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lzma-sys"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac"
|
name = "mac"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -3510,6 +3555,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"argh",
|
"argh",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
|
"async_zip",
|
||||||
"bincode",
|
"bincode",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3754,6 +3800,7 @@ checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -4565,6 +4612,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xz2"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||||
|
dependencies = [
|
||||||
|
"lzma-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yansi"
|
name = "yansi"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ sled = { version = "0.34.7", features = ["compression"] }
|
|||||||
url = "2.2"
|
url = "2.2"
|
||||||
uuid = { version = "1.1", features = ["serde", "v4"] }
|
uuid = { version = "1.1", features = ["serde", "v4"] }
|
||||||
zip = "0.5"
|
zip = "0.5"
|
||||||
|
async_zip = { version = "0.0.13", features = ["full"] }
|
||||||
|
|
||||||
chrono = { version = "0.4.19", features = ["serde"] }
|
chrono = { version = "0.4.19", features = ["serde"] }
|
||||||
daedalus = { version = "0.1.16", features = ["bincode"] }
|
daedalus = { version = "0.1.18", features = ["bincode"] }
|
||||||
dirs = "4.0"
|
dirs = "4.0"
|
||||||
# TODO: possibly replace with tracing to have structured logging
|
# TODO: possibly replace with tracing to have structured logging
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! API for interacting with Theseus
|
//! API for interacting with Theseus
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod pack;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod profile_create;
|
pub mod profile_create;
|
||||||
@@ -17,7 +18,7 @@ pub mod prelude {
|
|||||||
pub use crate::{
|
pub use crate::{
|
||||||
auth::{self, Credentials},
|
auth::{self, Credentials},
|
||||||
data::*,
|
data::*,
|
||||||
process,
|
pack, process,
|
||||||
profile::{self, Profile},
|
profile::{self, Profile},
|
||||||
profile_create, settings, State,
|
profile_create, settings, State,
|
||||||
};
|
};
|
||||||
|
|||||||
338
theseus/src/api/pack.rs
Normal file
338
theseus/src/api/pack.rs
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
use crate::config::{MODRINTH_API_URL, REQWEST_CLIENT};
|
||||||
|
use crate::data::ModLoader;
|
||||||
|
use crate::state::{ModrinthProject, ModrinthVersion, SideType};
|
||||||
|
use crate::util::fetch::{fetch, fetch_mirrors, write, write_cached_icon};
|
||||||
|
use crate::State;
|
||||||
|
use async_zip::tokio::read::seek::ZipFileReader;
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::path::{Component, PathBuf};
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct PackFormat {
|
||||||
|
pub game: String,
|
||||||
|
pub format_version: i32,
|
||||||
|
pub version_id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub summary: Option<String>,
|
||||||
|
pub files: Vec<PackFile>,
|
||||||
|
pub dependencies: HashMap<PackDependency, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct PackFile {
|
||||||
|
pub path: String,
|
||||||
|
pub hashes: HashMap<PackFileHash, String>,
|
||||||
|
pub env: Option<HashMap<EnvType, SideType>>,
|
||||||
|
pub downloads: Vec<String>,
|
||||||
|
pub file_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||||
|
#[serde(rename_all = "camelCase", from = "String")]
|
||||||
|
enum PackFileHash {
|
||||||
|
Sha1,
|
||||||
|
Sha512,
|
||||||
|
Unknown(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for PackFileHash {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
return match s.as_str() {
|
||||||
|
"sha1" => PackFileHash::Sha1,
|
||||||
|
"sha512" => PackFileHash::Sha512,
|
||||||
|
_ => PackFileHash::Unknown(s),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
enum EnvType {
|
||||||
|
Client,
|
||||||
|
Server,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
enum PackDependency {
|
||||||
|
Forge,
|
||||||
|
FabricLoader,
|
||||||
|
QuiltLoader,
|
||||||
|
Minecraft,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn install_pack_from_version_id(
|
||||||
|
version_id: String,
|
||||||
|
) -> crate::Result<PathBuf> {
|
||||||
|
let version: ModrinthVersion = REQWEST_CLIENT
|
||||||
|
.get(format!("{}version/{}", MODRINTH_API_URL, version_id))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (url, hash) =
|
||||||
|
if let Some(file) = version.files.iter().find(|x| x.primary) {
|
||||||
|
Some((file.url.clone(), file.hashes.get("sha1")))
|
||||||
|
} else {
|
||||||
|
version
|
||||||
|
.files
|
||||||
|
.first()
|
||||||
|
.map(|file| (file.url.clone(), file.hashes.get("sha1")))
|
||||||
|
}
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::InputError(
|
||||||
|
"Specified version has no files".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let file = async {
|
||||||
|
let state = &State::get().await?;
|
||||||
|
let semaphore = state.io_semaphore.acquire().await?;
|
||||||
|
fetch(&url, hash.map(|x| &**x), &semaphore).await
|
||||||
|
}
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let project: ModrinthProject = REQWEST_CLIENT
|
||||||
|
.get(format!(
|
||||||
|
"{}project/{}",
|
||||||
|
MODRINTH_API_URL, version.project_id
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let icon = if let Some(icon_url) = project.icon_url {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let semaphore = state.io_semaphore.acquire().await?;
|
||||||
|
|
||||||
|
let icon_bytes = fetch(&icon_url, None, &semaphore).await?;
|
||||||
|
|
||||||
|
let filename = icon_url.rsplit('/').next();
|
||||||
|
|
||||||
|
if let Some(filename) = filename {
|
||||||
|
Some(
|
||||||
|
write_cached_icon(
|
||||||
|
filename,
|
||||||
|
&state.directories.caches_dir(),
|
||||||
|
icon_bytes,
|
||||||
|
&semaphore,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
install_pack(file, icon, Some(version.project_id)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn install_pack_from_file(path: PathBuf) -> crate::Result<PathBuf> {
|
||||||
|
let file = fs::read(path).await?;
|
||||||
|
|
||||||
|
install_pack(bytes::Bytes::from(file), None, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_pack(
|
||||||
|
file: bytes::Bytes,
|
||||||
|
icon: Option<PathBuf>,
|
||||||
|
project_id: Option<String>,
|
||||||
|
) -> crate::Result<PathBuf> {
|
||||||
|
let state = &State::get().await?;
|
||||||
|
|
||||||
|
let reader = Cursor::new(&file);
|
||||||
|
|
||||||
|
// Create zip reader around file
|
||||||
|
let mut zip_reader = ZipFileReader::new(reader).await.map_err(|_| {
|
||||||
|
crate::Error::from(crate::ErrorKind::InputError(
|
||||||
|
"Failed to read input modpack zip".to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Extract index of modrinth.index.json
|
||||||
|
let zip_index_option = zip_reader
|
||||||
|
.file()
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.position(|f| f.entry().filename() == "modrinth.index.json");
|
||||||
|
if let Some(zip_index) = zip_index_option {
|
||||||
|
let mut manifest = String::new();
|
||||||
|
let entry = zip_reader
|
||||||
|
.file()
|
||||||
|
.entries()
|
||||||
|
.get(zip_index)
|
||||||
|
.unwrap()
|
||||||
|
.entry()
|
||||||
|
.clone();
|
||||||
|
let mut reader = zip_reader.entry(zip_index).await?;
|
||||||
|
reader.read_to_string_checked(&mut manifest, &entry).await?;
|
||||||
|
|
||||||
|
let pack: PackFormat = serde_json::from_str(&manifest)?;
|
||||||
|
|
||||||
|
if &*pack.game != "minecraft" {
|
||||||
|
return Err(crate::ErrorKind::InputError(
|
||||||
|
"Pack does not support Minecraft".to_string(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut game_version = None;
|
||||||
|
let mut mod_loader = None;
|
||||||
|
let mut loader_version = None;
|
||||||
|
for (key, value) in pack.dependencies {
|
||||||
|
match key {
|
||||||
|
PackDependency::Forge => {
|
||||||
|
mod_loader = Some(ModLoader::Forge);
|
||||||
|
loader_version = Some(value);
|
||||||
|
}
|
||||||
|
PackDependency::FabricLoader => {
|
||||||
|
mod_loader = Some(ModLoader::Fabric);
|
||||||
|
loader_version = Some(value);
|
||||||
|
}
|
||||||
|
PackDependency::QuiltLoader => {
|
||||||
|
mod_loader = Some(ModLoader::Quilt);
|
||||||
|
loader_version = Some(value);
|
||||||
|
}
|
||||||
|
PackDependency::Minecraft => game_version = Some(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let game_version = if let Some(game_version) = game_version {
|
||||||
|
game_version
|
||||||
|
} else {
|
||||||
|
return Err(crate::ErrorKind::InputError(
|
||||||
|
"Pack did not specify Minecraft version".to_string(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let profile = crate::api::profile_create::profile_create(
|
||||||
|
pack.name,
|
||||||
|
game_version.clone(),
|
||||||
|
mod_loader.unwrap_or(ModLoader::Vanilla),
|
||||||
|
loader_version,
|
||||||
|
icon,
|
||||||
|
project_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
use futures::StreamExt;
|
||||||
|
futures::stream::iter(pack.files.into_iter())
|
||||||
|
.map(Ok::<PackFile, crate::Error>)
|
||||||
|
.try_for_each_concurrent(None, |project| {
|
||||||
|
let profile = profile.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
// TODO: Future update: prompt user for optional files in a modpack
|
||||||
|
if let Some(env) = project.env {
|
||||||
|
if env
|
||||||
|
.get(&EnvType::Client)
|
||||||
|
.map(|x| x == &SideType::Unsupported)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let permit = state.io_semaphore.acquire().await?;
|
||||||
|
|
||||||
|
let file = fetch_mirrors(
|
||||||
|
&project
|
||||||
|
.downloads
|
||||||
|
.iter()
|
||||||
|
.map(|x| &**x)
|
||||||
|
.collect::<Vec<&str>>(),
|
||||||
|
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
|
||||||
|
&permit,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let path =
|
||||||
|
std::path::Path::new(&project.path).components().next();
|
||||||
|
if let Some(path) = path {
|
||||||
|
match path {
|
||||||
|
Component::CurDir | Component::Normal(_) => {
|
||||||
|
let path = profile.join(project.path);
|
||||||
|
write(&path, &file, &permit).await?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let extract_overrides = |overrides: String| async {
|
||||||
|
let reader = Cursor::new(&file);
|
||||||
|
|
||||||
|
let mut overrides_zip =
|
||||||
|
ZipFileReader::new(reader).await.map_err(|_| {
|
||||||
|
crate::Error::from(crate::ErrorKind::InputError(
|
||||||
|
"Failed extract overrides Zip".to_string(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let profile = profile.clone();
|
||||||
|
async move {
|
||||||
|
for index in 0..overrides_zip.file().entries().len() {
|
||||||
|
let file = overrides_zip
|
||||||
|
.file()
|
||||||
|
.entries()
|
||||||
|
.get(index)
|
||||||
|
.unwrap()
|
||||||
|
.entry()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let file_path = PathBuf::from(file.filename());
|
||||||
|
if file.filename().starts_with(&overrides)
|
||||||
|
&& !file.filename().ends_with('/')
|
||||||
|
{
|
||||||
|
// Reads the file into the 'content' variable
|
||||||
|
let mut content = Vec::new();
|
||||||
|
let mut reader = overrides_zip.entry(index).await?;
|
||||||
|
reader.read_to_end_checked(&mut content, &file).await?;
|
||||||
|
|
||||||
|
let mut new_path = PathBuf::new();
|
||||||
|
let components = file_path.components().skip(1);
|
||||||
|
|
||||||
|
for component in components {
|
||||||
|
new_path.push(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_path.file_name().is_some() {
|
||||||
|
let permit = state.io_semaphore.acquire().await?;
|
||||||
|
write(&profile.join(new_path), &content, &permit)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), crate::Error>(())
|
||||||
|
}
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
extract_overrides("overrides".to_string()).await?;
|
||||||
|
extract_overrides("client_overrides".to_string()).await?;
|
||||||
|
|
||||||
|
super::profile::sync(&profile).await?;
|
||||||
|
|
||||||
|
Ok(profile)
|
||||||
|
} else {
|
||||||
|
Err(crate::Error::from(crate::ErrorKind::InputError(
|
||||||
|
"No pack manifest found in mrpack".to_string(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,32 +12,12 @@ use std::{
|
|||||||
};
|
};
|
||||||
use tokio::{process::Command, sync::RwLock};
|
use tokio::{process::Command, sync::RwLock};
|
||||||
|
|
||||||
/// Add a profile to the in-memory state
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn add(profile: Profile) -> crate::Result<()> {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let mut profiles = state.profiles.write().await;
|
|
||||||
profiles.insert(profile)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a path as a profile in-memory
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn add_path(path: &Path) -> crate::Result<()> {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let mut profiles = state.profiles.write().await;
|
|
||||||
profiles.insert_from(path).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a profile
|
/// Remove a profile
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn remove(path: &Path) -> crate::Result<()> {
|
pub async fn remove(path: &Path) -> crate::Result<()> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let mut profiles = state.profiles.write().await;
|
let mut profiles = state.profiles.write().await;
|
||||||
profiles.remove(path)?;
|
profiles.remove(path).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -48,29 +28,7 @@ pub async fn get(path: &Path) -> crate::Result<Option<Profile>> {
|
|||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let profiles = state.profiles.read().await;
|
let profiles = state.profiles.read().await;
|
||||||
|
|
||||||
profiles.0.get(path).map_or(Ok(None), |prof| match prof {
|
Ok(profiles.0.get(path).cloned())
|
||||||
Some(prof) => Ok(Some(prof.clone())),
|
|
||||||
None => Err(crate::ErrorKind::UnloadedProfileError(
|
|
||||||
path.display().to_string(),
|
|
||||||
)
|
|
||||||
.as_error()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a profile is already managed by Theseus
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn is_managed(profile: &Path) -> crate::Result<bool> {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let profiles = state.profiles.read().await;
|
|
||||||
Ok(profiles.0.contains_key(profile))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a profile is loaded
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn is_loaded(profile: &Path) -> crate::Result<bool> {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let profiles = state.profiles.read().await;
|
|
||||||
Ok(profiles.0.get(profile).and_then(Option::as_ref).is_some())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Edit a profile using a given asynchronous closure
|
/// Edit a profile using a given asynchronous closure
|
||||||
@@ -85,11 +43,7 @@ where
|
|||||||
let mut profiles = state.profiles.write().await;
|
let mut profiles = state.profiles.write().await;
|
||||||
|
|
||||||
match profiles.0.get_mut(path) {
|
match profiles.0.get_mut(path) {
|
||||||
Some(&mut Some(ref mut profile)) => action(profile).await,
|
Some(ref mut profile) => action(profile).await,
|
||||||
Some(&mut None) => Err(crate::ErrorKind::UnloadedProfileError(
|
|
||||||
path.display().to_string(),
|
|
||||||
)
|
|
||||||
.as_error()),
|
|
||||||
None => Err(crate::ErrorKind::UnmanagedProfileError(
|
None => Err(crate::ErrorKind::UnmanagedProfileError(
|
||||||
path.display().to_string(),
|
path.display().to_string(),
|
||||||
)
|
)
|
||||||
@@ -99,13 +53,45 @@ where
|
|||||||
|
|
||||||
/// Get a copy of the profile set
|
/// Get a copy of the profile set
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn list(
|
pub async fn list() -> crate::Result<std::collections::HashMap<PathBuf, Profile>>
|
||||||
) -> crate::Result<std::collections::HashMap<PathBuf, Option<Profile>>> {
|
{
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let profiles = state.profiles.read().await;
|
let profiles = state.profiles.read().await;
|
||||||
Ok(profiles.0.clone())
|
Ok(profiles.0.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Query + sync profile's projects with the UI from the FS
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn sync(path: &Path) -> crate::Result<()> {
|
||||||
|
let state = State::get().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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
State::sync().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(
|
||||||
|
crate::ErrorKind::UnmanagedProfileError(path.display().to_string())
|
||||||
|
.as_error(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Run Minecraft using a profile
|
/// Run Minecraft using a profile
|
||||||
/// Returns Arc pointer to RwLock to Child
|
/// Returns Arc pointer to RwLock to Child
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
@@ -113,7 +99,7 @@ pub async fn run(
|
|||||||
path: &Path,
|
path: &Path,
|
||||||
credentials: &crate::auth::Credentials,
|
credentials: &crate::auth::Credentials,
|
||||||
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
||||||
let state = State::get().await.unwrap();
|
let state = State::get().await?;
|
||||||
let settings = state.settings.read().await;
|
let settings = state.settings.read().await;
|
||||||
let profile = get(path).await?.ok_or_else(|| {
|
let profile = get(path).await?.ok_or_else(|| {
|
||||||
crate::ErrorKind::OtherError(format!(
|
crate::ErrorKind::OtherError(format!(
|
||||||
@@ -141,19 +127,21 @@ pub async fn run(
|
|||||||
for hook in pre_launch_hooks.iter() {
|
for hook in pre_launch_hooks.iter() {
|
||||||
// TODO: hook parameters
|
// TODO: hook parameters
|
||||||
let mut cmd = hook.split(' ');
|
let mut cmd = hook.split(' ');
|
||||||
let result = Command::new(cmd.next().unwrap())
|
if let Some(command) = cmd.next() {
|
||||||
.args(&cmd.collect::<Vec<&str>>())
|
let result = Command::new(command)
|
||||||
.current_dir(path)
|
.args(&cmd.collect::<Vec<&str>>())
|
||||||
.spawn()?
|
.current_dir(path)
|
||||||
.wait()
|
.spawn()?
|
||||||
.await?;
|
.wait()
|
||||||
|
.await?;
|
||||||
|
|
||||||
if !result.success() {
|
if !result.success() {
|
||||||
return Err(crate::ErrorKind::LauncherError(format!(
|
return Err(crate::ErrorKind::LauncherError(format!(
|
||||||
"Non-zero exit code for pre-launch hook: {}",
|
"Non-zero exit code for pre-launch hook: {}",
|
||||||
result.code().unwrap_or(-1)
|
result.code().unwrap_or(-1)
|
||||||
))
|
))
|
||||||
.as_error());
|
.as_error());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! Theseus profile management interface
|
//! Theseus profile management interface
|
||||||
use crate::{prelude::ModLoader, profile};
|
use crate::prelude::ModLoader;
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
state::{JavaSettings, Profile},
|
state::{JavaSettings, Profile},
|
||||||
State,
|
State,
|
||||||
@@ -22,8 +22,9 @@ pub async fn profile_create_empty() -> crate::Result<PathBuf> {
|
|||||||
String::from(DEFAULT_NAME), // the name/path of the profile
|
String::from(DEFAULT_NAME), // the name/path of the profile
|
||||||
String::from("1.19.2"), // the game version of the profile
|
String::from("1.19.2"), // the game version of the profile
|
||||||
ModLoader::Vanilla, // the modloader to use
|
ModLoader::Vanilla, // the modloader to use
|
||||||
String::from("stable"), // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
None, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
||||||
None, // the icon for the profile
|
None, // the icon for the profile
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -32,17 +33,17 @@ pub async fn profile_create_empty() -> crate::Result<PathBuf> {
|
|||||||
// Returns filepath at which it can be accessed in the State
|
// Returns filepath at which it can be accessed in the State
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn profile_create(
|
pub async fn profile_create(
|
||||||
name: String, // the name of the profile, and relative path
|
name: String, // the name of the profile, and relative path
|
||||||
game_version: String, // the game version of the profile
|
game_version: String, // the game version of the profile
|
||||||
modloader: ModLoader, // the modloader to use
|
modloader: ModLoader, // the modloader to use
|
||||||
loader_version: String, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
loader_version: Option<String>, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader. defaults to latest
|
||||||
icon: Option<PathBuf>, // the icon for the profile
|
icon: Option<PathBuf>, // the icon for the profile
|
||||||
|
linked_project_id: Option<String>, // the linked project ID (mainly for modpacks)- used for updating
|
||||||
) -> crate::Result<PathBuf> {
|
) -> crate::Result<PathBuf> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
|
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
let path = state.directories.profiles_dir().join(uuid.to_string());
|
let path = state.directories.profiles_dir().join(uuid.to_string());
|
||||||
|
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
if !path.is_dir() {
|
if !path.is_dir() {
|
||||||
return Err(ProfileCreationError::NotFolder.into());
|
return Err(ProfileCreationError::NotFolder.into());
|
||||||
@@ -64,6 +65,7 @@ pub async fn profile_create(
|
|||||||
} else {
|
} else {
|
||||||
fs::create_dir_all(&path).await?;
|
fs::create_dir_all(&path).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Creating profile at path {}",
|
"Creating profile at path {}",
|
||||||
&canonicalize(&path)?.display()
|
&canonicalize(&path)?.display()
|
||||||
@@ -71,12 +73,12 @@ pub async fn profile_create(
|
|||||||
|
|
||||||
let loader = modloader;
|
let loader = modloader;
|
||||||
let loader = if loader != ModLoader::Vanilla {
|
let loader = if loader != ModLoader::Vanilla {
|
||||||
let version = loader_version;
|
let version = loader_version.unwrap_or_else(|| "latest".to_string());
|
||||||
|
|
||||||
let filter = |it: &LoaderVersion| match version.as_str() {
|
let filter = |it: &LoaderVersion| match version.as_str() {
|
||||||
"latest" => true,
|
"latest" => true,
|
||||||
"stable" => it.stable,
|
"stable" => it.stable,
|
||||||
id => it.id == *id,
|
id => it.id == *id || format!("{}-{}", game_version, id) == it.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
let loader_data = match loader {
|
let loader_data = match loader {
|
||||||
@@ -93,7 +95,12 @@ pub async fn profile_create(
|
|||||||
let loaders = &loader_data
|
let loaders = &loader_data
|
||||||
.game_versions
|
.game_versions
|
||||||
.iter()
|
.iter()
|
||||||
.find(|it| it.id == game_version)
|
.find(|it| {
|
||||||
|
it.id.replace(
|
||||||
|
daedalus::modded::DUMMY_REPLACE_STRING,
|
||||||
|
&game_version,
|
||||||
|
) == game_version
|
||||||
|
})
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ProfileCreationError::ModloaderUnsupported(
|
ProfileCreationError::ModloaderUnsupported(
|
||||||
loader.to_string(),
|
loader.to_string(),
|
||||||
@@ -130,13 +137,19 @@ pub async fn profile_create(
|
|||||||
let path = canonicalize(&path)?;
|
let path = canonicalize(&path)?;
|
||||||
let mut profile = Profile::new(name, game_version, path.clone()).await?;
|
let mut profile = Profile::new(name, game_version, path.clone()).await?;
|
||||||
if let Some(ref icon) = icon {
|
if let Some(ref icon) = icon {
|
||||||
profile.with_icon(icon).await?;
|
profile.set_icon(icon).await?;
|
||||||
}
|
}
|
||||||
if let Some((loader_version, loader)) = loader {
|
if let Some((loader_version, loader)) = loader {
|
||||||
profile.with_loader(loader, Some(loader_version));
|
profile.metadata.loader = loader;
|
||||||
|
profile.metadata.loader_version = Some(loader_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.metadata.linked_project_id = linked_project_id;
|
||||||
|
{
|
||||||
|
let mut profiles = state.profiles.write().await;
|
||||||
|
profiles.insert(profile)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
profile::add(profile).await?;
|
|
||||||
State::sync().await?;
|
State::sync().await?;
|
||||||
|
|
||||||
Ok(path)
|
Ok(path)
|
||||||
|
|||||||
@@ -11,10 +11,19 @@ pub static BINCODE_CONFIG: Lazy<bincode::config::Configuration> =
|
|||||||
});
|
});
|
||||||
|
|
||||||
pub static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
|
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()
|
reqwest::Client::builder()
|
||||||
|
.timeout(time::Duration::from_secs(15))
|
||||||
.tcp_keepalive(Some(time::Duration::from_secs(10)))
|
.tcp_keepalive(Some(time::Duration::from_secs(10)))
|
||||||
|
.default_headers(headers)
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.expect("Reqwest Client Building Failed")
|
||||||
});
|
});
|
||||||
|
|
||||||
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";
|
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";
|
||||||
|
|||||||
@@ -67,10 +67,8 @@ pub enum ErrorKind {
|
|||||||
#[error("Recv error: {0}")]
|
#[error("Recv error: {0}")]
|
||||||
RecvError(#[from] tokio::sync::oneshot::error::RecvError),
|
RecvError(#[from] tokio::sync::oneshot::error::RecvError),
|
||||||
|
|
||||||
#[error(
|
#[error("Error acquiring semaphore: {0}")]
|
||||||
"Tried to access unloaded profile {0}, loading it probably failed"
|
AcquireError(#[from] tokio::sync::AcquireError),
|
||||||
)]
|
|
||||||
UnloadedProfileError(String),
|
|
||||||
|
|
||||||
#[error("Profile {0} is not managed by Theseus!")]
|
#[error("Profile {0} is not managed by Theseus!")]
|
||||||
UnmanagedProfileError(String),
|
UnmanagedProfileError(String),
|
||||||
@@ -78,6 +76,9 @@ pub enum ErrorKind {
|
|||||||
#[error("Could not create profile: {0}")]
|
#[error("Could not create profile: {0}")]
|
||||||
ProfileCreationError(#[from] profile_create::ProfileCreationError),
|
ProfileCreationError(#[from] profile_create::ProfileCreationError),
|
||||||
|
|
||||||
|
#[error("Zip error: {0}")]
|
||||||
|
ZipError(#[from] async_zip::error::ZipError),
|
||||||
|
|
||||||
#[error("Error: {0}")]
|
#[error("Error: {0}")]
|
||||||
OtherError(String),
|
OtherError(String),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub fn get_class_paths(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(get_lib_path(libraries_path, &library.name))
|
Some(get_lib_path(libraries_path, &library.name, false))
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
@@ -62,17 +62,25 @@ pub fn get_class_paths_jar<T: AsRef<str>>(
|
|||||||
) -> crate::Result<String> {
|
) -> crate::Result<String> {
|
||||||
let cps = libraries
|
let cps = libraries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|library| get_lib_path(libraries_path, library.as_ref()))
|
.map(|library| get_lib_path(libraries_path, library.as_ref(), false))
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
Ok(cps.join(classpath_separator()))
|
Ok(cps.join(classpath_separator()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_lib_path(libraries_path: &Path, lib: &str) -> crate::Result<String> {
|
pub fn get_lib_path(
|
||||||
|
libraries_path: &Path,
|
||||||
|
lib: &str,
|
||||||
|
allow_not_exist: bool,
|
||||||
|
) -> crate::Result<String> {
|
||||||
let mut path = libraries_path.to_path_buf();
|
let mut path = libraries_path.to_path_buf();
|
||||||
|
|
||||||
path.push(get_path_from_artifact(lib)?);
|
path.push(get_path_from_artifact(lib)?);
|
||||||
|
|
||||||
|
if !path.exists() && allow_not_exist {
|
||||||
|
return Ok(path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let path = &canonicalize(&path).map_err(|_| {
|
let path = &canonicalize(&path).map_err(|_| {
|
||||||
crate::ErrorKind::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Library file at path {} does not exist",
|
"Library file at path {} does not exist",
|
||||||
@@ -343,13 +351,14 @@ pub fn get_processor_arguments<T: AsRef<str>>(
|
|||||||
get_lib_path(
|
get_lib_path(
|
||||||
libraries_path,
|
libraries_path,
|
||||||
&entry.client[1..entry.client.len() - 1],
|
&entry.client[1..entry.client.len() - 1],
|
||||||
|
true,
|
||||||
)?
|
)?
|
||||||
} else {
|
} else {
|
||||||
entry.client.clone()
|
entry.client.clone()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if argument.as_ref().starts_with('[') {
|
} else if argument.as_ref().starts_with('[') {
|
||||||
new_arguments.push(get_lib_path(libraries_path, trimmed_arg)?)
|
new_arguments.push(get_lib_path(libraries_path, trimmed_arg, true)?)
|
||||||
} else {
|
} else {
|
||||||
new_arguments.push(argument.as_ref().to_string())
|
new_arguments.push(argument.as_ref().to_string())
|
||||||
}
|
}
|
||||||
@@ -361,7 +370,7 @@ pub fn get_processor_arguments<T: AsRef<str>>(
|
|||||||
pub async fn get_processor_main_class(
|
pub async fn get_processor_main_class(
|
||||||
path: String,
|
path: String,
|
||||||
) -> crate::Result<Option<String>> {
|
) -> crate::Result<Option<String>> {
|
||||||
tokio::task::spawn_blocking(move || {
|
let main_class = tokio::task::spawn_blocking(move || {
|
||||||
let zipfile = std::fs::File::open(&path)?;
|
let zipfile = std::fs::File::open(&path)?;
|
||||||
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
|
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
|
||||||
crate::ErrorKind::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
@@ -394,6 +403,7 @@ pub async fn get_processor_main_class(
|
|||||||
|
|
||||||
Ok::<Option<String>, crate::Error>(None)
|
Ok::<Option<String>, crate::Error>(None)
|
||||||
})
|
})
|
||||||
.await
|
.await??;
|
||||||
.unwrap()
|
|
||||||
|
Ok(main_class)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref HYDRA_URL: Url =
|
static ref HYDRA_URL: Url = Url::parse("https://hydra.modrinth.com")
|
||||||
Url::parse("https://hydra.modrinth.com").unwrap();
|
.expect("Hydra URL parse failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Socket messages
|
// Socket messages
|
||||||
|
|||||||
@@ -39,11 +39,12 @@ pub async fn download_version_info(
|
|||||||
version: &GameVersion,
|
version: &GameVersion,
|
||||||
loader: Option<&LoaderVersion>,
|
loader: Option<&LoaderVersion>,
|
||||||
) -> crate::Result<GameVersionInfo> {
|
) -> crate::Result<GameVersionInfo> {
|
||||||
let version_id = loader.map_or(&version.id, |it| &it.id);
|
let version_id = loader
|
||||||
|
.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
|
||||||
log::debug!("Loading version info for Minecraft {version_id}");
|
log::debug!("Loading version info for Minecraft {version_id}");
|
||||||
let path = st
|
let path = st
|
||||||
.directories
|
.directories
|
||||||
.version_dir(version_id)
|
.version_dir(&version_id)
|
||||||
.join(format!("{version_id}.json"));
|
.join(format!("{version_id}.json"));
|
||||||
|
|
||||||
let res = if path.exists() {
|
let res = if path.exists() {
|
||||||
@@ -58,10 +59,10 @@ pub async fn download_version_info(
|
|||||||
if let Some(loader) = loader {
|
if let Some(loader) = loader {
|
||||||
let partial = d::modded::fetch_partial_version(&loader.url).await?;
|
let partial = d::modded::fetch_partial_version(&loader.url).await?;
|
||||||
info = d::modded::merge_partial_version(partial, info);
|
info = d::modded::merge_partial_version(partial, info);
|
||||||
info.id = loader.id.clone();
|
|
||||||
}
|
}
|
||||||
|
info.id = version_id.clone();
|
||||||
|
|
||||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
let permit = st.io_semaphore.acquire().await?;
|
||||||
write(&path, &serde_json::to_vec(&info)?, &permit).await?;
|
write(&path, &serde_json::to_vec(&info)?, &permit).await?;
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}?;
|
}?;
|
||||||
@@ -92,7 +93,7 @@ pub async fn download_client(
|
|||||||
.join(format!("{version}.jar"));
|
.join(format!("{version}.jar"));
|
||||||
|
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
let permit = st.io_semaphore.acquire().await?;
|
||||||
let bytes =
|
let bytes =
|
||||||
fetch(&client_download.url, Some(&client_download.sha1), &permit)
|
fetch(&client_download.url, Some(&client_download.sha1), &permit)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -122,7 +123,7 @@ pub async fn download_assets_index(
|
|||||||
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
|
||||||
} else {
|
} else {
|
||||||
let index = d::minecraft::fetch_assets_index(version).await?;
|
let index = d::minecraft::fetch_assets_index(version).await?;
|
||||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
let permit = st.io_semaphore.acquire().await?;
|
||||||
write(&path, &serde_json::to_vec(&index)?, &permit).await?;
|
write(&path, &serde_json::to_vec(&index)?, &permit).await?;
|
||||||
log::info!("Fetched assets index");
|
log::info!("Fetched assets index");
|
||||||
Ok(index)
|
Ok(index)
|
||||||
@@ -141,7 +142,7 @@ pub async fn download_assets(
|
|||||||
log::debug!("Loading assets");
|
log::debug!("Loading assets");
|
||||||
stream::iter(index.objects.iter())
|
stream::iter(index.objects.iter())
|
||||||
.map(Ok::<(&String, &Asset), crate::Error>)
|
.map(Ok::<(&String, &Asset), crate::Error>)
|
||||||
.try_for_each_concurrent(Some(st.settings.read().await.max_concurrent_downloads), |(name, asset)| async move {
|
.try_for_each_concurrent(None, |(name, asset)| async move {
|
||||||
let hash = &asset.hash;
|
let hash = &asset.hash;
|
||||||
let resource_path = st.directories.object_dir(hash);
|
let resource_path = st.directories.object_dir(hash);
|
||||||
let url = format!(
|
let url = format!(
|
||||||
@@ -153,7 +154,7 @@ pub async fn download_assets(
|
|||||||
tokio::try_join! {
|
tokio::try_join! {
|
||||||
async {
|
async {
|
||||||
if !resource_path.exists() {
|
if !resource_path.exists() {
|
||||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
let permit = st.io_semaphore.acquire().await?;
|
||||||
let resource = fetch_cell
|
let resource = fetch_cell
|
||||||
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
|
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
|
||||||
.await?;
|
.await?;
|
||||||
@@ -164,7 +165,7 @@ pub async fn download_assets(
|
|||||||
},
|
},
|
||||||
async {
|
async {
|
||||||
if with_legacy {
|
if with_legacy {
|
||||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
let permit = st.io_semaphore.acquire().await?;
|
||||||
let resource = fetch_cell
|
let resource = fetch_cell
|
||||||
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
|
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
|
||||||
.await?;
|
.await?;
|
||||||
@@ -201,7 +202,7 @@ pub async fn download_libraries(
|
|||||||
|
|
||||||
stream::iter(libraries.iter())
|
stream::iter(libraries.iter())
|
||||||
.map(Ok::<&Library, crate::Error>)
|
.map(Ok::<&Library, crate::Error>)
|
||||||
.try_for_each_concurrent(Some(st.settings.read().await.max_concurrent_downloads), |library| async move {
|
.try_for_each_concurrent(None, |library| async move {
|
||||||
if let Some(rules) = &library.rules {
|
if let Some(rules) = &library.rules {
|
||||||
if !rules.iter().all(super::parse_rule) {
|
if !rules.iter().all(super::parse_rule) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -218,7 +219,7 @@ pub async fn download_libraries(
|
|||||||
artifact: Some(ref artifact),
|
artifact: Some(ref artifact),
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
let permit = st.io_semaphore.acquire().await?;
|
||||||
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &permit)
|
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &permit)
|
||||||
.await?;
|
.await?;
|
||||||
write(&path, &bytes, &permit).await?;
|
write(&path, &bytes, &permit).await?;
|
||||||
@@ -234,7 +235,7 @@ pub async fn download_libraries(
|
|||||||
&artifact_path
|
&artifact_path
|
||||||
].concat();
|
].concat();
|
||||||
|
|
||||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
let permit = st.io_semaphore.acquire().await?;
|
||||||
let bytes = fetch(&url, None, &permit).await?;
|
let bytes = fetch(&url, None, &permit).await?;
|
||||||
write(&path, &bytes, &permit).await?;
|
write(&path, &bytes, &permit).await?;
|
||||||
log::info!("Fetched library {}", &library.name);
|
log::info!("Fetched library {}", &library.name);
|
||||||
@@ -262,12 +263,17 @@ pub async fn download_libraries(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if let Some(native) = classifiers.get(&parsed_key) {
|
if let Some(native) = classifiers.get(&parsed_key) {
|
||||||
let permit = st.io_semaphore.acquire().await.unwrap();
|
let permit = st.io_semaphore.acquire().await?;
|
||||||
let data = fetch(&native.url, Some(&native.sha1), &permit).await?;
|
let data = fetch(&native.url, Some(&native.sha1), &permit).await?;
|
||||||
let reader = std::io::Cursor::new(&data);
|
let reader = std::io::Cursor::new(&data);
|
||||||
let mut archive = zip::ZipArchive::new(reader).unwrap();
|
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
|
||||||
archive.extract(&st.directories.version_natives_dir(version)).unwrap();
|
match archive.extract(&st.directories.version_natives_dir(version)) {
|
||||||
log::info!("Fetched native {}", &library.name);
|
Ok(_) => log::info!("Fetched native {}", &library.name),
|
||||||
|
Err(err) => log::error!("Failed extracting native {}. err: {}", &library.name, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::error!("Failed extracting native {}", &library.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,9 +72,10 @@ pub async fn launch_minecraft(
|
|||||||
"Invalid game version: {game_version}"
|
"Invalid game version: {game_version}"
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
let version_jar = loader_version
|
let version_jar =
|
||||||
.as_ref()
|
loader_version.as_ref().map_or(version.id.clone(), |it| {
|
||||||
.map_or(version.id.clone(), |it| it.id.clone());
|
format!("{}-{}", version.id.clone(), it.id.clone())
|
||||||
|
});
|
||||||
|
|
||||||
let mut version_info = download::download_version_info(
|
let mut version_info = download::download_version_info(
|
||||||
&state,
|
&state,
|
||||||
@@ -85,7 +86,7 @@ pub async fn launch_minecraft(
|
|||||||
|
|
||||||
let client_path = state
|
let client_path = state
|
||||||
.directories
|
.directories
|
||||||
.version_dir(&version.id)
|
.version_dir(&version_jar)
|
||||||
.join(format!("{version_jar}.jar"));
|
.join(format!("{version_jar}.jar"));
|
||||||
|
|
||||||
download::download_minecraft(&state, &version_info).await?;
|
download::download_minecraft(&state, &version_info).await?;
|
||||||
@@ -133,6 +134,7 @@ pub async fn launch_minecraft(
|
|||||||
args::get_processor_main_class(args::get_lib_path(
|
args::get_processor_main_class(args::get_lib_path(
|
||||||
&state.directories.libraries_dir(),
|
&state.directories.libraries_dir(),
|
||||||
&processor.jar,
|
&processor.jar,
|
||||||
|
false,
|
||||||
)?)
|
)?)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
@@ -193,7 +195,7 @@ pub async fn launch_minecraft(
|
|||||||
args::get_jvm_arguments(
|
args::get_jvm_arguments(
|
||||||
args.get(&d::minecraft::ArgumentType::Jvm)
|
args.get(&d::minecraft::ArgumentType::Jvm)
|
||||||
.map(|x| x.as_slice()),
|
.map(|x| x.as_slice()),
|
||||||
&state.directories.version_natives_dir(&version.id),
|
&state.directories.version_natives_dir(&version_jar),
|
||||||
&state.directories.libraries_dir(),
|
&state.directories.libraries_dir(),
|
||||||
&args::get_class_paths(
|
&args::get_class_paths(
|
||||||
&state.directories.libraries_dir(),
|
&state.directories.libraries_dir(),
|
||||||
@@ -205,7 +207,6 @@ pub async fn launch_minecraft(
|
|||||||
Vec::from(java_args),
|
Vec::from(java_args),
|
||||||
)?
|
)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| r.replace(' ', r"\ "))
|
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
.arg(version_info.main_class.clone())
|
.arg(version_info.main_class.clone())
|
||||||
@@ -223,7 +224,6 @@ pub async fn launch_minecraft(
|
|||||||
*resolution,
|
*resolution,
|
||||||
)?
|
)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| r.replace(' ', r"\ "))
|
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
.current_dir(instance_path.clone())
|
.current_dir(instance_path.clone())
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use daedalus::{
|
|||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use std::collections::LinkedList;
|
use std::collections::LinkedList;
|
||||||
|
|
||||||
const METADATA_URL: &str = "https://meta.modrinth.com/gamedata";
|
const METADATA_URL: &str = "https://meta.modrinth.com";
|
||||||
const METADATA_DB_FIELD: &[u8] = b"metadata";
|
const METADATA_DB_FIELD: &[u8] = b"metadata";
|
||||||
const RETRY_ATTEMPTS: i32 = 3;
|
const RETRY_ATTEMPTS: i32 = 3;
|
||||||
|
|
||||||
|
|||||||
@@ -77,17 +77,17 @@ impl State {
|
|||||||
let settings =
|
let settings =
|
||||||
Settings::init(&directories.settings_file()).await?;
|
Settings::init(&directories.settings_file()).await?;
|
||||||
|
|
||||||
// Launcher data
|
|
||||||
let (metadata, profiles) = tokio::try_join! {
|
|
||||||
Metadata::init(&database),
|
|
||||||
Profiles::init(&database, &directories),
|
|
||||||
}?;
|
|
||||||
let users = Users::init(&database)?;
|
|
||||||
|
|
||||||
// Loose initializations
|
// Loose initializations
|
||||||
let io_semaphore =
|
let io_semaphore =
|
||||||
Semaphore::new(settings.max_concurrent_downloads);
|
Semaphore::new(settings.max_concurrent_downloads);
|
||||||
|
|
||||||
|
// Launcher data
|
||||||
|
let (metadata, profiles) = tokio::try_join! {
|
||||||
|
Metadata::init(&database),
|
||||||
|
Profiles::init(&database, &directories, &io_semaphore),
|
||||||
|
}?;
|
||||||
|
let users = Users::init(&database)?;
|
||||||
|
|
||||||
let children = Children::new();
|
let children = Children::new();
|
||||||
|
|
||||||
let auth_flow = AuthTask::new();
|
let auth_flow = AuthTask::new();
|
||||||
@@ -133,8 +133,7 @@ impl State {
|
|||||||
reader.sync(&state.directories.settings_file()).await?;
|
reader.sync(&state.directories.settings_file()).await?;
|
||||||
Ok::<_, crate::Error>(())
|
Ok::<_, crate::Error>(())
|
||||||
})
|
})
|
||||||
.await
|
.await?
|
||||||
.unwrap()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let sync_profiles = async {
|
let sync_profiles = async {
|
||||||
@@ -148,15 +147,16 @@ impl State {
|
|||||||
profiles.sync(&mut batch).await?;
|
profiles.sync(&mut batch).await?;
|
||||||
Ok::<_, crate::Error>(())
|
Ok::<_, crate::Error>(())
|
||||||
})
|
})
|
||||||
.await
|
.await?
|
||||||
.unwrap()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::try_join!(sync_settings, sync_profiles)?;
|
tokio::try_join!(sync_settings, sync_profiles)?;
|
||||||
|
|
||||||
state
|
state.database.apply_batch(
|
||||||
.database
|
Arc::try_unwrap(batch)
|
||||||
.apply_batch(Arc::try_unwrap(batch).unwrap().into_inner())?;
|
.expect("Error saving state by acquiring Arc")
|
||||||
|
.into_inner(),
|
||||||
|
)?;
|
||||||
state.database.flush_async().await?;
|
state.database.flush_async().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
use tokio::sync::Semaphore;
|
||||||
|
|
||||||
const PROFILE_JSON_PATH: &str = "profile.json";
|
const PROFILE_JSON_PATH: &str = "profile.json";
|
||||||
const PROFILE_SUBTREE: &[u8] = b"profiles";
|
const PROFILE_SUBTREE: &[u8] = b"profiles";
|
||||||
|
|
||||||
pub(crate) struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
|
pub(crate) struct Profiles(pub HashMap<PathBuf, Profile>);
|
||||||
|
|
||||||
// TODO: possibly add defaults to some of these values
|
// TODO: possibly add defaults to some of these values
|
||||||
pub const CURRENT_FORMAT_VERSION: u32 = 1;
|
pub const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||||
@@ -30,7 +31,6 @@ pub struct Profile {
|
|||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub metadata: ProfileMetadata,
|
pub metadata: ProfileMetadata,
|
||||||
pub projects: HashMap<PathBuf, Project>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub java: Option<JavaSettings>,
|
pub java: Option<JavaSettings>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -39,6 +39,7 @@ pub struct Profile {
|
|||||||
pub resolution: Option<WindowSize>,
|
pub resolution: Option<WindowSize>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub hooks: Option<Hooks>,
|
pub hooks: Option<Hooks>,
|
||||||
|
pub projects: HashMap<PathBuf, Project>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
@@ -52,6 +53,7 @@ pub struct ProfileMetadata {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub loader_version: Option<LoaderVersion>,
|
pub loader_version: Option<LoaderVersion>,
|
||||||
pub format_version: u32,
|
pub format_version: u32,
|
||||||
|
pub linked_project_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Quilt?
|
// TODO: Quilt?
|
||||||
@@ -64,6 +66,7 @@ pub enum ModLoader {
|
|||||||
Vanilla,
|
Vanilla,
|
||||||
Forge,
|
Forge,
|
||||||
Fabric,
|
Fabric,
|
||||||
|
Quilt,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ModLoader {
|
impl std::fmt::Display for ModLoader {
|
||||||
@@ -72,6 +75,7 @@ impl std::fmt::Display for ModLoader {
|
|||||||
Self::Vanilla => "Vanilla",
|
Self::Vanilla => "Vanilla",
|
||||||
Self::Forge => "Forge",
|
Self::Forge => "Forge",
|
||||||
Self::Fabric => "Fabric",
|
Self::Fabric => "Fabric",
|
||||||
|
Self::Quilt => "Quilt",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,6 +111,7 @@ impl Profile {
|
|||||||
loader: ModLoader::Vanilla,
|
loader: ModLoader::Vanilla,
|
||||||
loader_version: None,
|
loader_version: None,
|
||||||
format_version: CURRENT_FORMAT_VERSION,
|
format_version: CURRENT_FORMAT_VERSION,
|
||||||
|
linked_project_id: None,
|
||||||
},
|
},
|
||||||
projects: HashMap::new(),
|
projects: HashMap::new(),
|
||||||
java: None,
|
java: None,
|
||||||
@@ -116,16 +121,8 @@ impl Profile {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: deduplicate these builder methods
|
|
||||||
// They are flat like this in order to allow builder-style usage
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub fn with_name(&mut self, name: String) -> &mut Self {
|
pub async fn set_icon<'a>(
|
||||||
self.metadata.name = name;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn with_icon<'a>(
|
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
icon: &'a Path,
|
icon: &'a Path,
|
||||||
) -> crate::Result<&'a mut Self> {
|
) -> crate::Result<&'a mut Self> {
|
||||||
@@ -149,54 +146,24 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
pub fn get_profile_project_paths(&self) -> crate::Result<Vec<PathBuf>> {
|
||||||
pub fn with_game_version(&mut self, version: String) -> &mut Self {
|
let mut files = Vec::new();
|
||||||
self.metadata.game_version = version;
|
let mut read_paths = |path: &str| {
|
||||||
self
|
let new_path = self.path.join(path);
|
||||||
}
|
if new_path.exists() {
|
||||||
|
for path in std::fs::read_dir(self.path.join(path))? {
|
||||||
|
files.push(path?.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok::<(), crate::Error>(())
|
||||||
|
};
|
||||||
|
|
||||||
#[tracing::instrument]
|
read_paths("mods")?;
|
||||||
pub fn with_loader(
|
read_paths("shaders")?;
|
||||||
&mut self,
|
read_paths("resourcepacks")?;
|
||||||
loader: ModLoader,
|
read_paths("datapacks")?;
|
||||||
version: Option<LoaderVersion>,
|
|
||||||
) -> &mut Self {
|
|
||||||
self.metadata.loader = loader;
|
|
||||||
self.metadata.loader_version = version;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
Ok(files)
|
||||||
pub fn with_java_settings(
|
|
||||||
&mut self,
|
|
||||||
settings: Option<JavaSettings>,
|
|
||||||
) -> &mut Self {
|
|
||||||
self.java = settings;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub fn with_memory(
|
|
||||||
&mut self,
|
|
||||||
settings: Option<MemorySettings>,
|
|
||||||
) -> &mut Self {
|
|
||||||
self.memory = settings;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub fn with_resolution(
|
|
||||||
&mut self,
|
|
||||||
resolution: Option<WindowSize>,
|
|
||||||
) -> &mut Self {
|
|
||||||
self.resolution = resolution;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub fn with_hooks(&mut self, hooks: Option<Hooks>) -> &mut Self {
|
|
||||||
self.hooks = hooks;
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +172,7 @@ impl Profiles {
|
|||||||
pub async fn init(
|
pub async fn init(
|
||||||
db: &sled::Db,
|
db: &sled::Db,
|
||||||
dirs: &DirectoryInfo,
|
dirs: &DirectoryInfo,
|
||||||
|
io_sempahore: &Semaphore,
|
||||||
) -> crate::Result<Self> {
|
) -> crate::Result<Self> {
|
||||||
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
|
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
|
||||||
Ok(Default::default()),
|
Ok(Default::default()),
|
||||||
@@ -229,38 +197,34 @@ impl Profiles {
|
|||||||
};
|
};
|
||||||
(path, prof)
|
(path, prof)
|
||||||
})
|
})
|
||||||
.collect::<HashMap<PathBuf, Option<Profile>>>()
|
.filter_map(|(key, opt_value)| async move {
|
||||||
|
opt_value.map(|value| (key, value))
|
||||||
|
})
|
||||||
|
.collect::<HashMap<PathBuf, Profile>>()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// project path, parent profile path
|
// project path, parent profile path
|
||||||
let mut files: HashMap<PathBuf, PathBuf> = HashMap::new();
|
let mut files: HashMap<PathBuf, PathBuf> = HashMap::new();
|
||||||
{
|
{
|
||||||
for (profile_path, _profile_opt) in profiles.iter() {
|
for (profile_path, profile) in profiles.iter() {
|
||||||
let mut read_paths = |path: &str| {
|
let paths = profile.get_profile_project_paths()?;
|
||||||
let new_path = profile_path.join(path);
|
|
||||||
if new_path.exists() {
|
for path in paths {
|
||||||
for path in std::fs::read_dir(profile_path.join(path))?
|
files.insert(path, profile_path.clone());
|
||||||
{
|
}
|
||||||
files.insert(path?.path(), profile_path.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok::<(), crate::Error>(())
|
|
||||||
};
|
|
||||||
read_paths("mods")?;
|
|
||||||
read_paths("shaders")?;
|
|
||||||
read_paths("resourcepacks")?;
|
|
||||||
read_paths("datapacks")?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let inferred = super::projects::infer_data_from_files(
|
let inferred = super::projects::infer_data_from_files(
|
||||||
files.keys().cloned().collect(),
|
files.keys().cloned().collect(),
|
||||||
dirs.caches_dir(),
|
dirs.caches_dir(),
|
||||||
|
io_sempahore,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for (key, value) in inferred {
|
for (key, value) in inferred {
|
||||||
if let Some(profile_path) = files.get(&key) {
|
if let Some(profile_path) = files.get(&key) {
|
||||||
if let Some(Some(profile)) = profiles.get_mut(profile_path) {
|
if let Some(profile) = profiles.get_mut(profile_path) {
|
||||||
profile.projects.insert(key, value);
|
profile.projects.insert(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,7 +242,7 @@ impl Profiles {
|
|||||||
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
|
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
|
||||||
)?
|
)?
|
||||||
.into(),
|
.into(),
|
||||||
Some(profile),
|
profile,
|
||||||
);
|
);
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
@@ -292,9 +256,15 @@ impl Profiles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
|
pub async fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
|
||||||
let path = PathBuf::from(&canonicalize(path)?.to_str().unwrap());
|
let path =
|
||||||
|
PathBuf::from(&canonicalize(path)?.to_string_lossy().to_string());
|
||||||
self.0.remove(&path);
|
self.0.remove(&path);
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
fs::remove_dir_all(path).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,8 +278,8 @@ impl Profiles {
|
|||||||
.try_for_each_concurrent(None, |(path, profile)| async move {
|
.try_for_each_concurrent(None, |(path, profile)| async move {
|
||||||
let json = serde_json::to_vec_pretty(&profile)?;
|
let json = serde_json::to_vec_pretty(&profile)?;
|
||||||
|
|
||||||
let json_path =
|
let json_path = Path::new(&path.to_string_lossy().to_string())
|
||||||
Path::new(path.to_str().unwrap()).join(PROFILE_JSON_PATH);
|
.join(PROFILE_JSON_PATH);
|
||||||
|
|
||||||
fs::write(json_path, json).await?;
|
fs::write(json_path, json).await?;
|
||||||
Ok::<_, crate::Error>(())
|
Ok::<_, crate::Error>(())
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
//! Project management + inference
|
//! Project management + inference
|
||||||
|
|
||||||
use crate::config::{MODRINTH_API_URL, REQWEST_CLIENT};
|
use crate::config::{MODRINTH_API_URL, REQWEST_CLIENT};
|
||||||
|
use crate::util::fetch::write_cached_icon;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use zip::ZipArchive;
|
use tokio::sync::Semaphore;
|
||||||
|
// use zip::ZipArchive;
|
||||||
|
use async_zip::tokio::read::fs::ZipFileReader;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
@@ -33,8 +33,8 @@ pub struct ModrinthProject {
|
|||||||
pub published: DateTime<Utc>,
|
pub published: DateTime<Utc>,
|
||||||
pub updated: DateTime<Utc>,
|
pub updated: DateTime<Utc>,
|
||||||
|
|
||||||
pub client_side: String,
|
pub client_side: SideType,
|
||||||
pub server_side: String,
|
pub server_side: SideType,
|
||||||
|
|
||||||
pub downloads: u32,
|
pub downloads: u32,
|
||||||
pub followers: u32,
|
pub followers: u32,
|
||||||
@@ -46,7 +46,75 @@ pub struct ModrinthProject {
|
|||||||
|
|
||||||
pub versions: Vec<String>,
|
pub versions: Vec<String>,
|
||||||
|
|
||||||
pub icon_url: String,
|
pub icon_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A specific version of a project
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct ModrinthVersion {
|
||||||
|
pub id: String,
|
||||||
|
pub project_id: String,
|
||||||
|
pub author_id: String,
|
||||||
|
|
||||||
|
pub featured: bool,
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
pub version_number: String,
|
||||||
|
pub changelog: String,
|
||||||
|
pub changelog_url: Option<String>,
|
||||||
|
|
||||||
|
pub date_published: DateTime<Utc>,
|
||||||
|
pub downloads: u32,
|
||||||
|
pub version_type: String,
|
||||||
|
|
||||||
|
pub files: Vec<ModrinthVersionFile>,
|
||||||
|
pub dependencies: Vec<Dependency>,
|
||||||
|
pub game_versions: Vec<String>,
|
||||||
|
pub loaders: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct ModrinthVersionFile {
|
||||||
|
pub hashes: HashMap<String, String>,
|
||||||
|
pub url: String,
|
||||||
|
pub filename: String,
|
||||||
|
pub primary: bool,
|
||||||
|
pub size: u32,
|
||||||
|
pub file_type: Option<FileType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Dependency {
|
||||||
|
pub version_id: Option<String>,
|
||||||
|
pub project_id: Option<String>,
|
||||||
|
pub file_name: Option<String>,
|
||||||
|
pub dependency_type: DependencyType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Copy, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum DependencyType {
|
||||||
|
Required,
|
||||||
|
Optional,
|
||||||
|
Incompatible,
|
||||||
|
Embedded,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum SideType {
|
||||||
|
Required,
|
||||||
|
Optional,
|
||||||
|
Unsupported,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum FileType {
|
||||||
|
RequiredResourcePack,
|
||||||
|
OptionalResourcePack,
|
||||||
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
@@ -63,9 +131,57 @@ pub enum ProjectMetadata {
|
|||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn read_icon_from_file(
|
||||||
|
icon_path: Option<String>,
|
||||||
|
cache_dir: &Path,
|
||||||
|
path: &PathBuf,
|
||||||
|
io_semaphore: &Semaphore,
|
||||||
|
) -> crate::Result<Option<PathBuf>> {
|
||||||
|
if let Some(icon_path) = icon_path {
|
||||||
|
// we have to repoen the zip twice here :(
|
||||||
|
let zip_file_reader = ZipFileReader::new(path).await;
|
||||||
|
if let Ok(zip_file_reader) = zip_file_reader {
|
||||||
|
// Get index of icon file and open it
|
||||||
|
let zip_index_option = zip_file_reader
|
||||||
|
.file()
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.position(|f| f.entry().filename() == icon_path);
|
||||||
|
if let Some(index) = zip_index_option {
|
||||||
|
let entry = zip_file_reader
|
||||||
|
.file()
|
||||||
|
.entries()
|
||||||
|
.get(index)
|
||||||
|
.unwrap()
|
||||||
|
.entry();
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
if zip_file_reader
|
||||||
|
.entry(zip_index_option.unwrap())
|
||||||
|
.await?
|
||||||
|
.read_to_end_checked(&mut bytes, entry)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
let bytes = bytes::Bytes::from(bytes);
|
||||||
|
let permit = io_semaphore.acquire().await?;
|
||||||
|
let path = write_cached_icon(
|
||||||
|
&icon_path, cache_dir, bytes, &permit,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(Some(path));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn infer_data_from_files(
|
pub async fn infer_data_from_files(
|
||||||
paths: Vec<PathBuf>,
|
paths: Vec<PathBuf>,
|
||||||
cache_dir: PathBuf,
|
cache_dir: PathBuf,
|
||||||
|
io_semaphore: &Semaphore,
|
||||||
) -> crate::Result<HashMap<PathBuf, Project>> {
|
) -> crate::Result<HashMap<PathBuf, Project>> {
|
||||||
let mut file_path_hashes = HashMap::new();
|
let mut file_path_hashes = HashMap::new();
|
||||||
|
|
||||||
@@ -139,51 +255,28 @@ pub async fn infer_data_from_files(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (hash, path) in further_analyze_projects {
|
for (hash, path) in further_analyze_projects {
|
||||||
let file = File::open(path.clone())?;
|
let zip_file_reader = if let Ok(zip_file_reader) =
|
||||||
|
ZipFileReader::new(path.clone()).await
|
||||||
// TODO: get rid of below unwrap
|
{
|
||||||
let mut zip = ZipArchive::new(file).unwrap();
|
zip_file_reader
|
||||||
|
} else {
|
||||||
let read_icon_from_file =
|
return_projects.insert(
|
||||||
|icon_path: Option<String>| -> crate::Result<Option<PathBuf>> {
|
path.clone(),
|
||||||
if let Some(icon_path) = icon_path {
|
Project {
|
||||||
// we have to repoen the zip twice here :(
|
sha512: hash,
|
||||||
let zip_file = File::open(path.clone())?;
|
disabled: path.ends_with(".disabled"),
|
||||||
if let Ok(mut zip) = ZipArchive::new(zip_file) {
|
metadata: ProjectMetadata::Unknown,
|
||||||
if let Ok(mut file) = zip.by_name(&icon_path) {
|
},
|
||||||
let mut bytes = Vec::new();
|
);
|
||||||
if file.read_to_end(&mut bytes).is_ok() {
|
continue;
|
||||||
let extension = Path::new(&icon_path)
|
};
|
||||||
.extension()
|
let zip_index_option = zip_file_reader
|
||||||
.and_then(OsStr::to_str);
|
.file()
|
||||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
.entries()
|
||||||
let path = cache_dir.join("icons").join(
|
.iter()
|
||||||
if let Some(ext) = extension {
|
.position(|f| f.entry().filename() == "META-INF/mods.toml");
|
||||||
format!("{hash}.{ext}")
|
if let Some(index) = zip_index_option {
|
||||||
} else {
|
let file = zip_file_reader.file().entries().get(index).unwrap();
|
||||||
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)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct ForgeModInfo {
|
struct ForgeModInfo {
|
||||||
@@ -201,18 +294,30 @@ pub async fn infer_data_from_files(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut file_str = String::new();
|
let mut file_str = String::new();
|
||||||
if file.read_to_string(&mut file_str).is_ok() {
|
if zip_file_reader
|
||||||
|
.entry(index)
|
||||||
|
.await?
|
||||||
|
.read_to_string_checked(&mut file_str, file.entry())
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
if let Ok(pack) =
|
if let Ok(pack) =
|
||||||
serde_json::from_str::<ForgeModInfo>(&file_str)
|
serde_json::from_str::<ForgeModInfo>(&file_str)
|
||||||
{
|
{
|
||||||
if let Some(pack) = pack.mods.first() {
|
if let Some(pack) = pack.mods.first() {
|
||||||
let icon = read_icon_from_file(pack.logo_file.clone())?;
|
let icon = read_icon_from_file(
|
||||||
|
pack.logo_file.clone(),
|
||||||
|
&cache_dir,
|
||||||
|
&path,
|
||||||
|
io_semaphore,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
return_projects.insert(
|
return_projects.insert(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
Project {
|
Project {
|
||||||
sha512: hash,
|
sha512: hash,
|
||||||
disabled: false,
|
disabled: path.ends_with(".disabled"),
|
||||||
metadata: ProjectMetadata::Inferred {
|
metadata: ProjectMetadata::Inferred {
|
||||||
title: Some(
|
title: Some(
|
||||||
pack.display_name
|
pack.display_name
|
||||||
@@ -236,7 +341,13 @@ pub async fn infer_data_from_files(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(mut file) = zip.by_name("mcmod.info") {
|
let zip_index_option = zip_file_reader
|
||||||
|
.file()
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.position(|f| f.entry().filename() == "mcmod.info");
|
||||||
|
if let Some(index) = zip_index_option {
|
||||||
|
let file = zip_file_reader.file().entries().get(index).unwrap();
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct ForgeMod {
|
struct ForgeMod {
|
||||||
@@ -249,15 +360,27 @@ pub async fn infer_data_from_files(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut file_str = String::new();
|
let mut file_str = String::new();
|
||||||
if file.read_to_string(&mut file_str).is_ok() {
|
if zip_file_reader
|
||||||
|
.entry(index)
|
||||||
|
.await?
|
||||||
|
.read_to_string_checked(&mut file_str, file.entry())
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
if let Ok(pack) = serde_json::from_str::<ForgeMod>(&file_str) {
|
if let Ok(pack) = serde_json::from_str::<ForgeMod>(&file_str) {
|
||||||
let icon = read_icon_from_file(pack.logo_file)?;
|
let icon = read_icon_from_file(
|
||||||
|
pack.logo_file,
|
||||||
|
&cache_dir,
|
||||||
|
&path,
|
||||||
|
io_semaphore,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
return_projects.insert(
|
return_projects.insert(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
Project {
|
Project {
|
||||||
sha512: hash,
|
sha512: hash,
|
||||||
disabled: false,
|
disabled: path.ends_with(".disabled"),
|
||||||
metadata: ProjectMetadata::Inferred {
|
metadata: ProjectMetadata::Inferred {
|
||||||
title: Some(if pack.name.is_empty() {
|
title: Some(if pack.name.is_empty() {
|
||||||
pack.modid
|
pack.modid
|
||||||
@@ -276,7 +399,13 @@ pub async fn infer_data_from_files(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(mut file) = zip.by_name("fabric.mod.json") {
|
let zip_index_option = zip_file_reader
|
||||||
|
.file()
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.position(|f| f.entry().filename() == "fabric.mod.json");
|
||||||
|
if let Some(index) = zip_index_option {
|
||||||
|
let file = zip_file_reader.file().entries().get(index).unwrap();
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
enum FabricAuthor {
|
enum FabricAuthor {
|
||||||
@@ -295,15 +424,27 @@ pub async fn infer_data_from_files(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut file_str = String::new();
|
let mut file_str = String::new();
|
||||||
if file.read_to_string(&mut file_str).is_ok() {
|
if zip_file_reader
|
||||||
|
.entry(index)
|
||||||
|
.await?
|
||||||
|
.read_to_string_checked(&mut file_str, file.entry())
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
if let Ok(pack) = serde_json::from_str::<FabricMod>(&file_str) {
|
if let Ok(pack) = serde_json::from_str::<FabricMod>(&file_str) {
|
||||||
let icon = read_icon_from_file(pack.icon)?;
|
let icon = read_icon_from_file(
|
||||||
|
pack.icon,
|
||||||
|
&cache_dir,
|
||||||
|
&path,
|
||||||
|
io_semaphore,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
return_projects.insert(
|
return_projects.insert(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
Project {
|
Project {
|
||||||
sha512: hash,
|
sha512: hash,
|
||||||
disabled: false,
|
disabled: path.ends_with(".disabled"),
|
||||||
metadata: ProjectMetadata::Inferred {
|
metadata: ProjectMetadata::Inferred {
|
||||||
title: Some(pack.name.unwrap_or(pack.id)),
|
title: Some(pack.name.unwrap_or(pack.id)),
|
||||||
description: pack.description,
|
description: pack.description,
|
||||||
@@ -325,7 +466,13 @@ pub async fn infer_data_from_files(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(mut file) = zip.by_name("quilt.mod.json") {
|
let zip_index_option = zip_file_reader
|
||||||
|
.file()
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.position(|f| f.entry().filename() == "quilt.mod.json");
|
||||||
|
if let Some(index) = zip_index_option {
|
||||||
|
let file = zip_file_reader.file().entries().get(index).unwrap();
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct QuiltMetadata {
|
struct QuiltMetadata {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
@@ -341,17 +488,27 @@ pub async fn infer_data_from_files(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut file_str = String::new();
|
let mut file_str = String::new();
|
||||||
if file.read_to_string(&mut file_str).is_ok() {
|
if zip_file_reader
|
||||||
|
.entry(index)
|
||||||
|
.await?
|
||||||
|
.read_to_string_checked(&mut file_str, file.entry())
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
if let Ok(pack) = serde_json::from_str::<QuiltMod>(&file_str) {
|
if let Ok(pack) = serde_json::from_str::<QuiltMod>(&file_str) {
|
||||||
let icon = read_icon_from_file(
|
let icon = read_icon_from_file(
|
||||||
pack.metadata.as_ref().and_then(|x| x.icon.clone()),
|
pack.metadata.as_ref().and_then(|x| x.icon.clone()),
|
||||||
)?;
|
&cache_dir,
|
||||||
|
&path,
|
||||||
|
io_semaphore,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
return_projects.insert(
|
return_projects.insert(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
Project {
|
Project {
|
||||||
sha512: hash,
|
sha512: hash,
|
||||||
disabled: false,
|
disabled: path.ends_with(".disabled"),
|
||||||
metadata: ProjectMetadata::Inferred {
|
metadata: ProjectMetadata::Inferred {
|
||||||
title: Some(
|
title: Some(
|
||||||
pack.metadata
|
pack.metadata
|
||||||
@@ -383,23 +540,39 @@ pub async fn infer_data_from_files(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(mut file) = zip.by_name("pack.mcmeta") {
|
let zip_index_option = zip_file_reader
|
||||||
|
.file()
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.position(|f| f.entry().filename() == "pack.mcdata");
|
||||||
|
if let Some(index) = zip_index_option {
|
||||||
|
let file = zip_file_reader.file().entries().get(index).unwrap();
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Pack {
|
struct Pack {
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut file_str = String::new();
|
let mut file_str = String::new();
|
||||||
if file.read_to_string(&mut file_str).is_ok() {
|
if zip_file_reader
|
||||||
|
.entry(index)
|
||||||
|
.await?
|
||||||
|
.read_to_string_checked(&mut file_str, file.entry())
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
if let Ok(pack) = serde_json::from_str::<Pack>(&file_str) {
|
if let Ok(pack) = serde_json::from_str::<Pack>(&file_str) {
|
||||||
let icon =
|
let icon = read_icon_from_file(
|
||||||
read_icon_from_file(Some("pack.png".to_string()))?;
|
Some("pack.png".to_string()),
|
||||||
|
&cache_dir,
|
||||||
|
&path,
|
||||||
|
io_semaphore,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
return_projects.insert(
|
return_projects.insert(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
Project {
|
Project {
|
||||||
sha512: hash,
|
sha512: hash,
|
||||||
disabled: false,
|
disabled: path.ends_with(".disabled"),
|
||||||
metadata: ProjectMetadata::Inferred {
|
metadata: ProjectMetadata::Inferred {
|
||||||
title: None,
|
title: None,
|
||||||
description: pack.description,
|
description: pack.description,
|
||||||
@@ -415,10 +588,10 @@ pub async fn infer_data_from_files(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return_projects.insert(
|
return_projects.insert(
|
||||||
path,
|
path.clone(),
|
||||||
Project {
|
Project {
|
||||||
sha512: hash,
|
sha512: hash,
|
||||||
disabled: false,
|
disabled: path.ends_with(".disabled"),
|
||||||
metadata: ProjectMetadata::Unknown,
|
metadata: ProjectMetadata::Unknown,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,76 +1,85 @@
|
|||||||
//! Functions for fetching infromation from the Internet
|
//! Functions for fetching infromation from the Internet
|
||||||
use crate::config::REQWEST_CLIENT;
|
use crate::config::REQWEST_CLIENT;
|
||||||
use futures::prelude::*;
|
use bytes::Bytes;
|
||||||
use std::{collections::LinkedList, convert::TryInto, path::Path, sync::Arc};
|
use std::ffi::OsStr;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::AsyncWriteExt,
|
io::AsyncWriteExt,
|
||||||
sync::{Semaphore, SemaphorePermit},
|
sync::SemaphorePermit,
|
||||||
};
|
};
|
||||||
|
|
||||||
const FETCH_ATTEMPTS: usize = 3;
|
const FETCH_ATTEMPTS: usize = 3;
|
||||||
|
|
||||||
|
/// Downloads a file with retry and checksum functionality
|
||||||
#[tracing::instrument(skip(_permit))]
|
#[tracing::instrument(skip(_permit))]
|
||||||
pub async fn fetch<'a>(
|
pub async fn fetch<'a>(
|
||||||
url: &str,
|
url: &str,
|
||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
_permit: &SemaphorePermit<'a>,
|
_permit: &SemaphorePermit<'a>,
|
||||||
) -> crate::Result<bytes::Bytes> {
|
) -> crate::Result<bytes::Bytes> {
|
||||||
let mut attempts = LinkedList::new();
|
for attempt in 1..=(FETCH_ATTEMPTS + 1) {
|
||||||
for _ in 0..FETCH_ATTEMPTS {
|
let result = REQWEST_CLIENT.get(url).send().await;
|
||||||
attempts.push_back(
|
|
||||||
async {
|
|
||||||
let content = REQWEST_CLIENT.get(url).send().await?;
|
|
||||||
let bytes = content.bytes().await?;
|
|
||||||
|
|
||||||
if let Some(hash) = sha1 {
|
match result {
|
||||||
let actual_hash = sha1_async(bytes.clone()).await;
|
Ok(x) => {
|
||||||
if actual_hash != hash {
|
let bytes = x.bytes().await;
|
||||||
return Err(crate::ErrorKind::HashError(
|
|
||||||
actual_hash,
|
if let Ok(bytes) = bytes {
|
||||||
String::from(hash),
|
if let Some(sha1) = sha1 {
|
||||||
)
|
let hash = sha1_async(bytes.clone()).await?;
|
||||||
.into());
|
if &*hash != sha1 {
|
||||||
|
if attempt <= 3 {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return Err(crate::ErrorKind::HashError(
|
||||||
|
sha1.to_string(),
|
||||||
|
hash,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(bytes)
|
log::debug!("Done downloading URL {url}");
|
||||||
|
return Ok(bytes);
|
||||||
|
} else if attempt <= 3 {
|
||||||
|
continue;
|
||||||
|
} else if let Err(err) = bytes {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.boxed(),
|
Err(_) if attempt <= 3 => continue,
|
||||||
)
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Done downloading URL {url}");
|
unreachable!()
|
||||||
future::select_ok(attempts).map_ok(|it| it.0).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is implemented, as it will be useful in porting modpacks
|
/// Downloads a file from specified mirrors
|
||||||
// For now, allow it to be dead code
|
#[tracing::instrument(skip(permit))]
|
||||||
#[allow(dead_code)]
|
pub async fn fetch_mirrors<'a>(
|
||||||
#[tracing::instrument(skip(sem))]
|
mirrors: &[&str],
|
||||||
pub async fn fetch_mirrors(
|
|
||||||
urls: &[&str],
|
|
||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
permits: u32,
|
permit: &SemaphorePermit<'a>,
|
||||||
sem: &Semaphore,
|
|
||||||
) -> crate::Result<bytes::Bytes> {
|
) -> crate::Result<bytes::Bytes> {
|
||||||
let _permits = sem.acquire_many(permits).await.unwrap();
|
if mirrors.is_empty() {
|
||||||
let sem = Arc::new(Semaphore::new(permits.try_into().unwrap()));
|
return Err(crate::ErrorKind::InputError(
|
||||||
|
"No mirrors provided!".to_string(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
future::select_ok(urls.iter().map(|url| {
|
for (index, mirror) in mirrors.iter().enumerate() {
|
||||||
let sha1 = sha1.map(String::from);
|
let result = fetch(mirror, sha1, permit).await;
|
||||||
let url = String::from(*url);
|
|
||||||
let sem = Arc::clone(&sem);
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) {
|
||||||
let permit = sem.acquire().await.unwrap();
|
return result;
|
||||||
fetch(&url, sha1.as_deref(), &permit).await
|
}
|
||||||
})
|
}
|
||||||
.map(Result::unwrap)
|
|
||||||
.boxed()
|
unreachable!()
|
||||||
}))
|
|
||||||
.await
|
|
||||||
.map(|it| it.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(bytes, _permit))]
|
#[tracing::instrument(skip(bytes, _permit))]
|
||||||
@@ -89,8 +98,31 @@ pub async fn write<'a>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sha1_async(bytes: bytes::Bytes) -> String {
|
#[tracing::instrument(skip(bytes, permit))]
|
||||||
tokio::task::spawn_blocking(move || sha1::Sha1::from(bytes).hexdigest())
|
pub async fn write_cached_icon<'a>(
|
||||||
.await
|
icon_path: &str,
|
||||||
.unwrap()
|
cache_dir: &Path,
|
||||||
|
bytes: Bytes,
|
||||||
|
permit: &SemaphorePermit<'a>,
|
||||||
|
) -> crate::Result<PathBuf> {
|
||||||
|
let extension = Path::new(&icon_path).extension().and_then(OsStr::to_str);
|
||||||
|
let hash = sha1_async(bytes.clone()).await?;
|
||||||
|
let path = cache_dir.join("icons").join(if let Some(ext) = extension {
|
||||||
|
format!("{hash}.{ext}")
|
||||||
|
} else {
|
||||||
|
hash
|
||||||
|
});
|
||||||
|
|
||||||
|
write(&path, &bytes, permit).await?;
|
||||||
|
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sha1_async(bytes: Bytes) -> crate::Result<String> {
|
||||||
|
let hash = tokio::task::spawn_blocking(move || {
|
||||||
|
sha1::Sha1::from(bytes).hexdigest()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(hash)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,7 +209,8 @@ pub fn check_java_at_filepath(path: PathBuf) -> Option<JavaVersion> {
|
|||||||
// Extracting version numbers
|
// Extracting version numbers
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref JAVA_VERSION_CAPTURE: Regex =
|
static ref JAVA_VERSION_CAPTURE: Regex =
|
||||||
Regex::new(r#"version "([\d\._]+)""#).unwrap();
|
Regex::new(r#"version "([\d\._]+)""#)
|
||||||
|
.expect("Error creating java version capture regex");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract version info from it
|
// Extract version info from it
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use paris::*;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tabled::Tabled;
|
use tabled::Tabled;
|
||||||
use theseus::prelude::*;
|
use theseus::prelude::*;
|
||||||
|
use theseus::profile_create::profile_create;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio_stream::wrappers::ReadDirStream;
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
|
|
||||||
@@ -24,53 +25,12 @@ pub struct ProfileCommand {
|
|||||||
#[derive(argh::FromArgs, Debug)]
|
#[derive(argh::FromArgs, Debug)]
|
||||||
#[argh(subcommand)]
|
#[argh(subcommand)]
|
||||||
pub enum ProfileSubcommand {
|
pub enum ProfileSubcommand {
|
||||||
Add(ProfileAdd),
|
|
||||||
Init(ProfileInit),
|
Init(ProfileInit),
|
||||||
List(ProfileList),
|
List(ProfileList),
|
||||||
Remove(ProfileRemove),
|
Remove(ProfileRemove),
|
||||||
Run(ProfileRun),
|
Run(ProfileRun),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
#[argh(subcommand, name = "add")]
|
|
||||||
/// add a new profile to Theseus
|
|
||||||
pub struct ProfileAdd {
|
|
||||||
#[argh(positional, default = "std::env::current_dir().unwrap()")]
|
|
||||||
/// the profile to add
|
|
||||||
profile: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProfileAdd {
|
|
||||||
pub async fn run(
|
|
||||||
&self,
|
|
||||||
_args: &crate::Args,
|
|
||||||
_largs: &ProfileCommand,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!(
|
|
||||||
"Adding profile at path '{}' to Theseus",
|
|
||||||
self.profile.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
let profile = canonicalize(&self.profile)?;
|
|
||||||
let json_path = profile.join("profile.json");
|
|
||||||
|
|
||||||
ensure!(
|
|
||||||
json_path.exists(),
|
|
||||||
"Profile json does not exist. Perhaps you wanted `profile init` or `profile fetch`?"
|
|
||||||
);
|
|
||||||
ensure!(
|
|
||||||
!profile::is_managed(&profile).await?,
|
|
||||||
"Profile already managed by Theseus. If the contents of the profile are invalid or missing, the profile can be regenerated using `profile init` or `profile fetch`"
|
|
||||||
);
|
|
||||||
|
|
||||||
profile::add_path(&profile).await?;
|
|
||||||
State::sync().await?;
|
|
||||||
success!("Profile added!");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
#[derive(argh::FromArgs, Debug)]
|
||||||
#[argh(subcommand, name = "init")]
|
#[argh(subcommand, name = "init")]
|
||||||
/// create a new profile and manage it with Theseus
|
/// create a new profile and manage it with Theseus
|
||||||
@@ -87,10 +47,6 @@ pub struct ProfileInit {
|
|||||||
/// the game version of the profile
|
/// the game version of the profile
|
||||||
game_version: Option<String>,
|
game_version: Option<String>,
|
||||||
|
|
||||||
#[argh(option)]
|
|
||||||
/// the icon for the profile
|
|
||||||
icon: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[argh(option, from_str_fn(modloader_from_str))]
|
#[argh(option, from_str_fn(modloader_from_str))]
|
||||||
/// the modloader to use
|
/// the modloader to use
|
||||||
modloader: Option<ModLoader>,
|
modloader: Option<ModLoader>,
|
||||||
@@ -230,28 +186,15 @@ impl ProfileInit {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let icon = match &self.icon {
|
profile_create(
|
||||||
Some(icon) => Some(icon.clone()),
|
name,
|
||||||
None => Some(
|
game_version,
|
||||||
prompt_async("Icon".to_owned(), Some(String::new())).await?,
|
loader.clone().map(|x| x.1).unwrap_or(ModLoader::Vanilla),
|
||||||
)
|
loader.map(|x| x.0.id),
|
||||||
.filter(|it| !it.trim().is_empty())
|
None,
|
||||||
.map(PathBuf::from),
|
None,
|
||||||
};
|
)
|
||||||
|
.await?;
|
||||||
let mut profile =
|
|
||||||
Profile::new(name, game_version, self.path.clone()).await?;
|
|
||||||
|
|
||||||
if let Some(ref icon) = icon {
|
|
||||||
profile.with_icon(icon).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((loader_version, loader)) = loader {
|
|
||||||
profile.with_loader(loader, Some(loader_version));
|
|
||||||
}
|
|
||||||
|
|
||||||
profile::add(profile).await?;
|
|
||||||
State::sync().await?;
|
|
||||||
|
|
||||||
success!(
|
success!(
|
||||||
"Successfully created instance, it is now available to use with Theseus!"
|
"Successfully created instance, it is now available to use with Theseus!"
|
||||||
@@ -312,12 +255,7 @@ impl ProfileList {
|
|||||||
_largs: &ProfileCommand,
|
_largs: &ProfileCommand,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let profiles = profile::list().await?;
|
let profiles = profile::list().await?;
|
||||||
let rows = profiles.iter().map(|(path, prof)| {
|
let rows = profiles.values().map(ProfileRow::from);
|
||||||
prof.as_ref().map_or_else(
|
|
||||||
|| ProfileRow::from(path.as_path()),
|
|
||||||
ProfileRow::from,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let table = table(rows).with(
|
let table = table(rows).with(
|
||||||
tabled::Modify::new(tabled::Column(1..=1))
|
tabled::Modify::new(tabled::Column(1..=1))
|
||||||
@@ -348,13 +286,9 @@ impl ProfileRemove {
|
|||||||
info!("Removing profile {} from Theseus", self.profile.display());
|
info!("Removing profile {} from Theseus", self.profile.display());
|
||||||
|
|
||||||
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
||||||
if !profile::is_managed(&profile).await? {
|
profile::remove(&profile).await?;
|
||||||
warn!("Profile was not managed by Theseus!");
|
State::sync().await?;
|
||||||
} else {
|
success!("Profile removed!");
|
||||||
profile::remove(&profile).await?;
|
|
||||||
State::sync().await?;
|
|
||||||
success!("Profile removed!");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
error!("Aborted!");
|
error!("Aborted!");
|
||||||
}
|
}
|
||||||
@@ -385,11 +319,6 @@ impl ProfileRun {
|
|||||||
info!("Starting profile at path {}...", self.profile.display());
|
info!("Starting profile at path {}...", self.profile.display());
|
||||||
let path = canonicalize(&self.profile)?;
|
let path = canonicalize(&self.profile)?;
|
||||||
|
|
||||||
ensure!(
|
|
||||||
profile::is_managed(&path).await?,
|
|
||||||
"Profile not managed by Theseus (if it exists, try using `profile add` first!)",
|
|
||||||
);
|
|
||||||
|
|
||||||
let id = future::ready(self.user.ok_or(()))
|
let id = future::ready(self.user.ok_or(()))
|
||||||
.or_else(|_| async move {
|
.or_else(|_| async move {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
@@ -415,7 +344,6 @@ impl ProfileRun {
|
|||||||
impl ProfileCommand {
|
impl ProfileCommand {
|
||||||
pub async fn run(&self, args: &crate::Args) -> Result<()> {
|
pub async fn run(&self, args: &crate::Args) -> Result<()> {
|
||||||
dispatch!(&self.action, (args, self) => {
|
dispatch!(&self.action, (args, self) => {
|
||||||
ProfileSubcommand::Add,
|
|
||||||
ProfileSubcommand::Init,
|
ProfileSubcommand::Init,
|
||||||
ProfileSubcommand::List,
|
ProfileSubcommand::List,
|
||||||
ProfileSubcommand::Remove,
|
ProfileSubcommand::Remove,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use thiserror::Error;
|
|||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
|
||||||
|
pub mod pack;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod profile_create;
|
pub mod profile_create;
|
||||||
|
|||||||
17
theseus_gui/src-tauri/src/api/pack.rs
Normal file
17
theseus_gui/src-tauri/src/api/pack.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use crate::api::Result;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use theseus::prelude::*;
|
||||||
|
|
||||||
|
// Creates a pack from a version ID (returns a path to the created profile)
|
||||||
|
// invoke('pack_install_version_id', version_id)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn pack_install_version_id(version_id: String) -> Result<PathBuf> {
|
||||||
|
let res = pack::install_pack_from_version_id(version_id).await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn pack_install_file(path: &Path) -> Result<PathBuf> {
|
||||||
|
let res = pack::install_pack_from_file(path.to_path_buf()).await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
@@ -2,30 +2,11 @@ use crate::api::Result;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use theseus::prelude::*;
|
use theseus::prelude::*;
|
||||||
|
|
||||||
// Add a profile to the in-memory state
|
|
||||||
// invoke('profile_add',profile)
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn profile_add(profile: Profile) -> Result<()> {
|
|
||||||
profile::add(profile).await?;
|
|
||||||
State::sync().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a path as a profile in-memory
|
|
||||||
// invoke('profile_add_path',path)
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn profile_add_path(path: &Path) -> Result<()> {
|
|
||||||
profile::add_path(path).await?;
|
|
||||||
State::sync().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a profile
|
// Remove a profile
|
||||||
// invoke('profile_add_path',path)
|
// invoke('profile_add_path',path)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn profile_remove(path: &Path) -> Result<()> {
|
pub async fn profile_remove(path: &Path) -> Result<()> {
|
||||||
profile::remove(path).await?;
|
profile::remove(path).await?;
|
||||||
State::sync().await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,25 +15,6 @@ pub async fn profile_remove(path: &Path) -> Result<()> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn profile_get(path: &Path) -> Result<Option<Profile>> {
|
pub async fn profile_get(path: &Path) -> Result<Option<Profile>> {
|
||||||
let res = profile::get(path).await?;
|
let res = profile::get(path).await?;
|
||||||
State::sync().await?;
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a profile is already managed by Theseus
|
|
||||||
// invoke('profile_is_managed',profile)
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn profile_is_managed(profile: &Path) -> Result<bool> {
|
|
||||||
let res = profile::is_managed(profile).await?;
|
|
||||||
State::sync().await?;
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a profile is loaded
|
|
||||||
// invoke('profile_is_loaded',profile)
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn profile_is_loaded(profile: &Path) -> Result<bool> {
|
|
||||||
let res = profile::is_loaded(profile).await?;
|
|
||||||
State::sync().await?;
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,9 +22,8 @@ pub async fn profile_is_loaded(profile: &Path) -> Result<bool> {
|
|||||||
// invoke('profile_list')
|
// invoke('profile_list')
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn profile_list(
|
pub async fn profile_list(
|
||||||
) -> Result<std::collections::HashMap<PathBuf, Option<Profile>>> {
|
) -> Result<std::collections::HashMap<PathBuf, Profile>> {
|
||||||
let res = profile::list().await?;
|
let res = profile::list().await?;
|
||||||
State::sync().await?;
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,10 +32,7 @@ pub async fn profile_list(
|
|||||||
// for the actual Child in the state.
|
// for the actual Child in the state.
|
||||||
// invoke('profile_run')
|
// invoke('profile_run')
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn profile_run(
|
pub async fn profile_run(path: &Path, credentials: Credentials) -> Result<u32> {
|
||||||
path: &Path,
|
|
||||||
credentials: theseus::auth::Credentials,
|
|
||||||
) -> Result<u32> {
|
|
||||||
let proc_lock = profile::run(path, &credentials).await?;
|
let proc_lock = profile::run(path, &credentials).await?;
|
||||||
let pid = proc_lock.read().await.child.id().ok_or_else(|| {
|
let pid = proc_lock.read().await.child.id().ok_or_else(|| {
|
||||||
theseus::Error::from(theseus::ErrorKind::LauncherError(
|
theseus::Error::from(theseus::ErrorKind::LauncherError(
|
||||||
@@ -89,7 +47,7 @@ pub async fn profile_run(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn profile_run_wait(
|
pub async fn profile_run_wait(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
credentials: theseus::auth::Credentials,
|
credentials: Credentials,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let proc_lock = profile::run(path, &credentials).await?;
|
let proc_lock = profile::run(path, &credentials).await?;
|
||||||
let mut proc = proc_lock.write().await;
|
let mut proc = proc_lock.write().await;
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ pub async fn profile_create(
|
|||||||
name,
|
name,
|
||||||
game_version,
|
game_version,
|
||||||
modloader,
|
modloader,
|
||||||
loader_version,
|
Some(loader_version),
|
||||||
icon,
|
icon,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
State::sync().await?;
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,13 @@ fn main() {
|
|||||||
initialize_state,
|
initialize_state,
|
||||||
api::profile_create::profile_create_empty,
|
api::profile_create::profile_create_empty,
|
||||||
api::profile_create::profile_create,
|
api::profile_create::profile_create,
|
||||||
api::profile::profile_add,
|
|
||||||
api::profile::profile_add_path,
|
|
||||||
api::profile::profile_remove,
|
api::profile::profile_remove,
|
||||||
api::profile::profile_get,
|
api::profile::profile_get,
|
||||||
api::profile::profile_is_managed,
|
|
||||||
api::profile::profile_is_loaded,
|
|
||||||
api::profile::profile_list,
|
api::profile::profile_list,
|
||||||
api::profile::profile_run,
|
api::profile::profile_run,
|
||||||
api::profile::profile_run_wait,
|
api::profile::profile_run_wait,
|
||||||
|
api::pack::pack_install_version_id,
|
||||||
|
api::pack::pack_install_file,
|
||||||
api::auth::auth_authenticate_begin_flow,
|
api::auth::auth_authenticate_begin_flow,
|
||||||
api::auth::auth_authenticate_await_completion,
|
api::auth::auth_authenticate_await_completion,
|
||||||
api::auth::auth_refresh,
|
api::auth::auth_refresh,
|
||||||
|
|||||||
16
theseus_gui/src/helpers/pack.js
Normal file
16
theseus_gui/src/helpers/pack.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* All theseus API calls return serialized values (both return values and errors);
|
||||||
|
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||||
|
* and deserialized into a usable JS object.
|
||||||
|
*/
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
|
|
||||||
|
// Installs pack from a version ID
|
||||||
|
export async function install(version_id) {
|
||||||
|
return await invoke('pack_install_version_id', version_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installs pack from a path
|
||||||
|
export async function install_from_file(path) {
|
||||||
|
return await invoke('pack_install_file', path)
|
||||||
|
}
|
||||||
@@ -16,16 +16,6 @@ export async function create() {
|
|||||||
return await invoke('profile_create')
|
return await invoke('profile_create')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a profile to the in-memory state
|
|
||||||
export async function add(profile) {
|
|
||||||
return await invoke('profile_add', { profile })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a path as a profile in-memory
|
|
||||||
export async function add_path(path) {
|
|
||||||
return await invoke('profile_add_path', { path })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a profile
|
// Remove a profile
|
||||||
export async function remove(path) {
|
export async function remove(path) {
|
||||||
return await invoke('profile_remove', { path })
|
return await invoke('profile_remove', { path })
|
||||||
@@ -37,18 +27,6 @@ export async function get(path) {
|
|||||||
return await invoke('profile_get', { path })
|
return await invoke('profile_get', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a pathed profile is already managed by Theseus
|
|
||||||
// Returns bool
|
|
||||||
export async function is_managed(path) {
|
|
||||||
return await invoke('profile_is_managed', { path })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a pathed profile is loaded
|
|
||||||
// Returns bool
|
|
||||||
export async function is_loaded(path) {
|
|
||||||
return await invoke('profile_is_loaded', { path })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a copy of the profile set
|
// Get a copy of the profile set
|
||||||
// Returns hashmap of path -> Profile
|
// Returns hashmap of path -> Profile
|
||||||
export async function list() {
|
export async function list() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use dunce::canonicalize;
|
use dunce::canonicalize;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use theseus::{prelude::*, profile_create::profile_create};
|
use theseus::prelude::*;
|
||||||
use tokio::time::{sleep, Duration};
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
// A simple Rust implementation of the authentication run
|
// A simple Rust implementation of the authentication run
|
||||||
@@ -32,42 +32,23 @@ async fn main() -> theseus::Result<()> {
|
|||||||
|
|
||||||
// Initialize state
|
// Initialize state
|
||||||
let st = State::get().await?;
|
let st = State::get().await?;
|
||||||
|
st.settings.write().await.max_concurrent_downloads = 100;
|
||||||
// Set max concurrent downloads to 10
|
|
||||||
st.settings.write().await.max_concurrent_downloads = 10;
|
|
||||||
|
|
||||||
// Example variables for simple project case
|
|
||||||
let name = "Example".to_string();
|
|
||||||
let game_version = "1.19.2".to_string();
|
|
||||||
let modloader = ModLoader::Vanilla;
|
|
||||||
let loader_version = "stable".to_string();
|
|
||||||
|
|
||||||
// let icon = Some(
|
|
||||||
// Path::new("../icon_test.png")
|
|
||||||
// .canonicalize()
|
|
||||||
// .expect("Icon could be not be found. If not using, set to None"),
|
|
||||||
// );
|
|
||||||
let icon = None;
|
|
||||||
|
|
||||||
// Clear profiles
|
// Clear profiles
|
||||||
println!("Clearing profiles.");
|
println!("Clearing profiles.");
|
||||||
let h = profile::list().await?;
|
{
|
||||||
for (path, _) in h.into_iter() {
|
let h = profile::list().await?;
|
||||||
profile::remove(&path).await?;
|
for (path, _) in h.into_iter() {
|
||||||
|
profile::remove(&path).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Creating/adding profile.");
|
println!("Creating/adding profile.");
|
||||||
// Attempt to create a profile. If that fails, try adding one from the same path.
|
|
||||||
// TODO: actually do a nested error check for the correct profile error.
|
let profile_path =
|
||||||
let profile_path = profile_create(
|
pack::install_pack_from_version_id("KxUUUFh5".to_string())
|
||||||
name.clone(),
|
.await
|
||||||
game_version,
|
.unwrap();
|
||||||
modloader,
|
|
||||||
loader_version,
|
|
||||||
icon,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
State::sync().await?;
|
|
||||||
|
|
||||||
// async closure for testing any desired edits
|
// async closure for testing any desired edits
|
||||||
// (ie: changing the java runtime of an added profile)
|
// (ie: changing the java runtime of an added profile)
|
||||||
|
|||||||
Reference in New Issue
Block a user