s3find 0.16.1

A command line utility to walk an Amazon S3 hierarchy. s3find is an analog of find for Amazon S3.
Documentation
use std::io::Write;

use anyhow::Error;
use async_trait::async_trait;
use aws_smithy_types::date_time::Format;
use csv::WriterBuilder;
use serde::{Deserialize, Serialize};

use crate::adapters::aws::CommandS3Client;
use crate::arg::{AdvancedPrint, FastPrint, PrintFormat, S3Path};
use crate::command::StreamObject;

use super::RunCommand;

impl FastPrint {
    #[inline]
    pub(crate) fn print_stream_object<I: Write>(
        &self,
        io: &mut I,
        bucket: &str,
        stream_obj: &StreamObject,
    ) -> std::io::Result<()> {
        writeln!(io, "s3://{}/{}", bucket, stream_obj.display_key())
    }
}

#[async_trait]
impl RunCommand for FastPrint {
    async fn execute(
        &self,
        _c: &dyn CommandS3Client,
        path: &S3Path,
        list: &[StreamObject],
    ) -> Result<(), Error> {
        let mut stdout = std::io::stdout();
        for x in list {
            self.print_stream_object(&mut stdout, &path.bucket, x)?
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
pub(crate) struct ObjectRecord {
    pub(crate) e_tag: String,
    pub(crate) owner: String,
    pub(crate) size: i64,
    pub(crate) last_modified: String,
    pub(crate) key: String,
    pub(crate) storage_class: String,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub(crate) version_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub(crate) is_latest: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub(crate) is_delete_marker: Option<bool>,
}

impl From<&StreamObject> for ObjectRecord {
    fn from(stream_obj: &StreamObject) -> Self {
        let object = &stream_obj.object;
        ObjectRecord {
            e_tag: object.e_tag.clone().unwrap_or_default(),
            owner: object
                .owner
                .clone()
                .and_then(|x| x.display_name.clone())
                .unwrap_or_default(),
            size: object.size.unwrap_or_default(),
            last_modified: object
                .last_modified
                .and_then(|x| x.fmt(Format::DateTime).ok())
                .unwrap_or_default(),
            key: object.key.clone().unwrap_or_default(),
            storage_class: object
                .storage_class
                .clone()
                .map(|x| x.to_string())
                .unwrap_or_default(),
            version_id: stream_obj.version_id.clone(),
            is_latest: stream_obj.is_latest,
            is_delete_marker: if stream_obj.is_delete_marker {
                Some(true)
            } else {
                None
            },
        }
    }
}

impl AdvancedPrint {
    #[inline]
    pub(crate) fn print_stream_object<I: Write>(
        &self,
        io: &mut I,
        bucket: &str,
        stream_obj: &StreamObject,
    ) -> std::io::Result<()> {
        let object = &stream_obj.object;
        writeln!(
            io,
            "{0} {1} {2} {3} \"s3://{4}/{5}\" {6}",
            object.e_tag.as_deref().unwrap_or("NoEtag"),
            object
                .owner
                .as_ref()
                .and_then(|x| x.display_name.as_deref())
                .unwrap_or("None"),
            object.size.unwrap_or_default(),
            object
                .last_modified
                .and_then(|x| x.fmt(Format::DateTime).ok())
                .unwrap_or("None".to_string()),
            bucket,
            stream_obj.display_key(),
            object
                .storage_class
                .as_ref()
                .map(|x| x.as_str())
                .unwrap_or("NONE"),
        )
    }

    #[inline]
    pub(crate) fn print_json_stream_object<I: Write>(
        &self,
        io: &mut I,
        stream_obj: &StreamObject,
    ) -> Result<(), Error> {
        let record: ObjectRecord = stream_obj.into();
        writeln!(io, "{}", serde_json::to_string(&record)?)?;
        Ok(())
    }

    #[inline]
    pub(crate) fn print_csv_stream_objects<I: Write>(
        &self,
        io: &mut I,
        stream_objects: &[StreamObject],
    ) -> Result<(), Error> {
        let mut wtr = WriterBuilder::new().has_headers(false).from_writer(io);
        for stream_obj in stream_objects {
            let record: ObjectRecord = stream_obj.into();
            wtr.serialize(record)?;
        }
        wtr.flush()?;
        Ok(())
    }
}

#[async_trait]
impl RunCommand for AdvancedPrint {
    async fn execute(
        &self,
        _c: &dyn CommandS3Client,
        path: &S3Path,
        list: &[StreamObject],
    ) -> Result<(), Error> {
        let mut stdout = std::io::stdout();

        match self.format {
            PrintFormat::Json => {
                for x in list {
                    self.print_json_stream_object(&mut stdout, x)?;
                }
            }
            PrintFormat::Text => {
                for x in list {
                    self.print_stream_object(&mut stdout, &path.bucket, x)?;
                }
            }
            PrintFormat::Csv => {
                self.print_csv_stream_objects(&mut stdout, list)?;
            }
        }
        Ok(())
    }
}