extern crate zmodem2;
use anyhow::{bail, Context};
use argh::FromArgs;
use indicatif::{ProgressBar, ProgressStyle};
use lrzsz2::ReadWrite;
use std::ffi::{OsStr, OsString};
use std::fs::{File, OpenOptions};
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::path::{Component, Path, PathBuf};
#[derive(FromArgs, Debug)]
struct Arguments {
#[argh(option, short = 'p', default = "String::from(\"/dev/ttyS0\")")]
port: String,
#[argh(positional)]
path: String,
}
#[cfg(unix)]
fn filename_from_u8(bytes: &[u8]) -> anyhow::Result<OsString> {
let path = Path::new(OsStr::from_bytes(bytes));
let name = path
.components()
.next_back()
.and_then(|c| match c {
Component::Normal(name) => Some(name),
_ => None,
})
.with_context(|| format!("malformed filename: '{}'", String::from_utf8_lossy(bytes)))?;
Ok(name.to_os_string())
}
#[cfg(not(unix))]
fn filename_from_u8(bytes: &[u8]) -> anyhow::Result<OsString> {
let filename_str = std::str::from_utf8(bytes).context("invalid UTF-8 in filename")?;
let path = Path::new(filename_str);
let name = path
.components()
.next_back()
.and_then(|c| match c {
Component::Normal(name) => Some(name),
_ => None,
})
.with_context(|| format!("malformed filename: '{}'", filename_str))?;
Ok(name.to_os_string())
}
fn main() -> anyhow::Result<()> {
let args: Arguments = argh::from_env();
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 mut state = zmodem2::Transmission::new();
let dest_dir = PathBuf::from(&args.path);
if !dest_dir.is_dir() {
bail!("'{}' is not a directory", dest_dir.display());
}
let mut open_files: std::collections::HashMap<Vec<u8>, (File, ProgressBar)> =
std::collections::HashMap::new();
let mut current_filename = vec![];
let mut sink = std::io::sink();
while state.state() != zmodem2::State::SessionEnd {
let current_state = state.state();
if current_state == zmodem2::State::FileBegin
&& state.file_name() != current_filename.as_slice()
{
let filename_bytes = state.file_name();
let received_filename = filename_from_u8(filename_bytes)?;
let path = dest_dir.join(received_filename);
let file = File::create(&path)
.with_context(|| format!("could not create '{}'", path.display()))?;
let pb = ProgressBar::new(state.file_size() as u64);
pb.set_style(
ProgressStyle::default_bar()
.template(
"{msg}\n{spinner} [{elapsed_precise}] [{bar:40}] {bytes}/{total_bytes} ({eta})",
)?
.progress_chars("=>-"),
);
pb.set_message(format!("Receiving {}", path.display()));
open_files.insert(filename_bytes.to_vec(), (file, pb));
current_filename = filename_bytes.to_vec();
}
let has_file = open_files.contains_key(¤t_filename);
let result = if has_file {
let (file, _pb) = open_files.get_mut(¤t_filename).unwrap();
state.receive(&mut port, file)
} else {
state.receive(&mut port, &mut sink)
};
match result {
Ok(Some(())) => {
let count = state.count();
let current_state = state.state();
if let Some((_file, pb)) = open_files.get(¤t_filename) {
pb.set_position(count as u64);
}
if current_state == zmodem2::State::FileEnd {
if let Some((_file, pb)) = open_files.remove(¤t_filename) {
pb.finish_with_message("Done");
}
current_filename.clear();
}
}
Ok(None) => {
std::thread::sleep(std::time::Duration::from_millis(10));
}
Err(e) => {
for (_filename, (_file, pb)) in open_files.iter() {
pb.finish_with_message("Aborted");
}
bail!("ZMODEM error: {:#}", e);
}
}
}
for (_filename, (_file, pb)) in open_files.iter() {
if !pb.is_finished() {
pb.finish_with_message("Done");
}
}
Ok(())
}