mirror of
https://github.com/yuhkix/wuwa-downloader.git
synced 2025-06-06 17:53:44 +00:00
Compare commits
23 commits
Author | SHA1 | Date | |
---|---|---|---|
|
118b4f5fe0 | ||
|
29bd052ef4 | ||
|
bece26c433 | ||
|
cc837ec0cf | ||
|
f6477359e9 | ||
|
8c0ee23828 | ||
|
278b7a5794 | ||
|
808f929e62 | ||
|
7bd0b77bd0 | ||
|
9d1c7143ec | ||
|
1ba74834fd | ||
|
4a19233285 | ||
|
df699792e3 | ||
|
b52e2010f0 | ||
|
09f3712390 | ||
|
4d70933c4f | ||
|
ef9cbad14a | ||
|
31979bc66e | ||
|
6016da4b06 | ||
|
df32b7edae | ||
|
06e10b1f78 | ||
|
a4e0248ea5 | ||
|
4e0667371d |
19 changed files with 1064 additions and 467 deletions
47
.gitignore
vendored
47
.gitignore
vendored
|
@ -1,21 +1,26 @@
|
||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
debug/
|
debug/
|
||||||
target/
|
target/
|
||||||
|
.idea/
|
||||||
# These are backup files generated by rustfmt
|
|
||||||
**/*.rs.bk
|
# packaging
|
||||||
|
dist/
|
||||||
# Error / Debug Logging
|
|
||||||
*.log
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
|
||||||
*.pdb
|
# Error / Debug Logging
|
||||||
|
*.log
|
||||||
|
*.txt
|
||||||
# RustRover
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
*.pdb
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
# RustRover
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
193
Cargo.lock
generated
193
Cargo.lock
generated
|
@ -113,6 +113,19 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console"
|
||||||
|
version = "0.15.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
|
||||||
|
dependencies = [
|
||||||
|
"encode_unicode",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"unicode-width",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
@ -150,9 +163,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctrlc"
|
name = "ctrlc"
|
||||||
version = "3.4.5"
|
version = "3.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
|
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nix",
|
"nix",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
|
@ -170,22 +183,23 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "4.0.0"
|
version = "5.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs-sys",
|
"dirs-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-sys"
|
name = "dirs-sys"
|
||||||
version = "0.3.7"
|
version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
|
"option-ext",
|
||||||
"redox_users",
|
"redox_users",
|
||||||
"winapi",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -199,6 +213,12 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
|
@ -232,9 +252,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
|
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
|
@ -479,9 +499,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
|
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
@ -489,6 +509,7 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"libc",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -537,9 +558,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_locid_transform_data"
|
name = "icu_locid_transform_data"
|
||||||
version = "1.5.0"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
|
checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer"
|
name = "icu_normalizer"
|
||||||
|
@ -561,9 +582,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer_data"
|
name = "icu_normalizer_data"
|
||||||
version = "1.5.0"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
|
checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties"
|
name = "icu_properties"
|
||||||
|
@ -582,9 +603,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties_data"
|
name = "icu_properties_data"
|
||||||
version = "1.5.0"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
|
checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_provider"
|
name = "icu_provider"
|
||||||
|
@ -645,6 +666,19 @@ dependencies = [
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indicatif"
|
||||||
|
version = "0.17.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
|
||||||
|
dependencies = [
|
||||||
|
"console",
|
||||||
|
"number_prefix",
|
||||||
|
"portable-atomic",
|
||||||
|
"unicode-width",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
|
@ -768,9 +802,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.29.0"
|
version = "0.30.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
@ -778,6 +812,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "number_prefix"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.7"
|
version = "0.36.7"
|
||||||
|
@ -789,9 +829,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.1"
|
version = "1.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
|
@ -837,6 +877,12 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
@ -861,6 +907,12 @@ version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.94"
|
version = "1.0.94"
|
||||||
|
@ -972,9 +1024,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.0.3"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
|
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
|
@ -1013,9 +1065,9 @@ checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.0"
|
version = "0.103.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f"
|
checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
|
@ -1112,9 +1164,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shellexpand"
|
name = "shellexpand"
|
||||||
version = "3.1.0"
|
version = "3.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
|
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
]
|
]
|
||||||
|
@ -1142,9 +1194,9 @@ checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
|
@ -1378,6 +1430,12 @@ version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -1524,6 +1582,16 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
|
@ -1592,6 +1660,15 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
@ -1610,6 +1687,21 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -1642,6 +1734,12 @@ dependencies = [
|
||||||
"windows_x86_64_msvc 0.53.0",
|
"windows_x86_64_msvc 0.53.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -1654,6 +1752,12 @@ version = "0.53.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -1666,6 +1770,12 @@ version = "0.53.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -1690,6 +1800,12 @@ version = "0.53.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -1702,6 +1818,12 @@ version = "0.53.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -1714,6 +1836,12 @@ version = "0.53.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -1726,6 +1854,12 @@ version = "0.53.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
@ -1775,6 +1909,7 @@ dependencies = [
|
||||||
"colored",
|
"colored",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
"indicatif",
|
||||||
"md-5",
|
"md-5",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
21
Cargo.toml
21
Cargo.toml
|
@ -4,16 +4,25 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
winres = "0.1.12"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
colored = "3.0.0"
|
colored = "3.0.0"
|
||||||
md-5 = "0.10.6"
|
md-5 = "0.10.6"
|
||||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
serde = "1.0.219"
|
serde = "1.0.219"
|
||||||
|
ctrlc = "3.4.7"
|
||||||
|
shellexpand = "3.1.1"
|
||||||
|
flate2 = "1.1.1"
|
||||||
|
indicatif = "0.17.11"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
winconsole = "0.11.1"
|
winconsole = "0.11.1"
|
||||||
ctrlc = "3.4.5"
|
|
||||||
shellexpand = "3.1.0"
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
flate2 = "1.1.0"
|
winres = "0.1.12"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
||||||
|
lto = true
|
||||||
|
opt-level = 3
|
||||||
|
codegen-units = 1
|
42
LICENSE
42
LICENSE
|
@ -1,21 +1,21 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Yuhki
|
Copyright (c) 2025 Yuhki
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
furnished to do so, subject to the following conditions:
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The above copyright notice and this permission notice shall be included in all
|
||||||
copies or substantial portions of the Software.
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
155
README.md
155
README.md
|
@ -1,77 +1,78 @@
|
||||||
# 🌊 Wuthering Waves Downloader
|
# 🌊 Wuthering Waves Downloader
|
||||||
|
|
||||||
*A high-performance, reliable downloader for Wuthering Waves with multi-CDN support and verification*
|
*A high-performance, reliable downloader for Wuthering Waves with multi-CDN support and verification*
|
||||||
|
|
||||||
> Feel free to open Pull requests if you want to contribute with improvements!
|
> Feel free to open Pull requests if you want to contribute with improvements!
|
||||||
|
|
||||||
[](https://www.rust-lang.org/)
|
[](https://www.rust-lang.org/)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
## 📦 Requirements
|
## 📦 Requirements
|
||||||
- **Rust nightly toolchain** - 1.87.0-nightly or newer
|
- **Rust nightly toolchain** - 1.87.0-nightly or newer
|
||||||
- **Windows** - for full console feature support
|
- **Windows** - for full console feature support
|
||||||
|
- **Linux** - for simply being better
|
||||||
### 🛠️ Installation & Usage
|
|
||||||
- **Install the nightly toolchain:**
|
### 🛠️ Installation & Usage
|
||||||
```bash
|
- **Install the nightly toolchain:**
|
||||||
rustup toolchain install nightly
|
```bash
|
||||||
rustup default nightly
|
rustup toolchain install nightly
|
||||||
```
|
rustup default nightly
|
||||||
|
```
|
||||||
- **Clone the repository:**
|
|
||||||
```bash
|
- **Clone the repository:**
|
||||||
git clone https://github.com/yourusername/wuthering-waves-downloader.git
|
```bash
|
||||||
cd wuthering-waves-downloader
|
git clone https://github.com/yuhkix/wuwa-downloader.git
|
||||||
```
|
cd wuwa-downloader
|
||||||
|
```
|
||||||
- **Build the project:**
|
|
||||||
```bash
|
- **Build the project:**
|
||||||
cargo build --release
|
```bash
|
||||||
```
|
cargo build --release
|
||||||
|
```
|
||||||
## 🌟 Key Features
|
|
||||||
|
## 🌟 Key Features
|
||||||
### 🚀 Download Management
|
|
||||||
- **Multi-CDN Fallback** - Automatically tries all available CDN mirrors
|
### 🚀 Download Management
|
||||||
|
- **Multi-CDN Fallback** - Automatically tries all available CDN mirrors
|
||||||
- **Version Selection** - Interactive menu for Live/Beta versions
|
|
||||||
|
- **Version Selection** - Interactive menu for Live/Beta versions
|
||||||
- **Verified Downloads** - MD5 checksum validation for every file
|
|
||||||
|
- **Verified Downloads** - MD5 checksum validation for every file
|
||||||
- **Smart Retry Logic** - 3 retry attempts per CDN with timeout protection
|
|
||||||
|
- **Smart Retry Logic** - 3 retry attempts per CDN with timeout protection
|
||||||
- **GZIP Support** - Handles compressed responses efficiently
|
|
||||||
|
- **GZIP Support** - Handles compressed responses efficiently
|
||||||
### 🛡️ Reliability
|
|
||||||
- **Atomic Operations** - Thread-safe progress tracking
|
### 🛡️ Reliability
|
||||||
|
- **Atomic Operations** - Thread-safe progress tracking
|
||||||
- **Graceful Interrupt** - CTRL-C handling with summary display
|
|
||||||
|
- **Graceful Interrupt** - CTRL-C handling with summary display
|
||||||
- **Comprehensive Logging** - Detailed error logging with timestamps
|
|
||||||
|
- **Comprehensive Logging** - Detailed error logging with timestamps
|
||||||
- **Validation Failures** - Auto-removes files with checksum mismatches
|
|
||||||
|
- **Validation Failures** - Auto-removes files with checksum mismatches
|
||||||
### 📂 File Management
|
|
||||||
- **Smart Path Handling** - Cross-platform path support
|
### 📂 File Management
|
||||||
|
- **Smart Path Handling** - Cross-platform path support
|
||||||
- **Auto-directory Creation** - Builds full directory trees as needed
|
|
||||||
|
- **Auto-directory Creation** - Builds full directory trees as needed
|
||||||
- **Clean Failed Downloads** - Removes corrupted files automatically
|
|
||||||
|
- **Clean Failed Downloads** - Removes corrupted files automatically
|
||||||
### 💻 User Interface
|
|
||||||
- **Color-coded Output** - Clear visual feedback (success/warning/error)
|
### 💻 User Interface
|
||||||
|
- **Color-coded Output** - Clear visual feedback (success/warning/error)
|
||||||
- **Dynamic Title Updates** - Real-time progress in window title
|
|
||||||
|
- **Dynamic Title Updates** - Real-time progress in window title
|
||||||
- **Clean Progress Display** - Simplified download status without clutter
|
|
||||||
|
- **Clean Progress Display** - Simplified download status without clutter
|
||||||
- **Formatted Duration** - Clear elapsed time display (HH:MM:SS)
|
|
||||||
|
- **Formatted Duration** - Clear elapsed time display (HH:MM:SS)
|
||||||
### ⚙️ Technical Details
|
|
||||||
- **Streaming Downloads** - Memory-efficient chunked transfers
|
### ⚙️ Technical Details
|
||||||
|
- **Streaming Downloads** - Memory-efficient chunked transfers
|
||||||
- **HEAD Request Verification** - Pre-checks file availability
|
|
||||||
|
- **HEAD Request Verification** - Pre-checks file availability
|
||||||
- **Multi-threaded** - Safe concurrent progress tracking
|
|
||||||
|
- **Multi-threaded** - Safe concurrent progress tracking
|
||||||
- **Configurable Timeouts** - 30s for metadata, 300s for downloads
|
|
||||||
|
- **Configurable Timeouts** - 30s for metadata, 10000s for downloads
|
||||||
|
|
15
build.rs
15
build.rs
|
@ -1,7 +1,8 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" {
|
#[cfg(windows)]
|
||||||
let mut res = winres::WindowsResource::new();
|
{
|
||||||
res.set_icon("icon.ico");
|
let mut res = winres::WindowsResource::new();
|
||||||
res.compile().unwrap();
|
res.set_icon("cartethyia.ico");
|
||||||
}
|
res.compile().unwrap();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
BIN
cartethyia.ico
Normal file
BIN
cartethyia.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
BIN
icon.ico
BIN
icon.ico
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
113
package.sh
Normal file
113
package.sh
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BIN_NAME="wuwa-downloader"
|
||||||
|
DIST_DIR="dist"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
function info() {
|
||||||
|
echo -e "${CYAN}==> $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function success() {
|
||||||
|
echo -e "${GREEN}✔ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function warn() {
|
||||||
|
echo -e "${YELLOW}⚠ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function error() {
|
||||||
|
echo -e "${RED}✖ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_linux() {
|
||||||
|
local OUT_DIR="target/release"
|
||||||
|
local PACKAGE_NAME="${BIN_NAME}-linux-x86_64"
|
||||||
|
local PACKAGE_DIR="${DIST_DIR}/${PACKAGE_NAME}"
|
||||||
|
local ARCHIVE_NAME="${PACKAGE_NAME}.tar.gz"
|
||||||
|
|
||||||
|
clear
|
||||||
|
|
||||||
|
info "Cleaning binaries to rebuild..."
|
||||||
|
cargo clean
|
||||||
|
|
||||||
|
info "Building release binary for Linux..."
|
||||||
|
cargo build --release
|
||||||
|
clear
|
||||||
|
success "Linux build finished"
|
||||||
|
|
||||||
|
info "Creating package directory..."
|
||||||
|
rm -rf "$PACKAGE_DIR"
|
||||||
|
mkdir -p "$PACKAGE_DIR"
|
||||||
|
success "Package directory ready: $PACKAGE_DIR"
|
||||||
|
|
||||||
|
info "Copying binary..."
|
||||||
|
cp "$OUT_DIR/$BIN_NAME" "$PACKAGE_DIR/"
|
||||||
|
success "Copied binary to package directory"
|
||||||
|
|
||||||
|
info "Creating archive..."
|
||||||
|
cd "$DIST_DIR"
|
||||||
|
tar -czf "$ARCHIVE_NAME" "$PACKAGE_NAME"
|
||||||
|
cd -
|
||||||
|
success "Archive created: ${DIST_DIR}/${ARCHIVE_NAME}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_windows() {
|
||||||
|
local TARGET="x86_64-pc-windows-gnu"
|
||||||
|
local OUT_DIR="target/${TARGET}/release"
|
||||||
|
local PACKAGE_NAME="${BIN_NAME}-windows-x86_64"
|
||||||
|
local PACKAGE_DIR="${DIST_DIR}/${PACKAGE_NAME}"
|
||||||
|
local ARCHIVE_NAME="${PACKAGE_NAME}.zip"
|
||||||
|
|
||||||
|
clear
|
||||||
|
|
||||||
|
info "Cleaning binaries to rebuild..."
|
||||||
|
cargo clean
|
||||||
|
|
||||||
|
info "Building release binary for Windows..."
|
||||||
|
cargo build --release --target "$TARGET"
|
||||||
|
clear
|
||||||
|
success "Windows build finished"
|
||||||
|
|
||||||
|
info "Creating package directory..."
|
||||||
|
rm -rf "$PACKAGE_DIR"
|
||||||
|
mkdir -p "$PACKAGE_DIR"
|
||||||
|
success "Package directory ready: $PACKAGE_DIR"
|
||||||
|
|
||||||
|
info "Copying binary..."
|
||||||
|
cp "$OUT_DIR/${BIN_NAME}.exe" "$PACKAGE_DIR/"
|
||||||
|
success "Copied binary to package directory"
|
||||||
|
|
||||||
|
info "Creating archive..."
|
||||||
|
cd "$DIST_DIR"
|
||||||
|
zip -r "$ARCHIVE_NAME" "$PACKAGE_NAME"
|
||||||
|
cd -
|
||||||
|
success "Archive created: ${DIST_DIR}/${ARCHIVE_NAME}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
error "Usage: $0 [linux|windows]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
linux)
|
||||||
|
build_linux
|
||||||
|
;;
|
||||||
|
windows)
|
||||||
|
build_windows
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Unknown target: $1"
|
||||||
|
error "Usage: $0 [linux|windows]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
|
@ -2,4 +2,4 @@
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub index_url: String,
|
pub index_url: String,
|
||||||
pub zip_bases: Vec<String>,
|
pub zip_bases: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,25 @@ use colored::*;
|
||||||
pub struct Status;
|
pub struct Status;
|
||||||
|
|
||||||
impl Status {
|
impl Status {
|
||||||
pub fn info() -> ColoredString { "[*]".cyan() }
|
pub fn info() -> ColoredString {
|
||||||
pub fn success() -> ColoredString { "[+]".green() }
|
"[*]".cyan()
|
||||||
pub fn warning() -> ColoredString { "[!]".yellow() }
|
}
|
||||||
pub fn error() -> ColoredString { "[-]".red() }
|
pub fn success() -> ColoredString {
|
||||||
pub fn question() -> ColoredString { "[?]".blue() }
|
"[+]".green()
|
||||||
pub fn progress() -> ColoredString { "[→]".purple() }
|
}
|
||||||
pub fn matched() -> ColoredString { "[↓]".bright_purple() }
|
pub fn warning() -> ColoredString {
|
||||||
}
|
"[!]".yellow()
|
||||||
|
}
|
||||||
|
pub fn error() -> ColoredString {
|
||||||
|
"[-]".red()
|
||||||
|
}
|
||||||
|
pub fn question() -> ColoredString {
|
||||||
|
"[?]".blue()
|
||||||
|
}
|
||||||
|
pub fn progress() -> ColoredString {
|
||||||
|
"[→]".purple()
|
||||||
|
}
|
||||||
|
pub fn matched() -> ColoredString {
|
||||||
|
"[↓]".bright_purple()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{io, path::Path};
|
|
||||||
use colored::Colorize;
|
|
||||||
use crate::config::status::Status;
|
use crate::config::status::Status;
|
||||||
|
use colored::Colorize;
|
||||||
|
use std::{io, path::Path};
|
||||||
|
|
||||||
pub fn print_results(success: usize, total: usize, folder: &Path) {
|
pub fn print_results(success: usize, total: usize, folder: &Path) {
|
||||||
let title = if success == total {
|
let title = if success == total {
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
use std::{fs, io, io::Write, path::{Path, PathBuf}};
|
|
||||||
use md5::{Digest, Md5};
|
use md5::{Digest, Md5};
|
||||||
|
use std::{
|
||||||
|
fs, io,
|
||||||
|
io::Write,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::config::status::Status;
|
use crate::config::status::Status;
|
||||||
|
|
||||||
|
@ -10,7 +14,11 @@ pub fn calculate_md5(path: &Path) -> String {
|
||||||
format!("{:x}", hasher.finalize())
|
format!("{:x}", hasher.finalize())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_existing_file(path: &Path, expected_md5: Option<&str>, expected_size: Option<u64>) -> bool {
|
pub fn check_existing_file(
|
||||||
|
path: &Path,
|
||||||
|
expected_md5: Option<&str>,
|
||||||
|
expected_size: Option<u64>,
|
||||||
|
) -> bool {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -41,7 +49,7 @@ pub fn get_filename(path: &str) -> String {
|
||||||
pub fn get_dir() -> PathBuf {
|
pub fn get_dir() -> PathBuf {
|
||||||
loop {
|
loop {
|
||||||
print!(
|
print!(
|
||||||
"{} Enter download directory (Enter for current): ",
|
"{} Please specify the directory where the game should be downloaded (press Enter to use the current directory): ",
|
||||||
Status::question()
|
Status::question()
|
||||||
);
|
);
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
|
@ -74,4 +82,4 @@ pub fn get_dir() -> PathBuf {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
use std::{fs::{self, OpenOptions}, io::Write, time::{Duration, SystemTime}};
|
use std::{
|
||||||
|
fs::{self, OpenOptions},
|
||||||
use colored::Colorize;
|
io::Write,
|
||||||
use reqwest::blocking::Client;
|
time::SystemTime,
|
||||||
use serde_json::Value;
|
};
|
||||||
|
|
||||||
use crate::config::{cfg::Config, status::Status};
|
|
||||||
|
|
||||||
pub fn setup_logging() -> fs::File {
|
pub fn setup_logging() -> fs::File {
|
||||||
OpenOptions::new()
|
OpenOptions::new()
|
||||||
|
@ -21,83 +19,3 @@ pub fn log_error(mut log_file: &fs::File, message: &str) {
|
||||||
.as_secs();
|
.as_secs();
|
||||||
writeln!(log_file, "[{}] ERROR: {}", timestamp, message).unwrap();
|
writeln!(log_file, "[{}] ERROR: {}", timestamp, message).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_duration(duration: Duration) -> String {
|
|
||||||
let secs = duration.as_secs();
|
|
||||||
format!("{:02}:{:02}:{:02}", secs / 3600, (secs % 3600) / 60, secs % 60)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bytes_to_human(bytes: u64) -> String {
|
|
||||||
match bytes {
|
|
||||||
b if b > 1_000_000_000 => format!("{:.2} GB", b as f64 / 1_000_000_000.0),
|
|
||||||
b if b > 1_000_000 => format!("{:.2} MB", b as f64 / 1_000_000.0),
|
|
||||||
b if b > 1_000 => format!("{:.2} KB", b as f64 / 1_000.0),
|
|
||||||
b => format!("{} B", b),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Config) -> u64 {
|
|
||||||
let mut total_size = 0;
|
|
||||||
let mut failed_urls = 0;
|
|
||||||
|
|
||||||
println!("{} Processing files...", Status::info());
|
|
||||||
|
|
||||||
for (i, item) in resources.iter().enumerate() {
|
|
||||||
if let Some(dest) = item.get("dest").and_then(Value::as_str) {
|
|
||||||
let mut file_size = 0;
|
|
||||||
let mut found_valid_url = false;
|
|
||||||
|
|
||||||
for base_url in &config.zip_bases {
|
|
||||||
let url = format!("{}/{}", base_url, dest);
|
|
||||||
match client.head(&url).send() {
|
|
||||||
Ok(response) => {
|
|
||||||
if let Some(len) = response.headers().get("content-length") {
|
|
||||||
if let Ok(len_str) = len.to_str() {
|
|
||||||
if let Ok(len_num) = len_str.parse::<u64>() {
|
|
||||||
file_size = len_num;
|
|
||||||
found_valid_url = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("{} Failed to HEAD {}: {}", Status::warning(), url, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if found_valid_url {
|
|
||||||
total_size += file_size;
|
|
||||||
} else {
|
|
||||||
failed_urls += 1;
|
|
||||||
println!("{} Could not determine size for file: {}", Status::error(), dest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if i % 10 == 0 {
|
|
||||||
println!(
|
|
||||||
"{} Processed {}/{} files...",
|
|
||||||
Status::info(),
|
|
||||||
i + 1,
|
|
||||||
resources.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if failed_urls > 0 {
|
|
||||||
println!(
|
|
||||||
"{} Warning: Could not determine size for {} files",
|
|
||||||
Status::warning(),
|
|
||||||
failed_urls
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"{} Total download size: {}",
|
|
||||||
Status::info(),
|
|
||||||
bytes_to_human(total_size).cyan()
|
|
||||||
);
|
|
||||||
|
|
||||||
total_size
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod console;
|
pub mod console;
|
||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
|
pub mod util;
|
||||||
|
|
279
src/io/util.rs
Normal file
279
src/io/util.rs
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
use colored::Colorize;
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
io::{self, Write},
|
||||||
|
sync::Arc,
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
use winconsole::console::{clear, set_title};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{cfg::Config, status::Status},
|
||||||
|
download::progress::DownloadProgress,
|
||||||
|
io::logging::log_error,
|
||||||
|
network::client::download_file,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn format_duration(duration: Duration) -> String {
|
||||||
|
let secs = duration.as_secs();
|
||||||
|
format!(
|
||||||
|
"{:02}:{:02}:{:02}",
|
||||||
|
secs / 3600,
|
||||||
|
(secs % 3600) / 60,
|
||||||
|
secs % 60
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bytes_to_human(bytes: u64) -> String {
|
||||||
|
match bytes {
|
||||||
|
b if b > 1_000_000_000 => format!("{:.2} GB", b as f64 / 1_000_000_000.0),
|
||||||
|
b if b > 1_000_000 => format!("{:.2} MB", b as f64 / 1_000_000.0),
|
||||||
|
b if b > 1_000 => format!("{:.2} KB", b as f64 / 1_000.0),
|
||||||
|
b => format!("{} B", b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_url(url: &str) {
|
||||||
|
let sanitized_url = if let Some(index) = url.find("://") {
|
||||||
|
let (scheme, rest) = url.split_at(index + 3);
|
||||||
|
format!("{}{}", scheme, rest.replace("//", "/"))
|
||||||
|
} else {
|
||||||
|
url.replace("//", "/")
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(mut url_log) = fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open("urls.txt")
|
||||||
|
{
|
||||||
|
let _ = writeln!(url_log, "{}", sanitized_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Config) -> u64 {
|
||||||
|
let mut total_size = 0;
|
||||||
|
let mut failed_urls = 0;
|
||||||
|
|
||||||
|
println!("{} Processing files...", Status::info());
|
||||||
|
|
||||||
|
for (i, item) in resources.iter().enumerate() {
|
||||||
|
if let Some(dest) = item.get("dest").and_then(Value::as_str) {
|
||||||
|
let mut file_size = 0;
|
||||||
|
let mut found_valid_url = false;
|
||||||
|
|
||||||
|
for base_url in &config.zip_bases {
|
||||||
|
let url = format!("{}/{}", base_url, dest);
|
||||||
|
log_url(&url);
|
||||||
|
match client.head(&url).send() {
|
||||||
|
Ok(response) => {
|
||||||
|
if let Some(len) = response.headers().get("content-length") {
|
||||||
|
if let Ok(len_str) = len.to_str() {
|
||||||
|
if let Ok(len_num) = len_str.parse::<u64>() {
|
||||||
|
file_size = len_num;
|
||||||
|
found_valid_url = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{} Failed to HEAD {}: {}", Status::warning(), url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if found_valid_url {
|
||||||
|
total_size += file_size;
|
||||||
|
} else {
|
||||||
|
failed_urls += 1;
|
||||||
|
println!(
|
||||||
|
"{} Could not determine size for file: {}",
|
||||||
|
Status::error(),
|
||||||
|
dest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i % 10 == 0 {
|
||||||
|
println!(
|
||||||
|
"{} Processed {}/{} files...",
|
||||||
|
Status::info(),
|
||||||
|
i + 1,
|
||||||
|
resources.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if failed_urls > 0 {
|
||||||
|
println!(
|
||||||
|
"{} Warning: Could not determine size for {} files",
|
||||||
|
Status::warning(),
|
||||||
|
failed_urls
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} Total download size: {}",
|
||||||
|
Status::info(),
|
||||||
|
bytes_to_human(total_size).cyan()
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
Command::new("clear").status().unwrap();
|
||||||
|
|
||||||
|
total_size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_version(data: &Value, category: &str, version: &str) -> Result<String, String> {
|
||||||
|
data[category][version]
|
||||||
|
.as_str()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.ok_or_else(|| format!("Missing {} URL", version))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit_with_error(log_file: &File, error: &str) -> ! {
|
||||||
|
log_error(log_file, error);
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
clear().unwrap();
|
||||||
|
|
||||||
|
println!("{} {}", Status::error(), error);
|
||||||
|
println!("\n{} Press Enter to exit...", Status::warning());
|
||||||
|
let _ = io::stdin().read_line(&mut String::new());
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn track_progress(
|
||||||
|
total_size: u64,
|
||||||
|
) -> (
|
||||||
|
Arc<std::sync::atomic::AtomicBool>,
|
||||||
|
Arc<std::sync::atomic::AtomicUsize>,
|
||||||
|
DownloadProgress,
|
||||||
|
) {
|
||||||
|
let should_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||||
|
let success = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||||
|
|
||||||
|
let progress = DownloadProgress {
|
||||||
|
total_bytes: Arc::new(std::sync::atomic::AtomicU64::new(total_size)),
|
||||||
|
downloaded_bytes: Arc::new(std::sync::atomic::AtomicU64::new(0)),
|
||||||
|
start_time: Instant::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(should_stop, success, progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_title_thread(
|
||||||
|
should_stop: Arc<std::sync::atomic::AtomicBool>,
|
||||||
|
success: Arc<std::sync::atomic::AtomicUsize>,
|
||||||
|
progress: DownloadProgress,
|
||||||
|
total_files: usize,
|
||||||
|
) -> thread::JoinHandle<()> {
|
||||||
|
thread::spawn(move || {
|
||||||
|
while !should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
let elapsed = progress.start_time.elapsed();
|
||||||
|
let elapsed_secs = elapsed.as_secs();
|
||||||
|
let downloaded_bytes = progress
|
||||||
|
.downloaded_bytes
|
||||||
|
.load(std::sync::atomic::Ordering::SeqCst);
|
||||||
|
let total_bytes = progress
|
||||||
|
.total_bytes
|
||||||
|
.load(std::sync::atomic::Ordering::SeqCst);
|
||||||
|
let current_success = success.load(std::sync::atomic::Ordering::SeqCst);
|
||||||
|
|
||||||
|
let speed = if elapsed_secs > 0 {
|
||||||
|
downloaded_bytes / elapsed_secs
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let (speed_value, speed_unit) = if speed > 1_000_000 {
|
||||||
|
(speed / 1_000_000, "MB/s")
|
||||||
|
} else {
|
||||||
|
(speed / 1_000, "KB/s")
|
||||||
|
};
|
||||||
|
|
||||||
|
let remaining_files = total_files - current_success;
|
||||||
|
let remaining_bytes = total_bytes.saturating_sub(downloaded_bytes);
|
||||||
|
let eta_secs = if speed > 0 && remaining_files > 0 {
|
||||||
|
remaining_bytes / speed
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let eta_str = format_duration(Duration::from_secs(eta_secs));
|
||||||
|
|
||||||
|
let progress_percent = if total_bytes > 0 {
|
||||||
|
format!(" ({}%)", (downloaded_bytes * 100 / total_bytes))
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let title = format!(
|
||||||
|
"Wuthering Waves Downloader - {}/{} files - Total Downloaded: {}{} - Speed: {}{} - Total ETA: {}",
|
||||||
|
current_success,
|
||||||
|
total_files,
|
||||||
|
bytes_to_human(downloaded_bytes),
|
||||||
|
progress_percent,
|
||||||
|
speed_value,
|
||||||
|
speed_unit,
|
||||||
|
eta_str
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
set_title(&title).unwrap();
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_ctrlc(should_stop: Arc<std::sync::atomic::AtomicBool>) {
|
||||||
|
ctrlc::set_handler(move || {
|
||||||
|
should_stop.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
clear().unwrap();
|
||||||
|
|
||||||
|
println!("\n{} Download interrupted by user", Status::warning());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn download_resources(
|
||||||
|
client: &Client,
|
||||||
|
config: &Config,
|
||||||
|
resources: &[Value],
|
||||||
|
folder: &std::path::Path,
|
||||||
|
log_file: &File,
|
||||||
|
should_stop: &Arc<std::sync::atomic::AtomicBool>,
|
||||||
|
progress: &DownloadProgress,
|
||||||
|
success: &Arc<std::sync::atomic::AtomicUsize>,
|
||||||
|
) {
|
||||||
|
for item in resources {
|
||||||
|
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dest) = item.get("dest").and_then(Value::as_str) {
|
||||||
|
let md5 = item.get("md5").and_then(Value::as_str);
|
||||||
|
if download_file(
|
||||||
|
client,
|
||||||
|
config,
|
||||||
|
dest,
|
||||||
|
folder,
|
||||||
|
md5,
|
||||||
|
log_file,
|
||||||
|
should_stop,
|
||||||
|
progress,
|
||||||
|
) {
|
||||||
|
success.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
169
src/main.rs
169
src/main.rs
|
@ -1,64 +1,56 @@
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{
|
|
||||||
io,
|
#[cfg(not(target_os = "windows"))]
|
||||||
sync::Arc,
|
use std::process::Command;
|
||||||
thread,
|
|
||||||
time::{Duration, Instant},
|
#[cfg(windows)]
|
||||||
};
|
|
||||||
use winconsole::console::{clear, set_title};
|
use winconsole::console::{clear, set_title};
|
||||||
|
|
||||||
use wuwa_downloader::{
|
use wuwa_downloader::{
|
||||||
config::status::Status,
|
config::status::Status,
|
||||||
download::progress::DownloadProgress,
|
|
||||||
io::{
|
io::{
|
||||||
console::print_results,
|
console::print_results,
|
||||||
file::get_dir,
|
file::get_dir,
|
||||||
logging::{
|
logging::setup_logging,
|
||||||
bytes_to_human, calculate_total_size, format_duration, log_error, setup_logging,
|
util::{
|
||||||
|
calculate_total_size, download_resources, exit_with_error, setup_ctrlc,
|
||||||
|
start_title_thread, track_progress,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
network::client::{download_file, fetch_index, get_predownload},
|
network::client::{fetch_index, get_config},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
#[cfg(windows)]
|
||||||
set_title("Wuthering Waves Downloader").unwrap();
|
set_title("Wuthering Waves Downloader").unwrap();
|
||||||
|
|
||||||
let log_file = setup_logging();
|
let log_file = setup_logging();
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
let config = match get_predownload(&client) {
|
let config = match get_config(&client) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => exit_with_error(&log_file, &e),
|
||||||
log_error(&log_file, &e);
|
|
||||||
clear().unwrap();
|
|
||||||
println!("{} {}", Status::error(), e);
|
|
||||||
println!("\n{} Press Enter to exit...", Status::warning());
|
|
||||||
let _ = io::stdin().read_line(&mut String::new());
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let folder = get_dir();
|
let folder = get_dir();
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
clear().unwrap();
|
clear().unwrap();
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
Command::new("clear").status().unwrap();
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"\n{} Download folder: {}\n",
|
"\n{} Download folder: {}\n",
|
||||||
Status::info(),
|
Status::info(),
|
||||||
folder.display().to_string().cyan()
|
folder.display().to_string().cyan()
|
||||||
);
|
);
|
||||||
|
|
||||||
clear().unwrap();
|
|
||||||
let data = fetch_index(&client, &config, &log_file);
|
let data = fetch_index(&client, &config, &log_file);
|
||||||
|
|
||||||
let resources = match data.get("resource").and_then(Value::as_array) {
|
let resources = match data.get("resource").and_then(Value::as_array) {
|
||||||
Some(res) => res,
|
Some(res) => res,
|
||||||
None => {
|
None => exit_with_error(&log_file, "No resources found in index file"),
|
||||||
log_error(&log_file, "No resources found in index file");
|
|
||||||
println!("{} No resources found in index file", Status::warning());
|
|
||||||
println!("\n{} Press Enter to exit...", Status::warning());
|
|
||||||
let _ = io::stdin().read_line(&mut String::new());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
|
@ -66,116 +58,43 @@ fn main() {
|
||||||
Status::info(),
|
Status::info(),
|
||||||
resources.len().to_string().cyan()
|
resources.len().to_string().cyan()
|
||||||
);
|
);
|
||||||
|
|
||||||
let total_size = calculate_total_size(resources, &client, &config);
|
let total_size = calculate_total_size(resources, &client, &config);
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
clear().unwrap();
|
clear().unwrap();
|
||||||
|
|
||||||
let should_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
let (should_stop, success, progress) = track_progress(total_size);
|
||||||
let success = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
||||||
let total_files = resources.len();
|
|
||||||
|
|
||||||
let progress = DownloadProgress {
|
let title_thread = start_title_thread(
|
||||||
total_bytes: Arc::new(std::sync::atomic::AtomicU64::new(total_size)),
|
should_stop.clone(),
|
||||||
downloaded_bytes: Arc::new(std::sync::atomic::AtomicU64::new(0)),
|
success.clone(),
|
||||||
start_time: Instant::now(),
|
progress.clone(),
|
||||||
};
|
resources.len(),
|
||||||
|
);
|
||||||
|
|
||||||
let success_clone = success.clone();
|
setup_ctrlc(should_stop.clone());
|
||||||
let should_stop_clone = should_stop.clone();
|
|
||||||
let progress_clone = progress.clone();
|
|
||||||
let title_thread = thread::spawn(move || {
|
|
||||||
while !should_stop_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
|
||||||
let elapsed = progress_clone.start_time.elapsed();
|
|
||||||
let elapsed_secs = elapsed.as_secs();
|
|
||||||
let downloaded_bytes = progress_clone
|
|
||||||
.downloaded_bytes
|
|
||||||
.load(std::sync::atomic::Ordering::SeqCst);
|
|
||||||
let total_bytes = progress_clone
|
|
||||||
.total_bytes
|
|
||||||
.load(std::sync::atomic::Ordering::SeqCst);
|
|
||||||
let current_success = success_clone.load(std::sync::atomic::Ordering::SeqCst);
|
|
||||||
|
|
||||||
let speed = if elapsed_secs > 0 {
|
download_resources(
|
||||||
downloaded_bytes / elapsed_secs
|
&client,
|
||||||
} else {
|
&config,
|
||||||
0
|
resources,
|
||||||
};
|
&folder,
|
||||||
|
&log_file,
|
||||||
let (speed_value, speed_unit) = if speed > 1_000_000 {
|
&should_stop,
|
||||||
(speed / 1_000_000, "MB/s")
|
&progress,
|
||||||
} else {
|
&success,
|
||||||
(speed / 1_000, "KB/s")
|
);
|
||||||
};
|
|
||||||
|
|
||||||
let remaining_files = total_files - current_success;
|
|
||||||
let remaining_bytes = total_bytes.saturating_sub(downloaded_bytes);
|
|
||||||
|
|
||||||
let eta_secs = if speed > 0 && remaining_files > 0 {
|
|
||||||
remaining_bytes / speed
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
let eta_str = format_duration(Duration::from_secs(eta_secs));
|
|
||||||
|
|
||||||
let progress_percent = if total_bytes > 0 {
|
|
||||||
format!(" ({}%)", (downloaded_bytes * 100 / total_bytes))
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let title = format!(
|
|
||||||
"Wuthering Waves Downloader - {}/{} files - {}{} - Speed: {}{} - Total ETA: {}",
|
|
||||||
current_success,
|
|
||||||
total_files,
|
|
||||||
bytes_to_human(downloaded_bytes),
|
|
||||||
progress_percent,
|
|
||||||
speed_value,
|
|
||||||
speed_unit,
|
|
||||||
eta_str
|
|
||||||
);
|
|
||||||
|
|
||||||
set_title(&title).unwrap();
|
|
||||||
thread::sleep(Duration::from_secs(1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let should_stop_ctrlc = should_stop.clone();
|
|
||||||
|
|
||||||
ctrlc::set_handler(move || {
|
|
||||||
should_stop_ctrlc.store(true, std::sync::atomic::Ordering::SeqCst);
|
|
||||||
clear().unwrap();
|
|
||||||
println!("\n{} Download interrupted by user", Status::warning());
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
for item in resources.iter() {
|
|
||||||
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(dest) = item.get("dest").and_then(Value::as_str) {
|
|
||||||
let md5 = item.get("md5").and_then(Value::as_str);
|
|
||||||
if download_file(
|
|
||||||
&client,
|
|
||||||
&config,
|
|
||||||
dest,
|
|
||||||
&folder,
|
|
||||||
md5,
|
|
||||||
&log_file,
|
|
||||||
&should_stop,
|
|
||||||
&progress,
|
|
||||||
) {
|
|
||||||
success.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
should_stop.store(true, std::sync::atomic::Ordering::SeqCst);
|
should_stop.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
title_thread.join().unwrap();
|
title_thread.join().unwrap();
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
clear().unwrap();
|
clear().unwrap();
|
||||||
|
|
||||||
print_results(
|
print_results(
|
||||||
success.load(std::sync::atomic::Ordering::SeqCst),
|
success.load(std::sync::atomic::Ordering::SeqCst),
|
||||||
total_files,
|
resources.len(),
|
||||||
&folder,
|
&folder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,31 @@
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use serde_json::{from_reader, from_str, Value};
|
use serde_json::{from_reader, from_str, Value};
|
||||||
use winconsole::console::{self};
|
#[cfg(not(target_os = "windows"))]
|
||||||
use std::{io::{Read, Write}, fs, io, path::Path, time::Duration};
|
use std::process::Command;
|
||||||
|
use std::{
|
||||||
|
fs::{self, OpenOptions},
|
||||||
|
io::{self, Read, Write},
|
||||||
|
path::Path,
|
||||||
|
time::Duration,
|
||||||
|
u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
use winconsole::console::clear;
|
||||||
|
|
||||||
use crate::config::cfg::Config;
|
use crate::config::cfg::Config;
|
||||||
|
use crate::config::status::Status;
|
||||||
use crate::download::progress::DownloadProgress;
|
use crate::download::progress::DownloadProgress;
|
||||||
use crate::io::file::{calculate_md5, check_existing_file, get_filename};
|
use crate::io::file::{calculate_md5, check_existing_file, get_filename};
|
||||||
use crate::io::logging::log_error;
|
use crate::io::{logging::log_error, util::get_version};
|
||||||
use crate::config::status::Status;
|
|
||||||
|
|
||||||
const INDEX_URL: &str = "https://gist.githubusercontent.com/yuhkix/b8796681ac2cd3bab11b7e8cdc022254/raw/30a8e747debe9e333d5f4ec5d8700dab500594a2/wuwa.json";
|
const INDEX_URL: &str = "https://gist.githubusercontent.com/yuhkix/b8796681ac2cd3bab11b7e8cdc022254/raw/4435fd290c07f7f766a6d2ab09ed3096d83b02e3/wuwa.json";
|
||||||
const MAX_RETRIES: usize = 3;
|
const MAX_RETRIES: usize = 3;
|
||||||
const DOWNLOAD_TIMEOUT: u64 = 300;
|
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(10000);
|
||||||
const BUFFER_SIZE: usize = 8192;
|
const BUFFER_SIZE: usize = 65536;
|
||||||
|
|
||||||
pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Value {
|
pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Value {
|
||||||
println!("{} Fetching index file...", Status::info());
|
println!("{} Fetching index file...", Status::info());
|
||||||
|
@ -27,7 +38,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
||||||
Ok(resp) => resp,
|
Ok(resp) => resp,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_error(log_file, &format!("Error fetching index file: {}", e));
|
log_error(log_file, &format!("Error fetching index file: {}", e));
|
||||||
console::clear().unwrap();
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
clear().unwrap();
|
||||||
|
|
||||||
println!("{} Error fetching index file: {}", Status::error(), e);
|
println!("{} Error fetching index file: {}", Status::error(), e);
|
||||||
println!("\n{} Press Enter to exit...", Status::warning());
|
println!("\n{} Press Enter to exit...", Status::warning());
|
||||||
let _ = io::stdin().read_line(&mut String::new());
|
let _ = io::stdin().read_line(&mut String::new());
|
||||||
|
@ -38,7 +52,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
let msg = format!("Error fetching index file: HTTP {}", response.status());
|
let msg = format!("Error fetching index file: HTTP {}", response.status());
|
||||||
log_error(log_file, &msg);
|
log_error(log_file, &msg);
|
||||||
console::clear().unwrap();
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
clear().unwrap();
|
||||||
|
|
||||||
println!("{} {}", Status::error(), msg);
|
println!("{} {}", Status::error(), msg);
|
||||||
println!("\n{} Press Enter to exit...", Status::warning());
|
println!("\n{} Press Enter to exit...", Status::warning());
|
||||||
let _ = io::stdin().read_line(&mut String::new());
|
let _ = io::stdin().read_line(&mut String::new());
|
||||||
|
@ -55,7 +72,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
if let Err(e) = response.copy_to(&mut buffer) {
|
if let Err(e) = response.copy_to(&mut buffer) {
|
||||||
log_error(log_file, &format!("Error reading index file bytes: {}", e));
|
log_error(log_file, &format!("Error reading index file bytes: {}", e));
|
||||||
console::clear().unwrap();
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
clear().unwrap();
|
||||||
|
|
||||||
println!("{} Error reading index file: {}", Status::error(), e);
|
println!("{} Error reading index file: {}", Status::error(), e);
|
||||||
println!("\n{} Press Enter to exit...", Status::warning());
|
println!("\n{} Press Enter to exit...", Status::warning());
|
||||||
let _ = io::stdin().read_line(&mut String::new());
|
let _ = io::stdin().read_line(&mut String::new());
|
||||||
|
@ -66,7 +86,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
||||||
let mut decompressed_text = String::new();
|
let mut decompressed_text = String::new();
|
||||||
if let Err(e) = gz.read_to_string(&mut decompressed_text) {
|
if let Err(e) = gz.read_to_string(&mut decompressed_text) {
|
||||||
log_error(log_file, &format!("Error decompressing index file: {}", e));
|
log_error(log_file, &format!("Error decompressing index file: {}", e));
|
||||||
console::clear().unwrap();
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
clear().unwrap();
|
||||||
|
|
||||||
println!("{} Error decompressing index file: {}", Status::error(), e);
|
println!("{} Error decompressing index file: {}", Status::error(), e);
|
||||||
println!("\n{} Press Enter to exit...", Status::warning());
|
println!("\n{} Press Enter to exit...", Status::warning());
|
||||||
let _ = io::stdin().read_line(&mut String::new());
|
let _ = io::stdin().read_line(&mut String::new());
|
||||||
|
@ -81,7 +104,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
||||||
log_file,
|
log_file,
|
||||||
&format!("Error reading index file response: {}", e),
|
&format!("Error reading index file response: {}", e),
|
||||||
);
|
);
|
||||||
console::clear().unwrap();
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
clear().unwrap();
|
||||||
|
|
||||||
println!("{} Error reading index file: {}", Status::error(), e);
|
println!("{} Error reading index file: {}", Status::error(), e);
|
||||||
println!("\n{} Press Enter to exit...", Status::warning());
|
println!("\n{} Press Enter to exit...", Status::warning());
|
||||||
let _ = io::stdin().read_line(&mut String::new());
|
let _ = io::stdin().read_line(&mut String::new());
|
||||||
|
@ -96,7 +122,10 @@ pub fn fetch_index(client: &Client, config: &Config, log_file: &fs::File) -> Val
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_error(log_file, &format!("Error parsing index file JSON: {}", e));
|
log_error(log_file, &format!("Error parsing index file JSON: {}", e));
|
||||||
console::clear().unwrap();
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
clear().unwrap();
|
||||||
|
|
||||||
println!("{} Error parsing index file: {}", Status::error(), e);
|
println!("{} Error parsing index file: {}", Status::error(), e);
|
||||||
println!("\n{} Press Enter to exit...", Status::warning());
|
println!("\n{} Press Enter to exit...", Status::warning());
|
||||||
let _ = io::stdin().read_line(&mut String::new());
|
let _ = io::stdin().read_line(&mut String::new());
|
||||||
|
@ -133,13 +162,16 @@ pub fn download_file(
|
||||||
let url = format!("{}{}", base_url, dest);
|
let url = format!("{}{}", base_url, dest);
|
||||||
|
|
||||||
if let Ok(head_response) = client.head(&url).timeout(Duration::from_secs(10)).send() {
|
if let Ok(head_response) = client.head(&url).timeout(Duration::from_secs(10)).send() {
|
||||||
if let Some(size) = head_response.headers()
|
if let Some(size) = head_response
|
||||||
|
.headers()
|
||||||
.get("content-length")
|
.get("content-length")
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.and_then(|s| s.parse::<u64>().ok())
|
.and_then(|s| s.parse::<u64>().ok())
|
||||||
{
|
{
|
||||||
file_size = Some(size);
|
file_size = Some(size);
|
||||||
progress.total_bytes.fetch_add(size, std::sync::atomic::Ordering::SeqCst);
|
progress
|
||||||
|
.total_bytes
|
||||||
|
.fetch_add(size, std::sync::atomic::Ordering::SeqCst);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,7 +179,11 @@ pub fn download_file(
|
||||||
|
|
||||||
if let (Some(md5), Some(size)) = (expected_md5, file_size) {
|
if let (Some(md5), Some(size)) = (expected_md5, file_size) {
|
||||||
if should_skip_download(&path, Some(md5), Some(size)) {
|
if should_skip_download(&path, Some(md5), Some(size)) {
|
||||||
println!("{} File is valid: {}", Status::matched(), filename.bright_purple());
|
println!(
|
||||||
|
"{} File is valid: {}",
|
||||||
|
Status::matched(),
|
||||||
|
filename.bright_purple()
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,61 +202,93 @@ pub fn download_file(
|
||||||
let head_response = match client.head(&url).timeout(Duration::from_secs(10)).send() {
|
let head_response = match client.head(&url).timeout(Duration::from_secs(10)).send() {
|
||||||
Ok(resp) if resp.status().is_success() => resp,
|
Ok(resp) if resp.status().is_success() => resp,
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
log_error(log_file, &format!("CDN {} failed for {} (HTTP {})", i+1, dest, resp.status()));
|
log_error(
|
||||||
|
log_file,
|
||||||
|
&format!("CDN {} failed for {} (HTTP {})", i + 1, dest, resp.status()),
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_error(log_file, &format!("CDN {} failed for {}: {}", i+1, dest, e));
|
log_error(
|
||||||
|
log_file,
|
||||||
|
&format!("CDN {} failed for {}: {}", i + 1, dest, e),
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let expected_size = file_size.or_else(|| head_response.headers()
|
let expected_size = file_size.or_else(|| {
|
||||||
.get("content-length")
|
head_response
|
||||||
.and_then(|v| v.to_str().ok())
|
.headers()
|
||||||
.and_then(|s| s.parse::<u64>().ok()));
|
.get("content-length")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.and_then(|s| s.parse::<u64>().ok())
|
||||||
|
});
|
||||||
|
|
||||||
if let (Some(md5), Some(size)) = (expected_md5, expected_size) {
|
if let (Some(md5), Some(size)) = (expected_md5, expected_size) {
|
||||||
if check_existing_file(&path, Some(md5), Some(size)) {
|
if check_existing_file(&path, Some(md5), Some(size)) {
|
||||||
println!("{} File is valid: {}", Status::matched(), filename.bright_purple());
|
println!(
|
||||||
|
"{} File is valid: {}",
|
||||||
|
Status::matched(),
|
||||||
|
filename.bright_purple()
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{} Downloading: {}", Status::progress(), filename.purple());
|
println!("{} Downloading: {}", Status::progress(), filename.purple());
|
||||||
|
|
||||||
|
let pb = ProgressBar::new(expected_size.unwrap_or(0));
|
||||||
|
pb.set_style(ProgressStyle::default_bar()
|
||||||
|
.template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta}, {binary_bytes_per_sec})")
|
||||||
|
.unwrap()
|
||||||
|
.progress_chars("#>-"));
|
||||||
|
|
||||||
let mut retries = MAX_RETRIES;
|
let mut retries = MAX_RETRIES;
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
|
|
||||||
while retries > 0 {
|
while retries > 0 {
|
||||||
let result = download_single_file(&client, &url, &path, should_stop, progress);
|
let result = download_single_file(&client, &url, &path, should_stop, progress, &pb);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => break,
|
Ok(_) => break,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
pb.finish_and_clear();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
last_error = Some(e);
|
last_error = Some(e);
|
||||||
retries -= 1;
|
retries -= 1;
|
||||||
let _ = fs::remove_file(&path);
|
let _ = fs::remove_file(&path);
|
||||||
|
|
||||||
if retries > 0 {
|
if retries > 0 {
|
||||||
println!("{} Retrying {}... ({} left)",
|
println!(
|
||||||
Status::warning(), filename.yellow(), retries);
|
"{} Retrying {}... ({} left)",
|
||||||
|
Status::warning(),
|
||||||
|
filename.yellow(),
|
||||||
|
retries
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pb.finish_and_clear();
|
||||||
|
|
||||||
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if retries == 0 {
|
if retries == 0 {
|
||||||
log_error(log_file, &format!("Failed after retries for {}: {}", dest,
|
log_error(
|
||||||
last_error.unwrap_or_default()));
|
log_file,
|
||||||
|
&format!(
|
||||||
|
"Failed after retries for {}: {}",
|
||||||
|
dest,
|
||||||
|
last_error.unwrap_or_default()
|
||||||
|
),
|
||||||
|
);
|
||||||
println!("{} Failed: {}", Status::error(), filename.red());
|
println!("{} Failed: {}", Status::error(), filename.red());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -229,11 +297,16 @@ pub fn download_file(
|
||||||
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let actual = calculate_md5(&path);
|
let actual = calculate_md5(&path);
|
||||||
if actual != expected {
|
if actual != expected {
|
||||||
log_error(log_file, &format!("Checksum failed for {}: expected {}, got {}",
|
log_error(
|
||||||
dest, expected, actual));
|
log_file,
|
||||||
|
&format!(
|
||||||
|
"Checksum failed for {}: expected {}, got {}",
|
||||||
|
dest, expected, actual
|
||||||
|
),
|
||||||
|
);
|
||||||
fs::remove_file(&path).unwrap();
|
fs::remove_file(&path).unwrap();
|
||||||
println!("{} Checksum failed: {}", Status::error(), filename.red());
|
println!("{} Checksum failed: {}", Status::error(), filename.red());
|
||||||
return false;
|
return false;
|
||||||
|
@ -255,44 +328,80 @@ fn download_single_file(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
should_stop: &std::sync::atomic::AtomicBool,
|
should_stop: &std::sync::atomic::AtomicBool,
|
||||||
progress: &DownloadProgress,
|
progress: &DownloadProgress,
|
||||||
|
pb: &ProgressBar,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut response = client
|
let mut downloaded: u64 = 0;
|
||||||
.get(url)
|
if path.exists() {
|
||||||
.timeout(Duration::from_secs(DOWNLOAD_TIMEOUT))
|
downloaded = fs::metadata(path)
|
||||||
|
.map_err(|e| format!("Metadata error: {}", e))?
|
||||||
|
.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = client.get(url).timeout(DOWNLOAD_TIMEOUT);
|
||||||
|
|
||||||
|
let request = if downloaded > 0 {
|
||||||
|
request.header("Range", format!("bytes={}-", downloaded))
|
||||||
|
} else {
|
||||||
|
request
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut response = request
|
||||||
.send()
|
.send()
|
||||||
.map_err(|e| format!("Network error: {}", e))?;
|
.map_err(|e| format!("Network error: {}", e))?;
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if response.status() == reqwest::StatusCode::RANGE_NOT_SATISFIABLE {
|
||||||
|
return Err("Range not satisfiable. File may already be fully downloaded.".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !response.status().is_success() && response.status() != reqwest::StatusCode::PARTIAL_CONTENT
|
||||||
|
{
|
||||||
return Err(format!("HTTP error: {}", response.status()));
|
return Err(format!("HTTP error: {}", response.status()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut file = fs::File::create(path)
|
let mut file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(path)
|
||||||
.map_err(|e| format!("File error: {}", e))?;
|
.map_err(|e| format!("File error: {}", e))?;
|
||||||
|
|
||||||
|
pb.set_position(downloaded);
|
||||||
|
progress
|
||||||
|
.downloaded_bytes
|
||||||
|
.store(downloaded, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
|
||||||
let mut buffer = [0; BUFFER_SIZE];
|
let mut buffer = [0; BUFFER_SIZE];
|
||||||
loop {
|
loop {
|
||||||
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
if should_stop.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
return Err("Download interrupted".into());
|
return Err("Download interrupted".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytes_read = response.read(&mut buffer)
|
let bytes_read = response
|
||||||
|
.read(&mut buffer)
|
||||||
.map_err(|e| format!("Read error: {}", e))?;
|
.map_err(|e| format!("Read error: {}", e))?;
|
||||||
|
|
||||||
if bytes_read == 0 {
|
if bytes_read == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
file.write_all(&buffer[..bytes_read])
|
file.write_all(&buffer[..bytes_read])
|
||||||
.map_err(|e| format!("Write error: {}", e))?;
|
.map_err(|e| format!("Write error: {}", e))?;
|
||||||
|
|
||||||
progress.downloaded_bytes.fetch_add(bytes_read as u64, std::sync::atomic::Ordering::SeqCst);
|
downloaded += bytes_read as u64;
|
||||||
|
pb.set_position(downloaded);
|
||||||
|
progress
|
||||||
|
.downloaded_bytes
|
||||||
|
.store(downloaded, std::sync::atomic::Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_predownload(client: &Client) -> Result<Config, String> {
|
pub fn get_config(client: &Client) -> Result<Config, String> {
|
||||||
let selected_index_url = fetch_gist(client)?;
|
let selected_index_url = fetch_gist(client)?;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
clear().unwrap();
|
||||||
|
|
||||||
println!("{} Fetching download configuration...", Status::info());
|
println!("{} Fetching download configuration...", Status::info());
|
||||||
|
|
||||||
let mut response = client
|
let mut response = client
|
||||||
|
@ -327,26 +436,64 @@ pub fn get_predownload(client: &Client) -> Result<Config, String> {
|
||||||
from_reader(response).map_err(|e| format!("Invalid JSON: {}", e))?
|
from_reader(response).map_err(|e| format!("Invalid JSON: {}", e))?
|
||||||
};
|
};
|
||||||
|
|
||||||
let predownload_config = config
|
let has_default = config.get("default").is_some();
|
||||||
.get("predownload")
|
let has_predownload = config.get("predownload").is_some();
|
||||||
.and_then(|p| p.get("config"))
|
|
||||||
.ok_or("Missing predownload.config in response")?;
|
|
||||||
|
|
||||||
let base_url = predownload_config
|
let selected_config = match (has_default, has_predownload) {
|
||||||
|
(true, false) => {
|
||||||
|
println!("{} Using default.config", Status::info());
|
||||||
|
"default"
|
||||||
|
}
|
||||||
|
(false, true) => {
|
||||||
|
println!("{} Using predownload.config", Status::info());
|
||||||
|
"predownload"
|
||||||
|
}
|
||||||
|
(true, true) => loop {
|
||||||
|
print!(
|
||||||
|
"{} Choose config to use (1=default, 2=predownload): ",
|
||||||
|
Status::question()
|
||||||
|
);
|
||||||
|
io::stdout()
|
||||||
|
.flush()
|
||||||
|
.map_err(|e| format!("Failed to flush stdout: {}", e))?;
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin()
|
||||||
|
.read_line(&mut input)
|
||||||
|
.map_err(|e| format!("Failed to read input: {}", e))?;
|
||||||
|
|
||||||
|
match input.trim() {
|
||||||
|
"1" => break "default",
|
||||||
|
"2" => break "predownload",
|
||||||
|
_ => println!("{} Invalid choice, please enter 1 or 2", Status::error()),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(false, false) => {
|
||||||
|
return Err(
|
||||||
|
"Neither default.config nor predownload.config found in response".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let config_data = config
|
||||||
|
.get(selected_config)
|
||||||
|
.ok_or_else(|| format!("Missing {} config in response", selected_config))?;
|
||||||
|
|
||||||
|
let base_config = config_data
|
||||||
|
.get("config")
|
||||||
|
.ok_or_else(|| format!("Missing config in {} response", selected_config))?;
|
||||||
|
|
||||||
|
let base_url = base_config
|
||||||
.get("baseUrl")
|
.get("baseUrl")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.ok_or("Missing or invalid baseUrl")?;
|
.ok_or("Missing or invalid baseUrl")?;
|
||||||
|
|
||||||
let index_file = predownload_config
|
let index_file = base_config
|
||||||
.get("indexFile")
|
.get("indexFile")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.ok_or("Missing or invalid indexFile")?;
|
.ok_or("Missing or invalid indexFile")?;
|
||||||
|
|
||||||
let default_config = config
|
let cdn_list = config_data
|
||||||
.get("default")
|
|
||||||
.ok_or("Missing default config in response")?;
|
|
||||||
|
|
||||||
let cdn_list = default_config
|
|
||||||
.get("cdnList")
|
.get("cdnList")
|
||||||
.and_then(Value::as_array)
|
.and_then(Value::as_array)
|
||||||
.ok_or("Missing or invalid cdnList")?;
|
.ok_or("Missing or invalid cdnList")?;
|
||||||
|
@ -362,10 +509,10 @@ pub fn get_predownload(client: &Client) -> Result<Config, String> {
|
||||||
return Err("No valid CDN URLs found".to_string());
|
return Err("No valid CDN URLs found".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let full_index_url = format!("{}/{}", cdn_urls[0], index_file.trim_start_matches('/'));
|
let full_index_url = format!("{}//{}", cdn_urls[0], index_file.trim_start_matches('/'));
|
||||||
let zip_bases = cdn_urls
|
let zip_bases = cdn_urls
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cdn| format!("{}/{}", cdn, base_url.trim_start_matches('/')))
|
.map(|cdn| format!("{}//{}", cdn, base_url.trim_start_matches('/')))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
|
@ -401,24 +548,78 @@ pub fn fetch_gist(client: &Client) -> Result<String, String> {
|
||||||
|
|
||||||
let gist_data: Value = if content_encoding.contains("gzip") {
|
let gist_data: Value = if content_encoding.contains("gzip") {
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
response.copy_to(&mut buffer)
|
response
|
||||||
|
.copy_to(&mut buffer)
|
||||||
.map_err(|e| format!("Error reading response: {}", e))?;
|
.map_err(|e| format!("Error reading response: {}", e))?;
|
||||||
|
|
||||||
let mut gz = GzDecoder::new(&buffer[..]);
|
let mut gz = GzDecoder::new(&buffer[..]);
|
||||||
let mut decompressed = String::new();
|
let mut decompressed = String::new();
|
||||||
gz.read_to_string(&mut decompressed)
|
gz.read_to_string(&mut decompressed)
|
||||||
.map_err(|e| format!("Error decompressing: {}", e))?;
|
.map_err(|e| format!("Error decompressing: {}", e))?;
|
||||||
|
|
||||||
from_str(&decompressed).map_err(|e| format!("Invalid JSON: {}", e))?
|
from_str(&decompressed).map_err(|e| format!("Invalid JSON: {}", e))?
|
||||||
} else {
|
} else {
|
||||||
from_reader(response).map_err(|e| format!("Invalid JSON: {}", e))?
|
from_reader(response).map_err(|e| format!("Invalid JSON: {}", e))?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
Command::new("clear").status().unwrap();
|
||||||
|
|
||||||
|
let entries = [
|
||||||
|
("live", "os", "Live - OS"),
|
||||||
|
("live", "cn", "Live - CN"),
|
||||||
|
("beta", "os", "Beta - OS"),
|
||||||
|
("beta", "cn", "Beta - CN"),
|
||||||
|
];
|
||||||
|
|
||||||
println!("{} Available versions:", Status::info());
|
println!("{} Available versions:", Status::info());
|
||||||
println!("1. Preload - OS");
|
|
||||||
println!("2. Live - CN (Needs Update)");
|
for (i, (cat, ver, label)) in entries.iter().enumerate() {
|
||||||
println!("3. Beta - OS (Needs Update)");
|
let index_url = get_version(&gist_data, cat, ver)?;
|
||||||
println!("4. Beta - CN (Needs Update)");
|
|
||||||
|
let mut resp = client
|
||||||
|
.get(&index_url)
|
||||||
|
.send()
|
||||||
|
.map_err(|e| format!("Error fetching index.json: {}", e))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let version_json: Value = {
|
||||||
|
let content_encoding = resp
|
||||||
|
.headers()
|
||||||
|
.get("content-encoding")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
if content_encoding.contains("gzip") {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
resp.copy_to(&mut buffer)
|
||||||
|
.map_err(|e| format!("Error reading response: {}", e))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut gz = GzDecoder::new(&buffer[..]);
|
||||||
|
let mut decompressed = String::new();
|
||||||
|
gz.read_to_string(&mut decompressed)
|
||||||
|
.map_err(|e| format!("Error decompressing: {}", e))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
from_str(&decompressed)
|
||||||
|
.map_err(|e| format!("Invalid JSON: {}", e))
|
||||||
|
.unwrap()
|
||||||
|
} else {
|
||||||
|
from_reader(resp)
|
||||||
|
.map_err(|e| format!("Invalid JSON: {}", e))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let version = version_json
|
||||||
|
.get("default")
|
||||||
|
.and_then(|d| d.get("config"))
|
||||||
|
.and_then(|c| c.get("version"))
|
||||||
|
.or_else(|| version_json.get("default").and_then(|d| d.get("version")))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
|
||||||
|
println!("{}. {} ({})", i + 1, label, version);
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
print!("{} Select version: ", Status::question());
|
print!("{} Select version: ", Status::question());
|
||||||
|
@ -428,18 +629,11 @@ pub fn fetch_gist(client: &Client) -> Result<String, String> {
|
||||||
io::stdin().read_line(&mut input).unwrap();
|
io::stdin().read_line(&mut input).unwrap();
|
||||||
|
|
||||||
match input.trim() {
|
match input.trim() {
|
||||||
"1" => return get_version_url(&gist_data, "live", "os-live"),
|
"1" => return get_version(&gist_data, "live", "os"),
|
||||||
"2" => return get_version_url(&gist_data, "live", "cn-live"),
|
"2" => return get_version(&gist_data, "live", "cn"),
|
||||||
"3" => return get_version_url(&gist_data, "beta", "os-beta"),
|
"3" => return get_version(&gist_data, "beta", "os"),
|
||||||
"4" => return get_version_url(&gist_data, "beta", "cn-beta"),
|
"4" => return get_version(&gist_data, "beta", "cn"),
|
||||||
_ => println!("{} Invalid selection", Status::error()),
|
_ => println!("{} Invalid selection", Status::error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_version_url(data: &Value, category: &str, version: &str) -> Result<String, String> {
|
|
||||||
data[category][version]
|
|
||||||
.as_str()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.ok_or_else(|| format!("Missing {} URL", version))
|
|
||||||
}
|
|
|
@ -1 +1 @@
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
|
Loading…
Reference in a new issue