use crate::cfg::config::Config; use crate::pkgtoolkit::pkgtools::Package; use futures_util::stream::TryStreamExt; use indicatif::{ProgressBar, ProgressStyle}; use reqwest; use serde::Deserialize; use std::collections::HashMap; use std::path::Path; use tokio::fs::File; use tokio::io::AsyncWriteExt; pub struct HTTPPackage { config: Config, index_packages: Option>, } #[derive(Deserialize, Debug)] struct IndexData { packages: Vec, } impl HTTPPackage { /// Creates a new HTTPPackage object with the given configuration. /// /// # Returns /// /// A new HTTPPackage object with the given configuration. pub fn new(config: Config) -> Self { HTTPPackage { config, index_packages: None, } } /// Downloads the INDEX.tar.gz file from the configured repository /// and stores it in the configured cache directory with a progress bar. /// /// # Errors /// /// Returns an error if the request fails, if the response status is not successful, or if there's an issue while reading or writing the file. pub async fn fetch_index_http(&mut self) -> Result> { let repo_url_str = &self.config.repo.repo_url; let cache_dir = &self.config.paths.cache_dir; let index_url = if repo_url_str.ends_with(".tar.gz") { repo_url_str.clone() } else { format!("{}/INDEX.tar.gz", repo_url_str.trim_end_matches('/')) }; let client = reqwest::Client::new(); // Make a HEAD request to get the content length for the progress bar let head_response = client.head(&index_url).send().await?; let content_length: u64 = head_response .headers() .get(reqwest::header::CONTENT_LENGTH) .and_then(|ct_len| ct_len.to_str().ok()) .and_then(|ct_len| ct_len.parse().ok()) .unwrap_or(0); // Create progress bar let pb = if content_length > 0 { let pb = ProgressBar::new(content_length); pb.set_style( ProgressStyle::default_bar() .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")? .progress_chars("#>-"), ); pb } else { let pb = ProgressBar::new_spinner(); pb.set_style( ProgressStyle::default_spinner() .template("{spinner:.green} [{elapsed_precise}] Fetching INDEX.tar.gz...")?, ); pb }; // Send GET request and stream the response body let response = client.get(&index_url).send().await?; if !response.status().is_success() { return Err(format!("HTTP Error: {}", response.status()).into()); } let mut stream = response.bytes_stream(); let file_path = Path::new(cache_dir).join("INDEX.tar.gz"); let mut file = File::create(&file_path).await?; let mut downloaded: u64 = 0; while let Some(chunk) = stream.try_next().await? { file.write_all(&chunk).await?; let chunk_len = chunk.len() as u64; downloaded += chunk_len; pb.set_position(downloaded); } pb.finish_with_message("INDEX.tar.gz download finished"); // --- Извлечение и парсинг INDEX.toml --- log::info!("Extracting INDEX.tar.gz to cache directory..."); Package::extract_archive(&file_path.to_string_lossy())?; // Используем существующую функцию из pkgtoolkit let index_toml_path = Path::new(cache_dir).join("INDEX.toml"); if !index_toml_path.exists() { log::warn!("INDEX.toml not found in INDEX.tar.gz. Proceeding without index data."); self.index_packages = Some(HashMap::new()); return Ok(true); } let index_content = tokio::fs::read_to_string(&index_toml_path).await?; let index_data: IndexData = toml::from_str(&index_content)?; let mut package_map = HashMap::new(); for pkg in index_data.packages { // PKG_URL = /repo/package.mesk // FULL URL = "http://mesk.anthrill.i2p/i2p/repo/pkg.mesk" let base_url = url::Url::parse(&self.config.repo.repo_url)?; let full_url = base_url.join(&pkg.url)?; let mut pkg_clone = pkg.clone(); pkg_clone.url = full_url.to_string(); package_map.insert(pkg_clone.name.clone(), pkg_clone); } self.index_packages = Some(package_map.clone()); log::info!( "Index loaded successfully, {} packages found.", package_map.len() ); Ok(true) } /// An internal auxiliary function for downloading data and writing it to a file with a progress display. /// /// # Arguments /// * `url` - The URL to download from. /// * `file_path` - The path to the file to write the data to. /// * `description` - Description of the operation for the progress bar. /// /// # Errors /// /// Returns an error if the request fails, if the response status is not successful, or if there's an issue while writing the file. async fn download_file_with_progress( client: &reqwest::Client, url: &str, file_path: &Path, description: &str, ) -> Result<(), Box> { // Make a HEAD request to get the content length for the progress bar let head_response = client.head(url).send().await?; let content_length: u64 = head_response .headers() .get(reqwest::header::CONTENT_LENGTH) .and_then(|ct_len| ct_len.to_str().ok()) .and_then(|ct_len| ct_len.parse().ok()) .unwrap_or(0); let pb = if content_length > 0 { let pb = ProgressBar::new(content_length); pb.set_style( ProgressStyle::default_bar() .template(&format!( "{{spinner:.green}} [{{elapsed_precise}}] [{{bar:40.cyan/blue}}] {{bytes}}/{{total_bytes}} ({} {{eta}})", description ))? .progress_chars("#>-"), ); pb } else { let pb = ProgressBar::new_spinner(); pb.set_style(ProgressStyle::default_spinner().template(&format!( "{{spinner:.green}} [{{elapsed_precise}}] {}...", description ))?); pb }; // Send GET request and stream the response body let response = client.get(url).send().await?; if !response.status().is_success() { return Err(format!("HTTP Error: {}", response.status()).into()); } let mut stream = response.bytes_stream(); let mut file = File::create(&file_path).await?; let mut downloaded: u64 = 0; while let Some(chunk) = stream.try_next().await? { file.write_all(&chunk).await?; let chunk_len = chunk.len() as u64; downloaded += chunk_len; pb.set_position(downloaded); } pb.finish_with_message(format!("{} download finished", description)); Ok(()) } /// Fetches a specific package identified by `package_name`. /// Assumes `fetch_index_http` has been called and `self.index_packages` is populated to get the URL. /// Downloads the package file (.mesk) to the cache directory with a progress bar. /// /// # Errors /// /// Returns an error if the index is not loaded, the package is not found in the index, /// the package URL is invalid, the request fails, or if there's an issue writing the file. pub async fn fetch_package_http( &self, package_name: &str, ) -> Result> { let package_info = self.fetch_package_info(package_name)?; let url = &package_info.url; let client = reqwest::Client::new(); let file_name = Path::new(url) .file_name() .ok_or("Could not determine filename from URL")? .to_str() .ok_or("Filename is not valid UTF-8")?; let cache_dir = &self.config.paths.cache_dir; let file_path = Path::new(cache_dir).join(file_name); Self::download_file_with_progress(&client, url, &file_path, file_name).await?; log::info!( "Package '{}' downloaded successfully to {:?}", package_name, file_path ); Ok(true) } /// Fetches a specific package identified by `index` (likely the package name). /// Assumes `fetch_index_http` has been called and `self.index_packages` is populated. pub fn fetch_package_info( &self, package_name: &str, ) -> Result<&Package, Box> { let packages = self .index_packages .as_ref() .ok_or("Index not loaded. Call fetch_index_http first.")?; let pkg_info = packages .get(package_name) .ok_or(format!("Package '{}' not found in index.", package_name))?; Ok(pkg_info) } }