hdfs-native 0.14.1

Native HDFS client implementation in Rust
Documentation
#[cfg(feature = "integration-test")]
mod common;

#[cfg(feature = "integration-test")]
mod test {
    use hdfs_native::{
        Client, ClientBuilder, HdfsError, Result, WriteOptions,
        minidfs::{DfsFeatures, MiniDfs},
    };
    use serial_test::serial;
    use std::collections::HashSet;
    use whoami::username;

    async fn touch(client: &Client, path: &str) -> Result<()> {
        client
            .create(path, WriteOptions::default())
            .await?
            .close()
            .await
    }

    #[tokio::test]
    #[serial]
    async fn test_trash() {
        let _ = env_logger::builder().is_test(true).try_init();

        let features = HashSet::from([DfsFeatures::Trash]);
        let _dfs = MiniDfs::with_features(&features);
        let client = ClientBuilder::new().build().unwrap();

        test_trash_behavior(&client).await.unwrap();
    }

    #[tokio::test]
    #[serial]
    async fn test_trash_disabled() {
        let _ = env_logger::builder().is_test(true).try_init();

        let _dfs = MiniDfs::default();
        let client = ClientBuilder::new().build().unwrap();

        touch(&client, "/trash_disabled/file").await.unwrap();
        assert!(matches!(
            client.trash("/trash_disabled/file").await,
            Err(HdfsError::TrashNotEnabled)
        ));
        assert!(client.get_file_info("/trash_disabled/file").await.is_ok());
    }

    async fn test_trash_behavior(client: &Client) -> Result<()> {
        let trash_current = format!("/user/{}/.Trash/Current", username().unwrap());

        touch(client, "/trash_normal/file").await?;
        assert_eq!(
            client.trash("/trash_normal/file").await?,
            Some(format!("{trash_current}/trash_normal/file"))
        );
        assert!(client.get_file_info("/trash_normal/file").await.is_err());
        assert!(
            client
                .get_file_info(&format!("{trash_current}/trash_normal/file"))
                .await
                .is_ok()
        );

        assert_eq!(
            client
                .trash(&format!("{trash_current}/trash_normal/file"))
                .await?,
            None
        );

        touch(client, "/trash_collision/file").await?;
        touch(client, &format!("{trash_current}/trash_collision/file")).await?;
        let collision_trash_path = client.trash("/trash_collision/file").await?.unwrap();
        assert!(client.get_file_info("/trash_collision/file").await.is_err());
        assert!(
            client
                .get_file_info(&format!("{trash_current}/trash_collision/file"))
                .await
                .is_ok()
        );
        assert_ne!(
            collision_trash_path,
            format!("{trash_current}/trash_collision/file")
        );

        let collision_entries = client
            .list_status(&format!("{trash_current}/trash_collision"), false)
            .await?;
        assert!(
            collision_entries.iter().any(|status| {
                status
                    .path
                    .starts_with(&format!("{trash_current}/trash_collision/file"))
                    && status.path != format!("{trash_current}/trash_collision/file")
            }),
            "{collision_entries:?}"
        );
        assert!(client.get_file_info(&collision_trash_path).await.is_ok());

        touch(client, "/trash_obstructed/child").await?;
        touch(client, &format!("{trash_current}/trash_obstructed")).await?;
        let obstructed_trash_path = client.trash("/trash_obstructed/child").await?.unwrap();
        assert!(
            client
                .get_file_info("/trash_obstructed/child")
                .await
                .is_err()
        );

        let root_entries = client.list_status(&trash_current, false).await?;
        let fallback_dir = root_entries
            .iter()
            .find(|status| {
                status.isdir
                    && status
                        .path
                        .starts_with(&format!("{trash_current}/trash_obstructed"))
                    && status.path != format!("{trash_current}/trash_obstructed")
            })
            .expect("non-directory ancestor should get a timestamped fallback directory");
        assert_eq!(
            obstructed_trash_path,
            format!("{}/child", fallback_dir.path)
        );
        assert!(client.get_file_info(&obstructed_trash_path).await.is_ok());

        Ok(())
    }
}