Attempt to guess the content type of a file when opening with --raw (#13521)

# Description
Attempt to guess the content type of a file when opening with --raw and
set it in the pipeline metadata.

<img width="644" alt="Screenshot 2024-08-02 at 11 30 10"
src="https://github.com/user-attachments/assets/071f0967-c4dd-405a-b8c8-f7aa073efa98">


# User-Facing Changes
- Content of files can be directly piped into commands like `http post`
with the content type set appropriately when using `--raw`.
This commit is contained in:
Jack Wright 2024-08-06 02:36:24 -07:00 committed by GitHub
parent 4e83ccdf86
commit 73e8de9753
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 70 additions and 6 deletions

View file

@ -146,11 +146,19 @@ impl Command for Open {
}
};
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 stream = PipelineData::ByteStream(
ByteStream::file(file, call_span, engine_state.signals().clone()),
Some(PipelineMetadata {
data_source: DataSource::FilePath(path.to_path_buf()),
content_type: None,
content_type,
}),
);
@ -268,3 +276,22 @@ fn extract_extensions(filename: &str) -> Vec<String> {
extensions
}
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()),
}
}
#[cfg(test)]
mod test {
#[test]
fn test_content_type() {}
}

View file

@ -1,6 +1,6 @@
use chrono::{DateTime, Datelike, FixedOffset, Timelike};
use nu_engine::command_prelude::*;
use nu_protocol::ast::PathMember;
use nu_protocol::{ast::PathMember, PipelineMetadata};
#[derive(Clone)]
pub struct ToToml;
@ -100,9 +100,18 @@ fn toml_into_pipeline_data(
toml_value: &toml::Value,
value_type: Type,
span: Span,
metadata: Option<PipelineMetadata>,
) -> Result<PipelineData, ShellError> {
let new_md = Some(
metadata
.unwrap_or_default()
.with_content_type(Some("text/x-toml".into())),
);
match toml::to_string_pretty(&toml_value) {
Ok(serde_toml_string) => Ok(Value::string(serde_toml_string, span).into_pipeline_data()),
Ok(serde_toml_string) => {
Ok(Value::string(serde_toml_string, span).into_pipeline_data_with_metadata(new_md))
}
_ => Ok(Value::error(
ShellError::CantConvert {
to_type: "TOML".into(),
@ -112,7 +121,7 @@ fn toml_into_pipeline_data(
},
span,
)
.into_pipeline_data()),
.into_pipeline_data_with_metadata(new_md)),
}
}
@ -139,6 +148,7 @@ fn to_toml(
input: PipelineData,
span: Span,
) -> Result<PipelineData, ShellError> {
let metadata = input.metadata();
let value = input.into_value(span)?;
let toml_value = value_to_toml_value(engine_state, &value, span)?;
@ -148,10 +158,11 @@ fn to_toml(
vec.iter().next().expect("this should never trigger"),
value.get_type(),
span,
metadata,
),
_ => toml_into_pipeline_data(&toml_value, value.get_type(), span),
_ => toml_into_pipeline_data(&toml_value, value.get_type(), span, metadata),
},
_ => toml_into_pipeline_data(&toml_value, value.get_type(), span),
_ => toml_into_pipeline_data(&toml_value, value.get_type(), span, metadata),
}
}

View file

@ -98,6 +98,7 @@ fn to_yaml(input: PipelineData, head: Span) -> Result<PipelineData, ShellError>
let metadata = input
.metadata()
.unwrap_or_default()
// Per RFC-9512, application/yaml should be used
.with_content_type(Some("application/yaml".into()));
let value = input.into_value(head)?;

View file

@ -379,3 +379,27 @@ fn open_files_inside_glob_metachars_dir() {
assert!(actual.out.contains("hello"));
});
}
#[test]
fn test_content_types_with_open_raw() {
Playground::setup("open_files_content_type_test", |dirs, _| {
let result = nu!(cwd: dirs.formats(), "open --raw random_numbers.csv | metadata");
assert!(result.out.contains("text/csv"));
let result = nu!(cwd: dirs.formats(), "open --raw caco3_plastics.tsv | metadata");
assert!(result.out.contains("text/tab-separated-values"));
let result = nu!(cwd: dirs.formats(), "open --raw sample-simple.json | metadata");
assert!(result.out.contains("application/json"));
let result = nu!(cwd: dirs.formats(), "open --raw sample.ini | metadata");
assert!(result.out.contains("text/plain"));
let result = nu!(cwd: dirs.formats(), "open --raw sample_data.xlsx | metadata");
assert!(result.out.contains("vnd.openxmlformats-officedocument"));
let result = nu!(cwd: dirs.formats(), "open --raw sample_def.nu | metadata");
assert!(!result.out.contains("content_type"));
let result = nu!(cwd: dirs.formats(), "open --raw sample.eml | metadata");
assert!(result.out.contains("message/rfc822"));
let result = nu!(cwd: dirs.formats(), "open --raw cargo_sample.toml | metadata");
assert!(result.out.contains("text/x-toml"));
let result = nu!(cwd: dirs.formats(), "open --raw appveyor.yml | metadata");
assert!(result.out.contains("application/yaml"));
})
}

View file

@ -9,6 +9,7 @@ use std::process::ExitStatus;
// Needs to be reexported for `nu!` macro
pub use nu_path;
#[derive(Debug)]
pub struct Outcome {
pub out: String,
pub err: String,