use attohttpc::Session;
use chrono::{DateTime, Utc};
use log::{info, warn};
use crate::error::{KopiError, Result};
use crate::indicator::ProgressIndicator;
use crate::metadata::index::{IndexFile, IndexFileEntry};
use crate::metadata::source::{MetadataSource, PackageDetails};
use crate::models::metadata::JdkMetadata;
use crate::platform::{get_current_architecture, get_current_os, get_foojay_libc_type};
use crate::user_agent;
pub struct HttpMetadataSource {
base_url: String,
client: Session,
}
impl HttpMetadataSource {
pub fn new(base_url: String) -> Self {
let mut client = Session::new();
client.header("User-Agent", user_agent::metadata_client());
Self {
base_url: base_url.trim_end_matches('/').to_string(),
client,
}
}
pub(crate) fn fetch_index(&self) -> Result<IndexFile> {
let url = format!("{}/index.json", self.base_url);
let response = self
.client
.get(&url)
.send()
.map_err(|e| KopiError::MetadataFetch(format!("Failed to fetch index: {e}")))?;
if !response.is_success() {
return Err(KopiError::MetadataFetch(format!(
"Failed to fetch index: HTTP {}",
response.status()
)));
}
let index: IndexFile = response
.json()
.map_err(|e| KopiError::MetadataFetch(format!("Failed to parse index: {e}")))?;
Ok(index)
}
fn filter_files_for_platform(&self, files: Vec<IndexFileEntry>) -> Vec<IndexFileEntry> {
let current_arch = get_current_architecture();
let current_os = get_current_os();
let current_libc = get_foojay_libc_type();
files
.into_iter()
.filter(|entry| {
if let Some(ref archs) = entry.architectures
&& !archs.contains(¤t_arch)
{
return false;
}
if let Some(ref oses) = entry.operating_systems
&& !oses.contains(¤t_os)
{
return false;
}
if current_os == "linux"
&& let Some(ref lib_c_types) = entry.lib_c_types
&& !lib_c_types.contains(¤t_libc.to_string())
{
return false;
}
true
})
.collect()
}
fn fetch_metadata_file(&self, path: &str) -> Result<Vec<JdkMetadata>> {
let url = format!("{}/{}", self.base_url, path);
let response = self
.client
.get(&url)
.send()
.map_err(|e| KopiError::MetadataFetch(format!("Failed to fetch {path}: {e}")))?;
if !response.is_success() {
return Err(KopiError::MetadataFetch(format!(
"Failed to fetch {}: HTTP {}",
path,
response.status()
)));
}
let metadata: Vec<JdkMetadata> = response
.json()
.map_err(|e| KopiError::MetadataFetch(format!("Failed to parse {path}: {e}")))?;
Ok(metadata)
}
}
impl MetadataSource for HttpMetadataSource {
fn id(&self) -> &str {
"http"
}
fn name(&self) -> &str {
"HTTP/Web"
}
fn is_available(&self) -> Result<bool> {
match self.fetch_index() {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
fn fetch_all(&self, progress: &mut dyn ProgressIndicator) -> Result<Vec<JdkMetadata>> {
let mut all_metadata = Vec::new();
progress.set_message("Fetching metadata index from HTTP source...".to_string());
let index = self.fetch_index()?;
let platform_files = self.filter_files_for_platform(index.files);
info!(
"Filtered to {} files for current platform (arch: {}, os: {}, libc: {})",
platform_files.len(),
get_current_architecture(),
get_current_os(),
get_foojay_libc_type()
);
let mut child = progress.create_child();
let config = crate::indicator::ProgressConfig::new(crate::indicator::ProgressStyle::Count)
.with_total(platform_files.len() as u64);
child.start(config);
for (idx, entry) in platform_files.iter().enumerate() {
child.update(idx as u64, Some(platform_files.len() as u64));
child.set_message(format!("Fetching {}: {}", idx + 1, entry.path));
match self.fetch_metadata_file(&entry.path) {
Ok(metadata) => {
all_metadata.extend(metadata);
}
Err(e) => warn!("Failed to fetch {}: {}", entry.path, e),
}
}
child.complete(Some(format!(
"Loaded {} packages from HTTP source",
all_metadata.len()
)));
progress.set_message(format!(
"Loaded {} packages from HTTP source",
all_metadata.len()
));
Ok(all_metadata)
}
fn fetch_distribution(
&self,
distribution: &str,
progress: &mut dyn ProgressIndicator,
) -> Result<Vec<JdkMetadata>> {
let mut metadata = Vec::new();
progress.set_message(format!(
"Fetching metadata index for distribution '{distribution}' from HTTP source..."
));
let index = self.fetch_index()?;
let filtered_files: Vec<IndexFileEntry> = self
.filter_files_for_platform(index.files)
.into_iter()
.filter(|entry| entry.distribution == distribution)
.collect();
let mut child = progress.create_child();
let config = crate::indicator::ProgressConfig::new(crate::indicator::ProgressStyle::Count)
.with_total(filtered_files.len() as u64);
child.start(config);
for (idx, entry) in filtered_files.iter().enumerate() {
child.update(idx as u64, Some(filtered_files.len() as u64));
child.set_message(format!("Fetching {}: {}", idx + 1, entry.path));
match self.fetch_metadata_file(&entry.path) {
Ok(pkg_metadata) => {
metadata.extend(pkg_metadata);
}
Err(e) => warn!("Failed to fetch {}: {}", entry.path, e),
}
}
child.complete(Some(format!(
"Loaded {} packages for '{distribution}' from HTTP source",
metadata.len()
)));
progress.set_message(format!(
"Loaded {} packages for distribution '{distribution}' from HTTP source",
metadata.len()
));
Ok(metadata)
}
fn fetch_package_details(
&self,
_package_id: &str,
progress: &mut dyn ProgressIndicator,
) -> Result<PackageDetails> {
progress.set_message("HTTP source provides complete metadata".to_string());
Err(KopiError::MetadataFetch(
"HTTP source provides complete metadata".to_string(),
))
}
fn last_updated(&self) -> Result<Option<DateTime<Utc>>> {
let index = self.fetch_index()?;
let updated = DateTime::parse_from_rfc3339(&index.updated)
.map(|dt| dt.with_timezone(&Utc))
.ok();
Ok(updated)
}
}
#[cfg(test)]
#[path = "http_tests.rs"]
mod tests;