qcp 0.8.3

Secure remote file copy utility which uses the QUIC protocol over UDP
Documentation
//! Create Directory command
// (c) 2025 Ross Younger

use anyhow::Result;
use async_trait::async_trait;
use tokio::io::AsyncWriteExt;
use tracing::{debug, trace};

use crate::Parameters;
use crate::protocol::common::{ProtocolMessage, ReceivingStream, SendingStream};
use crate::protocol::session::{Command, CreateDirectoryArgs, Response, Status};
use crate::session::common::send_ok;
use crate::session::handler::SessionCommandInner;
use crate::session::{RequestResult, error_and_return, handler::CommandHandler};

pub(crate) struct CreateDirectoryHandler;

#[async_trait]
impl CommandHandler for CreateDirectoryHandler {
    type Args = CreateDirectoryArgs;

    async fn send_impl<'a, S: SendingStream, R: ReceivingStream>(
        &mut self,
        inner: &mut SessionCommandInner<'a, S, R>,
        job: &crate::client::CopyJobSpec,
        _params: Parameters,
    ) -> Result<RequestResult> {
        anyhow::ensure!(
            inner
                .compat
                .supports(crate::protocol::compat::Feature::MKDIR_SETMETA_LS),
            "Operation not supported by remote"
        );
        anyhow::ensure!(
            job.destination.user_at_host.is_some(),
            "logic error: mkdir called for local destination"
        );

        trace!("sending command");
        let mut outbound = &mut inner.stream.send;
        let cmd = Command::CreateDirectory(CreateDirectoryArgs {
            dir_name: job.destination.filename.clone(),
            options: vec![],
        });
        cmd.to_writer_async_framed(&mut outbound).await?;
        outbound.flush().await?;

        trace!("await response");
        let _ = Response::from_reader_async_framed(&mut inner.stream.recv)
            .await?
            .into_result()?;
        Ok(RequestResult::default())
    }

    async fn handle_impl<'a, S: SendingStream, R: ReceivingStream>(
        &mut self,
        inner: &mut SessionCommandInner<'a, S, R>,
        args: &CreateDirectoryArgs,
    ) -> Result<()> {
        let path = &args.dir_name;
        let stream = &mut inner.stream;

        let meta = tokio::fs::metadata(&path).await;
        if let Ok(meta) = meta {
            if meta.is_file() {
                error_and_return!(stream, Status::ItIsAFile);
            }
            if meta.is_dir() {
                // it already exists: this is not an error.
            } else {
                anyhow::bail!("mkdir: existing entity {path:?} is neither file nor directory");
            }
        } else {
            let result = tokio::fs::create_dir(path).await;
            if let Err(e) = result {
                let str = e.to_string();
                debug!("Could not mkdir: {str}");
                error_and_return!(stream, e);
            }
        }
        send_ok(&mut stream.send).await
    }
}

#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod test {
    use crate::{
        Configuration, Parameters,
        client::CopyJobSpec,
        protocol::{
            control::Compatibility,
            session::{Command, Status},
            test_helpers::{new_test_plumbing, read_from_stream},
        },
        session::RequestResult,
    };
    use anyhow::{Result, bail};
    use littertray::LitterTray;
    use std::io::ErrorKind;

    async fn test_mkdir_main(path: &str) -> Result<(Result<RequestResult>, Result<()>)> {
        let (pipe1, mut pipe2) = new_test_plumbing();
        let spec = CopyJobSpec::from_parts(path, &format!("somehost:{path}"), false, true).unwrap();

        let params = Parameters::default();
        let (mut sender, _) = crate::session::factory::client_sender(
            pipe1,
            &spec,
            crate::session::factory::TransferPhase::Transfer,
            Compatibility::Level(4),
            &params,
            None,
            Configuration::system_default(),
        );

        let sender_fut = sender.send(&spec, params);
        tokio::pin!(sender_fut);

        let result = read_from_stream(&mut pipe2.recv, &mut sender_fut).await;
        let cmd = result.expect_left("sender should not have completed early")?;
        let Command::CreateDirectory(ref _args) = cmd else {
            bail!("expected CreateDirectory command");
        };

        let (mut handler, _) = crate::session::factory::command_handler(
            pipe2,
            cmd,
            Compatibility::Level(4),
            Configuration::system_default(),
        );

        let (r1, r2) = tokio::join!(sender_fut, handler.handle());
        Ok((r1, r2))
    }

    async fn is_dir(path: &str) -> Result<bool> {
        let res = tokio::fs::metadata(path).await;
        if res.as_ref().is_err_and(|e| e.kind() == ErrorKind::NotFound) {
            return Ok(false);
        }
        Ok(res?.is_dir())
    }

    #[tokio::test]
    async fn mkdir_success() -> Result<()> {
        LitterTray::try_with_async(async |_| {
            let (r1, r2) = test_mkdir_main("d").await?;
            assert!(r1.is_ok());
            assert!(r2.is_ok());
            assert!(is_dir("d").await.expect("is_dir failed"));
            Ok(())
        })
        .await
    }
    #[tokio::test]
    async fn mkdir_missing_parent() -> Result<()> {
        LitterTray::try_with_async(async |_| {
            let (r1, r2) = test_mkdir_main("d/e").await?;
            assert!(r2.is_ok());
            let err = r1.expect_err("r1 should have errored");
            let st = Status::from(err);
            assert_eq!(st, Status::FileNotFound); // TODO: This should really be DirectoryDoesNotExist
            Ok(())
        })
        .await
    }
    #[tokio::test]
    async fn mkdir_directory_already_exists() -> Result<()> {
        LitterTray::try_with_async(async |tray| {
            let _ = tray.make_dir("d")?;
            let _ = tray.make_dir("d/e")?;
            let (r1, r2) = test_mkdir_main("d/e").await?;
            eprintln!("{r1:?}");
            assert!(r1.is_ok());
            assert!(r2.is_ok());
            assert!(is_dir("d/e").await.expect("is_dir failed"));
            Ok(())
        })
        .await
    }

    #[tokio::test]
    async fn mkdir_file_exists() -> Result<()> {
        LitterTray::try_with_async(async |tray| {
            let _ = tray.create_text("f", "ff")?;
            let (r1, r2) = test_mkdir_main("f").await?;
            let msg = r1.unwrap_err().to_string();
            assert_eq!(msg, "ItIsAFile");
            assert!(r2.is_ok());
            let rr = is_dir("f").await;
            assert!(rr.is_ok_and(|b| !b));
            Ok(())
        })
        .await
    }
}