nfs3_client 0.9.0

Provides an implementation of NFS3 client
Documentation
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Component, Path, PathBuf};

use nfs3_client::Nfs3ConnectionBuilder;
use nfs3_client::io::{AsyncRead, AsyncWrite};
use nfs3_client::nfs3_types::nfs3::{self, Nfs3Option, filename3};
use nfs3_client::nfs3_types::portmap::PMAP_PORT;
use nfs3_client::nfs3_types::rpc::{auth_unix, opaque_auth};
use nfs3_client::nfs3_types::xdr_codec::Opaque;
use nfs3_client::tokio::TokioConnector;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = env::args().collect::<Vec<_>>();
    if args.len() < 5 {
        eprintln!(
            "Usage: download_folder <server_ip> <mount_path> <remote_folder> <local_folder> \
             [portmapper_port]"
        );
        return Ok(());
    }

    let ip = args[1].clone();
    let mount_path = args[2].clone();
    let remote_folder = args[3].clone();
    let local_folder = args[4].clone();
    let portmapper_port = args
        .get(5)
        .and_then(|port| port.parse::<u16>().ok())
        .unwrap_or(PMAP_PORT);

    let auth_unix = auth_unix {
        stamp: 0xaaaa_aaaa,
        machinename: Opaque::borrowed(b"unknown"),
        uid: 0xffff_fffe,
        gid: 0xffff_fffe,
        gids: vec![],
    };
    let credential = opaque_auth::auth_unix(&auth_unix);

    let mut connection = Nfs3ConnectionBuilder::new(TokioConnector, ip, mount_path)
        .portmapper_port(portmapper_port)
        .credential(credential)
        .mount()
        .await?;

    let mut remote_folder_fh = connection.root_nfs_fh3();
    let path = Path::new(&remote_folder);
    for component in path.components() {
        if let Component::Normal(name) = component {
            let name = name.to_str().expect("failed to convert name to utf-8");
            let lookup = connection
                .lookup(&nfs3::LOOKUP3args {
                    what: nfs3::diropargs3 {
                        dir: remote_folder_fh.clone(),
                        name: filename3(Opaque::borrowed(name.as_bytes())),
                    },
                })
                .await?
                .unwrap();

            remote_folder_fh = lookup.object;
        } else {
            panic!("Invalid remote folder path format: {remote_folder}");
        }
    }

    download_folder(&mut connection, remote_folder_fh, &local_folder).await?;

    connection.unmount().await?;
    Ok(())
}

#[allow(clippy::or_fun_call)]
async fn download_folder(
    connection: &mut nfs3_client::Nfs3Connection<impl AsyncRead + AsyncWrite>,
    folder_fh: nfs3::nfs_fh3,
    local_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    fs::create_dir_all(local_path)?;

    let mut queue = vec![(PathBuf::from(local_path), folder_fh)];
    while let Some((local_path, folder_fh)) = queue.pop() {
        let mut cookie = nfs3::cookie3::default();
        let mut cookieverf = nfs3::cookieverf3::default();
        loop {
            let readdirplus = connection
                .readdirplus(&nfs3::READDIRPLUS3args {
                    dir: folder_fh.clone(),
                    cookie,
                    cookieverf,
                    maxcount: 128 * 1024,
                    dircount: 128 * 1024,
                })
                .await?
                .unwrap();

            let entries = readdirplus.reply.entries.into_inner();
            cookie = entries
                .last()
                .map_or(nfs3::cookie3::default(), |e| e.cookie);
            cookieverf = readdirplus.cookieverf;

            for entry in entries {
                let name = String::from_utf8_lossy(&entry.name.0).into_owned();
                if name == "." || name == ".." {
                    continue;
                }

                let (fh, attrs) = if let (Nfs3Option::Some(fh), Nfs3Option::Some(attr)) =
                    (entry.name_handle, entry.name_attributes)
                {
                    (fh, attr)
                } else {
                    let lookup = connection
                        .lookup(&nfs3::LOOKUP3args {
                            what: nfs3::diropargs3 {
                                dir: folder_fh.clone(),
                                name: entry.name,
                            },
                        })
                        .await?
                        .unwrap();

                    if lookup.obj_attributes.is_none() {
                        eprintln!("Failed to lookup entry: {name}");
                        continue;
                    }
                    (lookup.object, lookup.obj_attributes.unwrap())
                };

                let local_entry_path = Path::new(&local_path).join(name);
                let is_dir = matches!(attrs.type_, nfs3::ftype3::NF3DIR);
                if is_dir {
                    fs::create_dir_all(&local_entry_path)?;
                    queue.push((local_entry_path, fh));
                } else {
                    download_file(connection, fh, &local_entry_path.to_string_lossy()).await?;
                }
            }

            if readdirplus.reply.eof {
                break;
            }
        }
    }

    Ok(())
}

async fn download_file(
    connection: &mut nfs3_client::Nfs3Connection<impl AsyncRead + AsyncWrite>,
    file_fh: nfs3::nfs_fh3,
    local_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    println!("Downloading file: {local_path}");
    let mut file = File::create(local_path)?;
    let mut offset = 0;
    loop {
        let read = connection
            .read(&nfs3::READ3args {
                file: file_fh.clone(),
                offset,
                count: 128 * 1024,
            })
            .await?
            .unwrap();

        let read_data = read.data.0;
        file.write_all(&read_data)?;
        offset += read_data.len() as u64;

        if read.eof {
            break;
        }
    }

    Ok(())
}