Compare commits

...

3 commits
0.1.9 ... main

Author SHA1 Message Date
Yuhki
118b4f5fe0
Update README.md 2025-05-30 23:20:09 +02:00
Yuhki
29bd052ef4 chore: add package.sh script and apply code formatting 2025-05-30 23:18:05 +02:00
Yuhki
bece26c433 Add support for resuming partial downloads using HTTP Range requests
- Modified `download_single_file` to check for an existing partially downloaded file
- If present, uses HTTP Range headers to resume the download from the last byte
- Opens file in append mode rather than overwriting it
- Handles HTTP 206 Partial Content and 416 Range Not Satisfiable responses
- Updates progress bar and download state accordingly during resumed downloads
2025-05-27 18:27:30 +02:00
12 changed files with 196 additions and 31 deletions

3
.gitignore vendored
View file

@ -4,6 +4,9 @@ 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

View file

@ -10,6 +10,7 @@
## 📦 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:**

113
package.sh Normal file
View 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

View file

@ -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()
}
} }

View file

@ -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 {

View file

@ -1,5 +1,8 @@
use std::{fs::{self, OpenOptions}, io::Write, time::SystemTime}; use std::{
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

@ -2,7 +2,11 @@ use colored::Colorize;
use reqwest::blocking::Client; use reqwest::blocking::Client;
use serde_json::Value; use serde_json::Value;
use std::{ use std::{
fs::{self, File}, io::{self, Write}, sync::Arc, thread, time::{Duration, Instant} fs::{self, File},
io::{self, Write},
sync::Arc,
thread,
time::{Duration, Instant},
}; };
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]

View file

@ -13,10 +13,11 @@ use wuwa_downloader::{
io::{ io::{
console::print_results, console::print_results,
file::get_dir, file::get_dir,
util::{
calculate_total_size, download_resources, exit_with_error, setup_ctrlc, track_progress, start_title_thread
},
logging::setup_logging, logging::setup_logging,
util::{
calculate_total_size, download_resources, exit_with_error, setup_ctrlc,
start_title_thread, track_progress,
},
}, },
network::client::{fetch_index, get_config}, network::client::{fetch_index, get_config},
}; };

View file

@ -2,11 +2,11 @@ use colored::Colorize;
use flate2::read::GzDecoder; use flate2::read::GzDecoder;
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use reqwest::blocking::Client; use reqwest::blocking::Client;
use serde_json::{Value, from_reader, from_str}; use serde_json::{from_reader, from_str, Value};
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
use std::process::Command; use std::process::Command;
use std::{ use std::{
fs, fs::{self, OpenOptions},
io::{self, Read, Write}, io::{self, Read, Write},
path::Path, path::Path,
time::Duration, time::Duration,
@ -330,20 +330,46 @@ fn download_single_file(
progress: &DownloadProgress, progress: &DownloadProgress,
pb: &ProgressBar, pb: &ProgressBar,
) -> Result<(), String> { ) -> Result<(), String> {
let mut response = client let mut downloaded: u64 = 0;
.get(url) if path.exists() {
.timeout(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).map_err(|e| format!("File error: {}", e))?; let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.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];
let mut downloaded: u64 = 0;
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());
@ -364,7 +390,7 @@ fn download_single_file(
pb.set_position(downloaded); pb.set_position(downloaded);
progress progress
.downloaded_bytes .downloaded_bytes
.fetch_add(bytes_read as u64, std::sync::atomic::Ordering::SeqCst); .store(downloaded, std::sync::atomic::Ordering::SeqCst);
} }
Ok(()) Ok(())
@ -541,7 +567,7 @@ pub fn fetch_gist(client: &Client) -> Result<String, String> {
("live", "os", "Live - OS"), ("live", "os", "Live - OS"),
("live", "cn", "Live - CN"), ("live", "cn", "Live - CN"),
("beta", "os", "Beta - OS"), ("beta", "os", "Beta - OS"),
("beta", "cn", "Beta - CN (wicked-waifus-rs)"), ("beta", "cn", "Beta - CN"),
]; ];
println!("{} Available versions:", Status::info()); println!("{} Available versions:", Status::info());