rvimage 0.7.0

A remote image viewer with a labeling tool
Documentation
#![allow(clippy::indexing_slicing)]
use std::{
    fmt::Debug,
    io::{Read, Write},
    net::TcpStream,
    path::Path,
};

use rvimage_domain::{RvError, RvResult, rverr, to_rv};
use ssh2::{Channel, Session};

use crate::cfg::SshCfg;

fn to_cmd_err<E>(cmd: &str, e: E) -> RvError
where
    E: Debug,
{
    rverr!("could not run {} due to {:?}", cmd, e)
}

fn command(cmd: &str, sess: &Session) -> RvResult<String> {
    let mut channel = sess.channel_session().map_err(|e| to_cmd_err(cmd, e))?;

    let mut s = String::new();
    channel.exec(cmd).map_err(|e| to_cmd_err(cmd, e))?;

    channel
        .read_to_string(&mut s)
        .map_err(|e| to_cmd_err(cmd, e))?;
    channel.wait_close().map_err(|e| to_cmd_err(cmd, e))?;
    Ok(s)
}

pub fn file_info(path: &str, sess: &Session) -> RvResult<String> {
    let cmd = format!("ls -lh {path}");
    command(cmd.as_str(), sess)
}

fn close(mut remote_file: Channel) -> RvResult<()> {
    remote_file.send_eof().map_err(to_rv)?;
    remote_file.wait_eof().map_err(to_rv)?;
    remote_file.close().map_err(to_rv)?;
    remote_file.wait_close().map_err(to_rv)?;
    Ok(())
}

pub fn download(remote_src_file_path: &str, sess: &Session) -> RvResult<Vec<u8>> {
    let (mut remote_file, _) = sess
        .scp_recv(Path::new(remote_src_file_path))
        .map_err(to_rv)?;
    {
        let mut content = vec![];
        remote_file.read_to_end(&mut content).map_err(to_rv)?;
        close(remote_file)?;
        Ok(content)
    }
    .map_err(|e: RvError| rverr!("could not download {} due to {e:?}", remote_src_file_path))
}

pub fn write_bytes(content_bytes: &[u8], remote_dst_path: &Path, sess: &Session) -> RvResult<()> {
    let n_bytes = content_bytes.len();
    let mut remote_file = sess
        .scp_send(
            Path::new(remote_dst_path),
            0o644,
            content_bytes.len() as u64,
            None,
        )
        .map_err(to_rv)?;
    let mut total_bytes_written = 0;
    while total_bytes_written < n_bytes {
        total_bytes_written += remote_file
            .write(&content_bytes[total_bytes_written..])
            .map_err(|e| rverr!("could not write to {remote_dst_path:?} due to {e:?}"))?;
    }
    close(remote_file)?;
    Ok(())
}

pub fn find(
    remote_folder_path: &str,
    filter_extensions: &[&str],
    sess: &Session,
) -> RvResult<Vec<String>> {
    let cmd = format!("find '{remote_folder_path}'");
    let s = command(cmd.as_str(), sess)?;
    fn ext_predicate(s: &str, filter_extensions: &[&str]) -> bool {
        let n_s = s.len();
        filter_extensions
            .iter()
            .filter(|ext| {
                let n_e = ext.len();
                if n_e > n_s {
                    false
                } else {
                    &s[n_s - n_e..n_s] == **ext
                }
            })
            .count()
            > 0
            || filter_extensions.is_empty()
    }
    Ok(s.split('\n')
        .filter(|s| ext_predicate(s, filter_extensions))
        .map(|s| s.to_string())
        .collect::<Vec<_>>())
}

pub fn auth(ssh_cfg: &SshCfg) -> RvResult<Session> {
    let tcp = TcpStream::connect(&ssh_cfg.prj.address)
        .map_err(|e| rverr!("TCP stream connection error, {:?}", e))?;
    let mut sess = Session::new().map_err(|e| rverr!("could not create ssh session, {:?}", e))?;
    sess.set_tcp_stream(tcp);
    sess.handshake()
        .map_err(|e| rverr!("ssh handshake error, {:?}", e))?;
    let keyfile = Path::new(&ssh_cfg.usr.ssh_identity_file_path);
    if !keyfile.exists() {
        return Err(rverr!("could not find private key file {keyfile:?}"));
    }
    sess.userauth_pubkey_file(&ssh_cfg.usr.user, None, keyfile, None)
        .map_err(|e| rverr!("ssh user auth error, {:?}", e))?;
    assert!(sess.authenticated());
    tracing::info!("ssh session authenticated");
    Ok(sess)
}