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 bytes::Buf;
use log::debug;
use nu_plugin::{EngineInterface, PluginCommand};
use nu_protocol::{
    ByteStream, ByteStreamType, Category, DataSource, Example, IntoInterruptiblePipelineData,
    LabeledError, PipelineData, PipelineMetadata, ShellError, Signature, Spanned, SyntaxShape,
    Type,
};
use url::Url;

use crate::CloudPlugin;

pub struct Open;

impl PluginCommand for Open {
    type Plugin = CloudPlugin;

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

    fn signature(&self) -> nu_protocol::Signature {
        Signature::build("cloud open")
            .input_output_types(vec![(Type::Nothing, Type::Any), (Type::String, Type::Any)])
            .rest("url", SyntaxShape::String, "The cloud url to file to open.")
            .switch("raw", "open file as raw binary", Some('r'))
            .category(Category::FileSystem)
    }

    fn description(&self) -> &str {
        "Load a file into a cell, converting to table if possible (avoid by appending '--raw')."
    }

    fn examples(&self) -> Vec<Example<'_>> {
        vec![Example {
            description: "Load a file from s3.",
            example: "cloud open s3://mybucket/file.txt",
            result: None,
        }]
    }

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

async fn command(
    plugin: &CloudPlugin,
    engine: &EngineInterface,
    call: &nu_plugin::EvaluatedCall,
    _input: PipelineData,
) -> Result<PipelineData, ShellError> {
    let call_span = call.head;
    let raw = call.has_flag("raw")?;
    let spanned_path: Spanned<PathBuf> = call.req(0)?;
    let path = spanned_path.item;
    let 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 bytes = plugin.cache.get(engine, &url, call_span).await?;

    let content_type = if raw {
        path.extension()
            .map(|ext| ext.to_string_lossy().to_string())
            .and_then(|ref s| detect_content_type(s))
    } else {
        None
    };

    let extension: Option<String> = if raw {
        None
    } else {
        path.extension()
            .map(|ext| ext.to_string_lossy().to_string().to_owned())
            .map(|s| s.to_lowercase())
    };

    let converter = if !raw {
        if let Some(ext) = &extension {
            debug!("Attempting to use converter: {ext}");
            engine.find_decl(format!("from {ext}"))?
        } else {
            None
        }
    } else {
        None
    };

    let stream = PipelineData::ByteStream(
        ByteStream::read(
            bytes.reader(),
            call_span,
            engine.signals().clone(),
            ByteStreamType::Unknown,
        ),
        Some(
            PipelineMetadata::default()
                .with_data_source(DataSource::FilePath(path.to_path_buf()))
                .with_content_type(content_type.clone()),
        ),
    );

    match converter {
        Some(converter_id) => {
            debug!("converter id: {converter_id:?}");
            let command_output =
                engine.call_decl(converter_id, call.clone(), stream, true, false)?;
            Ok(command_output.into_pipeline_data_with_metadata(
                call.head,
                engine.signals().clone(),
                PipelineMetadata::default()
                    .with_data_source(DataSource::FilePath(path.to_path_buf())),
            ))
        }
        None => Ok(stream),
    }
}

fn detect_content_type(extension: &str) -> Option<String> {
    // This will allow the overriding of metadata to be consistent with
    // the content type
    match extension {
        // Per RFC-9512, application/yaml should be used
        "yaml" | "yml" => Some("application/yaml".to_string()),
        _ => mime_guess::from_ext(extension)
            .first()
            .map(|mime| mime.to_string()),
    }
}