use std::{
path::Path,
time::{Duration, Instant},
};
use color_eyre::eyre::Context;
use futures::StreamExt;
use tokio::io::AsyncWriteExt;
use crate::{NetErr, ZvError, app::utils::ProgressHandle};
const TARGET: &str = "zv::network::download";
pub(in crate::app::network) async fn download_file(
client: &reqwest::Client,
url: &str,
dest_path: &Path,
expected_size: u64,
progress_handle: &ProgressHandle,
) -> Result<(), NetErr> {
tracing::debug!(target: TARGET, "Starting download request for URL: {}", url);
let response = client
.get(url)
.send()
.await
.map_err(|e| {
if e.is_timeout() {
tracing::warn!(target: TARGET, "Request timeout for URL: {} - This may indicate network connectivity issues or server overload", url);
NetErr::Timeout(format!("Request timeout for {}", url))
} else if e.is_connect() {
tracing::warn!(target: TARGET, "Connection error for URL: {} - Unable to establish connection to server", url);
NetErr::Reqwest(e)
} else {
tracing::error!(target: TARGET, "Network error during request to {}: {} - This may indicate DNS issues or network problems", url, e);
NetErr::Reqwest(e)
}
})?;
let status = response.status();
tracing::debug!(target: TARGET, "Received HTTP response with status: {} for URL: {}", status, url);
match status.as_u16() {
200 => {
tracing::trace!(target: TARGET, "HTTP 200 OK received, proceeding with file download from {}", url);
}
503 => {
tracing::warn!(target: TARGET, "HTTP 503 Service Unavailable for URL: {} - Mirror is experiencing scheduled downtime or maintenance. Will retry with different mirror.", url);
return Err(NetErr::HTTP(status));
}
429 => {
tracing::warn!(target: TARGET, "HTTP 429 Too Many Requests for URL: {} - Mirror is rate limiting requests. Will retry with different mirror after delay.", url);
return Err(NetErr::HTTP(status));
}
404 => {
tracing::warn!(target: TARGET, "HTTP 404 Not Found for URL: {} - File may not exist on this mirror (common for old Zig versions ≤0.5.0). Will retry with different mirror.", url);
return Err(NetErr::HTTP(status));
}
504 => {
tracing::warn!(target: TARGET, "HTTP 504 Gateway Timeout for URL: {} - Mirror gateway is experiencing issues or ziglang.org is inaccessible. Will retry with different mirror.", url);
return Err(NetErr::HTTP(status));
}
500..=599 => {
tracing::warn!(target: TARGET, "HTTP {} Server Error for URL: {} - Mirror is experiencing server-side issues. Will retry with different mirror.", status, url);
return Err(NetErr::HTTP(status));
}
400..=499 => {
tracing::warn!(target: TARGET, "HTTP {} Client Error for URL: {} - Request may be malformed or unauthorized. Will retry with different mirror.", status, url);
return Err(NetErr::HTTP(status));
}
_ => {
tracing::warn!(target: TARGET, "Unexpected HTTP status {} for URL: {} - Unknown response code. Will retry with different mirror.", status, url);
return Err(NetErr::HTTP(status));
}
}
tracing::trace!(target: TARGET, "Initiating streaming download for {} bytes from {}", expected_size, url);
match stream_download_file(client, url, dest_path, expected_size, progress_handle).await {
Ok(()) => {
tracing::debug!(target: TARGET, "Successfully completed download from {}", url);
Ok(())
}
Err(e) => {
tracing::error!(target: TARGET, "Download failed from {}: {}", url, e);
Err(e)
}
}
}
pub(in crate::app::network) async fn stream_download_file(
client: &reqwest::Client,
url: &str,
dest_path: &Path,
expected_size: u64,
progress_handle: &ProgressHandle,
) -> Result<(), NetErr> {
let response = client.get(url).send().await.map_err(|e| {
if e.is_timeout() {
tracing::warn!(target: TARGET, "Download timeout for URL: {}", url);
NetErr::Timeout(format!("Request timeout for {}", url))
} else if e.is_connect() {
tracing::warn!(target: TARGET, "Connection error for URL: {}", url);
NetErr::Reqwest(e)
} else {
tracing::error!(target: TARGET, "Network error during download: {}", e);
NetErr::Reqwest(e)
}
})?;
if !response.status().is_success() {
let status = response.status();
tracing::error!(target: TARGET, "HTTP error {} for URL: {}", status, url);
return Err(NetErr::HTTP(status));
}
let content_length = response.content_length().unwrap_or(expected_size);
let actual_size = if expected_size == 0 {
content_length
} else {
expected_size
};
tracing::debug!(target: TARGET, "Starting download: {} bytes from {} (content-length: {})", actual_size, url, content_length);
let mut file = tokio::fs::File::create(dest_path)
.await
.map_err(ZvError::Io)
.wrap_err_with(|| format!("Failed to create destination file: {}", dest_path.display()))?;
let mut stream = response.bytes_stream();
let mut downloaded = 0u64;
let mut last_progress_update = Instant::now();
const PROGRESS_UPDATE_INTERVAL: Duration = Duration::from_millis(250);
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result.map_err(|e| {
tracing::error!(target: TARGET, "Error reading chunk during download: {}", e);
NetErr::Reqwest(e)
})?;
file.write_all(&chunk)
.await
.map_err(ZvError::Io)
.wrap_err_with(|| {
format!(
"Failed to write to destination file: {}",
dest_path.display()
)
})?;
downloaded += chunk.len() as u64;
let now = Instant::now();
if now.duration_since(last_progress_update) >= PROGRESS_UPDATE_INTERVAL {
let percentage = if actual_size > 0 {
(downloaded * 100) / actual_size
} else {
0
};
let downloaded_mb = downloaded as f64 / 1_048_576.0; let total_mb = actual_size as f64 / 1_048_576.0;
let progress_msg = if actual_size > 0 {
format!(
"Downloading {:.1}/{:.1} MB ({}%)",
downloaded_mb, total_mb, percentage
)
} else {
format!("Downloading {:.1} MB", downloaded_mb)
};
if let Err(e) = progress_handle.update(progress_msg).await {
tracing::warn!(target: TARGET, "Failed to update progress: {}", e);
}
last_progress_update = now;
}
}
file.flush()
.await
.map_err(ZvError::Io)
.wrap_err_with(|| format!("Failed to flush file: {}", dest_path.display()))?;
let downloaded_mb = downloaded as f64 / 1_048_576.0;
let final_msg = format!("Download completed: {:.1} MB", downloaded_mb);
if let Err(e) = progress_handle.update(final_msg).await {
tracing::warn!(target: TARGET, "Failed to update final progress: {}", e);
}
tracing::trace!(target: TARGET, "Successfully downloaded {} bytes to {}", downloaded, dest_path.display());
Ok(())
}
pub(in crate::app::network) async fn move_to_final_location(
temp_path: &Path,
final_path: &Path,
) -> Result<(), std::io::Error> {
tracing::debug!(target: TARGET, "Starting atomic move from {} to {}", temp_path.display(), final_path.display());
if !temp_path.exists() {
let error_msg = format!("Source file does not exist: {}", temp_path.display());
tracing::error!(target: TARGET, "{}", error_msg);
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, error_msg));
}
let file_size = match tokio::fs::metadata(temp_path).await {
Ok(metadata) => {
let size = metadata.len();
tracing::debug!(target: TARGET, "Source file size: {} bytes ({:.1} MB)", size, size as f64 / 1_048_576.0);
Some(size)
}
Err(e) => {
tracing::warn!(target: TARGET, "Could not read source file metadata: {} - proceeding with move", e);
None
}
};
if let Some(parent) = final_path.parent() {
if !parent.exists() {
tracing::debug!(target: TARGET, "Creating parent directory: {}", parent.display());
match tokio::fs::create_dir_all(parent).await {
Ok(()) => {
tracing::debug!(target: TARGET, "Successfully created parent directory: {}", parent.display());
}
Err(e) => {
tracing::error!(target: TARGET, "Failed to create parent directory {}: {}", parent.display(), e);
match e.kind() {
std::io::ErrorKind::PermissionDenied => {
tracing::error!(target: TARGET, "Permission denied creating directory {} - check write permissions", parent.display());
}
std::io::ErrorKind::AlreadyExists => {
tracing::debug!(target: TARGET, "Parent directory {} already exists - this is normal", parent.display());
}
_ => {
tracing::error!(target: TARGET, "Unexpected error creating directory {}: {:?}", parent.display(), e.kind());
}
}
return Err(e);
}
}
} else {
tracing::debug!(target: TARGET, "Parent directory already exists: {}", parent.display());
}
}
if final_path.exists() {
tracing::warn!(target: TARGET, "Destination file already exists: {} - will overwrite", final_path.display());
if let Ok(existing_metadata) = tokio::fs::metadata(final_path).await {
let existing_size = existing_metadata.len();
tracing::debug!(target: TARGET, "Existing file size: {} bytes ({:.1} MB)", existing_size, existing_size as f64 / 1_048_576.0);
if let Some(new_size) = file_size {
if existing_size == new_size {
tracing::debug!(target: TARGET, "File sizes match - this may be a duplicate download");
} else {
tracing::debug!(target: TARGET, "File sizes differ - replacing with new version");
}
}
}
}
tracing::debug!(target: TARGET, "Performing atomic file move operation");
match tokio::fs::rename(temp_path, final_path).await {
Ok(()) => {
tracing::trace!(target: TARGET, "Successfully moved file from {} to {} ({})",
temp_path.display(), final_path.display(),
if let Some(size) = file_size {
format!("{:.1} MB", size as f64 / 1_048_576.0)
} else {
"unknown size".to_string()
});
if final_path.exists() && !temp_path.exists() {
tracing::debug!(target: TARGET, "Atomic move verification successful - file exists at destination and removed from source");
} else {
tracing::warn!(target: TARGET, "Atomic move verification failed - file state may be inconsistent");
}
Ok(())
}
Err(e) => {
tracing::error!(target: TARGET, "Failed to move file from {} to {}: {}", temp_path.display(), final_path.display(), e);
match e.kind() {
std::io::ErrorKind::PermissionDenied => {
tracing::error!(target: TARGET, "Permission denied during file move - check write permissions for destination directory");
}
std::io::ErrorKind::NotFound => {
tracing::error!(target: TARGET, "Source file disappeared during move operation - this indicates a race condition or external interference");
}
std::io::ErrorKind::AlreadyExists => {
tracing::error!(target: TARGET, "Destination file exists and cannot be overwritten - this may indicate a filesystem issue");
}
std::io::ErrorKind::InvalidInput => {
tracing::error!(target: TARGET, "Invalid file paths provided for move operation");
}
_ => {
tracing::error!(target: TARGET, "Unexpected I/O error during file move: {:?}", e.kind());
}
}
Err(e)
}
}
}