extern crate zmodem2;
use anyhow::{bail, Context};
use argh::FromArgs;
use indicatif::{ProgressBar, ProgressStyle};
use lrzsz2::ReadWrite;
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fs::{File, OpenOptions};
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
#[derive(FromArgs, Debug)]
struct Arguments {
#[argh(option, short = 'p', default = "String::from(\"/dev/ttyS0\")")]
port: String,
#[argh(positional)]
paths: Vec<String>,
}
#[cfg(unix)]
fn filename_u8_from_os(name: &OsStr) -> Cow<'_, [u8]> {
Cow::Borrowed(name.as_bytes())
}
#[cfg(not(unix))]
fn filename_u8_from_os(name: &OsStr) -> Cow<'_, [u8]> {
Cow::Owned(name.to_string_lossy().into_owned().into_bytes())
}
fn main() -> anyhow::Result<()> {
let args: Arguments = argh::from_env();
if args.paths.is_empty() {
std::process::exit(2);
}
let port_file = OpenOptions::new()
.read(true)
.write(true)
.open(&args.port)
.with_context(|| format!("'{}'", args.port))?;
lrzsz2::configure_raw_mode(&port_file).ok();
let mut port: Box<dyn ReadWrite> = Box::new(port_file);
let first_path = &args.paths[0];
let mut file = File::open(first_path).with_context(|| format!("'{first_path}'"))?;
let filename_path = Path::new(first_path);
let filename = filename_path
.file_name()
.with_context(|| format!("'{}'", first_path))?;
let filename_str = filename.to_string_lossy();
let filename_u8 = filename_u8_from_os(filename);
let size = file.metadata()?.len();
let mut state = zmodem2::Transmission::set_first_file_u8(filename_u8.as_ref(), size as u32)?;
let pb = ProgressBar::new(size);
pb.set_style(
ProgressStyle::default_bar()
.template(
"{msg}\n{spinner} [{elapsed_precise}] [{bar:40}] {bytes}/{total_bytes} ({eta})",
)?
.progress_chars("=>-"),
);
pb.set_message(format!("Sending {}", filename_str));
let mut file_started = false;
loop {
match state.send(&mut port, &mut file) {
Ok(Some(())) => {}
Ok(None) => {
std::thread::sleep(std::time::Duration::from_millis(10));
}
Err(e) => {
pb.finish_with_message("Aborted");
bail!("ZMODEM error: {:#}", e);
}
}
if state.state() != zmodem2::State::FileEnd {
file_started = true;
}
pb.set_position(state.count() as u64);
if file_started && state.state() == zmodem2::State::FileEnd {
break;
}
}
pb.finish_with_message(format!("Sent {}", filename_str));
for path in &args.paths[1..] {
let mut file = File::open(path).with_context(|| format!("'{path}'"))?;
let filename_path = Path::new(path);
let filename = filename_path
.file_name()
.with_context(|| format!("'{}'", path))?;
let filename_str = filename.to_string_lossy();
let filename_u8 = filename_u8_from_os(filename);
let size = file.metadata()?.len();
state.set_next_file_u8(filename_u8.as_ref(), size as u32)?;
let pb = ProgressBar::new(size);
pb.set_style(
ProgressStyle::default_bar()
.template(
"{msg}\n{spinner} [{elapsed_precise}] [{bar:40}] {bytes}/{total_bytes} ({eta})",
)?
.progress_chars("=>-"),
);
pb.set_message(format!("Sending {}", filename_str));
let mut file_started = false;
loop {
match state.send(&mut port, &mut file) {
Ok(Some(())) => {}
Ok(None) => {
std::thread::sleep(std::time::Duration::from_millis(10));
}
Err(e) => {
pb.finish_with_message("Aborted");
bail!("ZMODEM error: {:#}", e);
}
}
if state.state() != zmodem2::State::FileEnd {
file_started = true;
}
pb.set_position(state.count() as u64);
if file_started && state.state() == zmodem2::State::FileEnd {
break;
}
}
pb.finish_with_message(format!("Sent {}", filename_str));
}
while state.state() != zmodem2::State::SessionEnd {
match state.finish(&mut port) {
Ok(Some(())) => {}
Ok(None) => {
std::thread::sleep(std::time::Duration::from_millis(10));
}
Err(e) => {
bail!("ZMODEM error during finish: {:#}", e);
}
}
}
Ok(())
}