use std::collections::HashMap;
use std::convert::Infallible;
use std::path::Path;
use crate::content_disposition::ContentDisposition;
use crate::data_input::DataInput;
use crate::file_input::FileInput;
use crate::file_validator::Validator;
use crate::result::{MultipartError, MultipartResult};
use futures::StreamExt;
use ntex::http::Payload;
use ntex::web::{FromRequest, HttpRequest};
use ntex_multipart::Multipart as NtexMultipart;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
pub struct Multipart {
multipart: NtexMultipart,
file_inputs: HashMap<String, Vec<FileInput>>, data_inputs: HashMap<String, Vec<DataInput>>, }
impl<Err> FromRequest<Err> for Multipart {
type Error = Infallible;
async fn from_request(
req: &HttpRequest,
payload: &mut Payload,
) -> Result<Multipart, Infallible> {
let multipart = NtexMultipart::new(req.headers(), payload.take());
Ok(Multipart::new(multipart).await)
}
}
impl<'a> Multipart {
pub async fn new(multipart: NtexMultipart) -> Multipart {
Self {
multipart,
file_inputs: Default::default(),
data_inputs: Default::default(),
}
}
pub async fn process(&mut self) -> Result<&mut Multipart, MultipartError> {
while let Some(item) = self.multipart.next().await {
let mut field = item.map_err(MultipartError::NtexError)?;
if let Some(content_disposition) = field.headers().get("content-disposition") {
let content_disposition = content_disposition.to_str().ok();
if let Some(content_disposition) = content_disposition {
let content_disposition = ContentDisposition::create(content_disposition);
if !content_disposition.has_name_field() {
continue;
}
if !content_disposition.is_file_field() {
let value = self.collect_data_field_value(&mut field).await;
let field_name =
content_disposition.get_variable("name").unwrap_or_default();
self.data_inputs
.entry(field_name.to_string())
.or_default()
.push(DataInput {
value,
name: field_name.to_string(),
});
continue;
}
let mut info = FileInput::create(field.headers(), content_disposition)?;
let mut total_size = 0;
let mut bytes = Vec::new();
while let Some(chunk) = field.next().await {
let data = chunk.unwrap();
total_size += data.len();
bytes.push(data);
}
info.size = total_size;
info.bytes = bytes;
self.file_inputs
.entry(info.field_name.clone())
.or_default()
.push(info);
}
}
}
Ok(self)
}
async fn collect_data_field_value(&self, field: &mut ntex_multipart::Field) -> String {
let mut value = String::new();
while let Some(chunk) = field.next().await {
if let Ok(chunk_data) = chunk {
value.push_str(&String::from_utf8_lossy(&chunk_data));
}
}
value
}
pub async fn save_file(file_input: &FileInput, path: impl AsRef<Path>) -> MultipartResult<()> {
let mut file = File::create(path).await?;
for byte in &file_input.bytes {
file.write_all(byte).await?;
}
file.flush().await?;
Ok(())
}
pub fn all_data_inputs(&self) -> &HashMap<String, Vec<DataInput>> {
&self.data_inputs
}
pub fn data_input(&self, field: &str) -> Option<&Vec<DataInput>> {
self.data_inputs.get(field)
}
pub fn first_data_input(&self, field: &str) -> Option<&DataInput> {
self.data_inputs
.get(field)
.and_then(|inputs| inputs.first())
}
pub fn all_files(&self) -> &HashMap<String, Vec<FileInput>> {
&self.file_inputs
}
pub fn files(&self, field: &str) -> Option<&Vec<FileInput>> {
self.file_inputs.get(field)
}
pub fn first_file(&self, field: &str) -> Option<&FileInput> {
self.file_inputs.get(field).and_then(|files| files.first())
}
pub fn has_file(&self, field: &str) -> bool {
self.file_inputs.contains_key(field)
}
pub async fn validate(&mut self, validator: Validator) -> MultipartResult<&mut Multipart> {
self.process().await?;
validator.validate(&self.file_inputs).map(|_| self)
}
}
#[cfg(test)]
mod test {
use crate::data_input::DataInput;
use crate::file_input::FileInput;
use crate::{FileRules, Multipart};
use crate::file_validator::Validator;
use ntex::http::{HeaderMap, Payload};
use ntex::util::Bytes;
use ntex_multipart::Multipart as NtexMultipart;
use tokio::fs;
#[tokio::test]
async fn test_multipart_new() {
let headers = HeaderMap::new();
let payload = Payload::None;
let multipart = NtexMultipart::new(&headers, payload);
let multipart_instance = Multipart::new(multipart).await;
assert!(multipart_instance.all_data_inputs().is_empty());
assert!(multipart_instance.all_files().is_empty());
}
#[tokio::test]
async fn test_save_file() {
let file_input = FileInput {
field_name: "file".to_string(),
file_name: "test.txt".to_string(),
content_type: "text/plain".to_string(),
size: 11,
bytes: vec![Bytes::from("Hello World")],
extension: None,
content_disposition: Default::default(),
};
let path = "test_output.txt";
let result = Multipart::save_file(&file_input, &path).await;
assert!(result.is_ok());
let content = fs::read_to_string(path).await.unwrap();
assert_eq!(content, "Hello World");
fs::remove_file(path).await.unwrap(); }
#[tokio::test]
async fn test_multiple_data_fields() {
let headers = HeaderMap::new();
let payload = Payload::None;
let multipart = NtexMultipart::new(&headers, payload);
let mut multipart_instance = Multipart::new(multipart).await;
multipart_instance
.data_inputs
.entry("key1".to_string())
.or_insert_with(Vec::new)
.push(DataInput {
name: "key1".to_string(),
value: "value1".to_string(),
});
multipart_instance
.data_inputs
.entry("key1".to_string())
.or_insert_with(Vec::new)
.push(DataInput {
name: "key1".to_string(),
value: "value2".to_string(),
});
assert_eq!(multipart_instance.data_input("key1").unwrap().len(), 2);
}
#[tokio::test]
async fn test_multiple_files() {
let headers = HeaderMap::new();
let payload = Payload::None;
let multipart = NtexMultipart::new(&headers, payload);
let mut multipart_instance = Multipart::new(multipart).await;
multipart_instance
.file_inputs
.entry("file1".to_string())
.or_insert_with(Vec::new)
.push(FileInput {
field_name: "file1".to_string(),
file_name: "file1.txt".to_string(),
content_type: "text/plain".to_string(),
size: 11,
bytes: vec![Bytes::from("File 1 Content")],
extension: None,
content_disposition: Default::default(),
});
multipart_instance
.file_inputs
.entry("file1".to_string())
.or_insert_with(Vec::new)
.push(FileInput {
field_name: "file1".to_string(),
file_name: "file2.txt".to_string(),
content_type: "text/plain".to_string(),
size: 12,
bytes: vec![Bytes::from("File 2 Content")],
extension: None,
content_disposition: Default::default(),
});
assert_eq!(multipart_instance.files("file1").unwrap().len(), 2);
}
#[tokio::test]
async fn test_validate_files_too_few() {
let headers = HeaderMap::new();
let payload = Payload::None;
let multipart = NtexMultipart::new(&headers, payload);
let mut multipart_instance = Multipart::new(multipart).await;
let validator = Validator::new().add_rule("file1", FileRules {
min_files: Some(1),
max_files: Some(5),
..Default::default()
});
let result = multipart_instance.validate(validator).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_first_file_and_data_input() {
let headers = HeaderMap::new();
let payload = Payload::None;
let multipart = NtexMultipart::new(&headers, payload);
let mut multipart_instance = Multipart::new(multipart).await;
multipart_instance
.data_inputs
.entry("key1".to_string())
.or_insert_with(Vec::new)
.push(DataInput {
name: "key1".to_string(),
value: "value1".to_string(),
});
multipart_instance
.file_inputs
.entry("file1".to_string())
.or_insert_with(Vec::new)
.push(FileInput {
field_name: "file1".to_string(),
file_name: "file1.txt".to_string(),
content_type: "text/plain".to_string(),
size: 11,
bytes: vec![Bytes::from("File 1 Content")],
extension: None,
content_disposition: Default::default(),
});
let first_data = multipart_instance.first_data_input("key1");
assert_eq!(first_data.unwrap().value, "value1");
let first_file = multipart_instance.first_file("file1");
assert_eq!(first_file.unwrap().file_name, "file1.txt");
}
#[tokio::test]
async fn test_empty_file_field() {
let headers = HeaderMap::new();
let payload = Payload::None;
let multipart = NtexMultipart::new(&headers, payload);
let multipart_instance = Multipart::new(multipart).await;
assert!(multipart_instance.files("empty_file").is_none());
}
}