summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock78
-rw-r--r--Cargo.toml4
-rw-r--r--examples/INSTALL2
-rw-r--r--src/cfg/config.rs4
-rw-r--r--src/main.rs2
-rw-r--r--src/net/http_packages.rs269
-rw-r--r--src/net/i2p_package.rs310
-rw-r--r--src/pkgtoolkit/pkgtools.rs38
8 files changed, 545 insertions, 162 deletions
diff --git a/Cargo.lock b/Cargo.lock
index fe33c9c..c9d144f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -255,6 +255,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
+name = "console"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "unicode-width",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -509,6 +522,12 @@ dependencies = [
]
[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1062,6 +1081,19 @@ dependencies = [
]
[[package]]
+name = "indicatif"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88"
+dependencies = [
+ "console",
+ "portable-atomic",
+ "unicode-width",
+ "unit-prefix",
+ "web-time",
+]
+
+[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1172,6 +1204,8 @@ dependencies = [
"clap",
"emissary-core",
"flate2",
+ "futures-util",
+ "indicatif",
"log",
"reqwest",
"serde",
@@ -1434,6 +1468,12 @@ dependencies = [
]
[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
name = "potential_utf"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1533,6 +1573,7 @@ dependencies = [
"bytes",
"encoding_rs",
"futures-core",
+ "futures-util",
"h2",
"http",
"http-body",
@@ -1554,12 +1595,14 @@ dependencies = [
"sync_wrapper",
"tokio",
"tokio-native-tls",
+ "tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
+ "wasm-streams",
"web-sys",
]
@@ -2240,6 +2283,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "unit-prefix"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
+
+[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2374,6 +2429,19 @@ dependencies = [
]
[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
name = "web-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2384,6 +2452,16 @@ dependencies = [
]
[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 01c143b..86f510e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,8 @@ tar = "0.4.44"
emissary-core = "0.2.0"
yosemite = "0.6.1"
cc = "1.2.48"
-reqwest = "0.12.24"
+reqwest = { version = "0.12.24", features = ["stream"] }
url = "2.5.7"
+indicatif = "0.18.3"
+futures-util = "0.3.31"
diff --git a/examples/INSTALL b/examples/INSTALL
index 6296c67..76f08a0 100644
--- a/examples/INSTALL
+++ b/examples/INSTALL
@@ -2,6 +2,8 @@
name = "my-package"
version = "1.0.0"
arch = "X86_64"
+descr = "Just example INSTALL script"
+license = "BSD-2-Clause"
[install]
path = "/usr/bin/my-package"
diff --git a/src/cfg/config.rs b/src/cfg/config.rs
index cf6188e..88c98ff 100644
--- a/src/cfg/config.rs
+++ b/src/cfg/config.rs
@@ -37,6 +37,8 @@ pub struct Repo {
pub auto_update: bool,
#[serde(rename = "destination")]
pub destination: (String, String),
+ #[serde(rename = "repo_http_url")]
+ pub repo_http_url: Option<String>,
// #[serde(rename = "arch")]
// pub arch = arch;
}
@@ -68,6 +70,7 @@ impl Config {
repo_url: format!("https://mesk.anthrill.i2p/repo/{}/", std::env::consts::ARCH),
auto_update: true,
destination: (String::from("mesk"), String::from("mesk")),
+ repo_http_url: None,
// Its a hurt place, you need to generate destinations by i2pd and paste here (to mesk.toml)
// Better to leave it empty or set it to (mesk, mesk), now destination conn not implemented
},
@@ -99,6 +102,7 @@ impl Config {
},
auto_update: true,
destination: (String::from("mesk"), String::from("mesk")),
+ repo_http_url: None,
},
log: Log {
log_file: String::from("/var/log/mesk.log"),
diff --git a/src/main.rs b/src/main.rs
index e295722..8677fdb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -135,7 +135,7 @@ async fn main() -> Result<(), std::io::Error> {
let config = Config::parse().unwrap();
println!("Updating index from {}", config.repo.repo_url);
let mut i2pd = I2PPackage::new(config);
- let _index = I2PPackage::fetch_index(&mut i2pd).await?;
+ let _index = I2PPackage::fetch_index(&mut i2pd).await;
println!("Index updated");
return Ok(());
}
diff --git a/src/net/http_packages.rs b/src/net/http_packages.rs
index fe1833d..84b4444 100644
--- a/src/net/http_packages.rs
+++ b/src/net/http_packages.rs
@@ -1,53 +1,44 @@
use crate::cfg::config::Config;
+use crate::pkgtoolkit::pkgtools::Package;
+use indicatif::{ProgressBar, ProgressStyle};
use reqwest;
-use std::fs::File;
-use std::io::Write;
+use serde::Deserialize;
+use std::collections::HashMap;
use std::path::Path;
+use tokio::fs::File;
+use tokio::io::AsyncWriteExt;
+use futures_util::stream::TryStreamExt;
-#[allow(dead_code)]
pub struct HTTPPackage {
config: Config,
+ index_packages: Option<HashMap<String, Package>>,
+}
+
+#[derive(Deserialize, Debug)]
+struct IndexData {
+ packages: Vec<Package>,
}
-#[allow(dead_code)]
impl HTTPPackage {
- /// Creates a new Downloader object with the given configuration.
- ///
- /// # Arguments
- ///
- /// * `config` - The full mesk
+ /// Creates a new HTTPPackage object with the given configuration.
///
/// # Returns
///
- /// A new Downloader object with the given configuration.
+ /// A new HTTPPackage object with the given configuration.
pub fn new(config: Config) -> Self {
- Self { config }
- }
-
- /// Parse the mesk configuration file and return the Config object.
- ///
- /// This function reads the file at `config_path`, parses it and returns the Config object.
- ///
- /// # Arguments
- ///
- /// * `config_path` - A string representing the path to the mesk configuration file.
- ///
- /// # Returns
- ///
- /// A new Config object with the parsed configuration. If there's an error while reading or parsing the file, returns an Err containing a Box<dyn std::error::Error>.
- pub fn parse_config() -> Result<Config, Box<dyn std::error::Error>> {
- let config = Config::parse()?;
- Ok(config)
+ HTTPPackage {
+ config,
+ index_packages: None,
+ }
}
/// Downloads the INDEX.tar.gz file from the configured repository
- /// and stores it in the configured cache directory.
+ /// 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<bool, std::io::Error> {
+ pub async fn fetch_index_http(&mut self) -> Result<bool, Box<dyn std::error::Error>> {
let repo_url_str = &self.config.repo.repo_url;
let cache_dir = &self.config.paths.cache_dir;
@@ -59,49 +50,193 @@ impl HTTPPackage {
let client = reqwest::Client::new();
- let response = client.get(&index_url).send().await.map_err(|e| {
- std::io::Error::new(std::io::ErrorKind::Other, format!("Request failed: {}", e))
- })?;
+ // 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(std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("HTTP Error: {}", response.status()),
- ));
+ return Err(format!("HTTP Error: {}", response.status()).into());
}
- let index_data = response
- .bytes()
- .await
- .map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("Failed to read response body: {}", e),
- )
- })?
- .to_vec();
-
+ let mut stream = response.bytes_stream();
let file_path = Path::new(cache_dir).join("INDEX.tar.gz");
- let mut file = File::create(&file_path).map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("Failed to create file: {}", e),
- )
- })?;
- file.write_all(&index_data).map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("Failed to write file: {}", e),
- )
- })?;
- file.flush().map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("Failed to flush file: {}", e),
- )
- })?;
+ 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<dyn std::error::Error>> {
+ // 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);
+
+ // Create progress bar
+ 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<bool, Box<dyn std::error::Error>> {
+ 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<dyn std::error::Error>> {
+ 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)
+ }
+} \ No newline at end of file
diff --git a/src/net/i2p_package.rs b/src/net/i2p_package.rs
index d149454..970f719 100644
--- a/src/net/i2p_package.rs
+++ b/src/net/i2p_package.rs
@@ -1,6 +1,8 @@
use crate::cfg::config::Config;
-
+use crate::pkgtoolkit::pkgtools::Package;
+use serde::Deserialize;
use tokio;
+
/*
use emissary_core::runtime::{
AsyncRead,
@@ -8,10 +10,9 @@ use emissary_core::runtime::{
};
*/
-use std::{fs::File, io::Write, path::Path};
-// use emissary_core::Profile;
-// use emissary_core::i2np::Message;
+use std::{collections::HashMap, path::Path};
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
+use indicatif::{ProgressBar, ProgressStyle};
use url;
use yosemite::Session;
@@ -19,6 +20,12 @@ use yosemite::SessionOptions;
pub struct I2PPackage {
config: Config,
+ index_packages: Option<HashMap<String, Package>>,
+}
+
+#[derive(Deserialize, Debug)]
+struct IndexData {
+ packages: Vec<Package>,
}
impl I2PPackage {
@@ -28,7 +35,10 @@ impl I2PPackage {
///
/// A new I2P object with the given configuration. The session is initially set to None and the connected status is set to false.
pub fn new(config: Config) -> Self {
- I2PPackage { config: config }
+ I2PPackage {
+ config: config,
+ index_packages: None
+ }
}
/// Downloads the INDEX.tar.gz file from the configured repository
@@ -37,17 +47,12 @@ impl I2PPackage {
/// # 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(&mut self) -> Result<bool, std::io::Error> {
+ pub async fn fetch_index(&mut self) -> Result<bool, Box<dyn std::error::Error>> {
let repo_url_str = &self.config.repo.repo_url;
let cache_dir = &self.config.paths.cache_dir;
- let url = url::Url::parse(repo_url_str).map_err(|_| {
- std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid repo URL")
- })?;
-
- let host = url.host_str().ok_or_else(|| {
- std::io::Error::new(std::io::ErrorKind::InvalidInput, "No host in URL")
- })?;
+ let url = url::Url::parse(repo_url_str)?;
+ let host = url.host_str().ok_or("No host in URL")?;
let request_path = url.path();
let request_path = if request_path.ends_with(".tar.gz") {
@@ -57,91 +62,240 @@ impl I2PPackage {
};
let session_options = SessionOptions::default();
- let mut session = Session::new(session_options).await.map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("Failed to create SAM session: {}", e),
- )
- })?;
-
- let mut stream = session.connect(host).await.map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::ConnectionAborted,
- format!("Failed to connect: {}", e),
- )
- })?;
+ let mut session = Session::new(session_options).await?;
+ let mut stream = session.connect(host).await?;
let request = format!(
"GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
request_path, host
);
-
- stream.write_all(request.as_bytes()).await.map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("Failed to write request: {}", e),
- )
- })?;
+ stream.write_all(request.as_bytes()).await?;
let mut reader = BufReader::new(stream);
let mut response_buffer = Vec::new();
- reader
- .read_to_end(&mut response_buffer)
- .await
- .map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("Failed to read response: {}", e),
- )
- })?;
+ reader.read_to_end(&mut response_buffer).await?;
let headers_end = response_buffer
.windows(4)
.position(|window| window == b"\r\n\r\n")
- .ok_or_else(|| {
- std::io::Error::new(
- std::io::ErrorKind::InvalidData,
- "Invalid response: no headers end",
- )
- })?;
-
- let headers_str = std::str::from_utf8(&response_buffer[..headers_end]).map_err(|_| {
- std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid header encoding")
- })?;
+ .ok_or("Invalid response: no headers end")?;
+ let headers_str = std::str::from_utf8(&response_buffer[..headers_end])?;
if !headers_str.starts_with("HTTP/1.1 200") && !headers_str.starts_with("HTTP/1.0 200") {
- return Err(std::io::Error::new(
- std::io::ErrorKind::Other,
- format!(
- "HTTP Error: {}",
- headers_str.lines().next().unwrap_or("Unknown")
- ),
- ));
+ return Err(format!(
+ "HTTP Error: {}",
+ headers_str.lines().next().unwrap_or("Unknown")
+ ).into());
}
+ let content_length = headers_str
+ .lines()
+ .find(|line| line.to_lowercase().starts_with("content-length:"))
+ .and_then(|line| line.split_at(15).1.trim().parse::<u64>().ok())
+ .unwrap_or(0);
+
let body_start = headers_end + 4;
+ let mut body_reader = std::io::Cursor::new(&response_buffer[body_start..]);
let file_path = Path::new(cache_dir).join("INDEX.tar.gz");
- let mut file = File::create(&file_path).map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("Failed to create file: {}", e),
- )
- })?;
- file.write_all(&response_buffer[body_start..])
- .map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("Failed to write file: {}", e),
- )
- })?;
- file.flush().map_err(|e| {
- std::io::Error::new(
- std::io::ErrorKind::Other,
- format!("Failed to flush file: {}", e),
- )
- })?;
+ 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
+ };
+
+ let mut file = tokio::fs::File::create(&file_path).await?;
+
+ let chunk_size = 8192u64;
+ let mut buffer = vec![0; chunk_size as usize];
+
+ loop {
+
+ let bytes_read_result = std::io::Read::read(&mut body_reader, &mut buffer);
+ let bytes_read = match bytes_read_result {
+ Ok(n) => n,
+ Err(e) => return Err(e.into()),
+ };
+
+ if bytes_read == 0 {
+ break;
+ }
+ file.write_all(&buffer[..bytes_read]).await?;
+
+ pb.inc(bytes_read as u64);
+
+ if bytes_read < chunk_size as usize {
+ break;
+ }
+ }
+
+ 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"); // Предполагаем, что 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()); // Или None, если это ошибка
+ return Ok(true);
+ }
+
+ let index_content = std::fs::read_to_string(&index_toml_path)?;
+ 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
+ /// * `data' - Byte slice (&[u8]) with data to write.
+ /// * `file_path' is the path to the file to write the data to.
+ /// * `content_length' is the expected data size (for the progress bar). Maybe 0.
+ /// * `description' - Description of the operation for the progress bar.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if there is a problem when creating or writing to a file.
+ async fn download_and_write_file_with_progress(
+ data: &[u8],
+ file_path: &Path,
+ content_length: u64,
+ description: &str,
+ ) -> Result<(), Box<dyn std::error::Error>> {
+ // Прогресс-бар для загрузки
+ 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
+ };
+
+ let mut file = tokio::fs::File::create(&file_path).await?;
+
+ let chunk_size = 8192usize;
+ let mut pos = 0;
+
+ while pos < data.len() {
+ let end = std::cmp::min(pos + chunk_size, data.len());
+ let chunk = &data[pos..end];
+ file.write_all(chunk).await?;
+ pb.inc(chunk.len() as u64);
+ pos = end;
+ }
+
+ pb.finish_with_message(format!("{} download finished", description));
+ Ok(())
+ }
+
+ /// Fetches a specific package identified by `index` (likely the package name).
+ /// Assumes `fetch_index` has been called and `self.index_packages` is populated.
+ pub fn fetch_package_info(&self, package_name: &str) -> Result<&Package, Box<dyn std::error::Error>> {
+ let packages = self.index_packages.as_ref()
+ .ok_or("Index not loaded. Call fetch_index first.")?;
+ let pkg_info = packages.get(package_name)
+ .ok_or(format!("Package '{}' not found in index.", package_name))?;
+ Ok(pkg_info)
+ }
+
+ /// Fetches a specific package identified by `package_name`.
+ /// Assumes `fetch_index` has been called and `self.index_packages` is populated to get the URL.
+ /// Downloads the package file (.mesk) to the cache directory.
+ ///
+ /// # 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.
+ /// Why didn't I just abstract the download functionality into a separate function initially?
+ /// Yes, I'm scared to work with fetch_index, even I don't often write such shit code.
+ pub async fn fetch_package(&self, package_name: &str) -> Result<bool, Box<dyn std::error::Error>> {
+ let package_info = self.fetch_package_info(package_name)?;
+ let url = url::Url::parse(&package_info.url)?;
+ let host = url.host_str().ok_or("No host in package URL")?;
+ let request_path = url.path();
+
+ let session_options = SessionOptions::default();
+ let mut session = Session::new(session_options).await?;
+ let mut stream = session.connect(host).await?;
+
+ let request = format!(
+ "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
+ request_path, host
+ );
+ stream.write_all(request.as_bytes()).await?;
+
+ let mut reader = BufReader::new(stream);
+ let mut response_buffer = Vec::new();
+ reader.read_to_end(&mut response_buffer).await?;
+
+ let headers_end = response_buffer
+ .windows(4)
+ .position(|window| window == b"\r\n\r\n")
+ .ok_or("Invalid response: no headers end")?;
+
+ let headers_str = std::str::from_utf8(&response_buffer[..headers_end])?;
+ if !headers_str.starts_with("HTTP/1.1 200") && !headers_str.starts_with("HTTP/1.0 200") {
+ return Err(format!(
+ "HTTP Error: {}",
+ headers_str.lines().next().unwrap_or("Unknown")
+ ).into());
+ }
+
+ let content_length = headers_str
+ .lines()
+ .find(|line| line.to_lowercase().starts_with("content-length:"))
+ .and_then(|line| line.split_at(15).1.trim().parse::<u64>().ok())
+ .unwrap_or(0);
+
+ let body_start = headers_end + 4;
+ let body_bytes = &response_buffer[body_start..];
+
+ let file_name = Path::new(request_path)
+ .file_name()
+ .ok_or("Could not determine filename from URL path")?
+ .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_and_write_file_with_progress(body_bytes, &file_path, content_length, file_name).await?;
+
+ log::info!("Package '{}' downloaded successfully to {:?}", package_name, file_path);
Ok(true)
}
}
+
diff --git a/src/pkgtoolkit/pkgtools.rs b/src/pkgtoolkit/pkgtools.rs
index 6c37595..8198624 100644
--- a/src/pkgtoolkit/pkgtools.rs
+++ b/src/pkgtoolkit/pkgtools.rs
@@ -8,6 +8,7 @@ use std::{
path::Path,
process::Command,
str,
+ collections::HashMap
};
// use emissary_core::i2np::tunnel::build;
@@ -28,16 +29,16 @@ pub enum Archs {
#[derive(Serialize, Debug, Deserialize, Clone)]
pub struct Package {
- name: String,
- version: String,
- arch: Archs,
- descr: Option<String>,
+ pub name: String,
+ pub version: String,
+ pub arch: Archs,
+ pub descr: Option<String>,
+ pub license: Option<String>,
+ pub url: String,
}
-#[allow(dead_code)]
#[derive(Deserialize, Debug, Clone)]
-struct Install {
- package: Package,
+pub struct Install_meta {
path: String,
user: String,
group: String,
@@ -47,6 +48,13 @@ struct Install {
}
#[allow(dead_code)]
+#[derive(Deserialize, Debug, Clone)]
+struct Install {
+ package: Package,
+ install: Install_meta,
+}
+
+#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct Setts {
env: Option<String>, // Export environment variables if this needed
@@ -162,7 +170,7 @@ impl Package {
/// # Returns
/// * `Ok(())` if the archive is successfully unpacked.
/// * `Err(std::io::Error)` if there's an issue opening, reading, or unpacking the archive.
- fn extract_archive(path_to_archive: &str) -> Result<(), std::io::Error> {
+ pub fn extract_archive(path_to_archive: &str) -> Result<(), std::io::Error> {
let config = Config::parse().unwrap();
create_dir_all(&config.paths.cache_dir)?;
@@ -260,7 +268,7 @@ impl Package {
}
// Log if custom script is present
- if let Some(ref _script) = install_meta.custom_script {
+ if let Some(ref _script) = install_meta.install.custom_script {
println!(
"Custom script found for package: {}",
install_meta.package.name
@@ -396,11 +404,11 @@ impl Package {
match strategy {
Strategies::BIN => {
- if install_meta.custom_script.is_none() {
+ if install_meta.install.custom_script.is_none() {
log::info!("Strategy: BIN; No custom script. Running default install hook.");
} else {
log::info!("Strategy: BIN; Running custom script.");
- let script = install_meta.custom_script.as_ref().unwrap();
+ let script = install_meta.install.custom_script.as_ref().unwrap();
if !script.starts_with("./") {
let _output = std::process::Command::new(format!("{}", script));
} else {
@@ -471,7 +479,7 @@ impl Package {
"No BUILD file or it's empty. Treating as binary package. Installing via INSTALL config or custom script."
);
// Установка бинарного пакета
- if let Some(ref script) = install_meta.custom_script {
+ if let Some(ref script) = install_meta.install.custom_script {
log::info!(
"Executing custom install script for {}",
install_meta.package.name
@@ -505,7 +513,7 @@ impl Package {
let build_dir = Path::new(&config.paths.cache_dir)
.join(format!("{}-{}", self.name, self.version));
let src_path = build_dir.join(source_file_name);
- let dest_path = Path::new(&install_meta.path);
+ let dest_path = Path::new(&install_meta.install.path);
// Убедимся, что целевая директория существует
if let Some(parent) = dest_path.parent() {
@@ -524,7 +532,7 @@ impl Package {
)
})?;
- let mode = u32::from_str_radix(&install_meta.mode, 8).map_err(|_| {
+ let mode = u32::from_str_radix(&install_meta.install.mode, 8).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid mode string in INSTALL",
@@ -539,7 +547,7 @@ impl Package {
})?;
let output = Command::new("chown")
- .arg(&format!("{}:{}", install_meta.user, install_meta.group))
+ .arg(&format!("{}:{}", install_meta.install.user, install_meta.install.group))
.arg(dest_path)
.output()
.map_err(|e| {