From 73e8de9753b11e297633d2483bb2711f2d7a5cf2 Mon Sep 17 00:00:00 2001 From: Jack Wright <56345+ayax79@users.noreply.github.com> Date: Tue, 6 Aug 2024 02:36:24 -0700 Subject: [PATCH] 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. Screenshot 2024-08-02 at 11 30 10 # User-Facing Changes - Content of files can be directly piped into commands like `http post` with the content type set appropriately when using `--raw`. --- crates/nu-command/src/filesystem/open.rs | 29 +++++++++++++++++++++++- crates/nu-command/src/formats/to/toml.rs | 21 +++++++++++++---- crates/nu-command/src/formats/to/yaml.rs | 1 + crates/nu-command/tests/commands/open.rs | 24 ++++++++++++++++++++ crates/nu-test-support/src/lib.rs | 1 + 5 files changed, 70 insertions(+), 6 deletions(-) diff --git a/crates/nu-command/src/filesystem/open.rs b/crates/nu-command/src/filesystem/open.rs index 0351d1d9b2..23e95d1aed 100644 --- a/crates/nu-command/src/filesystem/open.rs +++ b/crates/nu-command/src/filesystem/open.rs @@ -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 { extensions } + +fn detect_content_type(extension: &str) -> Option { + // 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() {} +} diff --git a/crates/nu-command/src/formats/to/toml.rs b/crates/nu-command/src/formats/to/toml.rs index ed1490231c..c70864b7f8 100644 --- a/crates/nu-command/src/formats/to/toml.rs +++ b/crates/nu-command/src/formats/to/toml.rs @@ -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, ) -> Result { + 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 { + 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), } } diff --git a/crates/nu-command/src/formats/to/yaml.rs b/crates/nu-command/src/formats/to/yaml.rs index c1693cda7d..ae923c1fc8 100644 --- a/crates/nu-command/src/formats/to/yaml.rs +++ b/crates/nu-command/src/formats/to/yaml.rs @@ -98,6 +98,7 @@ fn to_yaml(input: PipelineData, head: Span) -> Result 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)?; diff --git a/crates/nu-command/tests/commands/open.rs b/crates/nu-command/tests/commands/open.rs index ac5e2f99e5..b50c6c9e6c 100644 --- a/crates/nu-command/tests/commands/open.rs +++ b/crates/nu-command/tests/commands/open.rs @@ -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")); + }) +} diff --git a/crates/nu-test-support/src/lib.rs b/crates/nu-test-support/src/lib.rs index c6cadbcbd8..930e3792dc 100644 --- a/crates/nu-test-support/src/lib.rs +++ b/crates/nu-test-support/src/lib.rs @@ -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,