diff --git a/Cargo.lock b/Cargo.lock index 67e9ca9..a3e718b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.9.4" @@ -305,15 +311,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.9.4", "crossterm_winapi", - "mio", + "mio 1.0.4", "parking_lot", "rustix 0.38.44", "signal-hook", @@ -327,11 +349,11 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.9.4", "crossterm_winapi", "derive_more 2.0.1", "document-features", - "mio", + "mio 1.0.4", "parking_lot", "rustix 1.1.2", "signal-hook", @@ -499,6 +521,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ego-tree" version = "0.6.3" @@ -650,6 +678,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "fxhash" version = "0.2.1" @@ -1052,6 +1089,20 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "inquire" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2628910d0114e9139056161d8644a2026be7b117f8498943f9437748b04c9e0a" +dependencies = [ + "bitflags 2.9.4", + "crossterm 0.29.0", + "dyn-clone", + "fuzzy-matcher", + "unicode-segmentation", + "unicode-width 0.2.0", +] + [[package]] name = "instability" version = "0.3.9" @@ -1071,7 +1122,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -1237,6 +1288,18 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.4" @@ -1309,7 +1372,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -1357,6 +1420,7 @@ dependencies = [ "crossterm 0.29.0", "html_parser", "indicatif", + "inquire", "md5", "num_cpus", "rand 0.9.2", @@ -1368,6 +1432,7 @@ dependencies = [ "serde", "serde_json", "spinners", + "tui", ] [[package]] @@ -1676,7 +1741,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cassowary", "compact_str", "crossterm 0.28.1", @@ -1697,7 +1762,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags", + "bitflags 2.9.4", ] [[package]] @@ -1797,7 +1862,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -1810,7 +1875,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.11.0", @@ -1899,7 +1964,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.9.4", "core-foundation", "core-foundation-sys", "libc", @@ -1922,7 +1987,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" dependencies = [ - "bitflags", + "bitflags 2.9.4", "cssparser", "derive_more 0.99.20", "fxhash", @@ -2039,7 +2104,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", + "mio 1.0.4", "signal-hook", ] @@ -2238,7 +2304,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.9.4", "core-foundation", "system-configuration-sys", ] @@ -2317,6 +2383,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -2337,7 +2412,7 @@ dependencies = [ "bytes", "io-uring", "libc", - "mio", + "mio 1.0.4", "pin-project-lite", "slab", "socket2", @@ -2398,7 +2473,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags", + "bitflags 2.9.4", "bytes", "futures-util", "http", @@ -2447,6 +2522,19 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" +dependencies = [ + "bitflags 1.3.2", + "cassowary", + "crossterm 0.25.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "typenum" version = "1.18.0" @@ -2736,6 +2824,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2772,6 +2869,21 @@ dependencies = [ "windows-link 0.2.0", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2805,6 +2917,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2817,6 +2935,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -2829,6 +2953,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2853,6 +2983,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -2865,6 +3001,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -2877,6 +3019,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2889,6 +3037,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 0dd6af4..0ea07c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,8 @@ md5 = "0.8.0" # HTTP reqwest = { version = "0.12.23", features = ["blocking", "json"] } semver = "1.0.27" +inquire = "0.9.1" +tui = "0.19.0" [features] # TUI feature flag diff --git a/src/downloader.rs b/src/downloader.rs index d33fb45..0634a04 100644 --- a/src/downloader.rs +++ b/src/downloader.rs @@ -1,122 +1,54 @@ +use anyhow::{Result, anyhow}; use std::collections::HashMap; use std::fs::{self, File}; use std::io::{Read, Write}; use std::path::Path; -use std::sync::Arc; use std::thread; -use console::style; -use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; -use reqwest::blocking::Client; - -fn verify_md5(file_path: &Path, expected_hash: &str) -> bool { - let mut f = match File::open(file_path) { - Ok(f) => f, - Err(_) => return false, - }; - let mut buffer = Vec::new(); - if f.read_to_end(&mut buffer).is_err() { - return false; - } - let digest = md5::compute(&buffer); - let hex = format!("{:x}", digest); - hex == expected_hash -} - pub fn download_files( - wget_list: &str, + files: &[String], target_dir: &Path, - package_mirror: Option, + mirror: Option<&str>, md5_map: Option<&HashMap>, -) -> Result<(), Box> { +) -> Result<()> { fs::create_dir_all(target_dir)?; - let urls: Vec<&str> = wget_list.lines().filter(|l| !l.trim().is_empty()).collect(); - let total = urls.len(); - let client = Arc::new(Client::new()); - let mp = Arc::new(MultiProgress::new()); - - let md5_map = md5_map.cloned(); - let mut handles = vec![]; - for (i, url) in urls.into_iter().enumerate() { - let client = Arc::clone(&client); - let mp = Arc::clone(&mp); + for url in files.iter().cloned() { let target_dir = target_dir.to_path_buf(); - let package_mirror = package_mirror.clone(); - let url = url.to_string(); - let md5_map = md5_map.clone(); + let mirror = mirror.map(|m| m.to_string()); + let md5_map = md5_map.cloned(); - let handle = thread::spawn(move || -> Result<(), Box> { - let filename = url.split('/').next_back().unwrap_or("file.tar.xz"); + let handle = thread::spawn(move || -> Result<()> { + let download_url = if let Some(m) = &mirror { + url.replace("ftp.gnu.org", m) + } else { + url.clone() + }; + + let filename = download_url + .split('/') + .last() + .ok_or_else(|| anyhow!("Failed to extract filename"))?; let filepath = target_dir.join(filename); - let download_url = if let Some(ref mirror) = package_mirror { - if url.contains("ftp.gnu.org") { - url.replacen("ftp.gnu.org", mirror, 1) - } else { - url.to_string() + let mut resp = reqwest::blocking::get(&download_url)?; + let mut buffer = Vec::new(); + resp.read_to_end(&mut buffer)?; + + let mut file = File::create(&filepath)?; + file.write_all(&buffer)?; + + if let Some(md5s) = md5_map.as_ref() { + if let Some(expected) = md5s.get(filename) { + let digest = md5::compute(&buffer); + if format!("{:x}", digest) != *expected { + return Err(anyhow!("MD5 mismatch for {}", filename)); + } } - } else { - url.to_string() - }; - - let pb = mp.add(ProgressBar::new(0)); - pb.set_style( - ProgressStyle::with_template( - "{bar:40.cyan/blue} {bytes}/{total_bytes} ({eta}) {msg}", - ) - .unwrap() - .progress_chars("=> "), - ); - pb.set_message(format!( - "[{}/{}] {}", - i + 1, - total, - style(filename).yellow() - )); - - let mut resp = client - .get(&download_url) - .send() - .map_err(|e| Box::new(e) as Box)?; - let total_size = resp.content_length().unwrap_or(0); - pb.set_length(total_size); - - let mut file = File::create(&filepath) - .map_err(|e| Box::new(e) as Box)?; - let mut downloaded: u64 = 0; - let mut buffer = [0u8; 8192]; - - loop { - let bytes_read = resp - .read(&mut buffer) - .map_err(|e| Box::new(e) as Box)?; - if bytes_read == 0 { - break; - } - file.write_all(&buffer[..bytes_read]) - .map_err(|e| Box::new(e) as Box)?; - downloaded += bytes_read as u64; - pb.set_position(downloaded); } - let status = if let Some(ref md5_map) = md5_map { - if let Some(expected_hash) = md5_map.get(filename) { - if verify_md5(&filepath, expected_hash) { - style("✅").green() - } else { - style("❌").red() - } - } else { - style("⚠️").yellow() - } - } else { - style("⚠️").yellow() - }; - - pb.finish_with_message(format!("{} {}", status, style(filename).yellow())); Ok(()) }); @@ -124,8 +56,7 @@ pub fn download_files( } for handle in handles { - let result = handle.join().unwrap(); - result.map_err(|e| e as Box)?; + handle.join().map_err(|_| anyhow!("Thread panicked"))??; } Ok(()) diff --git a/src/html.rs b/src/html.rs index 0d9281c..ea3c880 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,33 +1,14 @@ -use html_parser::Dom; -use reqwest::blocking::get; -use std::error::Error; +use scraper::{Html, Selector}; -/// Lädt die HTML-Seite von der angegebenen URL herunter und konvertiert sie in JSON -pub fn fetch_and_parse_html_to_json(url: &str) -> Result> { - // HTML herunterladen - let response = get(url)?; - if !response.status().is_success() { - return Err(format!("Fehler beim Abrufen der URL {}: {}", url, response.status()).into()); +pub fn fetch_pre_blocks(url: &str) -> anyhow::Result> { + let body = reqwest::blocking::get(url)?.text()?; + let document = Html::parse_document(&body); + let selector = Selector::parse("pre").unwrap(); + + let mut results = Vec::new(); + for element in document.select(&selector) { + results.push(element.inner_html()); } - let body = response.text()?; - - // HTML parsen - let dom = Dom::parse(&body)?; - - // In JSON konvertieren - let json = dom.to_json_pretty()?; - Ok(json) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_fetch_and_parse() { - let url = "https://www.linuxfromscratch.org/~thomas/multilib-m32/chapter02/hostreqs.html"; - let json = fetch_and_parse_html_to_json(url).expect("Fehler beim Parsen"); - assert!(json.contains("Host System Requirements")); - } + Ok(results) } diff --git a/src/main.rs b/src/main.rs index 33eb9f3..c67ba95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,76 +8,16 @@ mod wget_list; #[cfg(feature = "tui")] mod tui; -use console::style; -use rand::Rng; -use std::collections::HashMap; -use std::env; -use std::path::PathBuf; - fn main() -> Result<(), Box> { #[cfg(feature = "tui")] { - // TUI-Modus tui::tui_menu()?; return Ok(()); } #[cfg(not(feature = "tui"))] { - // --- Dynamische Version-Prüfung direkt aus HTML --- - let ok = version_check::run_version_checks_from_html( - "https://www.linuxfromscratch.org/~thomas/multilib-m32/chapter02/hostreqs.html", - )?; - - if !ok { - eprintln!( - "{} Some version checks failed. Exiting.", - style("❌").red().bold() - ); - std::process::exit(1); - } - - println!( - "{} All version checks passed. Starting downloader...", - style("✅").green().bold() - ); - - // --- Bestimme LFS-Sources-Pfad --- - let lfs_sources = match env::var("LFS") { - Ok(lfs) => PathBuf::from(lfs).join("sources"), - Err(_) => { - let mut rng = rand::thread_rng(); - let random_number: u32 = rng.gen_range(1000..=9999); - let tmp_path = format!("/tmp/lfs_{}", random_number); - println!( - "{} Using temporary path {}", - style("ℹ️").blue(), - style(&tmp_path).yellow() - ); - PathBuf::from(tmp_path).join("sources") - } - }; - - // --- CLI Mirror-Auswahl: default oder erweiterbar --- - let package_mirror: Option = None; - - // --- Hole wget-Liste --- - let wget_list = wget_list::get_wget_list()?; - - // --- Bereite MD5-Map vor --- - let mut md5_map: HashMap = HashMap::new(); - let md5_content = md5_utils::get_md5sums()?; - for line in md5_content.lines() { - let mut parts = line.split_whitespace(); - if let (Some(hash), Some(filename)) = (parts.next(), parts.next()) { - md5_map.insert(filename.to_string(), hash.to_string()); - } - } - - // --- Lade Dateien herunter --- - downloader::download_files(&wget_list, &lfs_sources, package_mirror, Some(&md5_map))?; - - println!("{} All done!", style("🎉").green().bold()); + println!("TUI feature not enabled. Compile with `--features tui` to run TUI."); Ok(()) } } diff --git a/src/md5_utils.rs b/src/md5_utils.rs index 7b624d0..d851cff 100644 --- a/src/md5_utils.rs +++ b/src/md5_utils.rs @@ -3,10 +3,14 @@ use reqwest::blocking::Client; use reqwest::redirect::Policy; pub fn get_md5sums() -> Result { - let client = Client::builder().redirect(Policy::none()).build()?; + let client = Client::builder().redirect(Policy::limited(5)).build()?; let res = client .get("https://www.linuxfromscratch.org/~thomas/multilib-m32/md5sums") - .send()? - .text()?; - Ok(res) + .send()?; + + if !res.status().is_success() { + anyhow::bail!("Failed to fetch MD5sums: HTTP {}", res.status()); + } + + Ok(res.text()?) } diff --git a/src/mirrors.rs b/src/mirrors.rs index c8a442e..0b4dd5c 100644 --- a/src/mirrors.rs +++ b/src/mirrors.rs @@ -9,13 +9,14 @@ pub fn fetch_mirrors() -> Result, Box> { .get("https://www.linuxfromscratch.org/lfs/mirrors.html#files") .send()? .text()?; + let document = Html::parse_document(&res); let selector = Selector::parse("a[href^='http']").unwrap(); + let mirrors = document .select(&selector) .filter_map(|element| { let href = element.value().attr("href")?; - // Basic filtering to get potential mirror URLs if href.contains("ftp.gnu.org") || href.contains("mirror") { Some(href.to_string()) } else { @@ -23,25 +24,20 @@ pub fn fetch_mirrors() -> Result, Box> { } }) .collect(); + Ok(mirrors) } pub fn choose_package_mirror() -> Option { - let mirrors = match fetch_mirrors() { - Ok(mirrors) => mirrors, - Err(e) => { - println!("Failed to fetch mirrors: {}", e); - // Fallback to a default list if fetching fails - vec![ - "ftp.fau.de".to_string(), - "mirror.kernel.org".to_string(), - "mirror.example.org".to_string(), - ] - } - }; - - println!("Optional: choose a mirror for GNU source packages (replace ftp.gnu.org):"); + let mirrors = fetch_mirrors().unwrap_or_else(|_| { + vec![ + "ftp.fau.de".to_string(), + "mirror.kernel.org".to_string(), + "mirror.example.org".to_string(), + ] + }); + println!("Optional: choose a mirror for GNU source packages:"); for (i, mirror) in mirrors.iter().enumerate() { println!(" [{}] {}", i + 1, mirror); } diff --git a/src/tui.rs b/src/tui.rs index c2feecf..49539b4 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,417 +1,104 @@ -#[cfg(feature = "tui")] -use crate::html::fetch_and_parse_html_to_json; -#[cfg(feature = "tui")] -use crate::{downloader, md5_utils, mirrors, wget_list}; -#[cfg(feature = "tui")] use crossterm::{ - event::{self, Event, KeyCode}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; -#[cfg(feature = "tui")] -use rand::Rng; -#[cfg(feature = "tui")] -use ratatui::{ +use std::{error::Error, io}; +use tui::{ Terminal, backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, - style::{Color, Style}, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, -}; -#[cfg(feature = "tui")] -use std::{ - collections::HashMap, - fs, - io::stdout, - path::PathBuf, - sync::{Arc, Mutex}, - thread, - time::{Duration, Instant}, + style::{Color, Modifier, Style}, + widgets::{Block, Borders, List, ListItem}, }; -#[cfg(feature = "tui")] -use reqwest::blocking::get; -#[cfg(feature = "tui")] -use scraper::{Html, Selector}; -#[cfg(feature = "tui")] -use serde_json::json; - -// ----------------- HTML fetch function ----------------- -#[cfg(feature = "tui")] -fn fetch_html_to_json(url: &str) -> Result> { - let body = get(url)?.text()?; - let document = Html::parse_document(&body); - - let selector = Selector::parse("body").unwrap(); - let mut contents = vec![]; - for element in document.select(&selector) { - contents.push(element.text().collect::>().join(" ")); - } - - Ok(serde_json::to_string_pretty( - &json!({ "body_text": contents }), - )?) -} - -// ----------------- TUI functions ----------------- -#[cfg(feature = "tui")] -fn init_environment() -> Result> { - let mut rng = rand::thread_rng(); - let random_number: u32 = rng.gen_range(1000..=9999); - let tmp_base_path = PathBuf::from(format!("/tmp/lfs_{}", random_number)); - let lfs_sources_path = tmp_base_path.join("sources"); - - fs::create_dir_all(&lfs_sources_path)?; - Ok(lfs_sources_path) -} - -#[cfg(feature = "tui")] -fn select_mirrors_tui(mirrors: Vec) -> Vec { - if mirrors.is_empty() { - return vec![]; - } - - let mut selected: Vec = vec![false; mirrors.len()]; - let mut state = ListState::default(); - state.select(Some(0)); - - let mut stdout = stdout(); - execute!(stdout, EnterAlternateScreen).unwrap(); - enable_raw_mode().unwrap(); - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend).unwrap(); - - loop { - terminal - .draw(|f| { - let size = f.size(); - let items: Vec = mirrors - .iter() - .enumerate() - .map(|(i, mirror)| { - let prefix = if selected[i] { "[x] " } else { "[ ] " }; - ListItem::new(format!("{}{}", prefix, mirror)) - }) - .collect(); - let list = List::new(items) - .block( - Block::default() - .title("Select mirrors") - .borders(Borders::ALL), - ) - .highlight_symbol(">> "); - f.render_stateful_widget(list, size, &mut state); - }) - .unwrap(); - - if let Event::Key(key) = event::read().unwrap() { - match key.code { - KeyCode::Down => { - let i = state.selected().unwrap_or(0); - if i < mirrors.len() - 1 { - state.select(Some(i + 1)); - } - } - KeyCode::Up => { - let i = state.selected().unwrap_or(0); - if i > 0 { - state.select(Some(i - 1)); - } - } - KeyCode::Char(' ') => { - let i = state.selected().unwrap_or(0); - selected[i] = !selected[i]; - } - KeyCode::Enter => { - disable_raw_mode().unwrap(); - execute!(terminal.backend_mut(), LeaveAlternateScreen).unwrap(); - return mirrors - .into_iter() - .enumerate() - .filter_map(|(i, m)| if selected[i] { Some(m) } else { None }) - .collect(); - } - KeyCode::Esc => { - disable_raw_mode().unwrap(); - execute!(terminal.backend_mut(), LeaveAlternateScreen).unwrap(); - return vec![]; - } - _ => {} - } - } - } -} - -#[cfg(feature = "tui")] -fn download_packages_tui(lfs_sources: &PathBuf) { - let mirrors_list = mirrors::fetch_mirrors().unwrap_or_default(); - let selected_mirrors = select_mirrors_tui(mirrors_list); - if selected_mirrors.is_empty() { - return; - } - - let wget_list_content = wget_list::get_wget_list().unwrap_or_default(); - let wget_list: Vec = wget_list_content.lines().map(|s| s.to_string()).collect(); - if wget_list.is_empty() { - return; - } - - let mut md5_map = HashMap::new(); - if let Ok(md5_content) = md5_utils::get_md5sums() { - for line in md5_content.lines() { - if let Some((hash, filename)) = line.split_once(' ') { - md5_map.insert(filename.to_string(), hash.to_string()); - } - } - } - - let download_state: Arc>> = Arc::new(Mutex::new( - wget_list - .iter() - .map(|f| (f.clone(), "Pending".into())) - .collect(), - )); - - let download_state_clone = Arc::clone(&download_state); - let mirrors_clone = selected_mirrors.clone(); - let lfs_sources = lfs_sources.clone(); - - thread::spawn(move || { - for file in &wget_list { - let mut status = "Failed".to_string(); - for mirror in &mirrors_clone { - if downloader::download_files( - file, - &lfs_sources, - Some(mirror.clone()), - Some(&md5_map), - ) - .is_ok() - { - status = format!("Downloaded from {}", mirror); - break; - } - } - let mut state = download_state_clone.lock().unwrap(); - if let Some(entry) = state.iter_mut().find(|(f, _)| f == file) { - entry.1 = status.clone(); - } - } - }); - - let mut stdout = stdout(); - execute!(stdout, EnterAlternateScreen).unwrap(); - enable_raw_mode().unwrap(); - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend).unwrap(); - - loop { - terminal - .draw(|f| { - let size = f.size(); - let items: Vec = { - let state = download_state.lock().unwrap(); - state - .iter() - .map(|(f, s)| ListItem::new(format!("{}: {}", f, s))) - .collect() - }; - let list = List::new(items).block( - Block::default() - .title("Downloading Packages") - .borders(Borders::ALL), - ); - f.render_widget(list, size); - }) - .unwrap(); - - let state = download_state.lock().unwrap(); - if state.iter().all(|(_, s)| s != "Pending") { - break; - } - thread::sleep(Duration::from_millis(100)); - } - - disable_raw_mode().unwrap(); - execute!(terminal.backend_mut(), LeaveAlternateScreen).unwrap(); -} - -// ----------------- Cleanup function ----------------- -#[cfg(feature = "tui")] -fn cleanup_temp_directories() -> Result> { - let mut cleaned_count = 0; - for entry in fs::read_dir("/tmp")? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - if let Some(name) = path.file_name().and_then(|s| s.to_str()) { - if name.starts_with("lfs_") { - fs::remove_dir_all(&path)?; - cleaned_count += 1; - } - } - } - } - Ok(cleaned_count) -} - -// ----------------- Main TUI menu ----------------- -#[cfg(feature = "tui")] -pub fn tui_menu() -> Result<(), Box> { - let mut stdout = stdout(); - execute!(stdout, EnterAlternateScreen)?; +pub fn tui_menu() -> Result<(), Box> { + // Setup terminal enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let menu_items = vec![ - "🌱 Init environment", - "📦 Download packages", - "🔍 Check status", - "🧹 Clean up temp directories", - "📄 Test JSON fetch", // NEW BUTTON - "❌ Exit", - ]; - let mut state = ListState::default(); - state.select(Some(0)); - - let mut lfs_sources: Option = None; - let mut status_message: Option = None; - let mut status_message_timer: Option = None; + let menu_items = vec!["Start downloader", "Exit"]; + let mut selected = 0; loop { terminal.draw(|f| { let size = f.size(); - let block = Block::default() - .title("✨ lpkg TUI 🌈") - .borders(Borders::ALL); - f.render_widget(block, size); - let chunks = Layout::default() .direction(Direction::Vertical) - .margin(2) - .constraints(vec![Constraint::Length(3); menu_items.len()]) + .margin(5) + .constraints([Constraint::Length(menu_items.len() as u16)].as_ref()) .split(size); - for (i, item) in menu_items.iter().enumerate() { - let mut style = Style::default(); - if Some(i) == state.selected() { - style = style.bg(Color::Red).fg(Color::White); - } - let list_item = ListItem::new(*item).style(style); - let list = List::new(vec![list_item]).block(Block::default().borders(Borders::ALL)); - f.render_widget(list, chunks[i]); - } + let items: Vec = menu_items + .iter() + .enumerate() + .map(|(i, m)| { + let style = if i == selected { + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD) + } else { + Style::default() + }; + ListItem::new(*m).style(style) + }) + .collect(); - if let Some(msg) = &status_message { - let paragraph = Paragraph::new(msg.as_str()).block( - Block::default() - .borders(Borders::NONE) - .style(Style::default().fg(Color::Yellow)), - ); - let msg_area = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(10), - Constraint::Percentage(80), - Constraint::Percentage(10), - ]) - .split(size)[1]; - let msg_area = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(1)]) - .split(msg_area)[1]; - f.render_widget(paragraph, msg_area); - } + let list = List::new(items).block( + Block::default() + .title("LFS Downloader") + .borders(Borders::ALL), + ); + f.render_widget(list, chunks[0]); })?; - if let Some(timer) = status_message_timer { - if timer.elapsed() > Duration::from_secs(3) { - status_message = None; - status_message_timer = None; - } - } - + // Handle input if let Event::Key(key) = event::read()? { match key.code { - KeyCode::Down => { - let i = state.selected().unwrap_or(0); - if i < menu_items.len() - 1 { - state.select(Some(i + 1)); - } - } KeyCode::Up => { - let i = state.selected().unwrap_or(0); - if i > 0 { - state.select(Some(i - 1)); + if selected > 0 { + selected -= 1; } } - KeyCode::Enter => match state.selected() { - Some(0) => { - // Init environment - match init_environment() { - Ok(path) => { - lfs_sources = Some(path.clone()); - status_message = - Some(format!("✅ Environment initialized: {}", path.display())); - } - Err(e) => { - status_message = - Some(format!("❌ Failed to init environment: {}", e)); - } + KeyCode::Down => { + if selected < menu_items.len() - 1 { + selected += 1; + } + } + KeyCode::Enter => { + match selected { + 0 => { + // Start downloader + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + super::start_downloader()?; // call your downloader function + return Ok(()); } - status_message_timer = Some(Instant::now()); - } - Some(1) => { - // Download packages - if let Some(path) = &lfs_sources { - download_packages_tui(path); + 1 => { + break; // Exit } + _ => {} } - Some(2) => { - // Status - status_message = Some("🔍 Status selected! (TODO)".to_string()); - status_message_timer = Some(Instant::now()); - } - Some(3) => { - // Cleanup - match cleanup_temp_directories() { - Ok(count) => { - status_message = - Some(format!("✅ Cleaned {} temporary dirs", count)); - } - Err(e) => { - status_message = Some(format!("❌ Failed cleanup: {}", e)); - } - } - status_message_timer = Some(Instant::now()); - } - Some(4) => { - // Test JSON fetch - // Leave TUI first - disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; - terminal.show_cursor()?; - - match fetch_and_parse_html_to_json( - "https://www.linuxfromscratch.org/~thomas/multilib-m32/chapter02/hostreqs.html", - ) { - Ok(json) => println!("✅ JSON output:\n{}", json), - Err(e) => eprintln!("❌ Error: {}", e), - } - - return Ok(()); - } - Some(5) | _ => break, - }, + } KeyCode::Esc => break, _ => {} } } } + // Restore terminal disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; - terminal.show_cursor()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; Ok(()) } diff --git a/src/version_check.rs b/src/version_check.rs index f14106b..8fe2dac 100644 --- a/src/version_check.rs +++ b/src/version_check.rs @@ -1,9 +1,6 @@ -use reqwest; -use scraper::{Html, Selector}; -use semver::Version; use std::process::Command; -/// Führt ein Kommando aus und gibt die erste Zeile der Version zurück +/// Führt ein Kommando aus und gibt stdout zurück fn run_command(cmd: &str, args: &[&str]) -> Option { let output = Command::new(cmd).args(args).output().ok()?; if output.status.success() { @@ -13,145 +10,64 @@ fn run_command(cmd: &str, args: &[&str]) -> Option { } } -/// Vergleicht zwei Versionen mit semver (für Programme) +/// Vergleicht Versionen (semver für Programme) fn check_version(installed: &str, required: &str) -> bool { - let i = Version::parse(installed).ok(); - let r = Version::parse(required).ok(); - match (i, r) { - (Some(i), Some(r)) => i >= r, + match ( + semver::Version::parse(installed), + semver::Version::parse(required), + ) { + (Ok(i), Ok(r)) => i >= r, _ => false, } } -/// Vergleicht Kernel-Versionen (numerisch) -fn check_kernel_version(installed: &str, required: &str) -> bool { - let parse_ver = |v: &str| { - v.split(['.', '-']) - .filter_map(|s| s.parse::().ok()) - .collect::>() - }; - - let i = parse_ver(installed); - let r = parse_ver(required); - - for (a, b) in i.iter().zip(r.iter()) { - if a > b { - return true; - } else if a < b { - return false; - } - } - i.len() >= r.len() -} - -/// Führt eine Version-Prüfung durch -fn ver_check(program: &str, cmd: &str, min_version: &str) -> bool { - match run_command(cmd, &["--version"]) { - Some(output) => { - let ver = output - .lines() - .next() - .unwrap_or("") - .split_whitespace() - .last() - .unwrap_or(""); - if check_version(ver, min_version) { - println!("OK: {:<12} {:<8} >= {}", program, ver, min_version); - true - } else { - eprintln!( - "ERROR: {:<12} version {} is too old ({} required)", - program, ver, min_version - ); - false - } - } - None => { - eprintln!("ERROR: Cannot find {}", program); - false - } - } -} - -/// Führt die Kernel-Prüfung durch -fn ver_kernel(min_version: &str) -> bool { - let kernel = run_command("uname", &["-r"]).unwrap_or_default(); - if check_kernel_version(&kernel, min_version) { - println!("OK: Linux Kernel {} >= {}", kernel, min_version); - true - } else { - eprintln!( - "ERROR: Linux Kernel {} is too old ({} required)", - kernel, min_version - ); - false - } -} - -/// Lädt die LFS-Seite und führt alle Versionsprüfungen aus -pub fn run_version_checks_from_html(url: &str) -> Result> { - let html_text = reqwest::blocking::get(url)?.text()?; - let document = Html::parse_document(&html_text); - let selector = Selector::parse("pre").unwrap(); - +/// Prüft einen
-Block auf Versionen
+pub fn run_version_checks_from_block(block: &str) -> bool {
     let mut ok = true;
 
-    for element in document.select(&selector) {
-        let pre_text = element.text().collect::>().join("\n");
-
-        for line in pre_text.lines() {
-            let line = line.trim();
-            if line.starts_with("ver_check") {
-                let parts: Vec<&str> = line.split_whitespace().collect();
-                if parts.len() >= 4 {
-                    let prog = parts[1];
-                    let cmd = parts[2];
-                    let ver = parts[3];
-                    ok &= ver_check(prog, cmd, ver);
+    for line in block.lines() {
+        let line = line.trim();
+        if line.starts_with("ver_check") {
+            let parts: Vec<&str> = line.split_whitespace().collect();
+            if parts.len() >= 4 {
+                let prog = parts[1];
+                let cmd = parts[2];
+                let ver = parts[3];
+                let installed = run_command(cmd, &["--version"]).unwrap_or_default();
+                let ver_inst = installed
+                    .lines()
+                    .next()
+                    .unwrap_or("")
+                    .split_whitespace()
+                    .last()
+                    .unwrap_or("");
+                if check_version(ver_inst, ver) {
+                    println!("OK: {} {} >= {}", prog, ver_inst, ver);
+                } else {
+                    eprintln!("ERROR: {} {} < {}", prog, ver_inst, ver);
+                    ok = false;
                 }
-            } else if line.starts_with("ver_kernel") {
-                if let Some(ver) = line.split_whitespace().nth(1) {
-                    ok &= ver_kernel(ver);
+            }
+        } else if line.starts_with("ver_kernel") {
+            if let Some(ver) = line.split_whitespace().nth(1) {
+                let kernel = run_command("uname", &["-r"]).unwrap_or_default();
+                let installed = kernel
+                    .split(['-', '.'])
+                    .filter_map(|s| s.parse::().ok())
+                    .collect::>();
+                let required = ver
+                    .split(['-', '.'])
+                    .filter_map(|s| s.parse::().ok())
+                    .collect::>();
+                if installed >= required {
+                    println!("OK: Linux Kernel {} >= {}", kernel, ver);
+                } else {
+                    eprintln!("ERROR: Linux Kernel {} < {}", kernel, ver);
+                    ok = false;
                 }
             }
         }
     }
 
-    // Alias-Checks
-    let alias_check = |cmd: &str, expected: &str| {
-        if let Some(output) = run_command(cmd, &["--version"]) {
-            if output.to_lowercase().contains(&expected.to_lowercase()) {
-                println!("OK:    {:<4} is {}", cmd, expected);
-            } else {
-                eprintln!("ERROR: {:<4} is NOT {}", cmd, expected);
-            }
-        }
-    };
-
-    alias_check("awk", "GNU");
-    alias_check("yacc", "Bison");
-    alias_check("sh", "Bash");
-
-    // Compiler-Test
-    if run_command("g++", &["--version"]).is_some() {
-        println!("OK:    g++ works");
-    } else {
-        eprintln!("ERROR: g++ does NOT work");
-        ok = false;
-    }
-
-    // nproc-Test
-    let nproc = run_command("nproc", &[]).unwrap_or_default();
-    if nproc.is_empty() {
-        eprintln!("ERROR: nproc is not available or empty");
-        ok = false;
-    } else {
-        println!("OK:    nproc reports {} logical cores available", nproc);
-    }
-
-    if !ok {
-        eprintln!("Some version checks failed.");
-    }
-
-    Ok(ok)
+    ok
 }
diff --git a/src/wget_list.rs b/src/wget_list.rs
index 41ced72..b8224ec 100644
--- a/src/wget_list.rs
+++ b/src/wget_list.rs
@@ -3,10 +3,14 @@ use reqwest::blocking::Client;
 use reqwest::redirect::Policy;
 
 pub fn get_wget_list() -> Result {
-    let client = Client::builder().redirect(Policy::none()).build()?;
+    let client = Client::builder().redirect(Policy::limited(5)).build()?;
     let res = client
         .get("https://www.linuxfromscratch.org/~thomas/multilib-m32/wget-list-sysv")
-        .send()?
-        .text()?;
-    Ok(res)
+        .send()?;
+
+    if !res.status().is_success() {
+        anyhow::bail!("Failed to fetch wget-list: HTTP {}", res.status());
+    }
+
+    Ok(res.text()?)
 }