From f84c61d8aadc05dca873d344fed88a41bdb02b6d Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Sun, 1 Feb 2026 17:21:12 +0800 Subject: [PATCH 01/15] feat: links --- .../src/game_version/get_origin_version.rs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src-tauri/src/game_version/get_origin_version.rs diff --git a/src-tauri/src/game_version/get_origin_version.rs b/src-tauri/src/game_version/get_origin_version.rs new file mode 100644 index 0000000..5cc83dd --- /dev/null +++ b/src-tauri/src/game_version/get_origin_version.rs @@ -0,0 +1,92 @@ +pub mod get_origin_version { + const version_suffix: &str = "/mc/game/version_manifest.json"; // http://launchermeta.mojang.com/mc/game/version_manifest.json + const version2_suffix: &str = "/mc/game/version_manifest_v2.json"; // http://launchermeta.mojang.com/mc/game/version_manifest_v2.json + + pub mod heads { + pub mod mojang_heads { + const launcher: &str = "https://launcher.mojang.com"; + const launcher_meta: &str = "https://launchermeta.mojang.com"; + const assests: &str = "http://resources.download.minecraft.net"; + const libraries: &str = "https://libraries.minecraft.net"; + } + + pub mod mirror_heads { + const bmclapi: &str = "https://bmclapi2.bangbang93.com"; // BMCLAPI 镜像站 + const jcut: &str = "https://mirrors.jcut.edu.cn/bmclapi"; // 荆楚理工学院镜像站 + const lzuoss: &str = "https://mirror.lzu.edu.cn/bmclapi"; // 兰州大学镜像站 + const nju: &str = "https://mirror.nju.edu.cn/bmclapi"; // 南京大学镜像站 + const nyist: &str = "https://mirror.nyist.edu.cn/bmclapi"; // 南阳理工学院镜像站 + const qlut: &str = "https://mirrors.qlu.edu.cn/bmclapi"; // 齐鲁工业大学镜像站 + const sjtug: &str = "https://mirror.sjtu.edu.cn/bmclapi"; // 思源镜像站 + const ustc: &str = "https://mirrors.ustc.edu.cn/bmclapi"; // 中国科学技术大学镜像站 + + pub fn get_assests(mirror: &str) -> &'static str { + match mirror { + "bmclapi" => bmclapi + "/assets", + "jcut" => jcut + "/assets", + "lzuoss" => lzuoss + "/assets", + "nju" => nju + "/assets", + "nyist" => nyist + "/assets", + "qlut" => qlut + "/assets", + "sjtug" => sjtug + "/assets", + "ustc" => ustc + "/assets", + _ => "https://resources.download.minecraft.net", + } + } + } + + pub mod authlib_injector { + const official: &str = "https://authlib-injector.yushi.moe"; + const bmclapi: &str = "https://bmclapi2.bangbang93.com/mirrors/authlib-injector"; + } + + pub mod mod_loaders { + pub mod forge { + const official: &str = "https://files.minecraftforge.net/maven"; + const bmclapi: &str = "https://bmclapi2.bangbang93.com/maven"; + } + + pub mod fabric { + const meta: &str = "https://meta.fabricmc.net"; + const bmcl_meta: &str = "https://bmclapi2.bangbang93.com/fabric-meta"; + const maven: &str = "https://maven.fabricmc.net"; + const bmcl_maven: &str = "https://bmclapi2.bangbang93.com/maven"; + } + + pub mod liteloader { + const official: &str = "http://dl.liteloader.com/versions/versions.json"; + const bmclapi: &str = + "https://bmclapi.bangbang93.com/maven/com/mumfrey/liteloader/versions.json"; + } + + pub mod neoforge { + const forge: &str = "https://maven.neoforged.net/releases/net/neoforged/forge"; + const bmcl_forge: &str = + "https://bmclapi2.bangbang93.com/maven/net/neoforged/forge"; + const neoforge: &str = + "https://maven.neoforged.net/releases/net/neoforged/neoforge"; + const bmcl_neoforge: &str = + "https://bmclapi2.bangbang93.com/maven/net/neoforged/neoforge"; + } + + pub mod quilt { + const maven: &str = "https://maven.quiltmc.org/repository/release"; + const bmcl_maven: &str = "https://bmclapi2.bangbang93.com/maven"; + const meta: &str = "https://meta.quiltmc.org"; + const bmcl_meta: &str = "https://bmclapi2.bangbang93.com/quilt-meta"; + } + + mod cyan{ + // https://www.mcmod.cn/class/4420.html + } + + mod flint{ + // https://www.mcmod.cn/class/14621.html + } + + mod m3l{ + // https://www.mcmod.cn/class/19181.html + } + } + } +} From 61b675a3fc9a0b09336e63c6f3ab8f62a935b317 Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Sun, 1 Feb 2026 17:29:38 +0800 Subject: [PATCH 02/15] Update get_origin_version.rs --- src-tauri/src/game_version/get_origin_version.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/game_version/get_origin_version.rs b/src-tauri/src/game_version/get_origin_version.rs index 5cc83dd..e7dda09 100644 --- a/src-tauri/src/game_version/get_origin_version.rs +++ b/src-tauri/src/game_version/get_origin_version.rs @@ -30,7 +30,7 @@ pub mod get_origin_version { "qlut" => qlut + "/assets", "sjtug" => sjtug + "/assets", "ustc" => ustc + "/assets", - _ => "https://resources.download.minecraft.net", + _ => mojang_heads::assests, } } } From 285d317a0d382f5d26279c2438c31fe168b72053 Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Sun, 1 Feb 2026 17:57:04 +0800 Subject: [PATCH 03/15] feat: json structure --- src-tauri/Cargo.lock | 24 ++++++------- src-tauri/Cargo.toml | 13 +++++-- .../{get_origin_version.rs => get_version.rs} | 2 +- src-tauri/src/game_version/parse_versions.rs | 34 +++++++++++++++++++ 4 files changed, 57 insertions(+), 16 deletions(-) rename src-tauri/src/game_version/{get_origin_version.rs => get_version.rs} (99%) create mode 100644 src-tauri/src/game_version/parse_versions.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7668c0b..73620da 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "RTLauncher" +version = "0.1.0" +dependencies = [ + "log", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-log", +] + [[package]] name = "adler2" version = "2.0.1" @@ -75,18 +87,6 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "app" -version = "0.1.0" -dependencies = [ - "log", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-log", -] - [[package]] name = "arrayvec" version = "0.7.6" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 23b83e3..432b858 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "app" +name = "RTLauncher" version = "0.1.0" -description = "A Tauri App" -authors = ["you"] +description = "为生电而生的多平台 Minecraft 启动器" +authors = ["Grey-Wind", "Moralts"] license = "" repository = "" edition = "2021" @@ -15,11 +15,18 @@ name = "app_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] +cxx-build = "1.0" tauri-build = { version = "2.5.3", features = [] } +[dependencies.cxx] +version = "1.0" +features = ["experimental-return-type"] + [dependencies] +reqwest = { version = "0.11", features = ["blocking", "json"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } log = "0.4" tauri = { version = "2.9.5", features = [] } tauri-plugin-log = "2" +thiserror = "1.0" diff --git a/src-tauri/src/game_version/get_origin_version.rs b/src-tauri/src/game_version/get_version.rs similarity index 99% rename from src-tauri/src/game_version/get_origin_version.rs rename to src-tauri/src/game_version/get_version.rs index e7dda09..9aa3c37 100644 --- a/src-tauri/src/game_version/get_origin_version.rs +++ b/src-tauri/src/game_version/get_version.rs @@ -1,4 +1,4 @@ -pub mod get_origin_version { +pub mod get_version { const version_suffix: &str = "/mc/game/version_manifest.json"; // http://launchermeta.mojang.com/mc/game/version_manifest.json const version2_suffix: &str = "/mc/game/version_manifest_v2.json"; // http://launchermeta.mojang.com/mc/game/version_manifest_v2.json diff --git a/src-tauri/src/game_version/parse_versions.rs b/src-tauri/src/game_version/parse_versions.rs new file mode 100644 index 0000000..5e18a90 --- /dev/null +++ b/src-tauri/src/game_version/parse_versions.rs @@ -0,0 +1,34 @@ +pub mod parse_versions { + use serde::{Deserialize, Serialize}; + use thiserror::Error; + + #[derive(Error, Debug)] + pub enum VersionError { + #[error("HTTP request failed: {0}")] + HttpError(#[from] reqwest::Error), + + #[error("JSON parsing failed: {0}")] + JsonError(#[from] serde_json::Error), + + #[error("Version not found: {0}")] + VersionNotFound(String), + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct LatestVersions { + pub release: String, + pub snapshot: String, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct VersionInfo { + pub id: String, + #[serde(rename = "type")] + pub version_type: String, + pub url: String, + pub time: String, + pub release_time: String, + pub sha1: String, + pub compliance_level: i32, + } +} From 06b05db7ad51e088a8e0c5f0b7a706ac2bc5ed8d Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Sun, 1 Feb 2026 17:58:17 +0800 Subject: [PATCH 04/15] remove: remove cxx --- src-tauri/Cargo.lock | 506 ++++++++++++++++++++++++++++++++++++++++--- src-tauri/Cargo.toml | 5 - 2 files changed, 477 insertions(+), 34 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 73620da..a8ce412 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7,11 +7,13 @@ name = "RTLauncher" version = "0.1.0" dependencies = [ "log", + "reqwest 0.11.27", "serde", "serde_json", "tauri", "tauri-build", "tauri-plugin-log", + "thiserror 1.0.69", ] [[package]] @@ -444,6 +446,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -467,9 +479,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -480,7 +492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -750,7 +762,7 @@ dependencies = [ "rustc_version", "toml 0.9.11+spec-1.1.0", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -759,6 +771,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -786,6 +807,22 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fdeflate" version = "0.3.7" @@ -836,6 +873,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -843,7 +889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -857,6 +903,12 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1260,6 +1312,25 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1305,6 +1376,17 @@ dependencies = [ "match_token", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.4.0" @@ -1315,6 +1397,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1322,7 +1415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.4.0", ] [[package]] @@ -1333,8 +1426,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -1344,6 +1437,36 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.8.1" @@ -1354,8 +1477,8 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "httparse", "itoa", "pin-project-lite", @@ -1365,6 +1488,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-util" version = "0.1.19" @@ -1376,14 +1512,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http", - "http-body", - "hyper", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.2", "tokio", "tower-service", "tracing", @@ -1741,6 +1877,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.1" @@ -1865,6 +2007,23 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2171,6 +2330,50 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2722,6 +2925,46 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.50.0", +] + [[package]] name = "reqwest" version = "0.12.28" @@ -2732,10 +2975,10 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-util", "js-sys", "log", @@ -2744,7 +2987,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tokio-util", "tower", @@ -2811,6 +3054,28 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -2832,6 +3097,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -2895,6 +3169,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3146,6 +3443,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.2" @@ -3274,6 +3581,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3294,6 +3607,27 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3315,7 +3649,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ "bitflags 2.10.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch", @@ -3386,7 +3720,7 @@ dependencies = [ "glob", "gtk", "heck 0.5.0", - "http", + "http 1.4.0", "jni", "libc", "log", @@ -3400,7 +3734,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serde_repr", @@ -3532,7 +3866,7 @@ dependencies = [ "cookie", "dpi", "gtk", - "http", + "http 1.4.0", "jni", "objc2", "objc2-ui-kit", @@ -3555,7 +3889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065" dependencies = [ "gtk", - "http", + "http 1.4.0", "jni", "log", "objc2", @@ -3588,7 +3922,7 @@ dependencies = [ "dunce", "glob", "html5ever", - "http", + "http 1.4.0", "infer", "json-patch", "kuchikiki", @@ -3624,6 +3958,19 @@ dependencies = [ "toml 0.9.11+spec-1.1.0", ] +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.4.3" @@ -3743,10 +4090,20 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "socket2", + "socket2 0.6.2", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3865,7 +4222,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower-layer", "tower-service", @@ -3880,8 +4237,8 @@ dependencies = [ "bitflags 2.10.0", "bytes", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "iri-string", "pin-project-lite", "tower", @@ -4074,6 +4431,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.1" @@ -4502,6 +4865,24 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -4544,6 +4925,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -4601,6 +4997,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4619,6 +5021,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4637,6 +5045,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4667,6 +5081,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4685,6 +5105,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4703,6 +5129,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4721,6 +5153,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4751,6 +5189,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winreg" version = "0.55.0" @@ -4789,7 +5237,7 @@ dependencies = [ "gdkx11", "gtk", "html5ever", - "http", + "http 1.4.0", "javascriptcore-rs", "jni", "kuchikiki", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 432b858..59024b5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -15,13 +15,8 @@ name = "app_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] -cxx-build = "1.0" tauri-build = { version = "2.5.3", features = [] } -[dependencies.cxx] -version = "1.0" -features = ["experimental-return-type"] - [dependencies] reqwest = { version = "0.11", features = ["blocking", "json"] } serde_json = "1.0" From 426b9f683df78699e52815a9020e9e3eef40794b Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Sun, 1 Feb 2026 18:01:33 +0800 Subject: [PATCH 05/15] =?UTF-8?q?remove:=20=E6=9A=82=E6=97=B6=E6=80=A7?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E8=A7=A3=E6=9E=90=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/game_version/parse_versions.rs | 34 -------------------- 1 file changed, 34 deletions(-) delete mode 100644 src-tauri/src/game_version/parse_versions.rs diff --git a/src-tauri/src/game_version/parse_versions.rs b/src-tauri/src/game_version/parse_versions.rs deleted file mode 100644 index 5e18a90..0000000 --- a/src-tauri/src/game_version/parse_versions.rs +++ /dev/null @@ -1,34 +0,0 @@ -pub mod parse_versions { - use serde::{Deserialize, Serialize}; - use thiserror::Error; - - #[derive(Error, Debug)] - pub enum VersionError { - #[error("HTTP request failed: {0}")] - HttpError(#[from] reqwest::Error), - - #[error("JSON parsing failed: {0}")] - JsonError(#[from] serde_json::Error), - - #[error("Version not found: {0}")] - VersionNotFound(String), - } - - #[derive(Debug, Clone, Serialize, Deserialize)] - pub struct LatestVersions { - pub release: String, - pub snapshot: String, - } - - #[derive(Debug, Clone, Serialize, Deserialize)] - pub struct VersionInfo { - pub id: String, - #[serde(rename = "type")] - pub version_type: String, - pub url: String, - pub time: String, - pub release_time: String, - pub sha1: String, - pub compliance_level: i32, - } -} From 883a487bdaaba781e5353d1854004c3b7544312b Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Sun, 1 Feb 2026 18:35:53 +0800 Subject: [PATCH 06/15] feat: fetch manifest --- src-tauri/Cargo.lock | 17 +---- src-tauri/src/game_version/get_version.rs | 77 ++++++++++++++++++++++- src-tauri/src/utils/delay_test.rs | 0 3 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 src-tauri/src/utils/delay_test.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b297c02..c217257 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7,13 +7,14 @@ name = "RTLauncher" version = "0.1.0" dependencies = [ "log", + "regex", "reqwest 0.11.27", "serde", "serde_json", + "serde_yaml", "tauri", "tauri-build", "tauri-plugin-log", - "thiserror 1.0.69", ] [[package]] @@ -89,20 +90,6 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "app" -version = "0.1.0" -dependencies = [ - "log", - "regex", - "serde", - "serde_json", - "serde_yaml", - "tauri", - "tauri-build", - "tauri-plugin-log", -] - [[package]] name = "arrayvec" version = "0.7.6" diff --git a/src-tauri/src/game_version/get_version.rs b/src-tauri/src/game_version/get_version.rs index 9aa3c37..3076019 100644 --- a/src-tauri/src/game_version/get_version.rs +++ b/src-tauri/src/game_version/get_version.rs @@ -76,17 +76,88 @@ pub mod get_version { const bmcl_meta: &str = "https://bmclapi2.bangbang93.com/quilt-meta"; } - mod cyan{ + mod cyan { // https://www.mcmod.cn/class/4420.html } - mod flint{ + mod flint { // https://www.mcmod.cn/class/14621.html } - mod m3l{ + mod m3l { // https://www.mcmod.cn/class/19181.html } } } + + async fn fetch_json(url: &str, timeout_seconds: Option) -> Result { + // 创建可复用的 HTTP 客户端(启用连接池) + let client = Client::builder() + .timeout(Duration::from_secs(timeout_seconds.unwrap_or(30))) + .pool_max_idle_per_host(5) + .gzip(true) + .brotli(true) + .build()?; + + // 发送请求并获取响应 + let response = client.get(url).send().await?; + + if !response.status().is_success() { + return Err(Error::from(std::io::Error::new( + std::io::ErrorKind::Other, + format!("HTTP错误: {}", response.status()), + ))); + } + + let content = response.text().await?; + + Ok(content) + } + + /// 镜像源枚举 + pub enum MirrorSource { + Mojang, + Bmclapi, + Jcut, + Lzuoss, + Nju, + Nyist, + Qlut, + Sjtug, + Ustc, + } + + impl MirrorSource { + /// 获取镜像源的基础URL + fn base_url(&self) -> &str { + match self { + MirrorSource::Mojang => &mojang_heads::launcher_meta, + MirrorSource::Bmclapi => &mirror_heads::bmclapi, + MirrorSource::Jcut => &mirror_heads::jcut, + MirrorSource::Lzuoss => &mirror_heads::lzuoss, + MirrorSource::Nju => &mirror_heads::nju, + MirrorSource::Nyist => &mirror_heads::nyist, + MirrorSource::Qlut => &mirror_heads::qlut, + MirrorSource::Sjtug => &mirror_heads::sjtug, + MirrorSource::Ustc => &mirror_heads::ustc, + } + } + } + + /// 统一的版本清单获取函数 + pub fn fetch_version_manifest( + source: MirrorSource, + params: Option<(bool, Option)>, + ) -> Result { + let (use_version2, timeout_seconds) = params.unwrap_or((true, Some(3))); + + let base_url = source.base_url(); + let url = if use_version2 { + base_url.to_string() + version2_suffix + } else { + base_url.to_string() + version_suffix + }; + + fetch_json(&url, timeout_seconds) + } } diff --git a/src-tauri/src/utils/delay_test.rs b/src-tauri/src/utils/delay_test.rs new file mode 100644 index 0000000..e69de29 From 8a903378c8420fa9dd16d84c1780e857279c31cd Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Sun, 1 Feb 2026 18:45:44 +0800 Subject: [PATCH 07/15] feat: tcp ping --- src-tauri/src/utils/delay_test.rs | 778 ++++++++++++++++++++++++ src-tauri/src/utils/delay_test_usage.md | 190 ++++++ 2 files changed, 968 insertions(+) create mode 100644 src-tauri/src/utils/delay_test_usage.md diff --git a/src-tauri/src/utils/delay_test.rs b/src-tauri/src/utils/delay_test.rs index e69de29..bf81a0e 100644 --- a/src-tauri/src/utils/delay_test.rs +++ b/src-tauri/src/utils/delay_test.rs @@ -0,0 +1,778 @@ +pub mod delay_test { + use futures::{stream, StreamExt}; + use serde::{Deserialize, Serialize}; + use std::collections::HashMap; + use std::net::{SocketAddr, ToSocketAddrs}; + use std::sync::Arc; + use std::time::{Duration, Instant}; + use tokio::net::TcpStream; + use tokio::sync::Semaphore; + + /// TCP Ping测试结果 + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct PingResult { + /// 原始目标字符串 + pub target: String, + /// 解析后的主机名或IP地址 + pub host: String, + /// 端口号 + pub port: u16, + /// 测试是否成功 + pub success: bool, + /// 延迟(毫秒),成功时有值 + pub latency_ms: Option, + /// 错误信息,失败时有值 + pub error: Option, + /// 测试时间戳 + pub timestamp: chrono::DateTime, + } + + /// TCP Ping统计信息 + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct PingStats { + /// 总测试次数 + pub total: usize, + /// 成功次数 + pub successful: usize, + /// 失败次数 + pub failed: usize, + /// 平均延迟(毫秒) + pub avg_latency_ms: Option, + /// 最小延迟(毫秒) + pub min_latency_ms: Option, + /// 最大延迟(毫秒) + pub max_latency_ms: Option, + /// 中位数延迟(毫秒) + pub median_latency_ms: Option, + /// 成功率(百分比) + pub success_rate: f64, + /// 所有测试结果的详细列表 + pub results: Vec, + } + + /// TCP Ping配置 + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct TcpPingConfig { + /// 连接超时时间 + pub timeout: Duration, + /// 最大并发连接数 + pub concurrency_limit: usize, + /// 失败重试次数 + pub retry_count: usize, + /// 重试间隔 + pub retry_delay: Duration, + /// 默认端口号(当目标中未指定端口时使用) + pub default_port: u16, + } + + impl Default for TcpPingConfig { + fn default() -> Self { + Self { + timeout: Duration::from_secs(5), + concurrency_limit: 100, + retry_count: 2, + retry_delay: Duration::from_millis(100), + default_port: 80, + } + } + } + + /// TCP Ping测试器 + #[derive(Debug, Clone)] + pub struct DelayTester { + config: TcpPingConfig, + } + + impl DelayTester { + /// 使用自定义配置创建测试器 + pub fn new(config: TcpPingConfig) -> Self { + Self { config } + } + + /// 使用默认配置创建测试器 + pub fn with_default_config() -> Self { + Self { + config: TcpPingConfig::default(), + } + } + + /// 获取当前配置 + pub fn config(&self) -> &TcpPingConfig { + &self.config + } + + /// 更新配置 + pub fn update_config(&mut self, config: TcpPingConfig) { + self.config = config; + } + + /// 解析目标地址字符串 + /// + /// # 支持的格式 + /// - `example.com:443` - 带端口的域名 + /// - `192.168.1.1:80` - 带端口的IP地址 + /// - `example.com` - 仅域名(使用默认端口) + /// - `192.168.1.1` - 仅IP地址(使用默认端口) + /// + /// # 参数 + /// - `target`: 目标地址字符串 + /// - `default_port`: 默认端口号 + /// + /// # 返回 + /// (主机名或IP地址, 端口号) + pub fn parse_target(target: &str, default_port: u16) -> (String, u16) { + let target = target.trim(); + + // 处理IPv6地址 + if target.contains('[') && target.contains(']') { + if let Some(port_start) = target.rfind(':') { + if port_start > target.rfind(']').unwrap() { + let host = target[..port_start].to_string(); + let port = target[port_start + 1..].parse().unwrap_or(default_port); + return (host, port); + } + } + return (target.to_string(), default_port); + } + + let parts: Vec<&str> = target.rsplitn(2, ':').collect(); + match parts.len() { + 1 => (parts[0].to_string(), default_port), + 2 => { + let host = parts[1].to_string(); + match parts[0].parse::() { + Ok(port) => (host, port), + Err(_) => (target.to_string(), default_port), + } + } + _ => (target.to_string(), default_port), + } + } + + /// 执行单个TCP Ping测试 + /// + /// # 参数 + /// - `target`: 目标地址字符串 + /// + /// # 返回 + /// 测试结果 + pub async fn ping_single(&self, target: &str) -> PingResult { + let (host, port) = Self::parse_target(target, self.config.default_port); + let target_str = format!("{}:{}", host, port); + + let mut last_error = None; + + // 重试机制 + for attempt in 0..=self.config.retry_count { + if attempt > 0 { + tokio::time::sleep(self.config.retry_delay).await; + } + + match self.do_ping(&host, port).await { + Ok(latency) => { + return PingResult { + target: target.to_string(), + host: host.clone(), + port, + success: true, + latency_ms: Some(latency), + error: None, + timestamp: chrono::Utc::now(), + }; + } + Err(e) => { + last_error = Some(e.to_string()); + if attempt < self.config.retry_count { + continue; + } + } + } + } + + PingResult { + target: target.to_string(), + host: host.clone(), + port, + success: false, + latency_ms: None, + error: last_error, + timestamp: chrono::Utc::now(), + } + } + + /// 执行实际的TCP连接测试 + async fn do_ping( + &self, + host: &str, + port: u16, + ) -> Result> { + let addr_string = format!("{}:{}", host, port); + + // 解析DNS + let socket_addr = tokio::net::lookup_host(&addr_string) + .await? + .next() + .ok_or_else(|| format!("无法解析地址: {}", addr_string))?; + + let start = Instant::now(); + + // 使用tokio::time::timeout设置超时 + let stream = tokio::time::timeout(self.config.timeout, TcpStream::connect(socket_addr)) + .await??; + + let latency = start.elapsed(); + + // 立即关闭连接 + drop(stream); + + Ok(latency.as_secs_f64() * 1000.0) // 转换为毫秒 + } + + /// 批量执行TCP Ping测试 + /// + /// # 参数 + /// - `targets`: 目标地址字符串列表 + /// + /// # 返回 + /// 统计信息和所有结果 + pub async fn ping_batch(&self, targets: Vec) -> PingStats { + let semaphore = Arc::new(Semaphore::new(self.config.concurrency_limit)); + let config = self.config.clone(); + + // 创建异步任务流 + let tasks = stream::iter(targets) + .map(move |target| { + let semaphore = Arc::clone(&semaphore); + let config = config.clone(); + + async move { + let _permit = semaphore.acquire().await.unwrap(); + + let (host, port) = DelayTester::parse_target(&target, config.default_port); + let mut last_error = None; + + for attempt in 0..=config.retry_count { + if attempt > 0 { + tokio::time::sleep(config.retry_delay).await; + } + + match Self::static_do_ping(&host, port, config.timeout).await { + Ok(latency) => { + return PingResult { + target: target.clone(), + host: host.clone(), + port, + success: true, + latency_ms: Some(latency), + error: None, + timestamp: chrono::Utc::now(), + }; + } + Err(e) => { + last_error = Some(e.to_string()); + if attempt < config.retry_count { + continue; + } + } + } + } + + PingResult { + target: target.clone(), + host: host.clone(), + port, + success: false, + latency_ms: None, + error: last_error, + timestamp: chrono::Utc::now(), + } + } + }) + .buffer_unordered(self.config.concurrency_limit); + + let results: Vec = tasks.collect().await; + + self.calculate_stats(results) + } + + /// 从文件读取目标列表并执行测试 + /// + /// # 参数 + /// - `file_path`: 文件路径,每行一个目标地址 + /// + /// # 返回 + /// 统计信息和所有结果 + pub async fn ping_from_file(&self, file_path: &str) -> std::io::Result { + let content = std::fs::read_to_string(file_path)?; + let targets: Vec = content + .lines() + .filter(|line| !line.trim().is_empty() && !line.trim().starts_with('#')) + .map(|line| line.trim().to_string()) + .collect(); + + Ok(self.ping_batch(targets).await) + } + + /// 静态方法用于执行ping + async fn static_do_ping( + host: &str, + port: u16, + timeout: Duration, + ) -> Result> { + let addr_string = format!("{}:{}", host, port); + + let socket_addr = tokio::net::lookup_host(&addr_string) + .await? + .next() + .ok_or_else(|| format!("无法解析地址: {}", addr_string))?; + + let start = Instant::now(); + + let stream = tokio::time::timeout(timeout, TcpStream::connect(socket_addr)).await??; + + let latency = start.elapsed(); + drop(stream); + + Ok(latency.as_secs_f64() * 1000.0) + } + + /// 计算统计数据 + fn calculate_stats(&self, results: Vec) -> PingStats { + let total = results.len(); + let successful_results: Vec<&PingResult> = + results.iter().filter(|r| r.success).collect(); + let successful = successful_results.len(); + let failed = total - successful; + let success_rate = if total > 0 { + successful as f64 / total as f64 * 100.0 + } else { + 0.0 + }; + + let latencies: Vec = successful_results + .iter() + .filter_map(|r| r.latency_ms) + .collect(); + + let (avg_latency, min_latency, max_latency, median_latency) = if !latencies.is_empty() { + let mut sorted = latencies.clone(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + let sum: f64 = latencies.iter().sum(); + let avg = sum / latencies.len() as f64; + let min = *sorted.first().unwrap(); + let max = *sorted.last().unwrap(); + let median = if latencies.len() % 2 == 0 { + let mid = latencies.len() / 2; + (sorted[mid - 1] + sorted[mid]) / 2.0 + } else { + sorted[latencies.len() / 2] + }; + + (Some(avg), Some(min), Some(max), Some(median)) + } else { + (None, None, None, None) + }; + + PingStats { + total, + successful, + failed, + avg_latency_ms: avg_latency, + min_latency_ms: min_latency, + max_latency_ms: max_latency, + median_latency_ms: median_latency, + success_rate, + results, + } + } + + /// 按主机分组统计结果 + pub fn group_by_host(&self, stats: &PingStats) -> HashMap> { + let mut groups = HashMap::new(); + + for result in &stats.results { + groups + .entry(result.host.clone()) + .or_insert_with(Vec::new) + .push(result); + } + + groups + } + + /// 按成功率排序结果 + pub fn sort_by_latency(&self, stats: &mut PingStats, ascending: bool) { + stats.results.sort_by(|a, b| { + let a_lat = a.latency_ms.unwrap_or(f64::INFINITY); + let b_lat = b.latency_ms.unwrap_or(f64::INFINITY); + + if ascending { + a_lat.partial_cmp(&b_lat).unwrap() + } else { + b_lat.partial_cmp(&a_lat).unwrap() + } + }); + } + + /// 过滤结果 + pub fn filter_results(&self, stats: &PingStats, success_only: bool) -> Vec<&PingResult> { + if success_only { + stats.results.iter().filter(|r| r.success).collect() + } else { + stats.results.iter().collect() + } + } + + /// 生成文本格式报告 + pub fn generate_text_report(&self, stats: &PingStats) -> String { + let mut report = String::new(); + + report.push_str("TCP Ping 测试报告\n"); + report.push_str("================\n\n"); + report.push_str(&format!( + "测试时间: {}\n", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S") + )); + report.push_str(&format!( + "配置: 超时={:?}, 并发数={}, 重试次数={}\n", + self.config.timeout, self.config.concurrency_limit, self.config.retry_count + )); + report.push_str(&format!("测试总数: {}\n", stats.total)); + report.push_str(&format!( + "成功: {} ({:.1}%)\n", + stats.successful, stats.success_rate + )); + report.push_str(&format!("失败: {}\n\n", stats.failed)); + + if let Some(avg) = stats.avg_latency_ms { + report.push_str(&format!("平均延迟: {:.2} ms\n", avg)); + } + if let Some(min) = stats.min_latency_ms { + report.push_str(&format!("最小延迟: {:.2} ms\n", min)); + } + if let Some(max) = stats.max_latency_ms { + report.push_str(&format!("最大延迟: {:.2} ms\n", max)); + } + if let Some(median) = stats.median_latency_ms { + report.push_str(&format!("中位数延迟: {:.2} ms\n\n", median)); + } + + report.push_str("详细结果:\n"); + report.push_str("--------\n"); + + for (i, result) in stats.results.iter().enumerate() { + report.push_str(&format!("{}. {}:{} - ", i + 1, result.host, result.port)); + if result.success { + report.push_str(&format!("✅ {:.2} ms", result.latency_ms.unwrap_or(0.0))); + } else { + report.push_str(&format!( + "❌ 失败: {}", + result.error.as_deref().unwrap_or("未知错误") + )); + } + report.push('\n'); + } + + report + } + + /// 生成Markdown格式报告 + pub fn generate_markdown_report(&self, stats: &PingStats) -> String { + let mut report = String::new(); + + report.push_str("# TCP Ping 测试报告\n\n"); + report.push_str(&format!( + "**测试时间**: {}\n", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S") + )); + report.push_str(&format!( + "**配置**: 超时={:?}, 并发数={}, 重试次数={}\n\n", + self.config.timeout, self.config.concurrency_limit, self.config.retry_count + )); + + report.push_str("## 统计概览\n\n"); + report.push_str("| 指标 | 值 |\n"); + report.push_str("|------|----|\n"); + report.push_str(&format!("| 测试总数 | {} |\n", stats.total)); + report.push_str(&format!("| 成功 | {} |\n", stats.successful)); + report.push_str(&format!("| 失败 | {} |\n", stats.failed)); + report.push_str(&format!("| 成功率 | {:.1}% |\n", stats.success_rate)); + + if let Some(avg) = stats.avg_latency_ms { + report.push_str(&format!("| 平均延迟 | {:.2} ms |\n", avg)); + } + if let Some(min) = stats.min_latency_ms { + report.push_str(&format!("| 最小延迟 | {:.2} ms |\n", min)); + } + if let Some(max) = stats.max_latency_ms { + report.push_str(&format!("| 最大延迟 | {:.2} ms |\n", max)); + } + if let Some(median) = stats.median_latency_ms { + report.push_str(&format!("| 中位数延迟 | {:.2} ms |\n\n", median)); + } + + report.push_str("## 详细结果\n\n"); + report.push_str("| # | 目标 | 主机:端口 | 状态 | 延迟(ms) | 错误信息 |\n"); + report.push_str("|---|------|-----------|------|----------|----------|\n"); + + for (i, result) in stats.results.iter().enumerate() { + let status = if result.success { + "✅ 成功" + } else { + "❌ 失败" + }; + let latency = result + .latency_ms + .map_or("N/A".to_string(), |l| format!("{:.2}", l)); + let error = result.error.as_deref().unwrap_or(""); + + report.push_str(&format!( + "| {} | {} | {}:{} | {} | {} | {} |\n", + i + 1, + result.target, + result.host, + result.port, + status, + latency, + error + )); + } + + report + } + + /// 保存结果为JSON文件 + pub fn save_as_json(&self, stats: &PingStats, file_path: &str) -> std::io::Result<()> { + let json = serde_json::to_string_pretty(stats)?; + std::fs::write(file_path, json) + } + + /// 保存结果为CSV文件 + pub fn save_as_csv(&self, stats: &PingStats, file_path: &str) -> std::io::Result<()> { + let mut wtr = csv::Writer::from_path(file_path)?; + + wtr.write_record(&[ + "目标", + "主机", + "端口", + "成功", + "延迟(ms)", + "错误信息", + "时间戳", + ])?; + + for result in &stats.results { + let success = if result.success { "是" } else { "否" }; + let latency = result + .latency_ms + .map_or("".to_string(), |l| format!("{:.2}", l)); + let error = result.error.as_deref().unwrap_or(""); + let timestamp = result.timestamp.format("%Y-%m-%d %H:%M:%S").to_string(); + + wtr.write_record(&[ + &result.target, + &result.host, + &result.port.to_string(), + success, + &latency, + error, + ×tamp, + ])?; + } + + wtr.flush()?; + Ok(()) + } + } + + /// 便捷函数:快速测试单个目标 + pub async fn quick_ping( + target: &str, + timeout_secs: u64, + ) -> Result> { + let config = TcpPingConfig { + timeout: Duration::from_secs(timeout_secs), + ..Default::default() + }; + + let tester = DelayTester::new(config); + Ok(tester.ping_single(target).await) + } + + /// 便捷函数:快速批量测试 + pub async fn quick_batch_ping(targets: Vec, concurrency: usize) -> PingStats { + let config = TcpPingConfig { + concurrency_limit: concurrency, + ..Default::default() + }; + + let tester = DelayTester::new(config); + tester.ping_batch(targets).await + } + + // 测试模块 + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_parse_target() { + // 测试带端口的地址 + assert_eq!( + DelayTester::parse_target("example.com:443", 80), + ("example.com".to_string(), 443) + ); + + // 测试不带端口的地址 + assert_eq!( + DelayTester::parse_target("example.com", 80), + ("example.com".to_string(), 80) + ); + + // 测试IP地址 + assert_eq!( + DelayTester::parse_target("192.168.1.1:8080", 80), + ("192.168.1.1".to_string(), 8080) + ); + + // 测试IPv6地址 + assert_eq!( + DelayTester::parse_target("[2001:db8::1]:443", 80), + ("[2001:db8::1]".to_string(), 443) + ); + + // 测试空格处理 + assert_eq!( + DelayTester::parse_target(" example.com:443 ", 80), + ("example.com".to_string(), 443) + ); + } + + #[tokio::test] + async fn test_ping_single() { + let tester = DelayTester::with_default_config(); + + // 测试已知可访问的地址(Google DNS) + let result = tester.ping_single("8.8.8.8:53").await; + + // 由于网络环境不同,可能成功也可能失败 + // 我们主要测试函数是否正常工作 + assert_eq!(result.port, 53); + assert_eq!(result.host, "8.8.8.8"); + + // 如果是成功的,应该有延迟值 + if result.success { + assert!(result.latency_ms.is_some()); + assert!(result.error.is_none()); + } else { + assert!(result.latency_ms.is_none()); + assert!(result.error.is_some()); + } + } + + #[tokio::test] + async fn test_ping_batch() { + let config = TcpPingConfig { + timeout: Duration::from_secs(3), + concurrency_limit: 5, + retry_count: 1, + retry_delay: Duration::from_millis(50), + default_port: 80, + }; + + let tester = DelayTester::new(config); + + let targets = vec![ + "google.com:80".to_string(), + "github.com:443".to_string(), + "cloudflare.com:443".to_string(), + "1.1.1.1:53".to_string(), + ]; + + let stats = tester.ping_batch(targets).await; + + // 验证统计数据 + assert_eq!(stats.total, 4); + assert!(stats.successful + stats.failed == 4); + + // 生成报告 + let text_report = tester.generate_text_report(&stats); + let markdown_report = tester.generate_markdown_report(&stats); + + assert!(text_report.contains("TCP Ping 测试报告")); + assert!(markdown_report.contains("# TCP Ping 测试报告")); + + // 测试分组功能 + let groups = tester.group_by_host(&stats); + assert!(!groups.is_empty()); + + // 测试排序 + let mut stats_clone = stats.clone(); + tester.sort_by_latency(&mut stats_clone, true); + + // 测试过滤 + let success_results = tester.filter_results(&stats, true); + assert_eq!(success_results.len(), stats.successful); + } + + #[test] + fn test_stats_calculation() { + let tester = DelayTester::with_default_config(); + + let results = vec![ + PingResult { + target: "test1.com".to_string(), + host: "test1.com".to_string(), + port: 80, + success: true, + latency_ms: Some(10.0), + error: None, + timestamp: chrono::Utc::now(), + }, + PingResult { + target: "test2.com".to_string(), + host: "test2.com".to_string(), + port: 80, + success: true, + latency_ms: Some(20.0), + error: None, + timestamp: chrono::Utc::now(), + }, + PingResult { + target: "test3.com".to_string(), + host: "test3.com".to_string(), + port: 80, + success: false, + latency_ms: None, + error: Some("Connection failed".to_string()), + timestamp: chrono::Utc::now(), + }, + ]; + + let stats = tester.calculate_stats(results); + + assert_eq!(stats.total, 3); + assert_eq!(stats.successful, 2); + assert_eq!(stats.failed, 1); + assert_eq!(stats.success_rate, 66.66666666666667); + assert_eq!(stats.avg_latency_ms, Some(15.0)); + assert_eq!(stats.min_latency_ms, Some(10.0)); + assert_eq!(stats.max_latency_ms, Some(20.0)); + assert_eq!(stats.median_latency_ms, Some(15.0)); + } + + #[tokio::test] + async fn test_quick_functions() { + // 测试快速单个ping + let result = quick_ping("8.8.8.8:53", 3).await; + assert!(result.is_ok()); + + // 测试快速批量ping + let targets = vec!["google.com:80".to_string(), "github.com:443".to_string()]; + + let stats = quick_batch_ping(targets, 10).await; + assert_eq!(stats.total, 2); + } + } +} diff --git a/src-tauri/src/utils/delay_test_usage.md b/src-tauri/src/utils/delay_test_usage.md new file mode 100644 index 0000000..d2e4f2a --- /dev/null +++ b/src-tauri/src/utils/delay_test_usage.md @@ -0,0 +1,190 @@ +```rust +use util::delay_test::{DelayTester, TcpPingConfig, quick_batch_ping}; +use std::time::Duration; +use clap::{Parser, Subcommand}; + +/// TCP Ping测试工具 +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// 测试单个目标 + Single { + /// 目标地址(格式:host:port 或 host) + target: String, + + /// 超时时间(秒) + #[arg(short, long, default_value_t = 5)] + timeout: u64, + + /// 重试次数 + #[arg(short, long, default_value_t = 2)] + retry: usize, + }, + + /// 批量测试多个目标 + Batch { + /// 目标地址列表文件(每行一个目标) + #[arg(short, long)] + file: Option, + + /// 目标地址(可多个) + #[arg()] + targets: Vec, + + /// 并发数 + #[arg(short, long, default_value_t = 50)] + concurrency: usize, + + /// 超时时间(秒) + #[arg(short, long, default_value_t = 5)] + timeout: u64, + + /// 输出格式 + #[arg(short, long, default_value = "text")] + format: String, + + /// 保存结果到文件 + #[arg(short, long)] + output: Option, + }, + + /// 生成测试报告 + Report { + /// 输入文件(JSON格式) + #[arg(short, long)] + input: String, + + /// 输出格式 + #[arg(short, long, default_value = "text")] + format: String, + }, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + + match args.command { + Commands::Single { target, timeout, retry } => { + let config = TcpPingConfig { + timeout: Duration::from_secs(timeout), + retry_count: retry, + ..Default::default() + }; + + let tester = DelayTester::new(config); + let result = tester.ping_single(&target).await; + + println!("TCP Ping 测试结果:"); + println!("=================="); + println!("目标: {}", result.target); + println!("主机: {}", result.host); + println!("端口: {}", result.port); + println!("状态: {}", if result.success { "✅ 成功" } else { "❌ 失败" }); + + if let Some(latency) = result.latency_ms { + println!("延迟: {:.2} ms", latency); + } + + if let Some(error) = result.error { + println!("错误: {}", error); + } + + println!("时间: {}", result.timestamp.format("%Y-%m-%d %H:%M:%S")); + } + + Commands::Batch { file, targets, concurrency, timeout, format, output } => { + let config = TcpPingConfig { + timeout: Duration::from_secs(timeout), + concurrency_limit: concurrency, + ..Default::default() + }; + + let tester = DelayTester::new(config); + let mut all_targets = targets; + + // 如果指定了文件,从文件读取目标 + if let Some(file_path) = file { + let file_targets = std::fs::read_to_string(&file_path)? + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| line.trim().to_string()) + .collect::>(); + + all_targets.extend(file_targets); + } + + if all_targets.is_empty() { + eprintln!("错误: 未指定测试目标"); + std::process::exit(1); + } + + println!("开始批量TCP Ping测试..."); + println!("目标数量: {}", all_targets.len()); + println!("并发限制: {}", concurrency); + println!("超时时间: {}秒", timeout); + println!(); + + let stats = tester.ping_batch(all_targets).await; + + // 输出报告 + match format.as_str() { + "text" => { + println!("{}", tester.generate_text_report(&stats)); + } + "markdown" => { + println!("{}", tester.generate_markdown_report(&stats)); + } + "json" => { + let json = serde_json::to_string_pretty(&stats)?; + println!("{}", json); + } + _ => { + println!("{}", tester.generate_text_report(&stats)); + } + } + + // 保存结果 + if let Some(output_path) = output { + if output_path.ends_with(".json") { + tester.save_as_json(&stats, &output_path)?; + println!("结果已保存到: {}", output_path); + } else if output_path.ends_with(".csv") { + tester.save_as_csv(&stats, &output_path)?; + println!("结果已保存到: {}", output_path); + } else { + std::fs::write(&output_path, tester.generate_text_report(&stats))?; + println!("结果已保存到: {}", output_path); + } + } + } + + Commands::Report { input, format } => { + let content = std::fs::read_to_string(&input)?; + let stats: util::delay_test::PingStats = serde_json::from_str(&content)?; + + let tester = DelayTester::with_default_config(); + + match format.as_str() { + "text" => { + println!("{}", tester.generate_text_report(&stats)); + } + "markdown" => { + println!("{}", tester.generate_markdown_report(&stats)); + } + _ => { + println!("{}", tester.generate_text_report(&stats)); + } + } + } + } + + Ok(()) +} +``` From aab56bc4292771c036194ba0475c76be7725322c Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Mon, 2 Feb 2026 22:06:31 +0800 Subject: [PATCH 08/15] feat: get_version() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用于获取版本清单 --- src-tauri/src/game_version/get_version.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src-tauri/src/game_version/get_version.rs b/src-tauri/src/game_version/get_version.rs index 3076019..71fdb7a 100644 --- a/src-tauri/src/game_version/get_version.rs +++ b/src-tauri/src/game_version/get_version.rs @@ -20,6 +20,21 @@ pub mod get_version { const sjtug: &str = "https://mirror.sjtu.edu.cn/bmclapi"; // 思源镜像站 const ustc: &str = "https://mirrors.ustc.edu.cn/bmclapi"; // 中国科学技术大学镜像站 + pub fn get_version(mirror: &str) -> &'static str { + match mirror { + "mojang" => mojang_heads::launcher_meta + version_suffix, + "mojang_v2" => mojang_heads::launcher_meta + version2_suffix, + "bmclapi" => bmclapi + version_suffix, + "jcut" => jcut + version_suffix, + "lzuoss" => lzuoss + version_suffix, + "nju" => nju + version_suffix, + "nyist" => nyist + version_suffix, + "qlut" => qlut + version_suffix, + "sjtug" => sjtug + version_suffix, + "ustc" => ustc + version_suffix, + _ => mojang_heads::launcher_meta + version_suffix, + } + pub fn get_assests(mirror: &str) -> &'static str { match mirror { "bmclapi" => bmclapi + "/assets", From 07cedf10c5f80d188172ea0cad33c222a60d1394 Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Mon, 2 Feb 2026 22:08:43 +0800 Subject: [PATCH 09/15] fix error --- src-tauri/src/game_version/get_version.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src-tauri/src/game_version/get_version.rs b/src-tauri/src/game_version/get_version.rs index 71fdb7a..db62808 100644 --- a/src-tauri/src/game_version/get_version.rs +++ b/src-tauri/src/game_version/get_version.rs @@ -33,6 +33,7 @@ pub mod get_version { "sjtug" => sjtug + version_suffix, "ustc" => ustc + version_suffix, _ => mojang_heads::launcher_meta + version_suffix, + } } pub fn get_assests(mirror: &str) -> &'static str { From 4955ecc2267f2d0b661a75a89101ec7fe3866d48 Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Mon, 2 Feb 2026 22:10:01 +0800 Subject: [PATCH 10/15] =?UTF-8?q?=E8=A1=A5=E5=85=A8v2=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/game_version/get_version.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src-tauri/src/game_version/get_version.rs b/src-tauri/src/game_version/get_version.rs index db62808..9b8aa47 100644 --- a/src-tauri/src/game_version/get_version.rs +++ b/src-tauri/src/game_version/get_version.rs @@ -25,13 +25,21 @@ pub mod get_version { "mojang" => mojang_heads::launcher_meta + version_suffix, "mojang_v2" => mojang_heads::launcher_meta + version2_suffix, "bmclapi" => bmclapi + version_suffix, + "bmclapi_v2" => bmclapi + version2_suffix, "jcut" => jcut + version_suffix, + "jcut_v2" => jcut + version2_suffix, "lzuoss" => lzuoss + version_suffix, + "lzuoss_v2" => lzuoss + version2_suffix, "nju" => nju + version_suffix, + "nju_v2" => nju + version2_suffix, "nyist" => nyist + version_suffix, + "nyist_v2" => nyist + version2_suffix, "qlut" => qlut + version_suffix, + "qlut_v2" => qlut + version2_suffix, "sjtug" => sjtug + version_suffix, + "sjtug_v2" => sjtug + version2_suffix, "ustc" => ustc + version_suffix, + "ustc_v2" => ustc + version2_suffix, _ => mojang_heads::launcher_meta + version_suffix, } } From 9dc73a0c0d5f126019830aa5ffa14207b8573e1d Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Mon, 2 Feb 2026 22:13:45 +0800 Subject: [PATCH 11/15] =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/game_version/get_version.rs | 76 +++++++++++------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src-tauri/src/game_version/get_version.rs b/src-tauri/src/game_version/get_version.rs index 9b8aa47..a134016 100644 --- a/src-tauri/src/game_version/get_version.rs +++ b/src-tauri/src/game_version/get_version.rs @@ -19,44 +19,6 @@ pub mod get_version { const qlut: &str = "https://mirrors.qlu.edu.cn/bmclapi"; // 齐鲁工业大学镜像站 const sjtug: &str = "https://mirror.sjtu.edu.cn/bmclapi"; // 思源镜像站 const ustc: &str = "https://mirrors.ustc.edu.cn/bmclapi"; // 中国科学技术大学镜像站 - - pub fn get_version(mirror: &str) -> &'static str { - match mirror { - "mojang" => mojang_heads::launcher_meta + version_suffix, - "mojang_v2" => mojang_heads::launcher_meta + version2_suffix, - "bmclapi" => bmclapi + version_suffix, - "bmclapi_v2" => bmclapi + version2_suffix, - "jcut" => jcut + version_suffix, - "jcut_v2" => jcut + version2_suffix, - "lzuoss" => lzuoss + version_suffix, - "lzuoss_v2" => lzuoss + version2_suffix, - "nju" => nju + version_suffix, - "nju_v2" => nju + version2_suffix, - "nyist" => nyist + version_suffix, - "nyist_v2" => nyist + version2_suffix, - "qlut" => qlut + version_suffix, - "qlut_v2" => qlut + version2_suffix, - "sjtug" => sjtug + version_suffix, - "sjtug_v2" => sjtug + version2_suffix, - "ustc" => ustc + version_suffix, - "ustc_v2" => ustc + version2_suffix, - _ => mojang_heads::launcher_meta + version_suffix, - } - } - - pub fn get_assests(mirror: &str) -> &'static str { - match mirror { - "bmclapi" => bmclapi + "/assets", - "jcut" => jcut + "/assets", - "lzuoss" => lzuoss + "/assets", - "nju" => nju + "/assets", - "nyist" => nyist + "/assets", - "qlut" => qlut + "/assets", - "sjtug" => sjtug + "/assets", - "ustc" => ustc + "/assets", - _ => mojang_heads::assests, - } - } } pub mod authlib_injector { @@ -138,6 +100,44 @@ pub mod get_version { Ok(content) } + pub fn get_version(mirror: &str) -> &'static str { + match mirror { + "mojang" => mojang_heads::launcher_meta + version_suffix, + "mojang_v2" => mojang_heads::launcher_meta + version2_suffix, + "bmclapi" => bmclapi + version_suffix, + "bmclapi_v2" => bmclapi + version2_suffix, + "jcut" => jcut + version_suffix, + "jcut_v2" => jcut + version2_suffix, + "lzuoss" => lzuoss + version_suffix, + "lzuoss_v2" => lzuoss + version2_suffix, + "nju" => nju + version_suffix, + "nju_v2" => nju + version2_suffix, + "nyist" => nyist + version_suffix, + "nyist_v2" => nyist + version2_suffix, + "qlut" => qlut + version_suffix, + "qlut_v2" => qlut + version2_suffix, + "sjtug" => sjtug + version_suffix, + "sjtug_v2" => sjtug + version2_suffix, + "ustc" => ustc + version_suffix, + "ustc_v2" => ustc + version2_suffix, + _ => mojang_heads::launcher_meta + version_suffix, + } + } + + pub fn get_assests(mirror: &str) -> &'static str { + match mirror { + "bmclapi" => bmclapi + "/assets", + "jcut" => jcut + "/assets", + "lzuoss" => lzuoss + "/assets", + "nju" => nju + "/assets", + "nyist" => nyist + "/assets", + "qlut" => qlut + "/assets", + "sjtug" => sjtug + "/assets", + "ustc" => ustc + "/assets", + _ => mojang_heads::assests, + } + } + /// 镜像源枚举 pub enum MirrorSource { Mojang, From 5606e02d196242be20c115e40e6d59eb4a9c3b2c Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Mon, 2 Feb 2026 22:15:44 +0800 Subject: [PATCH 12/15] fix --- src-tauri/src/game_version/get_version.rs | 56 +++++++++++------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src-tauri/src/game_version/get_version.rs b/src-tauri/src/game_version/get_version.rs index a134016..2671589 100644 --- a/src-tauri/src/game_version/get_version.rs +++ b/src-tauri/src/game_version/get_version.rs @@ -102,39 +102,39 @@ pub mod get_version { pub fn get_version(mirror: &str) -> &'static str { match mirror { - "mojang" => mojang_heads::launcher_meta + version_suffix, - "mojang_v2" => mojang_heads::launcher_meta + version2_suffix, - "bmclapi" => bmclapi + version_suffix, - "bmclapi_v2" => bmclapi + version2_suffix, - "jcut" => jcut + version_suffix, - "jcut_v2" => jcut + version2_suffix, - "lzuoss" => lzuoss + version_suffix, - "lzuoss_v2" => lzuoss + version2_suffix, - "nju" => nju + version_suffix, - "nju_v2" => nju + version2_suffix, - "nyist" => nyist + version_suffix, - "nyist_v2" => nyist + version2_suffix, - "qlut" => qlut + version_suffix, - "qlut_v2" => qlut + version2_suffix, - "sjtug" => sjtug + version_suffix, - "sjtug_v2" => sjtug + version2_suffix, - "ustc" => ustc + version_suffix, - "ustc_v2" => ustc + version2_suffix, - _ => mojang_heads::launcher_meta + version_suffix, + "mojang" => heads::mojang_heads::launcher_meta + version_suffix, + "mojang_v2" => heads::mojang_heads::launcher_meta + version2_suffix, + "bmclapi" => heads::mirror_heads::bmclapi + version_suffix, + "bmclapi_v2" => heads::mirror_heads::bmclapi + version2_suffix, + "jcut" => heads::mirror_heads::jcut + version_suffix, + "jcut_v2" => heads::mirror_heads::jcut + version2_suffix, + "lzuoss" => heads::mirror_heads::lzuoss + version_suffix, + "lzuoss_v2" => heads::mirror_heads::lzuoss + version2_suffix, + "nju" => heads::mirror_heads::nju + version_suffix, + "nju_v2" => heads::mirror_heads::nju + version2_suffix, + "nyist" => heads::mirror_heads::nyist + version_suffix, + "nyist_v2" => heads::mirror_heads::nyist + version2_suffix, + "qlut" => heads::mirror_heads::qlut + version_suffix, + "qlut_v2" => heads::mirror_heads::qlut + version2_suffix, + "sjtug" => heads::mirror_heads::sjtug + version_suffix, + "sjtug_v2" => heads::mirror_heads::sjtug + version2_suffix, + "ustc" => heads::mirror_heads::ustc + version_suffix, + "ustc_v2" => heads::mirror_heads::ustc + version2_suffix, + _ => heads::mojang_heads::launcher_meta + version_suffix, } } pub fn get_assests(mirror: &str) -> &'static str { match mirror { - "bmclapi" => bmclapi + "/assets", - "jcut" => jcut + "/assets", - "lzuoss" => lzuoss + "/assets", - "nju" => nju + "/assets", - "nyist" => nyist + "/assets", - "qlut" => qlut + "/assets", - "sjtug" => sjtug + "/assets", - "ustc" => ustc + "/assets", - _ => mojang_heads::assests, + "bmclapi" => heads::mirror_heads::bmclapi + "/assets", + "jcut" => heads::mirror_heads::jcut + "/assets", + "lzuoss" => heads::mirror_heads::lzuoss + "/assets", + "nju" => heads::mirror_heads::nju + "/assets", + "nyist" => heads::mirror_heads::nyist + "/assets", + "qlut" => heads::mirror_heads::qlut + "/assets", + "sjtug" => heads::mirror_heads::sjtug + "/assets", + "ustc" => heads::mirror_heads::ustc + "/assets", + _ => heads::mojang_heads::assests, } } From cd03d25fb66a779214cde5cb47c618b7e9d6e6d5 Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Mon, 2 Feb 2026 22:22:39 +0800 Subject: [PATCH 13/15] fix manifest fetch method --- src-tauri/src/game_version/get_version.rs | 152 ++++++++++++++-------- 1 file changed, 96 insertions(+), 56 deletions(-) diff --git a/src-tauri/src/game_version/get_version.rs b/src-tauri/src/game_version/get_version.rs index 2671589..be83ad5 100644 --- a/src-tauri/src/game_version/get_version.rs +++ b/src-tauri/src/game_version/get_version.rs @@ -76,31 +76,7 @@ pub mod get_version { } } - async fn fetch_json(url: &str, timeout_seconds: Option) -> Result { - // 创建可复用的 HTTP 客户端(启用连接池) - let client = Client::builder() - .timeout(Duration::from_secs(timeout_seconds.unwrap_or(30))) - .pool_max_idle_per_host(5) - .gzip(true) - .brotli(true) - .build()?; - - // 发送请求并获取响应 - let response = client.get(url).send().await?; - - if !response.status().is_success() { - return Err(Error::from(std::io::Error::new( - std::io::ErrorKind::Other, - format!("HTTP错误: {}", response.status()), - ))); - } - - let content = response.text().await?; - - Ok(content) - } - - pub fn get_version(mirror: &str) -> &'static str { + pub fn get_version_manifest_link(mirror: &str) -> &'static str { match mirror { "mojang" => heads::mojang_heads::launcher_meta + version_suffix, "mojang_v2" => heads::mojang_heads::launcher_meta + version2_suffix, @@ -138,50 +114,114 @@ pub mod get_version { } } - /// 镜像源枚举 - pub enum MirrorSource { - Mojang, - Bmclapi, - Jcut, - Lzuoss, - Nju, - Nyist, - Qlut, - Sjtug, - Ustc, + #[derive(Debug)] + pub enum VersionError { + NetworkError(String), + ParseError(String), + InvalidMirror(String), } - impl MirrorSource { - /// 获取镜像源的基础URL - fn base_url(&self) -> &str { + impl std::fmt::Display for VersionError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - MirrorSource::Mojang => &mojang_heads::launcher_meta, - MirrorSource::Bmclapi => &mirror_heads::bmclapi, - MirrorSource::Jcut => &mirror_heads::jcut, - MirrorSource::Lzuoss => &mirror_heads::lzuoss, - MirrorSource::Nju => &mirror_heads::nju, - MirrorSource::Nyist => &mirror_heads::nyist, - MirrorSource::Qlut => &mirror_heads::qlut, - MirrorSource::Sjtug => &mirror_heads::sjtug, - MirrorSource::Ustc => &mirror_heads::ustc, + VersionError::NetworkError(e) => write!(f, "网络请求失败: {}", e), + VersionError::ParseError(e) => write!(f, "数据解析失败: {}", e), + VersionError::InvalidMirror(e) => write!(f, "无效的镜像源: {}", e), + } + } + } + + impl std::error::Error for VersionError {} + + pub struct VersionManifestParams { + pub use_version2: bool, + pub timeout_seconds: u64, + } + + impl Default for VersionManifestParams { + fn default() -> Self { + Self { + use_version2: true, + timeout_seconds: 3, } } } /// 统一的版本清单获取函数 - pub fn fetch_version_manifest( + pub async fn fetch_version_manifest( source: MirrorSource, - params: Option<(bool, Option)>, - ) -> Result { - let (use_version2, timeout_seconds) = params.unwrap_or((true, Some(3))); + params: Option, + ) -> Result { + let params = params.unwrap_or_default(); let base_url = source.base_url(); - let url = if use_version2 { - base_url.to_string() + version2_suffix + let url = if params.use_version2 { + format!("{}{}", base_url, version2_suffix) } else { - base_url.to_string() + version_suffix + format!("{}{}", base_url, version_suffix) }; - fetch_json(&url, timeout_seconds) + match fetch_json(&url, Some(params.timeout_seconds)).await { + Ok(content) => Ok(content), + Err(e) => Err(VersionError::NetworkError(e.to_string())), + } + } + + pub enum MirrorSource { + Mojang { use_v2: bool }, + Bmclapi { use_v2: bool }, + Jcut { use_v2: bool }, + Lzuoss { use_v2: bool }, + Nju { use_v2: bool }, + Nyist { use_v2: bool }, + Qlut { use_v2: bool }, + Sjtug { use_v2: bool }, + Ustc { use_v2: bool }, + } + + impl MirrorSource { + fn base_url(&self) -> &str { + match self { + MirrorSource::Mojang { use_v2: _ } => &heads::mojang_heads::launcher_meta, + MirrorSource::Bmclapi { use_v2: _ } => &heads::mirror_heads::bmclapi, + MirrorSource::Jcut { use_v2: _ } => &heads::mirror_heads::jcut, + MirrorSource::Lzuoss { use_v2: _ } => &heads::mirror_heads::lzuoss, + MirrorSource::Nju { use_v2: _ } => &heads::mirror_heads::nju, + MirrorSource::Nyist { use_v2: _ } => &heads::mirror_heads::nyist, + MirrorSource::Qlut { use_v2: _ } => &heads::mirror_heads::qlut, + MirrorSource::Sjtug { use_v2: _ } => &heads::mirror_heads::sjtug, + MirrorSource::Ustc { use_v2: _ } => &heads::mirror_heads::ustc, + } + } + } + + async fn fetch_json(url: &str, timeout_seconds: Option) -> Result { + let client = Client::builder() + .timeout(Duration::from_secs(timeout_seconds.unwrap_or(30))) + .pool_max_idle_per_host(5) + .gzip(true) + .brotli(true) + .build() + .map_err(|e| VersionError::NetworkError(e.to_string()))?; + + let response = client + .get(url) + .send() + .await + .map_err(|e| VersionError::NetworkError(e.to_string()))?; + + if !response.status().is_success() { + return Err(VersionError::NetworkError(format!( + "HTTP错误: {}", + response.status() + ))); + } + + let content = response + .text() + .await + .map_err(|e| VersionError::ParseError(e.to_string()))?; + + Ok(content) } } From 530f0bb19b8148b1f67ecccf4a3bfac8e581f9b2 Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Tue, 3 Feb 2026 17:40:30 +0800 Subject: [PATCH 14/15] add repository --- src-tauri/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e5057b4..43bb570 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "为生电而生的多平台 Minecraft 启动器" authors = ["Grey-Wind", "Moralts"] license = "" -repository = "" +repository = "https://github.com/Lighting-Team/RTLauncher" edition = "2021" rust-version = "1.77.2" From 72874b63b09fb33755321edaf746824d7b9d4d26 Mon Sep 17 00:00:00 2001 From: Grey Wind Date: Tue, 3 Feb 2026 18:16:47 +0800 Subject: [PATCH 15/15] feat: download --- src-tauri/Cargo.lock | 28 +++ src-tauri/Cargo.toml | 8 +- src-tauri/src/utils/download.rs | 413 ++++++++++++++++++++++++++++++++ 3 files changed, 448 insertions(+), 1 deletion(-) create mode 100644 src-tauri/src/utils/download.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c217257..a0ddaa3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6,6 +6,7 @@ version = 3 name = "RTLauncher" version = "0.1.0" dependencies = [ + "anyhow", "log", "regex", "reqwest 0.11.27", @@ -15,6 +16,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-log", + "tokio", ] [[package]] @@ -2958,10 +2960,12 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "winreg 0.50.0", ] @@ -3421,6 +3425,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -4103,11 +4117,25 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2 0.6.2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 43bb570..86ffefe 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2.5.3", features = [] } [dependencies] -reqwest = { version = "0.11", features = ["blocking", "json"] } +reqwest = { version = "0.11", features = ["blocking", "json", "stream"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } log = "0.4" @@ -26,3 +26,9 @@ tauri = { version = "2.9.5", features = [] } tauri-plugin-log = "2" serde_yaml = "0.9.34" regex = "1.12.2" +tokio = { version = "1.0", features = ["full"] } +anyhow = "1.0" +indicatif = "0.17" +futures = "0.3" +url = "2.4" +tempfile = "3.4" diff --git a/src-tauri/src/utils/download.rs b/src-tauri/src/utils/download.rs new file mode 100644 index 0000000..a663838 --- /dev/null +++ b/src-tauri/src/utils/download.rs @@ -0,0 +1,413 @@ +pub mod download { + use anyhow::{ Context, Result, anyhow }; + use reqwest::header::{ HeaderMap, HeaderValue, CONTENT_LENGTH, RANGE, ACCEPT_RANGES }; + use std::fs::{ File, OpenOptions }; + use std::io::{ Write, Seek, SeekFrom }; + use std::path::{ Path, PathBuf }; + use std::sync::Arc; + use std::time::{ Duration, Instant }; + use tokio::sync::Mutex; + use indicatif::{ ProgressBar, ProgressStyle, MultiProgress, HumanBytes }; + use futures::stream::{ StreamExt, TryStreamExt }; + use futures::future::join_all; + + #[derive(Debug, Clone)] + pub struct DownloadConfig { + pub url: String, + pub output_path: PathBuf, + pub max_retries: u32, + pub retry_delay: Duration, + pub timeout: Option, + pub user_agent: Option, + pub max_connections: usize, + pub min_chunk_size: u64, + pub show_progress: bool, + } + + impl Default for DownloadConfig { + fn default() -> Self { + Self { + url: String::new(), + output_path: PathBuf::new(), + max_retries: 3, + retry_delay: Duration::from_secs(2), + timeout: Some(Duration::from_secs(30)), + user_agent: Some( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36".to_string() + ), + max_connections: 4, + min_chunk_size: 1024 * 1024, + show_progress: true, + } + } + } + + /// 下载状态 + #[derive(Debug, Clone)] + pub struct DownloadStatus { + pub total_size: Option, + pub downloaded: u64, + pub speed: f64, + pub is_complete: bool, + pub elapsed_time: Duration, + } + + /// 下载器 + pub struct Downloader { + config: DownloadConfig, + client: reqwest::Client, + progress_bar: Option, + multi_progress: Option, + } + + impl Downloader { + pub fn new(config: DownloadConfig) -> Result { + let mut headers = HeaderMap::new(); + + if let Some(ua) = &config.user_agent { + headers.insert("User-Agent", HeaderValue::from_str(ua)?); + } + + let mut client_builder = reqwest::Client::builder().default_headers(headers); + + if let Some(timeout) = config.timeout { + client_builder = client_builder.timeout(timeout); + } + + let client = client_builder.build()?; + + Ok(Self { + config, + client, + progress_bar: None, + multi_progress: None, + }) + } + + pub async fn download(&mut self) -> Result { + let start_time = Instant::now(); + + let file_info = self.get_file_info().await?; + + if self.config.show_progress { + self.setup_progress_bar(file_info.total_size); + } + + let downloaded = if file_info.supports_partial && self.config.max_connections > 1 { + self.download_with_multiple_connections(&file_info).await? + } else { + self.download_single_connection(&file_info).await? + }; + + let elapsed_time = start_time.elapsed(); + let speed = (downloaded as f64) / elapsed_time.as_secs_f64(); + + if let Some(pb) = &self.progress_bar { + pb.finish_with_message("Complete"); + } + + Ok(DownloadStatus { + total_size: Some(file_info.total_size), + downloaded, + speed, + is_complete: true, + elapsed_time, + }) + } + + pub async fn download_file(url: &str, output_path: &Path) -> Result { + let config = DownloadConfig { + url: url.to_string(), + output_path: output_path.to_path_buf(), + ..Default::default() + }; + + let mut downloader = Downloader::new(config)?; + downloader.download().await + } + + pub async fn download_files(urls: &[(&str, &Path)]) -> Result>> { + let mut tasks = Vec::new(); + + for (url, path) in urls { + let url = (*url).to_string(); + let path = (*path).to_path_buf(); + + tasks.push( + tokio::spawn(async move { Downloader::download_file(&url, &path).await }) + ); + } + + let results = join_all(tasks).await; + let mut statuses = Vec::new(); + + for result in results { + statuses.push(result.map_err(|e| anyhow!("Failed: {}", e))?); + } + + Ok(statuses) + } + + async fn get_file_info(&self) -> Result { + let response = self.client.head(&self.config.url).send().await?; + + if !response.status().is_success() { + return Err(anyhow!("Failed: {}", response.status())); + } + + let total_size = response + .headers() + .get(CONTENT_LENGTH) + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + let supports_partial = response + .headers() + .get(ACCEPT_RANGES) + .and_then(|v| v.to_str().ok()) + .map(|s| s == "bytes") + .unwrap_or(false); + + Ok(FileInfo { + total_size, + supports_partial, + }) + } + + async fn download_single_connection(&self, file_info: &FileInfo) -> Result { + let mut response = self.client.get(&self.config.url).send().await?; + + if !response.status().is_success() { + return Err(anyhow!("Failed: {}", response.status())); + } + + let mut file = self.create_file(0, file_info.total_size)?; + let mut downloaded = 0; + let mut last_update = Instant::now(); + + while let Some(chunk) = response.chunk().await? { + file.write_all(&chunk)?; + downloaded += chunk.len() as u64; + + if self.config.show_progress && last_update.elapsed() > Duration::from_millis(100) { + if let Some(pb) = &self.progress_bar { + pb.set_position(downloaded); + } + last_update = Instant::now(); + } + } + + Ok(downloaded) + } + + async fn download_with_multiple_connections(&self, file_info: &FileInfo) -> Result { + let total_size = file_info.total_size; + let chunk_size = self.calculate_chunk_size(total_size); + let num_chunks = (total_size + chunk_size - 1) / chunk_size; + + println!("{} connections", num_chunks.min(self.config.max_connections)); + + let mut tasks = Vec::new(); + let progress = Arc::new(Mutex::new(0u64)); + + for i in 0..num_chunks.min(self.config.max_connections) { + let start = i * chunk_size; + let end = if i == num_chunks - 1 { + total_size - 1 + } else { + (i + 1) * chunk_size - 1 + }; + + let config = self.config.clone(); + let client = self.client.clone(); + let progress = Arc::clone(&progress); + let pb = self.progress_bar.clone(); + + tasks.push( + tokio::spawn(async move { + Self::download_chunk(&client, &config, start, end, i, progress, pb).await + }) + ); + } + + let results = join_all(tasks).await; + let mut total_downloaded = 0; + + for result in results { + total_downloaded += result.map_err(|e| anyhow!("Failed: {}", e))??; + } + + self.merge_chunks(num_chunks.min(self.config.max_connections))?; + + Ok(total_downloaded) + } + + async fn download_chunk( + client: &reqwest::Client, + config: &DownloadConfig, + start: u64, + end: u64, + chunk_id: u64, + progress: Arc>, + pb: Option + ) -> Result { + let mut retries = 0; + + while retries <= config.max_retries { + match + Self::download_chunk_internal( + client, + config, + start, + end, + chunk_id, + &progress, + &pb + ).await + { + Ok(size) => { + return Ok(size); + } + Err(e) if retries < config.max_retries => { + eprintln!( + "Chunk {} Failed (Retry {}/{}): {}", + chunk_id, + retries + 1, + config.max_retries, + e + ); + tokio::time::sleep(config.retry_delay).await; + retries += 1; + } + Err(e) => { + return Err(e); + } + } + } + + Err(anyhow!("Failed to download chunk {}, reached max retries", chunk_id)) + } + + async fn download_chunk_internal( + client: &reqwest::Client, + config: &DownloadConfig, + start: u64, + end: u64, + chunk_id: u64, + progress: &Arc>, + pb: &Option + ) -> Result { + let range_header = format!("bytes={}-{}", start, end); + + let mut response = client.get(&config.url).header(RANGE, range_header).send().await?; + + if !response.status().is_success() { + return Err(anyhow!("Failed to request chunk: {}", response.status())); + } + + let chunk_path = config.output_path.with_extension(format!("part{}", chunk_id)); + let mut file = File::create(&chunk_path)?; + let mut downloaded = 0; + + while let Some(chunk) = response.chunk().await? { + file.write_all(&chunk)?; + downloaded += chunk.len() as u64; + + let mut total_progress = progress.lock().await; + *total_progress += chunk.len() as u64; + + if let Some(pb) = pb { + pb.set_position(*total_progress); + } + } + + Ok(downloaded) + } + + fn merge_chunks(&self, num_chunks: usize) -> Result<()> { + let mut output_file = File::create(&self.config.output_path)?; + + for i in 0..num_chunks { + let chunk_path = self.config.output_path.with_extension(format!("part{}", i)); + let mut chunk_file = File::open(&chunk_path)?; + std::io::copy(&mut chunk_file, &mut output_file)?; + + let _ = std::fs::remove_file(&chunk_path); + } + + Ok(()) + } + + fn create_file(&self, start: u64, total_size: u64) -> Result { + let file = OpenOptions::new().create(true).write(true).open(&self.config.output_path)?; + + if start > 0 && total_size > 0 { + file.set_len(total_size)?; + } + + Ok(file) + } + + fn calculate_chunk_size(&self, total_size: u64) -> u64 { + let chunk_size = total_size / (self.config.max_connections as u64); + chunk_size.max(self.config.min_chunk_size) + } + + fn setup_progress_bar(&mut self, total_size: u64) { + let pb = ProgressBar::new(total_size); + + pb.set_style( + ProgressStyle::default_bar() + .template( + "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})" + ) + .unwrap() + .progress_chars("#>-") + ); + + self.progress_bar = Some(pb); + } + } + + #[derive(Debug)] + struct FileInfo { + total_size: u64, + supports_partial: bool, + } + + pub fn validate_url(url: &str) -> bool { + url.starts_with("http://") || url.starts_with("https://") + } + + pub fn get_filename_from_url(url: &str) -> Option { + url::Url + ::parse(url) + .ok() + .and_then(|u| { + u.path_segments().and_then(|segments| segments.last().map(String::from)) + }) + .filter(|name| !name.is_empty()) + } + + pub fn format_speed(speed: f64) -> String { + if speed >= 1024.0 * 1024.0 { + format!("{:.2} MB/s", speed / (1024.0 * 1024.0)) + } else if speed >= 1024.0 { + format!("{:.2} KB/s", speed / 1024.0) + } else { + format!("{:.0} B/s", speed) + } + } + + pub fn format_duration(duration: Duration) -> String { + let seconds = duration.as_secs(); + if seconds < 60 { + format!("{}s", seconds) + } else if seconds < 3600 { + format!("{}m{}s", seconds / 60, seconds % 60) + } else { + format!("{}h{}m{}s", seconds / 3600, (seconds % 3600) / 60, seconds % 60) + } + } +}