mirror of
https://github.com/nushell/nushell
synced 2024-12-27 05:23:11 +00:00
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:
parent
4e83ccdf86
commit
73e8de9753
5 changed files with 70 additions and 6 deletions
|
@ -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(
|
let stream = PipelineData::ByteStream(
|
||||||
ByteStream::file(file, call_span, engine_state.signals().clone()),
|
ByteStream::file(file, call_span, engine_state.signals().clone()),
|
||||||
Some(PipelineMetadata {
|
Some(PipelineMetadata {
|
||||||
data_source: DataSource::FilePath(path.to_path_buf()),
|
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
|
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() {}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use chrono::{DateTime, Datelike, FixedOffset, Timelike};
|
use chrono::{DateTime, Datelike, FixedOffset, Timelike};
|
||||||
use nu_engine::command_prelude::*;
|
use nu_engine::command_prelude::*;
|
||||||
use nu_protocol::ast::PathMember;
|
use nu_protocol::{ast::PathMember, PipelineMetadata};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ToToml;
|
pub struct ToToml;
|
||||||
|
@ -100,9 +100,18 @@ fn toml_into_pipeline_data(
|
||||||
toml_value: &toml::Value,
|
toml_value: &toml::Value,
|
||||||
value_type: Type,
|
value_type: Type,
|
||||||
span: Span,
|
span: Span,
|
||||||
|
metadata: Option<PipelineMetadata>,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> 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) {
|
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(
|
_ => Ok(Value::error(
|
||||||
ShellError::CantConvert {
|
ShellError::CantConvert {
|
||||||
to_type: "TOML".into(),
|
to_type: "TOML".into(),
|
||||||
|
@ -112,7 +121,7 @@ fn toml_into_pipeline_data(
|
||||||
},
|
},
|
||||||
span,
|
span,
|
||||||
)
|
)
|
||||||
.into_pipeline_data()),
|
.into_pipeline_data_with_metadata(new_md)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,6 +148,7 @@ fn to_toml(
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
span: Span,
|
span: Span,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let metadata = input.metadata();
|
||||||
let value = input.into_value(span)?;
|
let value = input.into_value(span)?;
|
||||||
|
|
||||||
let toml_value = value_to_toml_value(engine_state, &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"),
|
vec.iter().next().expect("this should never trigger"),
|
||||||
value.get_type(),
|
value.get_type(),
|
||||||
span,
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,7 @@ fn to_yaml(input: PipelineData, head: Span) -> Result<PipelineData, ShellError>
|
||||||
let metadata = input
|
let metadata = input
|
||||||
.metadata()
|
.metadata()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
// Per RFC-9512, application/yaml should be used
|
||||||
.with_content_type(Some("application/yaml".into()));
|
.with_content_type(Some("application/yaml".into()));
|
||||||
let value = input.into_value(head)?;
|
let value = input.into_value(head)?;
|
||||||
|
|
||||||
|
|
|
@ -379,3 +379,27 @@ fn open_files_inside_glob_metachars_dir() {
|
||||||
assert!(actual.out.contains("hello"));
|
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"));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ use std::process::ExitStatus;
|
||||||
// Needs to be reexported for `nu!` macro
|
// Needs to be reexported for `nu!` macro
|
||||||
pub use nu_path;
|
pub use nu_path;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Outcome {
|
pub struct Outcome {
|
||||||
pub out: String,
|
pub out: String,
|
||||||
pub err: String,
|
pub err: String,
|
||||||
|
|
Loading…
Reference in a new issue