nu_plugin_cloud 0.2.11+0.110.0

A nushell plugin for working with cloud storage services
Documentation
use std::{path::PathBuf, str::FromStr, vec};

use futures::StreamExt;
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand};
use nu_protocol::{
    Category, Example, LabeledError, PipelineData, ShellError, Signature, Spanned, SyntaxShape,
    Type, Value, record,
};
use url::Url;

use crate::CloudPlugin;

pub struct Ls;

impl PluginCommand for Ls {
    type Plugin = CloudPlugin;

    fn name(&self) -> &str {
        "cloud ls"
    }

    fn signature(&self) -> nu_protocol::Signature {
        Signature::build("cloud ls")
            .required("uri", SyntaxShape::String, "The url to use.")
            .category(Category::FileSystem)
            .input_output_types(vec![(Type::Nothing, Type::Any)])
    }

    fn description(&self) -> &str {
        "List the filenames, sizes, modificationtime , etags, and versions of a cloud location."
    }

    fn examples(&self) -> Vec<Example<'_>> {
        vec![Example {
            description: "List the files in a s3 bucket.",
            example: "cloud ls s3://mybucket",
            result: None,
        }]
    }

    fn run(
        &self,
        plugin: &Self::Plugin,
        engine: &EngineInterface,
        call: &EvaluatedCall,
        _input: PipelineData,
    ) -> Result<PipelineData, LabeledError> {
        plugin
            .rt
            .block_on(command(engine, plugin, call))
            .map_err(LabeledError::from)
    }
}

async fn command(
    engine: &EngineInterface,
    plugin: &CloudPlugin,
    call: &EvaluatedCall,
) -> Result<PipelineData, ShellError> {
    let call_span = call.head;
    let spanned_path: Spanned<PathBuf> = call.req(0)?;
    let url_path = spanned_path.item;
    let url = url_path
        .to_str()
        .expect("The path should already be unicode")
        .to_string();
    let url = Spanned {
        item: Url::from_str(&url).map_err(|e| ShellError::IncorrectValue {
            msg: format!("Invalid Url: {e}"),
            val_span: spanned_path.span,
            call_span,
        })?,
        span: spanned_path.span,
    };

    let (object_store, path) = plugin.parse_url(engine, &url, call_span).await?;
    let list_stream = object_store.object_store().list(Some(&path));

    let values: Vec<Value> = list_stream
        .map(|v| match v {
            Ok(meta) => Value::record(
                record!(
                    "name" => Value::string(meta.location.to_string(), call_span),
                    "size" => Value::filesize(meta.size as i64, call_span),
                    "modified" => Value::date(meta.last_modified.fixed_offset(), call_span),
                    "etag" => meta.e_tag.map(|s| Value::string(s, call_span)).unwrap_or(Value::nothing(call_span)),
                    "version" => meta.version.map(|s| Value::string(s, call_span)).unwrap_or(Value::nothing(call_span)),
                ),
                call_span,
            ),
            Err(e) => {
                let se = ShellError::GenericError {
                    error: format!("Error fetching data from object store: {e}"),
                    msg: "".into(),
                    span: None,
                    help: None,
                    inner: vec![],
                };
                Value::error(se, call_span)
            }
        })
        .collect()
        .await;

    Ok(PipelineData::Value(Value::list(values, call_span), None))
}