use std::{
io::{ErrorKind, Read},
path::PathBuf,
str::FromStr,
vec,
};
use bytes::Bytes;
use log::debug;
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand};
use nu_protocol::{
ByteStreamSource, Category, Example, LabeledError, ListStream, PipelineData, ShellError,
Signals, Signature, Span, Spanned, SyntaxShape, Type, Value, process::ChildPipe,
shell_error::io::IoError,
};
use object_store::{ObjectStoreExt, PutPayload, WriteMultipart};
use url::Url;
use crate::CloudPlugin;
pub struct Save;
impl PluginCommand for Save {
type Plugin = CloudPlugin;
fn name(&self) -> &str {
"cloud save"
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("cloud save")
.input_output_types(vec![(Type::Any, Type::Nothing)])
.required("uri", SyntaxShape::String, "The file url to use.")
.switch("raw", "save file as raw binary", Some('r'))
.category(Category::FileSystem)
}
fn examples(&self) -> Vec<Example<'_>> {
vec![Example {
description: "Save a csv file to s3.",
example: "[[a b]; [1 1] [1 2] [2 1] [2 2] [3 1] [3 2]] | to csv | cloud save s3://mybucket/file.csv",
result: None,
}]
}
fn description(&self) -> &str {
"Save a file to cloud storage"
}
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(|e| LabeledError::from(*e))
}
}
async fn command(
plugin: &CloudPlugin,
engine: &EngineInterface,
call: &nu_plugin::EvaluatedCall,
input: PipelineData,
) -> Result<PipelineData, Box<ShellError>> {
let raw = call.has_flag("raw")?;
let call_span = call.head;
let url_path: Spanned<PathBuf> = call.req(0)?;
let url = url_path
.item
.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: url_path.span,
call_span,
})?,
span: url_path.span,
};
match input {
PipelineData::ByteStream(stream, _metadata) => {
debug!("Handling byte stream");
match stream.into_source() {
ByteStreamSource::Read(read) => {
bytestream_to_cloud(plugin, engine, read, &url, call_span).await?;
}
ByteStreamSource::File(source) => {
bytestream_to_cloud(plugin, engine, source, &url, call_span).await?;
}
ByteStreamSource::Child(mut child) => {
if let Some(stdout) = child.stdout.take() {
let res = match stdout {
ChildPipe::Pipe(pipe) => {
bytestream_to_cloud(plugin, engine, pipe, &url, call_span).await
}
ChildPipe::Tee(tee) => {
bytestream_to_cloud(plugin, engine, tee, &url, call_span).await
}
};
res?;
};
}
}
Ok(PipelineData::Empty)
}
PipelineData::ListStream(ls, _pipeline_metadata) if raw => {
debug!("Handling list stream");
liststream_to_cloud(plugin, engine, ls, &url, call_span).await?;
Ok(PipelineData::empty())
}
input => {
debug!("Handling input");
let bytes = input_to_bytes(input, &url_path.item, raw, engine, call, call_span)?;
stream_bytes(plugin, engine, bytes, &url, call_span).await?;
Ok(PipelineData::empty())
}
}
}
async fn liststream_to_cloud(
plugin: &CloudPlugin,
engine: &EngineInterface,
ls: ListStream,
url: &Spanned<Url>,
span: Span,
) -> Result<(), Box<ShellError>> {
let signals = engine.signals();
let (object_store, path) = plugin.parse_url(engine, url, span).await?;
let upload = object_store
.object_store()
.put_multipart(&path)
.await
.unwrap();
let mut write = WriteMultipart::new(upload);
for v in ls {
signals.check(&span)?;
let bytes = value_to_bytes(v)?;
write.write(&bytes)
}
let _ = write.finish().await.map_err(|e| ShellError::GenericError {
error: format!("Could not write to S3: {e}"),
msg: "".into(),
span: None,
help: None,
inner: vec![],
})?;
Ok(())
}
async fn bytestream_to_cloud(
plugin: &CloudPlugin,
engine: &EngineInterface,
source: impl Read,
url: &Spanned<Url>,
span: Span,
) -> Result<(), Box<ShellError>> {
stream_to_cloud_async(plugin, engine, source, url, span).await
}
async fn stream_to_cloud_async(
plugin: &CloudPlugin,
engine: &EngineInterface,
source: impl Read,
url: &Spanned<Url>,
span: Span,
) -> Result<(), Box<ShellError>> {
let signals = engine.signals();
let (object_store, path) = plugin.parse_url(engine, url, span).await?;
let upload = object_store
.object_store()
.put_multipart(&path)
.await
.unwrap();
let mut write = WriteMultipart::new(upload);
let _ = generic_copy(source, &mut write, span, signals)?;
let _ = write.finish().await.map_err(|e| ShellError::GenericError {
error: format!("Could not write to S3: {e}"),
msg: "".into(),
span: None,
help: None,
inner: vec![],
})?;
Ok(())
}
const DEFAULT_BUF_SIZE: usize = 8192;
fn generic_copy(
mut reader: impl Read,
writer: &mut WriteMultipart,
span: Span,
signals: &Signals,
) -> Result<u64, Box<ShellError>> {
let buf = &mut [0; DEFAULT_BUF_SIZE];
let mut len = 0;
loop {
signals.check(&span)?;
let n = match reader.read(buf) {
Ok(0) => break,
Ok(n) => n,
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(Box::new(ShellError::Io(IoError::new(e, span, None)))),
};
len += n;
writer.write(&buf[..n]);
}
Ok(len as u64)
}
fn value_to_bytes(value: Value) -> Result<Vec<u8>, Box<ShellError>> {
match value {
Value::String { val, .. } => Ok(val.into_bytes()),
Value::Binary { val, .. } => Ok(val),
Value::List { vals, .. } => {
let val = vals
.into_iter()
.map(Value::coerce_into_string)
.collect::<Result<Vec<String>, ShellError>>()?
.join("\n")
+ "\n";
Ok(val.into_bytes())
}
Value::Error { error, .. } => Err(Box::new(*error)),
other => Ok(other.coerce_into_string()?.into_bytes()),
}
}
fn input_to_bytes(
input: PipelineData,
path: &std::path::Path,
raw: bool,
engine: &EngineInterface,
call: &EvaluatedCall,
span: Span,
) -> Result<Vec<u8>, Box<ShellError>> {
let ext = if raw {
None
} else if let PipelineData::ByteStream(..) = input {
None
} else if let PipelineData::Value(Value::String { .. }, ..) = input {
None
} else {
path.extension()
.map(|name| name.to_string_lossy().to_string())
};
let input = if let Some(ext) = ext {
convert_to_extension(engine, &ext, input, call)?
} else {
input
};
value_to_bytes(input.into_value(span)?)
}
fn convert_to_extension(
engine: &EngineInterface,
extension: &str,
input: PipelineData,
call: &EvaluatedCall,
) -> Result<PipelineData, Box<ShellError>> {
if let Some(decl_id) = engine.find_decl(format!("to {extension}"))? {
debug!("Found to {extension} decl: converting input");
let command_output = engine.call_decl(decl_id, call.clone(), input, true, false)?;
Ok(command_output)
} else {
Ok(input)
}
}
async fn stream_bytes(
plugin: &CloudPlugin,
engine: &EngineInterface,
bytes: Vec<u8>,
url: &Spanned<Url>,
span: Span,
) -> Result<(), ShellError> {
let (object_store, path) = plugin.parse_url(engine, url, span).await?;
let payload = PutPayload::from_bytes(Bytes::from(bytes));
object_store
.object_store()
.put(&path, payload)
.await
.map_err(|e| ShellError::GenericError {
error: format!("Could not write to S3: {e}"),
msg: "".into(),
span: None,
help: None,
inner: vec![],
})?;
Ok(())
}