use color_eyre::eyre::{eyre, Result};
use std::fs;
use crate::lfs;
use std::path::Path;
use std::sync::atomic::Ordering;
use ureq::http;
use crate::mirror;
use crate::config;
use super::types::*;
use super::utils::*;
use super::validation::*;
use super::mirror::add_url_to_mirror_skip_list;
use super::aur::AUR_BASE_URL;
use super::file_ops::setup_download_file;
use super::progress::update_download_progress;
use super::chunk::{check_ondemand_chunking, validate_chunk_file_boundaries};
use crate::download::setup_task_progress_tracking;
use crate::download::extract_server_metadata;
use crate::download::should_redownload;
pub(crate) fn execute_download_request(
task: &DownloadTask,
resolved_url: &str,
existing_bytes: u64,
) -> Result<http::Response<ureq::Body>> {
let client = task.get_client()?;
let mut request = client.get(resolved_url.replace("///", "/"));
let part_path = &task.chunk_path;
let file_size = task.file_size.load(Ordering::Relaxed);
let chunk_size = task.chunk_size.load(Ordering::Relaxed);
let chunk_offset = task.chunk_offset.load(Ordering::Relaxed);
let resumed_bytes = task.resumed_bytes.load(Ordering::Relaxed);
match task.get_range_request() {
RangeRequest::Chunk => {
let end = chunk_offset + chunk_size - 1;
let start = chunk_offset + resumed_bytes;
if start >= end {
log::warn!("Invalid range detected: start={} >= end={} for {} (chunk_offset={}, chunk_size={}, resumed_bytes={}, file_size={}, chunk_path={})",
start, end, resolved_url, chunk_offset, chunk_size, resumed_bytes, file_size, part_path.display());
return Err(eyre!("Invalid range calculation: start > end"));
}
log::debug!("Setting Range header: bytes={}-{} (chunk_offset={}, chunk_size={}, resumed_bytes={}, chunk_path={})",
start, end, chunk_offset, chunk_size, resumed_bytes, part_path.display());
request = request.header("Range", &format!("bytes={}-{}", start, end));
}
RangeRequest::Resume => {
if resumed_bytes >= file_size && file_size > 0 {
log::warn!("Invalid range detected: resumed_bytes={} >= file_size={} for {} (chunk_offset={}, chunk_size={}, chunk_path={})",
resumed_bytes, file_size, resolved_url, chunk_offset, chunk_size, part_path.display());
return Err(eyre!("Invalid range calculation: resumed_bytes >= file_size"));
}
log::debug!("Setting Range header: bytes={}- (resume from existing bytes, chunk_path={})", resumed_bytes, part_path.display());
request = request.header("Range", &format!("bytes={}-", resumed_bytes));
}
RangeRequest::None => {
let file_to_check: Option<&Path> = if matches!(task.mutability, Mutability::Mutable) && task.final_path.exists() {
Some(&task.final_path)
} else if part_path.exists() {
Some(part_path)
} else {
log::debug!("Local file {} doesn't exist, skipping ETag header", part_path.display());
None
};
if let Some(file_to_check) = file_to_check {
let file_size = fs::metadata(file_to_check)
.map(|m| m.len())
.unwrap_or(0);
if file_size == 0 {
log::debug!("File {} is 0 bytes, skipping ETag header to force fresh download", file_to_check.display());
} else {
if let Ok(Some(metadata)) = task.load_remote_metadata() {
if let Some(etag) = metadata.serving_metadata.and_then(|m| m.etag) {
log::debug!("Adding If-None-Match header with ETag '{}' for conditional request (file={})", etag, file_to_check.display());
request = request.header("If-None-Match", &format!("\"{}\"", etag));
}
}
}
}
}
}
let request_start = std::time::Instant::now();
let call_result = request.call();
let latency = request_start.elapsed().as_millis() as u64;
log_http_event_safe(resolved_url, mirror::HttpEvent::Latency(latency));
match call_result {
Ok(response) => Ok(response),
Err(ureq::Error::StatusCode(code)) => handle_http_status_error(code, task, resolved_url, existing_bytes),
Err(ureq::Error::Io(e)) => handle_network_io_error(e, task, resolved_url),
Err(e) => handle_general_request_error(e, task, resolved_url),
}
}
fn handle_http_status_error(
code: u16,
task: &DownloadTask,
resolved_url: &str,
_existing_bytes: u64,
) -> Result<http::Response<ureq::Body>> {
log::debug!("HTTP error code {} for chunk_path={}", code, task.chunk_path.display());
log_http_event_safe(resolved_url, mirror::HttpEvent::HttpStatus(code));
let error_msg = format!("HTTP {}", code);
task.set_message(format!("{} - {}", error_msg, resolved_url));
if code == 429 {
let active_conns = {
let site = mirror::url2site(&resolved_url);
if let Ok(mirrors_guard) = mirror::MIRRORS.lock() {
mirrors_guard.mirrors.get(&site)
.map(|mirror| mirror.shared_usage.active_downloads.load(std::sync::atomic::Ordering::Relaxed))
.unwrap_or(0)
} else {
0
}
};
log::debug!("Received HTTP 429 Too Many Requests ({} active connections) for {} (chunk_path={})", active_conns, resolved_url, task.chunk_path.display());
log_http_event_safe(&resolved_url, mirror::HttpEvent::TooManyRequests(active_conns as u32));
return Err(DownloadError::TooManyRequests.into());
}
if code == 416 {
let chunk_offset = task.chunk_offset.load(Ordering::Relaxed);
let chunk_size = task.chunk_size.load(Ordering::Relaxed);
let resumed_bytes = task.resumed_bytes.load(Ordering::Relaxed);
let file_size = task.file_size.load(Ordering::Relaxed);
log::warn!("HTTP 416 Range Not Satisfiable for {} (chunk_path={}) - Range calculation details:", resolved_url, task.chunk_path.display());
log::warn!(" chunk_offset={}, chunk_size={}, resumed_bytes={}, file_size={}",
chunk_offset, chunk_size, resumed_bytes, file_size);
if chunk_offset > 0 || chunk_size != file_size {
let start = chunk_offset + resumed_bytes;
let end = chunk_offset + chunk_size - 1;
log::warn!(" Attempted range: bytes={}-{} (start={}, end={})", start, end, start, end);
if start > end {
log::error!(" INVALID RANGE: start > end - this is the root cause of the 416 error");
} else if end >= file_size && file_size > 0 {
log::warn!(" Range extends beyond file size: end={} >= file_size={}", end, file_size);
}
}
log::warn!("HTTP 416 error indicates invalid range request - will retry with different mirror or restart");
Err(DownloadError::UnexpectedResponse { code, details: format!("HTTP 416 Range Not Satisfiable: {}", error_msg) }.into())
} else if code == 502 {
log::warn!("HTTP 502 Bad Gateway for {} (chunk_path={}) - server may be unreliable, will retry", resolved_url, task.chunk_path.display());
Err(DownloadError::UnexpectedResponse { code, details: format!("HTTP 502 Bad Gateway - server temporarily unavailable: {}", error_msg) }.into())
} else if code >= HTTP_CLIENT_ERROR_START && code < HTTP_SERVER_ERROR_START {
log::debug!("Client error {} for {} (chunk_path={})", code, resolved_url, task.chunk_path.display());
if code == 404 {
add_url_to_mirror_skip_list(&task.url, resolved_url);
}
Err(DownloadError::Fatal { code, message: error_msg }.into())
} else {
log::debug!("Server error {} for {} (chunk_path={})", code, resolved_url, task.chunk_path.display());
Err(DownloadError::UnexpectedResponse { code, details: format!("HTTP error: {}", error_msg) }.into())
}
}
fn handle_network_io_error(
e: std::io::Error,
task: &DownloadTask,
resolved_url: &str,
) -> Result<http::Response<ureq::Body>> {
log_http_event_safe(resolved_url, mirror::HttpEvent::NetError(e.to_string()));
log::debug!("Network I/O error for {} (chunk_path={}): {}", resolved_url, task.chunk_path.display(), e);
let error_msg = format!("Network error: {} - {}", e, resolved_url);
task.set_message(error_msg.clone());
Err(DownloadError::Network { details: error_msg }.into())
}
fn handle_general_request_error(
e: ureq::Error,
task: &DownloadTask,
resolved_url: &str,
) -> Result<http::Response<ureq::Body>> {
let error_str = e.to_string();
let error_msg = format!("Error downloading: {} - {}", error_str, resolved_url);
log_http_event_safe(resolved_url, mirror::HttpEvent::NetError(error_str.clone()));
log::debug!("General request error for {} (chunk_path={}): {}", resolved_url, task.chunk_path.display(), error_str);
task.set_message(error_msg.clone());
Err(DownloadError::Network { details: error_msg }.into())
}
fn validate_response_content_type(
response: &http::Response<ureq::Body>,
url: &str,
task: &DownloadTask,
) -> Result<()> {
log::debug!("Validating response content type for {} (chunk_path={})", url, task.chunk_path.display());
if let Some(content_type) = response.headers().get("content-type").and_then(|v| v.to_str().ok()) {
if content_type.contains("text/html") {
if url.ends_with('/') || url.ends_with(".html") {
log::debug!("Allowing HTML content for directory listing or HTML file: {}", url);
return Ok(());
}
if url.starts_with(AUR_BASE_URL) {
eprintln!("\nError: Received HTML page (likely bot protection) instead of file from {}", url);
eprintln!("AUR downloads via HTTP are unreliable due to bot protection systems.");
eprintln!("Git is recommended for downloading AUR packages.");
eprintln!("\nPlease retry after installing git in either:");
eprintln!(" - Host OS: Install git using your system package manager (e.g., 'apt-get install git')");
eprintln!(" - Environment: Run 'epkg -e {} install git' to install git in current environment", config().common.env_name);
let error_msg = format!(
"AUR download failed: received HTML page (bot protection) instead of file. \
AUR downloads via HTTP are unreliable. Please install git (in host OS or environment) and retry."
);
task.set_message(error_msg.clone());
return Err(eyre!("Fatal error while downloading from {}: {}", url, error_msg));
}
if task.mutability == Mutability::Immutable ||
task.mutability == Mutability::AppendOnly {
let error_msg = "Received HTML page instead of file. This may indicate an authentication issue with the server.";
task.set_message(error_msg.to_string());
return Err(eyre!("Fatal error while downloading from {}: {}", url, error_msg.to_string()));
}
}
}
Ok(())
}
pub(crate) fn process_chunk_download_stream(
response: &mut http::Response<ureq::Body>,
task: &DownloadTask,
existing_bytes: u64,
) -> Result<u64> {
let has_compression = is_content_compressed(response);
let expected_response_size = if !has_compression {
parse_content_length(response)
} else {
log::debug!("Content-encoding detected, skipping content-length validation for {}", task.url);
None
};
let mut reader = response.body_mut().as_reader();
let mut buffer = vec![0; 8192];
let mut chunk_append_offset = existing_bytes;
let mut network_bytes = 0u64;
let mut last_update = std::time::Instant::now();
let mut last_ondemand_check = std::time::Instant::now();
let data_channel = task.get_data_channel();
let mut file = setup_download_file(task, existing_bytes)?;
log::debug!("process_chunk_download_stream: Starting to read response body for {} (existing_bytes={})",
task.chunk_path.display(), existing_bytes);
loop {
let bytes_read = read_chunk_from_stream(&mut reader, &mut buffer, task, chunk_append_offset)?;
if bytes_read == 0 {
if let Some(expected_size) = expected_response_size {
if network_bytes < expected_size {
log::error!(
"Premature EOF: received {} bytes but expected {} bytes for {}",
network_bytes, expected_size, task.chunk_path.display()
);
return Err(DownloadError::Network {
details: format!("Premature EOF: received {} bytes but expected {} bytes for {}",
network_bytes, expected_size, task.chunk_path.display())
}.into());
}
}
break;
}
let bytes_to_write = calculate_write_bytes(task, bytes_read, chunk_append_offset);
let written_bytes = write_chunk_data(&mut file, &buffer, bytes_to_write, task, chunk_append_offset)?;
if written_bytes == 0 {
break;
}
chunk_append_offset += written_bytes as u64;
network_bytes += written_bytes as u64;
task.received_bytes.store(network_bytes, Ordering::Relaxed);
if let Some(ref channel) = data_channel {
match channel.send(buffer[..written_bytes].to_vec()) {
Ok(_) => {
}
Err(e) => {
log::warn!("Data channel disconnected for {}: {}", task.get_resolved_url(), e);
}
}
}
if written_bytes < bytes_read {
break;
}
update_download_progress(task, &mut last_update);
check_ondemand_chunking(task, chunk_append_offset, &mut last_ondemand_check);
}
Ok(chunk_append_offset)
}
fn read_chunk_from_stream(
reader: &mut dyn std::io::Read,
buffer: &mut [u8],
task: &DownloadTask,
chunk_append_offset: u64,
) -> Result<usize> {
match reader.read(buffer) {
Ok(0) => {
log::debug!("read_chunk_from_stream: EOF reached at offset {} for {}", chunk_append_offset, task.chunk_path.display());
Ok(0)
},
Ok(n) => {
log::trace!("read_chunk_from_stream: Read {} bytes at offset {} for {}", n, chunk_append_offset, task.chunk_path.display());
Ok(n)
},
Err(e) => {
log::error!("read_chunk_from_stream: Read error at offset {} for {}: {}", chunk_append_offset, task.chunk_path.display(), e);
if task.is_master_task() {
let error_msg = format!("Read error at {} bytes: {}", chunk_append_offset,
task.resolved_url.lock().map(|r| r.clone()).unwrap_or_else(|_| task.url.clone()));
task.set_message(error_msg);
}
Err(eyre!("Failed to read from response (chunk_append_offset={}, buffer_size={}): {}", chunk_append_offset, buffer.len(), e))
}
}
}
pub(crate) fn finalize_chunk_download(
task: &DownloadTask,
chunk_append_offset: u64,
existing_bytes: u64,
) -> Result<u64> {
let network_bytes = chunk_append_offset - existing_bytes;
log::debug!("download_content completed: {} total bytes ({} network bytes) written to {}",
chunk_append_offset, network_bytes, task.chunk_path.display());
if task.is_master_task() && chunk_append_offset == 0 && matches!(task.mutability, Mutability::Mutable) {
log::info!("Download resulted in 0 bytes for {} - likely server issue (unreliable server like AUR), cleaning up and will retry", task.url);
if task.chunk_path.exists() {
if let Err(e) = lfs::remove_file(&task.chunk_path) {
log::warn!("Failed to remove 0-byte file {}: {}", task.chunk_path.display(), e);
} else {
log::debug!("Cleaned up 0-byte file: {}", task.chunk_path.display());
}
}
return Err(DownloadError::Network {
details: format!("Download resulted in 0 bytes for {} - server may be unreliable", task.url)
}.into());
}
validate_chunk_file_boundaries(task, chunk_append_offset)?;
Ok(chunk_append_offset)
}
pub(crate) fn validate_download_size(downloaded: u64, total_size: u64, part_path: &Path) -> Result<()> {
if total_size > 0 && downloaded != total_size {
log::error!(
"Download size mismatch: Downloaded size ({}) does not match expected size ({}) for {}",
downloaded,
total_size,
part_path.display()
);
if downloaded < total_size {
return Err(DownloadError::ContentIncomplete {
expected: total_size,
actual: downloaded,
}
.into());
}
return Err(DownloadError::ContentValidation {
expected: format!("{} bytes", total_size),
actual: format!("{} bytes", downloaded),
}
.into());
}
Ok(())
}
pub(crate) fn get_remote_size(task: &DownloadTask, response: &http::Response<ureq::Body>) -> Option<u64> {
if let Some(content_range) = response.headers().get("content-range") {
if let Ok(s) = content_range.to_str() {
if let Some(total_size) = parse_content_range_total(s) {
log::debug!("Got total size {} from Content-Range header: {}", total_size, s);
return Some(total_size);
}
}
}
let is_compressed = is_content_compressed(response);
if is_compressed {
log::debug!(
"Content is compressed with '{}', Content-Length refers to compressed size, not final size",
response.headers().get("content-encoding")
.and_then(|v| v.to_str().ok())
.unwrap_or("unknown")
);
}
if task.get_range_request() != RangeRequest::Chunk && !is_compressed {
if let Some(response_size) = parse_content_length(response) {
let resumed_bytes = task.resumed_bytes.load(Ordering::Relaxed);
let total_size = resumed_bytes + response_size;
log::debug!(
"Range request: Content-Length {} + resumed_bytes {} = total size {}",
response_size, resumed_bytes, total_size
);
return Some(total_size);
}
}
if let Some(x_content_length) = response.headers().get("x-content-length") {
if let Ok(s) = x_content_length.to_str() {
if let Ok(size) = s.parse::<u64>() {
log::debug!("Got size {} from X-Content-Length header", size);
return Some(size);
}
}
}
None
}
fn parse_content_range_total(range_str: &str) -> Option<u64> {
if let Some(slash_pos) = range_str.rfind('/') {
let total_part = &range_str[slash_pos + 1..];
if let Ok(size) = total_part.parse::<u64>() {
return Some(size);
}
}
None
}
fn is_content_compressed(response: &http::Response<ureq::Body>) -> bool {
response.headers()
.get("content-encoding")
.and_then(|v| v.to_str().ok())
.map(|encoding| {
let encoding_lower = encoding.to_lowercase();
encoding_lower.contains("gzip") ||
encoding_lower.contains("deflate") ||
encoding_lower.contains("compress") ||
encoding_lower.contains("br") ||
encoding_lower.contains("zstd") ||
encoding_lower.contains("xz") ||
encoding_lower.contains("bzip2") ||
encoding_lower.contains("lzma") ||
encoding_lower.contains("lz4")
})
.unwrap_or(false)
}
fn parse_content_length(response: &http::Response<ureq::Body>) -> Option<u64> {
response.headers()
.get("content-length")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
}
pub(crate) fn parse_etag(response: &http::Response<ureq::Body>) -> Option<String> {
let etag_value = response.headers().get("etag")
.or_else(|| response.headers().get("ETag"))
.and_then(|v| v.to_str().ok())?;
let cleaned_etag = etag_value.trim()
.strip_prefix("W/").unwrap_or(etag_value.trim())
.trim_matches('"');
if cleaned_etag.is_empty() {
None
} else {
Some(cleaned_etag.to_string())
}
}
fn handle_304_not_modified_response(
task: &DownloadTask,
) -> Result<()> {
log::debug!("Received 304 Not Modified - file unchanged on server");
task.set_message(format!("File unchanged, checking local copy - {}", task.final_path.display()));
send_file_to_channel(task)
.map_err(|e| eyre!("Failed to send cached file to channel: {}", e))?;
Err(DownloadError::AlreadyComplete.into())
}
pub(crate) fn process_download_response(
task: &DownloadTask,
response: &http::Response<ureq::Body>,
resolved_url: &str,
existing_bytes: u64
) -> Result<()> {
let metadata = match handle_304_and_extract_metadata(task, response, resolved_url)? {
None => return Ok(()),
Some(m) => m,
};
validate_range_request_response(task, response, resolved_url)?;
validate_response_content_type(response, resolved_url, task)?;
validate_metadata_consistency(task, &metadata, resolved_url)?;
setup_task_progress_tracking(task, response, existing_bytes)?;
Ok(())
}
fn handle_304_and_extract_metadata(
task: &DownloadTask,
response: &http::Response<ureq::Body>,
resolved_url: &str,
) -> Result<Option<ServerMetadata>> {
let metadata = extract_server_metadata(task, response, resolved_url);
log::debug!("process_download_response for {} chunk: {}, metadata: remote_size={:?}, etag={:?}, last_modified={:?}, response: {:?}",
resolved_url, task.chunk_path.display(), metadata.remote_size, metadata.etag, metadata.last_modified, response);
if task.is_master_task() {
if response.status() == 304 {
handle_304_not_modified_response(task)?;
return Ok(None);
}
if matches!(task.mutability, Mutability::Mutable) {
let decision = should_redownload(task, &metadata)?;
if matches!(decision, CacheDecision::UseCache { .. }) {
handle_304_not_modified_response(task)?;
return Ok(None);
}
}
}
Ok(Some(metadata))
}