diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/i2impl/mi2p.rs | 162 | ||||
| -rw-r--r-- | src/i2impl/mod.rs | 1 | ||||
| -rw-r--r-- | src/main.rs | 37 | ||||
| -rw-r--r-- | src/net/http_packages.rs | 92 | ||||
| -rw-r--r-- | src/net/i2p_package.rs | 120 | ||||
| -rw-r--r-- | src/net/i2p_tools.rs (renamed from src/i2impl/i2tools.rs) | 0 | ||||
| -rw-r--r-- | src/net/mod.rs | 2 | ||||
| -rw-r--r-- | src/pkgtoolkit/pkgtools.rs | 205 |
8 files changed, 403 insertions, 216 deletions
diff --git a/src/i2impl/mi2p.rs b/src/i2impl/mi2p.rs deleted file mode 100644 index cf2b8e5..0000000 --- a/src/i2impl/mi2p.rs +++ /dev/null @@ -1,162 +0,0 @@ - -use crate::cfg::config::Config; - -use tokio; -use emissary_core::runtime::{ - AsyncRead, - AsyncWrite, -}; - -use std::{io, - fs::File, - path::Path, - io::Write}; -// use emissary_core::Profile; -// use emissary_core::i2np::Message; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use yosemite::SessionOptions; -use yosemite::{Session, style::Stream}; -/* -use i2p_client::ClientType; -use i2p_client::I2PClient; -use i2p_client::SessionStyle::Stream; -use i2p_client::Session; - -struct I2PStatus { - Connected: bool, -} - -impl I2PStatus { - pub fn connect(&self) -> Result<bool, std::io::Error> { - - let config: Config = Config::parse().unwrap(); - let client= I2PClient::new(true, "MeskPKG-manager".to_string(), "2.0", "2.58.0", 10); - // let destination = Session::r#gen(&mut self, SigType::EdDsaSha512Ed25519) - let session = Session::create(config.repo.repo_url, - &config.repo.destination.0, - "MeskPKG-manager", - Stream, - "2.0", - "2.58"); - - Ok(true) - } -} -*/ - -pub struct I2P<Stream> { - session: Option<Session<Stream>>, - connected: bool, - config: Config, - -} - -impl I2P<Stream> { - /// Creates a new I2P object with the given configuration. - /// - /// # Returns - /// - /// 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 { - I2P { - session: None, - connected: false, - config: config, - } - } - - /// Fetches the list of packages from the repository specified in the configuration. - /// - /// This function connects to the repository specified in the configuration, sends a GET request for the repository list and returns true if the request is successful, false otherwise. - /// - /// # Errors - /// - /// Returns an error if the repository URL is invalid or if the request fails. - /// - /// # Shortcomings - /// - Currently, &str is used instead of Path. - /// - An incomplete I2P structure, the session is stored in the structure but - /// is recreated when called, perhaps the connection/disconnection methods should be encapsulated, - /// although the function is not used often so it will not carry major changes - pub async fn fetch_index(&mut self) -> Result<bool, std::io::Error> { - let repo_url_str = &self.config.repo.repo_url; - let cache_dir = &self.config.paths.cache_dir; - - let repo_pos = repo_url_str.find("/repo").ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidData, "URL does not contain '/repo'") - })?; - // BaseURL - let base_url = &repo_url_str[..repo_pos]; - // Url after /repo - let path_suffix = &repo_url_str[repo_pos + "/repo".len()..]; - // HTTP path - let request_path = format!("/repo{}{}", path_suffix, if path_suffix.ends_with(".tar.gz") { "" } else { "/INDEX.tar.gz" }); - - let opts = SessionOptions::default(); - // FIXME: Make sure, opts setted to I2P - // opts.set_host("127.0.0.1"); - // opts.set_port(7656); - let mut session = Session::<Stream>::new(opts).await.map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("Failed to create session: {}", e)) - })?; - - let mut stream = session.connect(base_url).await.map_err(|e| { - io::Error::new(io::ErrorKind::ConnectionAborted, format!("Failed to connect: {}", e)) - })?; - - let request = format!("GET {} HTTP/1.1\r\nHost: {}\r\n\r\n", request_path, base_url); - stream.write_all(request.as_bytes()).await.map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("Failed to write request: {}", e)) - })?; - - let mut response_buffer = Vec::new(); - let mut chunk = [0u8; 1024]; - - loop { - // FIXME: Check docs and make suro stream allows to AsyncReadExt - let bytes_read = stream.read(&mut chunk).await.map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("Failed to read response: {}", e)) - })?; - - if bytes_read == 0 { - break; - } - - response_buffer.extend_from_slice(&chunk[..bytes_read]); - - if let Some(headers_end) = Self::find_double_crlf(&response_buffer) { - let body_start = headers_end + 4; - let headers_str = std::str::from_utf8(&response_buffer[..headers_end]) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid header encoding"))?; - - if !headers_str.starts_with("HTTP/1.1 200") && !headers_str.starts_with("HTTP/1.0 200") { - return Err(io::Error::new(io::ErrorKind::Other, format!("HTTP Error: {}", headers_str.lines().next().unwrap_or("Unknown")))); - } - - let file_path = Path::new(cache_dir).join("INDEX.tar.gz"); - let mut file = File::create(&file_path)?; - file.write_all(&response_buffer[body_start..])?; - while let Ok(bytes_read) = stream.read(&mut chunk).await { - if bytes_read == 0 { - break; - } - file.write_all(&chunk[..bytes_read])?; - } - - file.flush()?; - break; - } - } - - Ok(true) - } - - fn find_double_crlf(buf: &[u8]) -> Option<usize> { - for i in 0..buf.len().saturating_sub(3) { - if buf[i] == b'\r' && buf[i+1] == b'\n' && buf[i+2] == b'\r' && buf[i+3] == b'\n' { - return Some(i); - } - } - None - } -}
\ No newline at end of file diff --git a/src/i2impl/mod.rs b/src/i2impl/mod.rs deleted file mode 100644 index 4fa58ff..0000000 --- a/src/i2impl/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod mi2p;
\ No newline at end of file diff --git a/src/main.rs b/src/main.rs index c38d265..463bd24 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,13 @@ mod cfg; -mod i2impl; +mod net; mod pkgtoolkit; use crate::cfg::config::Config; #[allow(unused_imports)] use crate::pkgtoolkit::pkgtools::Package; -use crate::i2impl::mi2p::I2P; +use crate::net::i2p_package::I2PPackage; use clap::{Args, Command, Parser, Subcommand}; -use yosemite::Stream; use std::io::Write; use std::path::Path; use std::fs::create_dir_all; @@ -41,6 +40,9 @@ enum Commands { Install{ pkgname: String, source: Option<String>, + #[command(flatten)] + args: RemoteInstallArgs + }, #[command(about = "Uninstall package")] Uninstall{ @@ -62,12 +64,13 @@ enum Commands { #[derive(Args, Clone)] #[command(about = "Remote install arguments")] -struct RemoteInstallArgs { - verbose: bool, - debug: bool, +struct RemoteInstallArgs { + #[arg(short = 'b', long = "bin" )] bin: bool, - source: bool, - i2p: bool, + #[arg(short = 'h', long = "http" )] + http: bool, + #[arg(short = 'c', long = "clean" )] + clean: bool } #[tokio::main] @@ -85,7 +88,7 @@ async fn main() -> Result<(), std::io::Error> { println!("Building {}", pkgname); return Ok(()) }, - Commands::Install { pkgname, source} => { + Commands::Install { pkgname, source, args} => { println!("Installing {}", pkgname); return Ok(()) }, @@ -109,9 +112,9 @@ async fn main() -> Result<(), std::io::Error> { log::warn!("Writing the default config to /etc/mesk/mesk.toml"); let path = Path::new("/etc/mesk/mesk.toml"); - create_dir_all(path.parent().unwrap()).unwrap(); - let mut file = File::create(path).unwrap(); - file.write(config.as_bytes()).unwrap(); + create_dir_all(path.parent().unwrap())?; + let mut file = File::create(path)?; + file.write(config.as_bytes())?; println!("Config tool ending work."); } else { @@ -124,9 +127,9 @@ async fn main() -> Result<(), std::io::Error> { log::warn!("Writing the default config to /etc/mesk/mesk.toml"); let path = Path::new("/etc/mesk/mesk.toml"); - create_dir_all(path.parent().unwrap()).unwrap(); - let mut file = File::create(path).unwrap(); - file.write_all(config.as_bytes()).unwrap(); + create_dir_all(path.parent().unwrap())?; + let mut file = File::create(path)?; + file.write_all(config.as_bytes())?; println!("Config tool ending work."); } @@ -135,8 +138,8 @@ async fn main() -> Result<(), std::io::Error> { Commands::Update => { let config = Config::parse().unwrap(); println!("Updating index from {}", config.repo.repo_url); - let mut i2pd = I2P::new(config); - let _index= I2P::fetch_index(&mut i2pd).await?; + let mut i2pd = I2PPackage::new(config); + 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 new file mode 100644 index 0000000..1e3c6ad --- /dev/null +++ b/src/net/http_packages.rs @@ -0,0 +1,92 @@ +use reqwest; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use crate::cfg::config::Config; + +pub struct HTTPPackage { + config: Config, +} + +impl HTTPPackage { + /// Creates a new Downloader object with the given configuration. + /// + /// # Arguments + /// + /// * `config` - The full mesk + /// + /// # Returns + /// + /// A new Downloader 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(config_path: &str) -> Result<Config, Box<dyn std::error::Error>> { + let config = Config::parse()?; + Ok(config) + } + + + /// Downloads the INDEX.tar.gz file from the configured repository + /// and stores it in the configured cache directory. + /// + /// # 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> { + 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(); + + let response = client + .get(&index_url) + .send() + .await + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Request failed: {}", e)))?; + + if !response.status().is_success() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("HTTP Error: {}", response.status()), + )); + } + + 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 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)))?; + + Ok(true) + } +} + diff --git a/src/net/i2p_package.rs b/src/net/i2p_package.rs new file mode 100644 index 0000000..2776eea --- /dev/null +++ b/src/net/i2p_package.rs @@ -0,0 +1,120 @@ + +use crate::cfg::config::Config; + +use tokio; +/* +use emissary_core::runtime::{ + AsyncRead, + AsyncWrite, +}; +*/ + +use std::{fs::File, + path::Path, + io::Write}; +// use emissary_core::Profile; +// use emissary_core::i2np::Message; +use tokio::io::{AsyncReadExt, + AsyncWriteExt, + BufReader}; + +use yosemite::SessionOptions; +use yosemite::{Session, style::Stream}; +use url; + +pub struct I2PPackage { + config: Config, + +} + + +impl I2PPackage { + /// Creates a new I2P object with the given configuration. + /// + /// # Returns + /// + /// 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, + } + } + + + /// Downloads the INDEX.tar.gz file from the configured repository + /// and stores it in the configured cache directory. + /// + /// # 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> { + 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 request_path = url.path(); + let request_path = if request_path.ends_with(".tar.gz") { + request_path.to_string() + } else { + format!("{}/INDEX.tar.gz", request_path.trim_end_matches('/')) + }; + + let sam_host = "127.0.0.1"; + let sam_port = 7656; + 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 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)))?; + + 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)))?; + + + 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"))?; + + 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")), + )); + } + + let body_start = headers_end + 4; + 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)))?; + + Ok(true) + } +} diff --git a/src/i2impl/i2tools.rs b/src/net/i2p_tools.rs index e69de29..e69de29 100644 --- a/src/i2impl/i2tools.rs +++ b/src/net/i2p_tools.rs diff --git a/src/net/mod.rs b/src/net/mod.rs new file mode 100644 index 0000000..54f5e7f --- /dev/null +++ b/src/net/mod.rs @@ -0,0 +1,2 @@ +pub mod i2p_package; +pub mod http_packages;
\ No newline at end of file diff --git a/src/pkgtoolkit/pkgtools.rs b/src/pkgtoolkit/pkgtools.rs index 5acbd11..cfd3d59 100644 --- a/src/pkgtoolkit/pkgtools.rs +++ b/src/pkgtoolkit/pkgtools.rs @@ -6,8 +6,10 @@ use std::{ io, path::Path, process::Command, - str}; + str, + os::unix::fs::PermissionsExt}; + use emissary_core::i2np::tunnel::build; // use emissary_core::i2np::tunnel::build; use flate2::read::GzDecoder; use serde::{Deserialize, Serialize}; @@ -81,44 +83,74 @@ impl archs { #[allow(dead_code)] impl Package { - fn builder_backend(&mut self) -> Result<bool, std::io::Error> { - let config: Config = Config::parse().unwrap(); - let metadata = Self::loadmeta(self).unwrap(); - let path = Path::new(&config.paths.cache_dir).join(format!("{}-{}/BUILD", metadata.0.package.name, metadata.0.package.version)); - let _ = create_dir_all(&path); - - if metadata.2.is_none() { - Err(std::io::Error::new(std::io::ErrorKind::NotFound, "BUILD file not found"))? - } - - match metadata.2.unwrap().build_system { + /// Execute the build script for the package. + /// + /// This function takes the `Build` meta information as an argument and + /// executes the build script accordingly. It also handles the different + /// build systems supported (Make, CMake, Meson, Cargo). + /// + /// # Errors + /// + /// Returns an error if the build command fails. + fn execute_build(&self, build_meta: &Build) -> Result<(), std::io::Error> { + let config = Config::parse() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; + let build_dir = Path::new(&config.paths.cache_dir) + .join(format!("{}-{}", self.name, self.version)); + + let mut cmd = match build_meta.build_system { BuildSystems::Make => { - let _setup = Command::new("make") - .arg("all") - .arg("all") - .output(); + let mut c = Command::new("make"); + c.current_dir(&build_dir); + if build_meta.script.is_some() { + c.arg("-f").arg(build_meta.script.as_ref().unwrap()); + } + c.arg("all"); + c } - BuildSystems::CMake => { - let _setup = Command::new("cmake") - .arg("-S") - .arg(&path) - .arg("-B") - .arg(&path) - .output(); - - let _make = Command::new("make") - .arg("-C") - .arg(&path) - .output(); + let build_dir_build = build_dir.join("build"); + create_dir_all(&build_dir_build)?; + let mut c = Command::new("cmake"); + c.arg("-S").arg(&build_dir) + .arg("-B").arg(&build_dir_build) + .current_dir(&build_dir); + c } - - _ => { - Err(std::io::Error::new(std::io::ErrorKind::NotFound, "BUILD file not found"))? + BuildSystems::Meson => { + let build_dir_build = build_dir.join("build"); + create_dir_all(&build_dir_build)?; + let mut c = Command::new("meson"); + c.arg("setup") + .arg(&build_dir_build) + .current_dir(&build_dir); + c + } + BuildSystems::Cargo => { + let mut c = Command::new("cargo"); + c.arg("build") + .arg("--release") + .current_dir(&build_dir); + c } + }; + + let output = cmd.output().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Build command failed: {}", e), + ) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Build failed:\n{}", stderr), + )); } - Ok(true) + Ok(()) } /// Extracts a .tar.gz archive to the cache directory specified in Config. @@ -366,15 +398,116 @@ impl Package { } Strategies::SOURCE => { log::info!("Strategy: SOURCE; Running default build hook."); - todo!(); + let _ = self.execute_build(&build_meta.unwrap()); } } Ok(true) } - pub fn install() -> Result<bool, std::io::Error> { - todo!(); - } + + + /// Installs the package according to the INSTALL file in the archive. + /// + /// There are two strategies for installing the package. If the BUILD file is empty, the package is assumed to be a binary package and the default install hook is skipped. If the BUILD file is not empty, the package is assumed to be a source package and the default build hook is skipped. + /// + /// If the BUILD file is empty and the INSTALL file contains a custom script, the custom script is run instead of the default install hook. + /// If the BUILD file is not empty and the INSTALL file contains a custom script, the custom script is ignored. + /// + /// # Errors + /// + /// Returns an error if the BUILD file is invalid or if the build or install hook fails. + pub fn install(&mut self) -> Result<bool, std::io::Error> { + let config = Config::parse() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; + let (install_meta, _setts_meta, build_meta) = Self::loadmeta(self) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; + + + let is_build_present_and_not_empty = build_meta.is_some(); + + if is_build_present_and_not_empty { + log::info!("Found BUILD file, preparing to build and install package: {}", self.name); + let build_meta_ref = build_meta.as_ref().unwrap(); + self.execute_build(build_meta_ref); + + + if matches!(build_meta_ref.build_system, BuildSystems::Make) { + log::info!("Running 'make install' for package: {}", self.name); + let build_dir = Path::new(&config.paths.cache_dir) + .join(format!("{}-{}", self.name, self.version)); + let output = Command::new("make") + .arg("install") + .current_dir(&build_dir) + .output() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("'make install' failed: {}", e)))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("'make install' failed:\n{}", stderr), + )); + } + } + } else { + log::info!("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 { + log::info!("Executing custom install script for {}", install_meta.package.name); + let status = if script.starts_with("./") || script.contains('/') { + Command::new("/bin/sh").arg("-c").arg(script).status() + } else { + Command::new(script).status() + } + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to run custom script: {}", e)))?; + + if !status.success() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Custom install script failed", + )); + } + } else { + log::info!("No custom script. Running default install hook for {}", install_meta.package.name); + // --- Дефолтный хук установки --- + // 1. Копируем файл из build_dir (предположим, что бинарный файл лежит в корне распакованного архива) + let source_file_name = &self.name; // Предполагаем имя файла = имя пакета + 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); + + // Убедимся, что целевая директория существует + if let Some(parent) = dest_path.parent() { + create_dir_all(parent) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to create parent dir: {}", e)))?; + } + + fs::copy(&src_path, dest_path) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to copy file: {}", e)))?; + + let mode = u32::from_str_radix(&install_meta.mode, 8) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid mode string in INSTALL"))?; + let perms = PermissionsExt::from_mode(mode); + fs::set_permissions(dest_path, perms) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to set permissions: {}", e)))?; + + let output = Command::new("chown") + .arg(&format!("{}:{}", install_meta.user, install_meta.group)) + .arg(dest_path) + .output() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("'chown' command failed: {}", e)))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + log::warn!("Warning: 'chown' command failed (requires root?):\n{}", stderr); + } + } + } + + log::info!("Package {} installed successfully.", self.name); + Ok(true) + } pub fn gen_index() -> Result<bool, std::io::Error> { todo!(); |
