From b1f0c9039eab1c660e206664f26484e2f462ad2b Mon Sep 17 00:00:00 2001 From: Lucas van Beek Date: Fri, 16 Dec 2022 22:29:55 +0100 Subject: [PATCH] Mega refactor --- package.json | 1 + src-tauri/Cargo.lock | 174 ++++++++++++ src-tauri/Cargo.toml | 5 +- src-tauri/src/main.rs | 396 +++++++++++++++++---------- src-tauri/src/mrpack.rs | 1 + src-tauri/src/mrpack/modrinthpack.rs | 74 +++++ src-tauri/src/types.rs | 27 ++ src-tauri/tauri.conf.json | 5 + src/api/downloadModpack.ts | 9 - src/api/filesystem.ts | 31 +++ src/api/getMinecraftDirectory.ts | 5 - src/api/getModpacks.ts | 69 ----- src/api/isDirectory.ts | 11 - src/api/isMinecraftDirectory.ts | 13 - src/api/modpacks.ts | 32 +++ src/api/types.ts | 6 + src/main.ts | 7 +- src/pages/Confirmation.vue | 24 +- src/pages/DirectorySelector.vue | 115 ++++---- src/pages/Installing.vue | 51 ++-- src/pages/ModpackSelector.vue | 50 +--- src/pages/Wrapper.vue | 46 +--- src/state/modpackStore.ts | 25 ++ yarn.lock | 18 ++ 24 files changed, 779 insertions(+), 416 deletions(-) create mode 100644 src-tauri/src/mrpack.rs create mode 100644 src-tauri/src/mrpack/modrinthpack.rs create mode 100644 src-tauri/src/types.rs delete mode 100644 src/api/downloadModpack.ts create mode 100644 src/api/filesystem.ts delete mode 100644 src/api/getMinecraftDirectory.ts delete mode 100644 src/api/getModpacks.ts delete mode 100644 src/api/isDirectory.ts delete mode 100644 src/api/isMinecraftDirectory.ts create mode 100644 src/api/modpacks.ts create mode 100644 src/api/types.ts create mode 100644 src/state/modpackStore.ts diff --git a/package.json b/package.json index cf18d7e..c67cff9 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@headlessui/vue": "^1.7.5", "@heroicons/vue": "^2.0.13", "@tauri-apps/api": "^1.2.0", + "pinia": "^2.0.28", "vue": "^3.2.45" }, "devDependencies": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c43c725..97de6db 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "opaque-debug", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -51,6 +63,7 @@ checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" name = "app" version = "0.1.0" dependencies = [ + "anyhow", "base64 0.20.0", "chrono", "directories", @@ -62,6 +75,8 @@ dependencies = [ "tauri", "tauri-build", "tokio", + "walkdir", + "zip", ] [[package]] @@ -122,6 +137,12 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" +[[package]] +name = "base64ct" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" + [[package]] name = "bit_field" version = "0.10.1" @@ -203,6 +224,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +[[package]] +name = "bzip2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.15.12" @@ -242,6 +284,9 @@ name = "cc" version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" +dependencies = [ + "jobserver", +] [[package]] name = "cesu8" @@ -298,6 +343,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "cocoa" version = "0.24.1" @@ -355,6 +409,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "convert_case" version = "0.4.0" @@ -633,6 +693,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1258,6 +1319,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "html5ever" version = "0.25.2" @@ -1525,6 +1595,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.3.0" @@ -1947,6 +2026,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "open" version = "3.2.0" @@ -2077,6 +2162,17 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.9" @@ -2089,6 +2185,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -2833,6 +2941,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.6" @@ -3008,6 +3127,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.105" @@ -4234,3 +4359,52 @@ name = "xml-rs" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "zip" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537ce7411d25e54e8ae21a7ce0b15840e7bfcff15b51d697ec3266cc76bdf080" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time 0.3.17", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.4+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa202f2ef00074143e219d15b62ffc317d17cc33909feac471c044087cad7b0" +dependencies = [ + "cc", + "libc", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c9c0873..01cc3fd 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -6,7 +6,7 @@ authors = ["you"] license = "GPL-3.0" repository = "" default-run = "app" -edition = "2018" +edition = "2021" build = "src/build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -25,6 +25,9 @@ chrono = "0.4" futures-util = "0.3.25" image = "0.24.5" base64 = "0.20.0" +zip = "0.6.3" +anyhow = "1.0" +walkdir = "2.3.2" [features] default = [ "custom-protocol" ] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5dbe8ef..88e3210 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,9 +3,28 @@ windows_subsystem = "windows" )] +mod mrpack; +mod types; +use anyhow::Result; +use std::fs; +use std::io::{BufReader, Cursor, Write}; +use std::path::Path; +use std::time::SystemTime; +use std::{fs::File, path::PathBuf}; + +use chrono::{DateTime, Utc}; use futures_util::StreamExt; -use std::io::{Write, Cursor}; -use image::io::Reader as ImageReader; +use image::DynamicImage; +use mrpack::modrinthpack::ModrinthPack; +use reqwest::Response; +use serde_json::{Map, Value}; +use types::*; +use walkdir::WalkDir; + +use crate::mrpack::modrinthpack::ModrinthManifest; +// use image::io::Reader as ImageReader; + +const BASE_URL: &str = "https://modpack.vloedje.nl"; fn main() { tauri::Builder::default() @@ -13,7 +32,7 @@ fn main() { get_minecraft_directory, directory_exists, path_join, - download_modpack + install_modpack ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); @@ -59,169 +78,266 @@ fn path_join(path_string: String, second_path_string: String) -> String { path.join(second_path).to_string_lossy().into() } -#[derive(serde::Serialize, Clone)] -struct ProgressPayload { - current_mod: String, - mod_index: usize, - progress: u64, -} - -#[derive(serde::Serialize, Clone)] -struct FinishedPayload { - finished: bool, - errors: bool, -} - -#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] -struct Mod { - id: String, - name: String, - filename: String, - state: String, - url: String, - hash: String, -} - -#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] -struct ModConfig { - id: String, - name: String, - filename: String, - state: String, - url: String, - hash: String, -} - - -#[allow(non_snake_case)] -#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] -struct Modpack { - id: String, - name: String, - description: String, - minecraftVersion: String, - loaderVersion: String, - mods: Vec, - configs: Vec, -} - -async fn install_loader(minecraft_version: &String, loader_version: &String, gamepath: &std::path::Path) { - - let version_string = format!("twilight-{}-{}", loader_version, minecraft_version); - let version_dir = gamepath.join("versions").join(&version_string); - std::fs::create_dir_all(&version_dir).unwrap(); - - let profile_resp = reqwest::get(format!("https://modpack.vloedje.nl/profile/{}/{}.json", minecraft_version, loader_version)).await.unwrap().text().await.unwrap(); - let mut file = std::fs::File::create(version_dir.join(format!("{}.json", &version_string))).unwrap(); - std::io::copy(&mut profile_resp.as_bytes(), &mut file).unwrap(); +async fn install_loader( + loader_id: &String, + minecraft_version: &String, + loader_version: &String, + gamepath: &std::path::Path, +) -> Result<()> { + let version_dir = gamepath.join("versions").join(&loader_id); + fs::create_dir_all(&version_dir).expect("Unable to create loader directory"); + + let response: Response = reqwest::get(format!( + "https://meta.quiltmc.org/v3/versions/loader/{}/{}/profile/json", + minecraft_version, loader_version + )) + .await + .expect("Unable to fetch loader configuration"); + let mut loader_config: Value = response + .json::() + .await + .expect("Unable to get body from loader configuration response"); + let obj = loader_config.as_object_mut().expect(""); + obj.insert("id".to_owned(), Value::String(loader_id.to_owned())); + + let file: File = File::create(version_dir.join(format!("{}.json", &loader_id))) + .expect("Unable to create loader config"); + serde_json::to_writer_pretty(file, &loader_config).expect("Unable to write to loader config"); + + Ok(()) } -async fn install_profile(gamepath: &std::path::Path, modpack: &Modpack, application_dir: &std::path::Path) { - let profiles_file = std::fs::File::open(gamepath.join("launcher_profiles.json")).unwrap(); - let mut profiles: serde_json::Value = serde_json::from_reader(std::io::BufReader::new(profiles_file)).unwrap(); - let now: chrono::DateTime = std::time::SystemTime::now().into(); - - let context = tauri::generate_context!(); - let img_path = tauri::api::path::resolve_path(context.config(), context.package_info(), &tauri::Env::default(), "resources/profile.png", Some(tauri::api::path::BaseDirectory::Resource)).unwrap(); - println!("{}", img_path.to_string_lossy()); - let img = ImageReader::open(img_path.to_string_lossy().to_string()).unwrap().decode().unwrap(); - let mut buf = vec![]; - img.write_to(&mut Cursor::new(&mut buf), image::ImageOutputFormat::Png).unwrap(); - let res_base64 = base64::encode(&buf); - - let profile = serde_json::json!({ +async fn install_profile( + gamepath: &std::path::Path, + modpack: &Modpack, + application_dir: &std::path::Path, + icon_path: &PathBuf, +) -> Result<()> { + let profiles_file: File = File::open(gamepath.join("launcher_profiles.json")) + .expect("Unable to open launcher_profiles.json"); + let mut profiles: Value = serde_json::from_reader(BufReader::new(&profiles_file)) + .expect("Unable to parse launcher_profiles.json"); + let now: DateTime = SystemTime::now().into(); + + let img: DynamicImage = image::open(icon_path).expect("Unable to open profile icon"); + let mut buf: Vec = vec![]; + img + .write_to(&mut Cursor::new(&mut buf), image::ImageOutputFormat::Png) + .expect("Unable to write profile icon to buffer"); + let res_base64: String = base64::encode(&buf); + + let profile: Value = serde_json::json!({ "created": now.to_rfc3339(), "gameDir": application_dir, "lastUsed": now.to_rfc3339(), - "lastVersionId": format!("twilight-{}-{}", &modpack.loaderVersion, &modpack.minecraftVersion), + "lastVersionId": &modpack.id, "type": "custom", "icon": format!("data:image/png;base64,{}", res_base64), "name": &modpack.name }); - let obj = profiles.get_mut("profiles").unwrap().as_object_mut().unwrap(); - obj.insert(modpack.id.clone(), profile); - - serde_json::to_writer(std::fs::File::create(gamepath.join("launcher_profiles.json")).unwrap(), &profiles).unwrap(); + let obj: &mut Map = profiles + .get_mut("profiles") + .expect("Unable to get profiles from json") + .as_object_mut() + .expect("Unable to make profile mutable"); + obj.insert(modpack.id.to_owned(), profile); + + let profiles_file_write: File = File::create(gamepath.join("launcher_profiles.json")) + .expect("Unable to open launcher_profiles for writing"); + serde_json::to_writer_pretty(profiles_file_write, &profiles).expect("Unable to write profiles"); + + Ok(()) } -async fn download_mod(mods_dir: &std::path::Path, mcmod: &Mod, window: &tauri::Window, index: usize) { - let resp = reqwest::get(&mcmod.url).await.unwrap(); - let total_size = resp.content_length().unwrap(); +async fn download_file(url: &str, path: &PathBuf, window: &tauri::Window, emit_progress: bool) { + let resp: Response = reqwest::get(url) + .await + .expect(format!("Error downloading file from {}", url).as_str()); let mut stream = resp.bytes_stream(); - let mut file = std::fs::File::create(mods_dir.join(&mcmod.filename)).unwrap(); - let mut downloaded: u64 = 0; + let mut file: File = File::create(path) + .expect(format!("Error creating file at {}", path.to_str().unwrap()).as_str()); while let Some(item) = stream.next().await { - let chunk = item.unwrap(); - file.write(&chunk).unwrap(); - downloaded = downloaded + (chunk.len() as u64); + let chunk = item.expect(format!("Error downloading file from {}", url).as_str()); + file + .write(&chunk) + .expect(format!("Error writing to file at {}", path.to_str().unwrap()).as_str()); + + if emit_progress { + window + .emit( + "install-progress", + ProgressPayload { + bytes: chunk.len() as u64, + }, + ) + .expect("Error while transmitting install progress"); + } + } +} - let percentage: f64 = (downloaded as f64) / (total_size as f64) * 100.0; +async fn get_mrpack( + modpack: &Modpack, + modpack_dir: &PathBuf, + window: &tauri::Window, +) -> Result { + let modpack_file_name: String = format!("{}-{}.mrpack", modpack.id, modpack.version); + let modpack_url: String = format!("{}/{}", BASE_URL, modpack_file_name); + let modpack_zip_path: PathBuf = modpack_dir.join(&modpack_file_name); + download_file(&modpack_url, &modpack_zip_path, &window, false).await; - window + let modpack_file = File::open(&modpack_zip_path).expect("Unable to open modpack file"); + + Ok(modpack_file) +} + +#[tauri::command] +async fn install_modpack( + modpack: serde_json::Value, + gamepath: String, + window: tauri::Window, + handle: tauri::AppHandle, +) -> Result<(), tauri::Error> { + let modpack: Modpack = serde_json::from_value(modpack).expect("Unable to map modpack data"); + let gamepath: &Path = Path::new(&gamepath); + + let modpack_dir: PathBuf = gamepath.join(".vloedje").join(&modpack.id); + fs::create_dir_all(&modpack_dir).expect("Unable to create modpack folder"); + + let pack_cache_dir: &PathBuf = &modpack_dir.join(".pack"); + + if pack_cache_dir.is_dir() && pack_cache_dir.starts_with(gamepath) { + fs::remove_dir_all(pack_cache_dir).expect("Unable to clear cache dir"); + } + + let modpack_file: File = get_mrpack(&modpack, &modpack_dir, &window) + .await + .expect("Unable to download modpack file"); + let modrinthpack: ModrinthPack = + ModrinthPack::from_mrpack(modpack_file, &modpack_dir).expect("Unable to extract modpack"); + let manifest: ModrinthManifest = modrinthpack + .get_manifest() + .expect("Unable to read modpack index"); + let manifest_files = &manifest.files; + + let total_filesize = manifest_files.into_iter().map(|x| x.file_size).sum(); + window .emit( - "install-progress", - ProgressPayload { - current_mod: mcmod.name.clone(), - mod_index: index + 1, - progress: percentage as u64, + "total-filesize", + TotalFileSizePayload { + total_bytes: total_filesize, }, ) .unwrap(); - } -} - -async fn configure_mod(config_dir: &std::path::Path, modconfig: &ModConfig) { - let resp = reqwest::get(&modconfig.url).await.unwrap(); - - let file_path = config_dir.join(std::path::Path::new(&modconfig.filename)); - std::fs::create_dir_all(file_path.parent().unwrap()).unwrap(); + install_loader( + &modpack.id, + &manifest.dependencies.minecraft, + &manifest.dependencies.quilt_loader, + &gamepath, + ) + .await + .expect("Unable to install loader"); + + let profile_icon: PathBuf = handle + .path_resolver() + .resolve_resource("resources/profile.png") + .expect("Unable to resolve profile icon"); + + install_profile(&gamepath, &modpack, &modpack_dir, &profile_icon) + .await + .expect("Unable to install profile"); + + let has_prev_paths = modpack_dir.join(".changed_paths").is_file(); + + if has_prev_paths { + let prev_paths: Vec = fs::read_to_string(&modpack_dir.join(".changed_paths")) + .expect("Error reading changed paths") + .split("\n") + .map(|s| s.to_string()) + .collect(); + + for file in WalkDir::new(&modpack_dir) + .into_iter() + .filter_map(|f| f.ok()) + { + if file.metadata().unwrap().is_file() { + let path: &Path = file + .path() + .strip_prefix(&modpack_dir) + .expect("Path not in modpack directory"); + let path_str: &str = path.to_str().expect("cannot convert path to str"); + let pack_cache_dir: &PathBuf = &modpack_dir.join(".pack"); + + let is_in_prev_paths: bool = prev_paths.contains(&path_str.to_owned()); + let is_in_manifest: bool = manifest_files + .into_iter() + .find(|&f| f.path == path_str) + .is_some(); + let is_in_overrides: bool = pack_cache_dir.join("overrides").join(path).is_file(); + let is_in_client_overrides: bool = + pack_cache_dir.join("client-overrides").join(path).is_file(); + + if is_in_prev_paths && !(is_in_manifest || is_in_overrides || is_in_client_overrides) { + println!("{}", &path_str); + + if path.starts_with(gamepath) { + fs::remove_file(path).expect("Unable to remove old file"); + } + } + } + } + } - let mut file = std::fs::File::create(file_path).unwrap(); + let mut changed_paths: Vec = vec![]; - let content = resp.text().await.unwrap(); - std::io::copy(&mut content.as_bytes(), &mut file).unwrap(); -} + for file in manifest_files.into_iter() { + changed_paths.push(file.path.to_owned()); -#[tauri::command] -async fn download_modpack(modpack: serde_json::Value, gamepath: String, window: tauri::Window) { - let modpack: Modpack = serde_json::from_value(modpack).unwrap(); - let gamepath = std::path::Path::new(&gamepath); - - install_loader(&modpack.minecraftVersion, &modpack.loaderVersion, &gamepath).await; - - let application_dir = gamepath.join(".twilight").join(&modpack.id); - std::fs::create_dir_all(&application_dir).unwrap(); - - install_profile(&gamepath, &modpack, &application_dir).await; - - let mods_dir = application_dir.join("mods"); - std::fs::create_dir_all(&mods_dir).unwrap(); - - let config_dir = application_dir.join("config"); - std::fs::create_dir_all(&config_dir).unwrap(); - - tauri::async_runtime::spawn(async move { - for (i, mcmod) in modpack.mods.iter().enumerate() { - download_mod(&mods_dir, &mcmod, &window, i).await; + let file_path = &modpack_dir.join(file.path.to_owned()); + if file_path.starts_with(&modpack_dir) { + fs::create_dir_all(file_path.parent().unwrap()).expect("Error creating parent directory"); + download_file(&file.downloads[0], file_path, &window, true).await; } + } - for (_i, modconfig) in modpack.configs.iter().enumerate() { - configure_mod(&config_dir, &modconfig).await; + let override_paths: [&str; 2] = ["overrides", "client-overrides"]; + + for override_path in override_paths.into_iter() { + for file in WalkDir::new(&modpack_dir.join(".pack").join(override_path)) + .into_iter() + .filter_map(|f| f.ok()) + { + if file.metadata().unwrap().is_file() { + let path = file + .path() + .strip_prefix(&modpack_dir.join(".pack").join(override_path)) + .expect("this is awkward"); + changed_paths.push(path.to_str().unwrap().to_owned()); + + fs::create_dir_all(modpack_dir.join(path).parent().unwrap()) + .expect("Error creating parent directory"); + + fs::copy(file.path(), modpack_dir.join(path)).expect("Unable to copy override file"); + } } + } - let mut lockfile = std::fs::File::create(application_dir.join("modpack.json")).unwrap(); - std::io::copy(&mut serde_json::to_string(&modpack).unwrap().as_bytes(), &mut lockfile).unwrap(); - - window - .emit( - "install-done", - FinishedPayload { - finished: true, - errors: false, - }, - ) - .unwrap(); - }); + fs::write( + &modpack_dir.join(".changed_paths"), + changed_paths.join("\n"), + ) + .expect("Error writing changed paths"); + + window + .emit( + "install-done", + FinishedPayload { + finished: true, + errors: false, + }, + ) + .unwrap(); + + Ok(()) } diff --git a/src-tauri/src/mrpack.rs b/src-tauri/src/mrpack.rs new file mode 100644 index 0000000..6f10b0f --- /dev/null +++ b/src-tauri/src/mrpack.rs @@ -0,0 +1 @@ +pub mod modrinthpack; \ No newline at end of file diff --git a/src-tauri/src/mrpack/modrinthpack.rs b/src-tauri/src/mrpack/modrinthpack.rs new file mode 100644 index 0000000..3f24a2a --- /dev/null +++ b/src-tauri/src/mrpack/modrinthpack.rs @@ -0,0 +1,74 @@ +use std::{fs::{File, self}, path::{PathBuf}}; +use serde::{Serialize, Deserialize}; +use zip::ZipArchive; +use anyhow::{Result}; + +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] +pub struct ModrinthFileHashes { + pub sha1: String, + pub sha512: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum ModrinthFileEnvTypes { + Required, + Optional, + Unsupported +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct ModrinthFileEnv { + pub client: ModrinthFileEnvTypes, + pub server: ModrinthFileEnvTypes, +} + + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct ModrinthFile { + pub path: String, + pub downloads: Vec, + #[serde(alias = "fileSize")] + pub file_size: u64, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct ModrinthDependencies { + pub minecraft: String, + #[serde(alias = "quilt-loader")] + pub quilt_loader: String, +} +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct ModrinthManifest { + #[serde(alias = "formatVersion")] + pub format_version: u8, + pub game: String, + #[serde(alias = "versionId")] + pub version_id: String, + pub name: String, + pub files: Vec, + pub dependencies: ModrinthDependencies, +} +pub struct ModrinthPack { + path: PathBuf +} + +impl ModrinthPack { + pub fn from_mrpack(file: File, path: &PathBuf) -> Result { + let mut archive: ZipArchive = ZipArchive::new(file).expect(""); + let path: PathBuf = path.join(".pack"); + std::fs::create_dir_all(&path).expect("Unable to create modpack folder"); + archive.extract(&path).expect("Unable to extract modpack"); + + Ok(ModrinthPack { + path + }) + } + + pub fn get_manifest(&self) -> Result { + let manifest_string = fs::read_to_string(&self.path.join("modrinth.index.json")).expect("Unable to read manifset"); + let modpack_manifest: ModrinthManifest = serde_json::from_str(&manifest_string).expect("Unable to map data"); + + Ok(modpack_manifest) + } + +} \ No newline at end of file diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs new file mode 100644 index 0000000..d726b31 --- /dev/null +++ b/src-tauri/src/types.rs @@ -0,0 +1,27 @@ +use serde::{Serialize}; + + + + #[derive(Serialize, Clone)] + pub struct ProgressPayload { + pub bytes: u64, + } + + #[derive(Serialize, Clone)] + pub struct TotalFileSizePayload { + pub total_bytes: u64, + } + + #[derive(Serialize, Clone)] + pub struct FinishedPayload { + pub finished: bool, + pub errors: bool, + } + #[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] + pub struct Modpack { + pub id: String, + pub name: String, + pub description: String, + pub version: String + } + diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 43b7804..31d732f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -54,6 +54,11 @@ "all": true, "request": true, "scope": ["https://modpack.vloedje.nl/**"] + }, + "fs": { + "readDir": true, + "all": false, + "scope": ["*"] } }, "windows": [ diff --git a/src/api/downloadModpack.ts b/src/api/downloadModpack.ts deleted file mode 100644 index 255d6bd..0000000 --- a/src/api/downloadModpack.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { invoke } from '@tauri-apps/api'; -import { Modpack } from './getModpacks'; - -export default async (data: Modpack, path: String) => { - invoke('download_modpack', { - modpack: data, - gamepath: path - }); -} \ No newline at end of file diff --git a/src/api/filesystem.ts b/src/api/filesystem.ts new file mode 100644 index 0000000..e859a8c --- /dev/null +++ b/src/api/filesystem.ts @@ -0,0 +1,31 @@ +import { fs, invoke } from "@tauri-apps/api"; + +export const isDirectory = async (directory: string): Promise => { + try { + await fs.readDir(directory); + + } catch(e) { + console.log(e); + return false; + } + return true; + +} + +export const isMinecraftDirectory = async (directory: string): Promise => { + try { + const entries = await fs.readDir(directory); + + if (!entries.find(x => x.name == "launcher_profiles.json")) return false; + if (!entries.find(x => x.name == "versions" && x.children)) return false; + + return true; + } catch { + return false; + } + +} + +export const getMinecraftDirectory = (): Promise => { + return invoke('get_minecraft_directory'); +} \ No newline at end of file diff --git a/src/api/getMinecraftDirectory.ts b/src/api/getMinecraftDirectory.ts deleted file mode 100644 index 3ecb4ae..0000000 --- a/src/api/getMinecraftDirectory.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { invoke } from '@tauri-apps/api'; - -export default (): Promise => { - return invoke('get_minecraft_directory'); -} \ No newline at end of file diff --git a/src/api/getModpacks.ts b/src/api/getModpacks.ts deleted file mode 100644 index b22cc28..0000000 --- a/src/api/getModpacks.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { fetch } from "@tauri-apps/api/http"; - -export interface Mod { - id: string; - name: string; - filename: string; - state: string; - url: string; - hash: string; -} - -export interface ModConfig { - id: string; - name: string; - filename: string; - state: string; - url: string; - hash: string; -} - -export interface Modpack { - id: string; - name: string; - description: string; - minecraftVersion: string; - loaderVersion: string; - mods: Mod[]; - configs: ModConfig[], -} - -export interface ModpackRequest { - modpacks: []; -} - -export const rawDataToModpackObject = (data: any): Modpack => ({ - id: data.id, - name: data.name, - description: data.description, - minecraftVersion: data.minecraft_version, - loaderVersion: data.loader_version, - mods: (data.mods || []).map(rawDataToModObject), - configs: (data.configs || []).map(rawDataToModConfigObject), -}); - -export const rawDataToModObject = (data: any): Mod => ({ - id: data.id, - name: data.name, - filename: data.filename, - state: data.state, - url: data.url, - hash: data.hash, -}); - -export const rawDataToModConfigObject = (data: any): ModConfig => ({ - id: data.id, - name: data.name, - filename: data.filename, - state: data.state, - url: data.url, - hash: data.hash, -}); - -export default async (): Promise => { - const result = await fetch('https://modpack.vloedje.nl/test.json'); - console.log(result); - const { modpacks } = result.data; - - return modpacks.map(rawDataToModpackObject); -} \ No newline at end of file diff --git a/src/api/isDirectory.ts b/src/api/isDirectory.ts deleted file mode 100644 index af3f7c9..0000000 --- a/src/api/isDirectory.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { path } from '@tauri-apps/api'; - -export default async (directory: string): Promise => { - try { - await path.resolve(directory); - } catch { - return false; - } - return true; - -} \ No newline at end of file diff --git a/src/api/isMinecraftDirectory.ts b/src/api/isMinecraftDirectory.ts deleted file mode 100644 index 76e5f54..0000000 --- a/src/api/isMinecraftDirectory.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { path } from '@tauri-apps/api'; - -export default async (directory: string): Promise => { - try { - await path.resolve(directory); - await path.resolve(directory, 'versions'); - await path.resolve(directory, 'launcher_profiles.json'); - } catch { - return false; - } - return true; - -} \ No newline at end of file diff --git a/src/api/modpacks.ts b/src/api/modpacks.ts new file mode 100644 index 0000000..09419e5 --- /dev/null +++ b/src/api/modpacks.ts @@ -0,0 +1,32 @@ +import { invoke } from "@tauri-apps/api"; +import { fetch } from "@tauri-apps/api/http"; +import { Modpack } from "./types"; + +const BASE_URL = "https://modpack.vloedje.nl"; + +interface getModpackResponse { + modpacks: Modpack[], +} + +export const getModpacks = async (): Promise => { + const result = await fetch(BASE_URL + '/modpacks.json') + + if (!result.ok) { + throw Error('Unable to reach modpacks api: error code ' + result.status) + } + + const { modpacks } = result.data + + if (modpacks.length == 0) { + throw Error('Unable to fetch modpacks: api returned empty array') + } + + return modpacks; +} + +export const downloadModpack = async (data: Modpack, path: String) => { + await invoke('install_modpack', { + modpack: data, + gamepath: path + }); +} \ No newline at end of file diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..f7ac3f2 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,6 @@ +export type Modpack = { + id: string; + name: string; + description: string; + version: string; +}; \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 367c450..6a02221 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,11 @@ +import { createPinia } from 'pinia' import { createApp } from 'vue' import App from './App.vue' import './assets/tailwind.pcss' -createApp(App).mount('#app') +const pinia = createPinia() +const app = createApp(App) + +app.use(pinia) +app.mount('#app') diff --git a/src/pages/Confirmation.vue b/src/pages/Confirmation.vue index d6781a2..0b0c36f 100644 --- a/src/pages/Confirmation.vue +++ b/src/pages/Confirmation.vue @@ -3,21 +3,21 @@

Modpack

- +
- +

Minecraft directory

- +
- +
@@ -25,19 +25,11 @@ \ No newline at end of file diff --git a/src/pages/DirectorySelector.vue b/src/pages/DirectorySelector.vue index dda76da..cf4c6f2 100644 --- a/src/pages/DirectorySelector.vue +++ b/src/pages/DirectorySelector.vue @@ -1,3 +1,55 @@ + + - - \ No newline at end of file + \ No newline at end of file diff --git a/src/pages/Installing.vue b/src/pages/Installing.vue index 57b8d03..cf2e057 100644 --- a/src/pages/Installing.vue +++ b/src/pages/Installing.vue @@ -2,40 +2,33 @@

Installing: - +

-
+
-

> \ No newline at end of file diff --git a/src/pages/Wrapper.vue b/src/pages/Wrapper.vue index 386534a..341158e 100644 --- a/src/pages/Wrapper.vue +++ b/src/pages/Wrapper.vue @@ -1,6 +1,5 @@