Compare commits

..

No commits in common. "main" and "0.1.3" have entirely different histories.
main ... 0.1.3

19 changed files with 241 additions and 710 deletions

4
.gitignore vendored
View file

@ -4,15 +4,11 @@ debug/
target/ target/
.idea/ .idea/
# packaging
dist/
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
# Error / Debug Logging # Error / Debug Logging
*.log *.log
*.txt
# MSVC Windows builds of rustc generate these, which store debugging information # MSVC Windows builds of rustc generate these, which store debugging information
*.pdb *.pdb

73
Cargo.lock generated
View file

@ -113,19 +113,6 @@ 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"
@ -163,9 +150,9 @@ dependencies = [
[[package]] [[package]]
name = "ctrlc" name = "ctrlc"
version = "3.4.7" version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
dependencies = [ dependencies = [
"nix", "nix",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@ -213,12 +200,6 @@ 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"
@ -666,19 +647,6 @@ 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"
@ -802,9 +770,9 @@ dependencies = [
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.30.1" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@ -812,12 +780,6 @@ 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"
@ -907,12 +869,6 @@ 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"
@ -1164,9 +1120,9 @@ dependencies = [
[[package]] [[package]]
name = "shellexpand" name = "shellexpand"
version = "3.1.1" version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
dependencies = [ dependencies = [
"dirs", "dirs",
] ]
@ -1430,12 +1386,6 @@ 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"
@ -1582,16 +1532,6 @@ 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"
@ -1909,7 +1849,6 @@ dependencies = [
"colored", "colored",
"ctrlc", "ctrlc",
"flate2", "flate2",
"indicatif",
"md-5", "md-5",
"reqwest", "reqwest",
"serde", "serde",

View file

@ -4,25 +4,16 @@ 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"
[target.'cfg(windows)'.build-dependencies] shellexpand = "3.1.0"
winres = "0.1.12" flate2 = "1.1.1"
[profile.release]
strip = true
lto = true
opt-level = 3
codegen-units = 1

View file

@ -10,7 +10,6 @@
## 📦 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 ### 🛠️ Installation & Usage
- **Install the nightly toolchain:** - **Install the nightly toolchain:**
@ -21,8 +20,8 @@ rustup default nightly
- **Clone the repository:** - **Clone the repository:**
```bash ```bash
git clone https://github.com/yuhkix/wuwa-downloader.git git clone https://github.com/yourusername/wuthering-waves-downloader.git
cd wuwa-downloader cd wuthering-waves-downloader
``` ```
- **Build the project:** - **Build the project:**
@ -75,4 +74,4 @@ cargo build --release
- **Multi-threaded** - Safe concurrent progress tracking - **Multi-threaded** - Safe concurrent progress tracking
- **Configurable Timeouts** - 30s for metadata, 10000s for downloads - **Configurable Timeouts** - 30s for metadata, 300s for downloads

View file

@ -1,8 +1,7 @@
fn main() { fn main() {
#[cfg(windows)] if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" {
{
let mut res = winres::WindowsResource::new(); let mut res = winres::WindowsResource::new();
res.set_icon("cartethyia.ico"); res.set_icon("zani.ico");
res.compile().unwrap(); res.compile().unwrap();
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

View file

@ -1,113 +0,0 @@
#!/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

View file

@ -4,25 +4,11 @@ use colored::*;
pub struct Status; pub struct Status;
impl Status { impl Status {
pub fn info() -> ColoredString { pub fn info() -> ColoredString { "[*]".cyan() }
"[*]".cyan() pub fn success() -> ColoredString { "[+]".green() }
} pub fn warning() -> ColoredString { "[!]".yellow() }
pub fn success() -> ColoredString { pub fn error() -> ColoredString { "[-]".red() }
"[+]".green() pub fn question() -> ColoredString { "[?]".blue() }
} pub fn progress() -> ColoredString { "[→]".purple() }
pub fn warning() -> ColoredString { pub fn matched() -> ColoredString { "[↓]".bright_purple() }
"[!]".yellow()
}
pub fn error() -> ColoredString {
"[-]".red()
}
pub fn question() -> ColoredString {
"[?]".blue()
}
pub fn progress() -> ColoredString {
"[→]".purple()
}
pub fn matched() -> ColoredString {
"[↓]".bright_purple()
}
} }

View file

@ -1,6 +1,6 @@
use crate::config::status::Status;
use colored::Colorize;
use std::{io, path::Path}; use std::{io, path::Path};
use colored::Colorize;
use crate::config::status::Status;
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 {

View file

@ -1,9 +1,5 @@
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;
@ -14,11 +10,7 @@ pub fn calculate_md5(path: &Path) -> String {
format!("{:x}", hasher.finalize()) format!("{:x}", hasher.finalize())
} }
pub fn check_existing_file( pub fn check_existing_file(path: &Path, expected_md5: Option<&str>, expected_size: Option<u64>) -> bool {
path: &Path,
expected_md5: Option<&str>,
expected_size: Option<u64>,
) -> bool {
if !path.exists() { if !path.exists() {
return false; return false;
} }
@ -49,7 +41,7 @@ pub fn get_filename(path: &str) -> String {
pub fn get_dir() -> PathBuf { pub fn get_dir() -> PathBuf {
loop { loop {
print!( print!(
"{} Please specify the directory where the game should be downloaded (press Enter to use the current directory): ", "{} Enter download directory (Enter for current): ",
Status::question() Status::question()
); );
io::stdout().flush().unwrap(); io::stdout().flush().unwrap();

View file

@ -1,8 +1,5 @@
use std::{ use std::{fs::{self, OpenOptions}, io::Write, time::SystemTime};
fs::{self, OpenOptions},
io::Write,
time::SystemTime,
};
pub fn setup_logging() -> fs::File { pub fn setup_logging() -> fs::File {
OpenOptions::new() OpenOptions::new()

View file

@ -1,35 +1,14 @@
use std::{fs::File, io, sync::Arc, thread, time::{Duration, Instant}};
use colored::Colorize; use colored::Colorize;
use reqwest::blocking::Client; use reqwest::blocking::Client;
use serde_json::Value; 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 winconsole::console::{clear, set_title};
use crate::{ use crate::{config::{cfg::Config, status::Status}, download::progress::DownloadProgress, io::logging::log_error, network::client::download_file};
config::{cfg::Config, status::Status},
download::progress::DownloadProgress,
io::logging::log_error,
network::client::download_file,
};
pub fn format_duration(duration: Duration) -> String { pub fn format_duration(duration: Duration) -> String {
let secs = duration.as_secs(); let secs = duration.as_secs();
format!( format!("{:02}:{:02}:{:02}", secs / 3600, (secs % 3600) / 60, secs % 60)
"{:02}:{:02}:{:02}",
secs / 3600,
(secs % 3600) / 60,
secs % 60
)
} }
pub fn bytes_to_human(bytes: u64) -> String { pub fn bytes_to_human(bytes: u64) -> String {
@ -41,23 +20,6 @@ pub fn bytes_to_human(bytes: u64) -> String {
} }
} }
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 { pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Config) -> u64 {
let mut total_size = 0; let mut total_size = 0;
let mut failed_urls = 0; let mut failed_urls = 0;
@ -71,7 +33,6 @@ pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Confi
for base_url in &config.zip_bases { for base_url in &config.zip_bases {
let url = format!("{}/{}", base_url, dest); let url = format!("{}/{}", base_url, dest);
log_url(&url);
match client.head(&url).send() { match client.head(&url).send() {
Ok(response) => { Ok(response) => {
if let Some(len) = response.headers().get("content-length") { if let Some(len) = response.headers().get("content-length") {
@ -94,11 +55,7 @@ pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Confi
total_size += file_size; total_size += file_size;
} else { } else {
failed_urls += 1; failed_urls += 1;
println!( println!("{} Could not determine size for file: {}", Status::error(), dest);
"{} Could not determine size for file: {}",
Status::error(),
dest
);
} }
} }
@ -126,9 +83,6 @@ pub fn calculate_total_size(resources: &[Value], client: &Client, config: &Confi
bytes_to_human(total_size).cyan() bytes_to_human(total_size).cyan()
); );
#[cfg(not(target_os = "windows"))]
Command::new("clear").status().unwrap();
total_size total_size
} }
@ -141,19 +95,14 @@ pub fn get_version(data: &Value, category: &str, version: &str) -> Result<String
pub fn exit_with_error(log_file: &File, error: &str) -> ! { pub fn exit_with_error(log_file: &File, error: &str) -> ! {
log_error(log_file, error); log_error(log_file, error);
#[cfg(windows)]
clear().unwrap(); clear().unwrap();
println!("{} {}", Status::error(), error); println!("{} {}", Status::error(), error);
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());
std::process::exit(1); std::process::exit(1);
} }
pub fn track_progress( pub fn track_progress(total_size: u64) -> (
total_size: u64,
) -> (
Arc<std::sync::atomic::AtomicBool>, Arc<std::sync::atomic::AtomicBool>,
Arc<std::sync::atomic::AtomicUsize>, Arc<std::sync::atomic::AtomicUsize>,
DownloadProgress, DownloadProgress,
@ -180,19 +129,11 @@ pub fn start_title_thread(
while !should_stop.load(std::sync::atomic::Ordering::SeqCst) { while !should_stop.load(std::sync::atomic::Ordering::SeqCst) {
let elapsed = progress.start_time.elapsed(); let elapsed = progress.start_time.elapsed();
let elapsed_secs = elapsed.as_secs(); let elapsed_secs = elapsed.as_secs();
let downloaded_bytes = progress let downloaded_bytes = progress.downloaded_bytes.load(std::sync::atomic::Ordering::SeqCst);
.downloaded_bytes let total_bytes = progress.total_bytes.load(std::sync::atomic::Ordering::SeqCst);
.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 current_success = success.load(std::sync::atomic::Ordering::SeqCst);
let speed = if elapsed_secs > 0 { let speed = if elapsed_secs > 0 { downloaded_bytes / elapsed_secs } else { 0 };
downloaded_bytes / elapsed_secs
} else {
0
};
let (speed_value, speed_unit) = if speed > 1_000_000 { let (speed_value, speed_unit) = if speed > 1_000_000 {
(speed / 1_000_000, "MB/s") (speed / 1_000_000, "MB/s")
} else { } else {
@ -201,11 +142,7 @@ pub fn start_title_thread(
let remaining_files = total_files - current_success; let remaining_files = total_files - current_success;
let remaining_bytes = total_bytes.saturating_sub(downloaded_bytes); let remaining_bytes = total_bytes.saturating_sub(downloaded_bytes);
let eta_secs = if speed > 0 && remaining_files > 0 { let eta_secs = if speed > 0 && remaining_files > 0 { remaining_bytes / speed } else { 0 };
remaining_bytes / speed
} else {
0
};
let eta_str = format_duration(Duration::from_secs(eta_secs)); let eta_str = format_duration(Duration::from_secs(eta_secs));
let progress_percent = if total_bytes > 0 { let progress_percent = if total_bytes > 0 {
@ -215,7 +152,7 @@ pub fn start_title_thread(
}; };
let title = format!( let title = format!(
"Wuthering Waves Downloader - {}/{} files - Total Downloaded: {}{} - Speed: {}{} - Total ETA: {}", "Wuthering Waves Downloader - {}/{} files - Current File: {}{} - Speed: {}{} - Total ETA: {}",
current_success, current_success,
total_files, total_files,
bytes_to_human(downloaded_bytes), bytes_to_human(downloaded_bytes),
@ -225,9 +162,7 @@ pub fn start_title_thread(
eta_str eta_str
); );
#[cfg(windows)]
set_title(&title).unwrap(); set_title(&title).unwrap();
thread::sleep(Duration::from_secs(1)); thread::sleep(Duration::from_secs(1));
} }
}) })
@ -236,10 +171,7 @@ pub fn start_title_thread(
pub fn setup_ctrlc(should_stop: Arc<std::sync::atomic::AtomicBool>) { pub fn setup_ctrlc(should_stop: Arc<std::sync::atomic::AtomicBool>) {
ctrlc::set_handler(move || { ctrlc::set_handler(move || {
should_stop.store(true, std::sync::atomic::Ordering::SeqCst); should_stop.store(true, std::sync::atomic::Ordering::SeqCst);
#[cfg(windows)]
clear().unwrap(); clear().unwrap();
println!("\n{} Download interrupted by user", Status::warning()); println!("\n{} Download interrupted by user", Status::warning());
}) })
.unwrap(); .unwrap();

View file

@ -1,11 +1,6 @@
use colored::*; use colored::*;
use reqwest::blocking::Client; use reqwest::blocking::Client;
use serde_json::Value; use serde_json::Value;
#[cfg(not(target_os = "windows"))]
use std::process::Command;
#[cfg(windows)]
use winconsole::console::{clear, set_title}; use winconsole::console::{clear, set_title};
use wuwa_downloader::{ use wuwa_downloader::{
@ -13,19 +8,16 @@ use wuwa_downloader::{
io::{ io::{
console::print_results, console::print_results,
file::get_dir, file::get_dir,
logging::setup_logging,
util::{ util::{
calculate_total_size, download_resources, exit_with_error, setup_ctrlc, calculate_total_size, download_resources, exit_with_error, setup_ctrlc, track_progress, start_title_thread
start_title_thread, track_progress,
}, },
logging::setup_logging,
}, },
network::client::{fetch_index, get_config}, 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();
@ -35,12 +27,7 @@ fn main() {
}; };
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(),
@ -60,8 +47,6 @@ fn main() {
); );
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, success, progress) = track_progress(total_size); let (should_stop, success, progress) = track_progress(total_size);
@ -89,9 +74,7 @@ fn main() {
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),
resources.len(), resources.len(),

View file

@ -1,31 +1,20 @@
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};
#[cfg(not(target_os = "windows"))] use winconsole::console::{self};
use std::process::Command; use std::{io::{Read, Write}, fs, io, path::Path, time::Duration};
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, util::get_version}; 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/4435fd290c07f7f766a6d2ab09ed3096d83b02e3/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: Duration = Duration::from_secs(10000); const DOWNLOAD_TIMEOUT: u64 = 300;
const BUFFER_SIZE: usize = 65536; const BUFFER_SIZE: usize = 8192;
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());
@ -38,10 +27,7 @@ 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());
@ -52,10 +38,7 @@ 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());
@ -72,10 +55,7 @@ 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());
@ -86,10 +66,7 @@ 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());
@ -104,10 +81,7 @@ 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());
@ -122,10 +96,7 @@ 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());
@ -162,16 +133,13 @@ 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 if let Some(size) = head_response.headers()
.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 progress.total_bytes.fetch_add(size, std::sync::atomic::Ordering::SeqCst);
.total_bytes
.fetch_add(size, std::sync::atomic::Ordering::SeqCst);
break; break;
} }
} }
@ -179,11 +147,7 @@ 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!( println!("{} File is valid: {}", Status::matched(), filename.bright_purple());
"{} File is valid: {}",
Status::matched(),
filename.bright_purple()
);
return true; return true;
} }
} }
@ -202,59 +166,39 @@ 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_error(log_file, &format!("CDN {} failed for {} (HTTP {})", i+1, dest, resp.status()));
log_file,
&format!("CDN {} failed for {} (HTTP {})", i + 1, dest, resp.status()),
);
continue; continue;
} },
Err(e) => { Err(e) => {
log_error( log_error(log_file, &format!("CDN {} failed for {}: {}", i+1, dest, e));
log_file,
&format!("CDN {} failed for {}: {}", i + 1, dest, e),
);
continue; continue;
} }
}; };
let expected_size = file_size.or_else(|| { let expected_size = file_size.or_else(|| head_response.headers()
head_response .get("content-length")
.headers() .and_then(|v| v.to_str().ok())
.get("content-length") .and_then(|s| s.parse::<u64>().ok()));
.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!( println!("{} File is valid: {}", Status::matched(), filename.bright_purple());
"{} 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, &pb); let result = download_single_file(&client, &url, &path, should_stop, progress);
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;
} }
@ -263,32 +207,20 @@ pub fn download_file(
let _ = fs::remove_file(&path); let _ = fs::remove_file(&path);
if retries > 0 { if retries > 0 {
println!( println!("{} Retrying {}... ({} left)",
"{} Retrying {}... ({} left)", Status::warning(), filename.yellow(), retries);
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_error(log_file, &format!("Failed after retries for {}: {}", dest,
log_file, last_error.unwrap_or_default()));
&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;
} }
@ -300,13 +232,8 @@ pub fn download_file(
let actual = calculate_md5(&path); let actual = calculate_md5(&path);
if actual != expected { if actual != expected {
log_error( log_error(log_file, &format!("Checksum failed for {}: expected {}, got {}",
log_file, dest, expected, actual));
&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;
@ -328,55 +255,27 @@ 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 downloaded: u64 = 0; let mut response = client
if path.exists() { .get(url)
downloaded = fs::metadata(path) .timeout(Duration::from_secs(DOWNLOAD_TIMEOUT))
.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() == reqwest::StatusCode::RANGE_NOT_SATISFIABLE { if !response.status().is_success() {
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 = OpenOptions::new() let mut file = fs::File::create(path)
.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 let bytes_read = response.read(&mut buffer)
.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 {
@ -386,11 +285,7 @@ fn download_single_file(
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))?;
downloaded += bytes_read as u64; progress.downloaded_bytes.fetch_add(bytes_read as u64, std::sync::atomic::Ordering::SeqCst);
pb.set_position(downloaded);
progress
.downloaded_bytes
.store(downloaded, std::sync::atomic::Ordering::SeqCst);
} }
Ok(()) Ok(())
@ -398,10 +293,6 @@ fn download_single_file(
pub fn get_config(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
@ -448,31 +339,24 @@ pub fn get_config(client: &Client) -> Result<Config, String> {
println!("{} Using predownload.config", Status::info()); println!("{} Using predownload.config", Status::info());
"predownload" "predownload"
} }
(true, true) => loop { (true, true) => {
print!( loop {
"{} Choose config to use (1=default, 2=predownload): ", print!("{} Choose config to use (1=default, 2=predownload): ", Status::question());
Status::question() io::stdout().flush().map_err(|e| format!("Failed to flush stdout: {}", e))?;
);
io::stdout()
.flush()
.map_err(|e| format!("Failed to flush stdout: {}", e))?;
let mut input = String::new(); let mut input = String::new();
io::stdin() io::stdin()
.read_line(&mut input) .read_line(&mut input)
.map_err(|e| format!("Failed to read input: {}", e))?; .map_err(|e| format!("Failed to read input: {}", e))?;
match input.trim() { match input.trim() {
"1" => break "default", "1" => break "default",
"2" => break "predownload", "2" => break "predownload",
_ => println!("{} Invalid choice, please enter 1 or 2", Status::error()), _ => 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(),
);
} }
(false, false) => return Err("Neither default.config nor predownload.config found in response".to_string()),
}; };
let config_data = config let config_data = config
@ -509,10 +393,10 @@ pub fn get_config(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 {
@ -548,78 +432,24 @@ 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 response.copy_to(&mut buffer)
.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. Live - OS");
for (i, (cat, ver, label)) in entries.iter().enumerate() { println!("2. Live - CN");
let index_url = get_version(&gist_data, cat, ver)?; println!("3. Beta - OS");
println!("4. Beta - CN");
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());

BIN
zani.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB