working
This commit is contained in:
parent
db62ec1d88
commit
85b4ad55b3
5 changed files with 279 additions and 270 deletions
25
src/main.rs
25
src/main.rs
|
|
@ -1,6 +1,7 @@
|
|||
mod downloader;
|
||||
mod md5_utils;
|
||||
mod mirrors;
|
||||
mod version_check;
|
||||
mod wget_list;
|
||||
|
||||
use console::style;
|
||||
|
|
@ -10,11 +11,26 @@ use std::env;
|
|||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// --- Run host system version checks ---
|
||||
if version_check::run_version_checks() {
|
||||
eprintln!(
|
||||
"{} Host system does not meet minimum requirements. Exiting.",
|
||||
style("❌").red().bold()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} All version checks passed. Starting downloader...",
|
||||
style("✅").green().bold()
|
||||
);
|
||||
|
||||
// --- Determine LFS sources path ---
|
||||
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 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 {}",
|
||||
|
|
@ -25,11 +41,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
};
|
||||
|
||||
// --- Choose mirror and fetch wget list ---
|
||||
let package_mirror = mirrors::choose_package_mirror();
|
||||
|
||||
let wget_list = wget_list::get_wget_list()?;
|
||||
|
||||
// MD5 Map vorbereiten
|
||||
// --- Prepare MD5 map ---
|
||||
let mut md5_map: HashMap<String, String> = HashMap::new();
|
||||
let md5_content = md5_utils::get_md5sums()?;
|
||||
for line in md5_content.lines() {
|
||||
|
|
@ -39,6 +55,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Download files ---
|
||||
downloader::download_files(&wget_list, &lfs_sources, package_mirror, Some(&md5_map))?;
|
||||
|
||||
println!("{} All done!", style("🎉").green().bold());
|
||||
|
|
|
|||
422
src/tui.rs
422
src/tui.rs
|
|
@ -5,8 +5,6 @@ use crossterm::{
|
|||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
#[cfg(feature = "tui")]
|
||||
use rand::Rng;
|
||||
#[cfg(feature = "tui")]
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::CrosstermBackend,
|
||||
|
|
@ -17,59 +15,113 @@ use ratatui::{
|
|||
#[cfg(feature = "tui")]
|
||||
use spinners::{Spinner, Spinners};
|
||||
#[cfg(feature = "tui")]
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
io::stdout,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex, mpsc::channel},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use std::io::{self, stdout};
|
||||
#[cfg(feature = "tui")]
|
||||
use std::path::PathBuf;
|
||||
#[cfg(feature = "tui")]
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
use crate::{downloader, md5_utils, mirrors, wget_list};
|
||||
use crate::{downloader, mirrors, wget_list};
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
fn init_environment() -> (PathBuf, String) {
|
||||
match env::var("LFS") {
|
||||
Ok(lfs) => (
|
||||
PathBuf::from(lfs).join("sources"),
|
||||
"Using LFS environment path.".into(),
|
||||
),
|
||||
Err(_) => {
|
||||
let mut rng = rand::rng();
|
||||
let random_number: u32 = rng.random_range(1000..=9999);
|
||||
let tmp_path = format!("/tmp/lfs_{}", random_number);
|
||||
(
|
||||
PathBuf::from(&tmp_path).join("sources"),
|
||||
format!("Using temporary path {}", tmp_path),
|
||||
)
|
||||
}
|
||||
}
|
||||
fn init_environment() -> PathBuf {
|
||||
let tmp_path = "/tmp/lfs_tmp"; // Simplified for demo
|
||||
PathBuf::from(tmp_path).join("sources")
|
||||
}
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
fn prepare_wget_list() -> Vec<String> {
|
||||
wget_list::get_wget_list()
|
||||
.unwrap_or_default()
|
||||
.lines()
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
fn download_packages(lfs_sources: &PathBuf) {
|
||||
let spinner = Spinner::new(Spinners::Dots9, "Downloading packages...".into());
|
||||
let wget_list = wget_list::get_wget_list().unwrap_or_default();
|
||||
let package_mirror =
|
||||
mirrors::choose_package_mirror().unwrap_or_else(|| "ftp.fau.de".to_string());
|
||||
|
||||
// Simplified download call
|
||||
let _ = downloader::download_files(&wget_list, lfs_sources, Some(package_mirror), None);
|
||||
|
||||
spinner.stop();
|
||||
}
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
fn prepare_md5_map() -> HashMap<String, String> {
|
||||
let mut map = HashMap::new();
|
||||
if let Ok(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()) {
|
||||
map.insert(filename.to_string(), hash.to_string());
|
||||
fn format_drive_tui() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Mocked drive list for demo
|
||||
let drives = vec!["/dev/sda".to_string(), "/dev/sdb".to_string()];
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(0));
|
||||
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let size = f.size();
|
||||
let block = Block::default()
|
||||
.title("💾 Format Drive")
|
||||
.borders(Borders::ALL);
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(vec![Constraint::Length(3); drives.len()])
|
||||
.split(size);
|
||||
|
||||
for (i, drive) in drives.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(drive.clone()).style(style);
|
||||
let list = List::new(vec![list_item]).block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(list, chunks[i]);
|
||||
}
|
||||
})?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Down => {
|
||||
let i = state.selected().unwrap_or(0);
|
||||
if i < drives.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::Enter => {
|
||||
if let Some(idx) = state.selected() {
|
||||
let drive = &drives[idx];
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
println!("⚠️ Confirm formatting {}? (y/n)", drive);
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
if matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
|
||||
println!("Formatting {}...", drive);
|
||||
let _ = Command::new("mkfs.ext4").arg(drive).status();
|
||||
println!("✅ Done!");
|
||||
}
|
||||
enable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
|
|
@ -77,244 +129,82 @@ pub fn tui_menu() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let mut stdout = stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let result = (|| -> Result<(), Box<dyn std::error::Error>> {
|
||||
let menu_items = [
|
||||
"🌱 Init environment",
|
||||
"🌐 Select mirror",
|
||||
"📦 Download packages",
|
||||
"🔍 Check status",
|
||||
"❌ Exit",
|
||||
];
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(0));
|
||||
let menu_items = vec![
|
||||
"🌱 Init environment",
|
||||
"📦 Download packages",
|
||||
"💾 Format drive",
|
||||
"❌ Exit",
|
||||
];
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(0));
|
||||
|
||||
let mut lfs_sources: Option<PathBuf> = None;
|
||||
let mut mirrors_list: Vec<String> = Vec::new();
|
||||
let mut selected_mirror: Option<String> = None;
|
||||
let log_messages: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let progress_state: Arc<Mutex<HashMap<String, Option<Spinner>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
let mut lfs_sources: Option<PathBuf> = None;
|
||||
|
||||
let (tx, rx) = channel::<String>();
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let size = f.size();
|
||||
let block = Block::default()
|
||||
.title("✨ lpkg TUI 🌈")
|
||||
.borders(Borders::ALL);
|
||||
f.render_widget(block, size);
|
||||
|
||||
loop {
|
||||
while let Ok(msg) = rx.try_recv() {
|
||||
log_messages.lock().unwrap().push(msg);
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(vec![Constraint::Length(3); menu_items.len()])
|
||||
.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]);
|
||||
}
|
||||
})?;
|
||||
|
||||
terminal.draw(|f| {
|
||||
let size = f.area();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
vec![Constraint::Length(3); menu_items.len()]
|
||||
.into_iter()
|
||||
.chain(vec![Constraint::Min(5)])
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.split(size);
|
||||
|
||||
for (i, item) in menu_items.iter().enumerate() {
|
||||
let style = if Some(i) == state.selected() {
|
||||
Style::default().bg(Color::Blue).fg(Color::White)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
let list_item = ListItem::new(*item).style(style);
|
||||
f.render_widget(
|
||||
List::new(vec![list_item]).block(Block::default().borders(Borders::ALL)),
|
||||
chunks[i],
|
||||
);
|
||||
}
|
||||
|
||||
let logs = log_messages.lock().unwrap();
|
||||
let mut combined_logs: Vec<ListItem> = logs
|
||||
.iter()
|
||||
.rev()
|
||||
.take(chunks.last().unwrap().height as usize - 2)
|
||||
.map(|l| ListItem::new(l.clone()))
|
||||
.collect();
|
||||
|
||||
let progress = progress_state.lock().unwrap();
|
||||
for (file, spinner_opt) in progress.iter() {
|
||||
let display_status = if let Some(spinner) = spinner_opt {
|
||||
spinner.to_string()
|
||||
} else {
|
||||
"✅ Done".to_string()
|
||||
};
|
||||
combined_logs.push(ListItem::new(format!("{}: {}", file, display_status)));
|
||||
}
|
||||
|
||||
f.render_widget(
|
||||
List::new(combined_logs)
|
||||
.block(Block::default().title("Logs").borders(Borders::ALL)),
|
||||
*chunks.last().unwrap(),
|
||||
);
|
||||
})?;
|
||||
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Down => state
|
||||
.select(state.selected().map(|i| (i + 1).min(menu_items.len() - 1))),
|
||||
KeyCode::Up => state.select(state.selected().map(|i| i.saturating_sub(1))),
|
||||
KeyCode::Enter => match state.selected() {
|
||||
Some(0) => {
|
||||
let (path, msg) = init_environment();
|
||||
lfs_sources = Some(path);
|
||||
log_messages.lock().unwrap().push(msg);
|
||||
}
|
||||
Some(1) => {
|
||||
if mirrors_list.is_empty() {
|
||||
mirrors_list = mirrors::fetch_mirrors().unwrap_or_else(|_| {
|
||||
vec![
|
||||
"ftp.fau.de".to_string(),
|
||||
"mirror.kernel.org".to_string(),
|
||||
"mirror.example.org".to_string(),
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
let mut mirror_state = ListState::default();
|
||||
mirror_state.select(Some(0));
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let size = f.area();
|
||||
let mirror_items: Vec<ListItem> = mirrors_list
|
||||
.iter()
|
||||
.map(|m| ListItem::new(m.clone()))
|
||||
.collect();
|
||||
f.render_widget(
|
||||
List::new(mirror_items)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Select Mirror")
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::Blue)
|
||||
.fg(Color::White),
|
||||
),
|
||||
size,
|
||||
);
|
||||
})?;
|
||||
|
||||
if let Event::Key(k) = event::read()? {
|
||||
match k.code {
|
||||
KeyCode::Down => mirror_state.select(
|
||||
mirror_state
|
||||
.selected()
|
||||
.map(|i| (i + 1).min(mirrors_list.len() - 1)),
|
||||
),
|
||||
KeyCode::Up => mirror_state.select(
|
||||
mirror_state
|
||||
.selected()
|
||||
.map(|i| i.saturating_sub(1)),
|
||||
),
|
||||
KeyCode::Enter => {
|
||||
if let Some(idx) = mirror_state.selected() {
|
||||
selected_mirror =
|
||||
Some(mirrors_list[idx].clone());
|
||||
log_messages.lock().unwrap().push(format!(
|
||||
"Selected mirror: {}",
|
||||
mirrors_list[idx]
|
||||
));
|
||||
}
|
||||
break;
|
||||
}
|
||||
KeyCode::Esc => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(2) => {
|
||||
if let Some(ref path) = lfs_sources {
|
||||
let mirror = selected_mirror
|
||||
.clone()
|
||||
.unwrap_or_else(|| "ftp.fau.de".to_string());
|
||||
let wget_list = prepare_wget_list();
|
||||
let md5_map = prepare_md5_map();
|
||||
|
||||
if wget_list.is_empty() {
|
||||
log_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push("⚠️ No packages to download!".into());
|
||||
continue;
|
||||
}
|
||||
|
||||
let progress_clone = Arc::clone(&progress_state);
|
||||
let tx_clone = tx.clone();
|
||||
let path_clone = path.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
for file in wget_list {
|
||||
let spinner = Spinner::new(
|
||||
Spinners::Dots9,
|
||||
format!("Downloading {}", file),
|
||||
);
|
||||
progress_clone
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(file.clone(), Some(spinner));
|
||||
|
||||
let result = downloader::download_files(
|
||||
&file,
|
||||
&path_clone,
|
||||
Some(mirror.clone()),
|
||||
Some(&md5_map),
|
||||
);
|
||||
progress_clone
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(file.clone(), None);
|
||||
|
||||
let status_msg = match result {
|
||||
Ok(_) => format!("✅ {}", file),
|
||||
Err(_) => format!("❌ {}", file),
|
||||
};
|
||||
let _ = tx_clone.send(status_msg);
|
||||
}
|
||||
let _ = tx_clone.send("🎉 All downloads complete!".into());
|
||||
});
|
||||
|
||||
log_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push("⬇️ Download started...".into());
|
||||
} else {
|
||||
log_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push("⚠️ Initialize environment first!".into());
|
||||
}
|
||||
}
|
||||
Some(3) => log_messages
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push("🔍 Status check (TODO)".into()),
|
||||
Some(4) => break,
|
||||
Some(_) | None => break,
|
||||
},
|
||||
KeyCode::Esc => break,
|
||||
_ => {}
|
||||
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));
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => match state.selected() {
|
||||
Some(0) => lfs_sources = Some(init_environment()),
|
||||
Some(1) => {
|
||||
if let Some(ref path) = lfs_sources {
|
||||
download_packages(path);
|
||||
} else {
|
||||
println!("⚠️ Please initialize environment first!");
|
||||
}
|
||||
}
|
||||
Some(2) => {
|
||||
format_drive_tui()?;
|
||||
}
|
||||
Some(3) | _ => break,
|
||||
},
|
||||
KeyCode::Esc => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})();
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
result
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
84
src/version_check.rs
Normal file
84
src/version_check.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub fn run_command(cmd: &str, args: &[&str]) -> Option<String> {
|
||||
let output = Command::new(cmd).args(args).output().ok()?;
|
||||
if output.status.success() {
|
||||
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_version(installed: &str, required: &str) -> bool {
|
||||
let parse_ver = |v: &str| {
|
||||
v.split(|c| c == '.' || c == '-')
|
||||
.filter_map(|s| s.parse::<u32>().ok())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn ver_check(program: &str, arg: &str, min_version: &str) -> bool {
|
||||
match run_command(program, &[arg]) {
|
||||
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 {
|
||||
println!(
|
||||
"ERROR: {:<12} version {} is too old ({} required)",
|
||||
program, ver, min_version
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("ERROR: Cannot find {}", program);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_version_checks() -> bool {
|
||||
let mut ok = true;
|
||||
|
||||
ok &= ver_check("bash", "--version", "3.2");
|
||||
ok &= ver_check("gcc", "--version", "5.4");
|
||||
ok &= ver_check("make", "--version", "4.0");
|
||||
ok &= ver_check("tar", "--version", "1.22");
|
||||
|
||||
// Kernel check
|
||||
if let Some(kernel) = run_command("uname", &["-r"]) {
|
||||
if check_version(&kernel, "5.4") {
|
||||
println!("OK: Linux Kernel {} >= 5.4", kernel);
|
||||
} else {
|
||||
println!("ERROR: Linux Kernel {} is too old (5.4 required)", kernel);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
// CPU cores
|
||||
let cores = num_cpus::get();
|
||||
println!("OK: {} logical cores available", cores);
|
||||
|
||||
ok
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue