This commit is contained in:
Lucy 2025-09-30 16:51:03 +02:00
commit 0d177729ab
15 changed files with 2263 additions and 0 deletions

137
src/downloader.rs Normal file
View file

@ -0,0 +1,137 @@
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 md5;
use reqwest::blocking::Client;
/// Prüft Datei gegen erwarteten MD5-Hash
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
}
/// Download + Live-MD5-Prüfung
pub fn download_files(
wget_list: &str,
target_dir: &Path,
package_mirror: Option<String>,
md5_map: Option<&HashMap<String, String>>,
) -> Result<(), Box<dyn std::error::Error>> {
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());
// Clone md5_map before the loop so we can move it into threads
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);
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 handle = thread::spawn(move || -> Result<(), Box<dyn std::error::Error + Send>> {
let filename = url.split('/').last().unwrap_or("file.tar.xz");
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()
}
} 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<dyn std::error::Error + Send>)?;
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<dyn std::error::Error + Send>)?;
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<dyn std::error::Error + Send>)?;
if bytes_read == 0 {
break;
}
file.write_all(&buffer[..bytes_read])
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send>)?;
downloaded += bytes_read as u64;
pb.set_position(downloaded);
}
// Live-MD5-Prüfung
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(())
});
handles.push(handle);
}
for handle in handles {
let result = handle.join().unwrap();
result.map_err(|e| e as Box<dyn std::error::Error>)?;
}
Ok(())
}

50
src/main.rs Normal file
View file

@ -0,0 +1,50 @@
mod downloader;
mod md5_utils;
mod mirrors;
mod wget_list;
use console::style;
use rand::Rng;
use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// LFS sources Pfad
let lfs_sources = match env::var("LFS") {
Ok(lfs) => PathBuf::from(lfs).join("sources"),
Err(_) => {
let mut rng = rand::rng();
let random_number: u32 = rng.random_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")
}
};
// Mirror für Pakete auswählen
let package_mirror = mirrors::choose_package_mirror();
// Wget-Liste vom Original LFS-Mirror holen
let wget_list = wget_list::get_wget_list()?;
// MD5 Map vorbereiten
let mut md5_map: HashMap<String, String> = 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());
}
}
// Pakete herunterladen + Live-MD5 prüfen
downloader::download_files(&wget_list, &lfs_sources, package_mirror, Some(&md5_map))?;
println!("{} All done!", style("🎉").green().bold());
Ok(())
}

59
src/md5_utils.rs Normal file
View file

@ -0,0 +1,59 @@
use console::style;
use md5;
use std::fs::File;
use std::io::{BufRead, BufReader, Read};
use std::path::Path;
pub fn get_md5sums() -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()?;
let res = client
.get("https://www.linuxfromscratch.org/~thomas/multilib-m32/md5sums")
.send()?
.text()?;
Ok(res)
}
pub fn save_md5sums(content: &str, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
std::fs::write(path, content)?;
Ok(())
}
pub fn verify_md5sums(
md5_file: &Path,
sources_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let file = File::open(md5_file)?;
for line in BufReader::new(file).lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let mut parts = line.split_whitespace();
let expected_hash = parts.next().ok_or("Malformed md5sums line")?;
let filename = parts.next().ok_or("Malformed md5sums line")?;
let file_path = sources_dir.join(filename);
let mut f = File::open(&file_path)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
let digest = md5::compute(&buffer);
let hex = format!("{:x}", digest);
if hex == expected_hash {
println!("{} {} OK", style("").green(), filename);
} else {
println!(
"{} {} FAILED (expected {}, got {})",
style("").red(),
filename,
expected_hash,
hex
);
}
}
Ok(())
}

33
src/mirrors Normal file
View file

@ -0,0 +1,33 @@
use console::Style;
use std::io::{self, Write};
pub fn choose_package_mirror() -> Option<String> {
let mirrors = vec![
"https://ftp.fau.de",
"https://mirror.kernel.org/linux",
"https://mirror.example.org/linux",
];
println!("Optional: choose a mirror for source packages:");
for (i, mirror) in mirrors.iter().enumerate() {
println!(" [{}] {}", i + 1, mirror);
}
print!("Enter number or press Enter for default: ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let input = input.trim();
if input.is_empty() {
None
} else {
let choice = input.parse::<usize>().unwrap_or(1);
let chosen = mirrors.get(choice.saturating_sub(1)).unwrap_or(&mirrors[0]);
println!("Using package mirror: {}", Style::new().green().apply_to(chosen));
Some(chosen.to_string())
}
}

31
src/mirrors.rs Normal file
View file

@ -0,0 +1,31 @@
use console::Style;
use std::io::{self, Write};
pub fn choose_package_mirror() -> Option<String> {
let mirrors = vec!["ftp.fau.de", "mirror.kernel.org", "mirror.example.org"];
println!("Optional: choose a mirror for GNU source packages (replace ftp.gnu.org):");
for (i, mirror) in mirrors.iter().enumerate() {
println!(" [{}] {}", i + 1, mirror);
}
print!("Enter number or press Enter for default: ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let input = input.trim();
if input.is_empty() {
None
} else {
let choice = input.parse::<usize>().unwrap_or(1);
let chosen = mirrors.get(choice.saturating_sub(1)).unwrap_or(&mirrors[0]);
println!(
"Using package mirror: {}",
Style::new().green().apply_to(chosen)
);
Some(chosen.to_string())
}
}

11
src/wget_list.rs Normal file
View file

@ -0,0 +1,11 @@
use reqwest::blocking::Client;
use reqwest::redirect::Policy;
pub fn get_wget_list() -> Result<String, Box<dyn std::error::Error>> {
let client = Client::builder().redirect(Policy::none()).build()?;
let res = client
.get("https://www.linuxfromscratch.org/~thomas/multilib-m32/wget-list-sysv")
.send()?
.text()?;
Ok(res)
}