use super::error::MultipartError;
use bytes::Bytes;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tempfile::NamedTempFile;
use tokio::io::AsyncReadExt;
#[derive(Debug)]
pub struct Field {
name: String,
file_name: Option<String>,
content_type: Option<String>,
headers: HashMap<String, String>,
data: FieldData,
}
#[derive(Debug)]
enum FieldData {
Text(String),
Bytes(Bytes),
File(NamedTempFile),
}
impl Field {
pub fn new(
name: String,
file_name: Option<String>,
content_type: Option<String>,
headers: HashMap<String, String>,
) -> Self {
Self {
name,
file_name,
content_type,
headers,
data: FieldData::Bytes(Bytes::new()),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn file_name(&self) -> Option<&str> {
self.file_name.as_deref()
}
pub fn content_type(&self) -> Option<&str> {
self.content_type.as_deref()
}
pub fn headers(&self) -> &HashMap<String, String> {
&self.headers
}
pub fn is_file(&self) -> bool {
self.file_name.is_some() || matches!(self.data, FieldData::File(_))
}
pub async fn bytes(self) -> Result<Bytes, MultipartError> {
match self.data {
FieldData::Bytes(bytes) => Ok(bytes),
FieldData::Text(text) => Ok(Bytes::from(text)),
FieldData::File(file) => {
let mut bytes = Vec::new();
let mut file_handle = tokio::fs::File::from_std(file.reopen()?);
file_handle.read_to_end(&mut bytes).await?;
Ok(Bytes::from(bytes))
}
}
}
pub async fn text(self) -> Result<String, MultipartError> {
match self.data {
FieldData::Text(text) => Ok(text),
FieldData::Bytes(bytes) => std::str::from_utf8(&bytes)
.map(|s| s.to_string())
.map_err(MultipartError::from),
FieldData::File(file) => {
let mut content = String::new();
let mut file_handle = tokio::fs::File::from_std(file.reopen()?);
file_handle.read_to_string(&mut content).await?;
Ok(content)
}
}
}
pub async fn save_to_file<P: AsRef<Path>>(
self,
path: P,
) -> Result<FileField, crate::multipart::MultipartError> {
let file_path = path.as_ref().to_path_buf();
match self.data {
FieldData::File(temp_file) => {
let path_for_metadata = file_path.clone();
match temp_file.persist(&file_path) {
Ok(_) => {
let metadata = tokio::fs::metadata(&path_for_metadata).await?;
Ok(FileField {
name: self.name,
file_name: self.file_name,
content_type: self.content_type,
path: path_for_metadata,
size: metadata.len(),
})
}
Err(persist_error) => {
tracing::debug!(
"Atomic persist failed, falling back to copy: {}",
persist_error.error
);
let temp_file = persist_error.file;
tokio::fs::copy(temp_file.path(), &file_path)
.await
.map_err(MultipartError::from)?;
let metadata = tokio::fs::metadata(&path_for_metadata).await?;
Ok(FileField {
name: self.name,
file_name: self.file_name,
content_type: self.content_type,
path: path_for_metadata,
size: metadata.len(),
})
}
}
}
FieldData::Bytes(bytes) => {
tokio::fs::write(&file_path, &bytes).await?;
let size = bytes.len() as u64;
Ok(FileField {
name: self.name,
file_name: self.file_name,
content_type: self.content_type,
size,
path: file_path,
})
}
FieldData::Text(text) => {
let bytes = text.as_bytes();
tokio::fs::write(&file_path, bytes).await?;
let size = bytes.len() as u64;
Ok(FileField {
name: self.name,
file_name: self.file_name,
content_type: self.content_type,
size,
path: file_path,
})
}
}
}
pub fn set_bytes(&mut self, bytes: Bytes) {
self.data = FieldData::Bytes(bytes);
}
pub fn set_text(&mut self, text: String) {
self.data = FieldData::Text(text);
}
pub fn set_file(&mut self, file: NamedTempFile) {
self.data = FieldData::File(file);
}
}
#[derive(Debug, Clone)]
pub struct FileField {
pub name: String,
pub file_name: Option<String>,
pub content_type: Option<String>,
pub path: PathBuf,
pub size: u64,
}
impl FileField {
pub async fn bytes(&self) -> Result<Bytes, MultipartError> {
let bytes = tokio::fs::read(&self.path).await?;
Ok(Bytes::from(bytes))
}
pub async fn text(&self) -> Result<String, MultipartError> {
let text = tokio::fs::read_to_string(&self.path).await?;
Ok(text)
}
pub async fn persist<P: AsRef<Path>>(self, new_path: P) -> Result<FileField, MultipartError> {
tokio::fs::rename(&self.path, new_path.as_ref()).await?;
Ok(FileField {
name: self.name,
file_name: self.file_name,
content_type: self.content_type,
path: new_path.as_ref().to_path_buf(),
size: self.size,
})
}
}
#[derive(Debug, Clone)]
pub struct TextField {
pub name: String,
pub value: String,
pub content_type: Option<String>,
}
impl TextField {
pub fn new(name: String, value: String) -> Self {
Self {
name,
value,
content_type: None,
}
}
pub fn with_content_type(mut self, content_type: String) -> Self {
self.content_type = Some(content_type);
self
}
}