use std::{borrow::Cow, io::Write, path::Path};
use async_fs::File as AsyncFile;
use futures_lite::{io::BufReader, AsyncBufRead, AsyncReadExt, Stream, StreamExt};
use http_types::Body;
use mime_guess::Mime;
use crate::{reader_stream::ReaderStream, Encoding, StreamChunk};
#[derive(Debug)]
pub(crate) struct Part<'p> {
name: Cow<'p, str>,
data: Body,
content_type: Mime,
file_data: Option<FileData<'p>>,
}
#[derive(Debug)]
struct FileData<'p> {
filename: Cow<'p, str>,
encoding: Option<Encoding>,
}
impl<'p> Part<'p> {
pub(crate) fn filename(&self) -> Option<&str> {
self.file_data.as_ref().map(|data| data.filename.as_ref())
}
pub(crate) fn encoding(&self) -> Option<Encoding> {
self.file_data.as_ref().and_then(|data| data.encoding)
}
pub(crate) fn into_stream(self, buf_size: Option<usize>) -> impl Stream<Item = StreamChunk> {
let header = self.header_bytes();
let header_stream = futures_lite::stream::once(Ok(header));
let buf_size = buf_size.or(self.data.len());
let encoding = self.encoding();
let data = ReaderStream::new(self.data.into_reader(), buf_size, encoding);
header_stream.chain(data)
}
pub(crate) fn into_reader(self, buf_size: Option<usize>) -> impl AsyncBufRead {
let header = self.header_bytes();
let header_reader = futures_lite::io::Cursor::new(header);
let encoding = self.encoding();
let buf_size = buf_size.or(self.data.len());
let data_reader = self.data.into_reader();
let data = ReaderStream::new(data_reader, buf_size, encoding);
header_reader.chain(data)
}
pub(crate) fn text(name: impl Into<Cow<'p, str>>, value: &str) -> Self {
Part {
name: name.into(),
data: Body::from(value),
content_type: "text/plain".parse().unwrap(),
file_data: None,
}
}
pub(crate) fn file_raw(
name: impl Into<Cow<'p, str>>,
filename: impl Into<Cow<'p, str>>,
content_type: Mime,
encoding: Option<Encoding>,
data: Body,
) -> Self {
Part {
name: name.into(),
data,
content_type,
file_data: Some(FileData {
filename: filename.into(),
encoding,
}),
}
}
pub(crate) fn file_raw_async(
name: impl Into<Cow<'p, str>>,
filename: impl Into<Cow<'p, str>>,
content_type: Mime,
encoding: Option<Encoding>,
data: impl AsyncBufRead + Unpin + Send + Sync + 'static,
data_len: Option<usize>, ) -> Self {
Part {
name: name.into(),
content_type,
data: Body::from_reader(data, data_len),
file_data: Some(FileData {
filename: filename.into(),
encoding,
}),
}
}
pub(crate) async fn file_async(
name: impl Into<Cow<'p, str>>,
path: impl AsRef<Path>,
encoding: Option<Encoding>,
) -> Result<Self, futures_lite::io::Error> {
let path = path.as_ref();
let filename = filename(path);
let content_type =
content_type(path).unwrap_or_else(|| "application/octet-stream".parse().unwrap());
let file = AsyncFile::open(path).await?;
let buf_reader = BufReader::new(file);
Ok(Part::file_raw_async(
name,
filename,
content_type,
encoding,
buf_reader,
None,
))
}
pub(crate) fn size_hint(&self) -> Option<usize> {
let data_len = self.data.len()?;
let header_len = self.header_len();
Some(data_len + header_len)
}
fn header_len(&self) -> usize {
let mut len = 41 + self.name.len(); if let Some(filename) = self.filename() {
len += 15 + filename.len(); }
len += 2; len += 14 + self.content_type.essence_str().len(); len += 2; if let Some(encoding) = self.encoding() {
len += 27 + encoding.to_str().len(); len += 2; }
len + 2 }
fn write_header<W: std::io::Write>(&self, mut buf: W) -> Result<(), std::io::Error> {
buf.write_all(
format!("Content-Disposition: form-data; name=\"{}\"", self.name).as_bytes(),
)?;
if let Some(filename) = self.filename() {
buf.write_all(format!("; filename=\"{}\"", filename).as_bytes())?;
}
buf.write_all(b"\r\n")?;
buf.write_all(format!("Content-Type: {}\r\n", self.content_type).as_bytes())?;
if let Some(encoding) = self.encoding() {
buf.write_all(
format!("Content-Transfer-Encoding: {}\r\n", encoding.to_str()).as_bytes(),
)?;
}
buf.write_all(b"\r\n")?; Ok(())
}
fn header_bytes(&self) -> Vec<u8> {
let mut header = Vec::with_capacity(self.header_len());
self.write_header(&mut header)
.expect("Failed to write header");
header
}
pub(crate) async fn extend(self, mut data: &mut [u8]) -> Result<(), futures_lite::io::Error> {
self.write_header(&mut data)?;
let mut stream = self.into_stream(None);
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
data.write_all(&chunk)?;
}
Ok(())
}
}
fn filename(path: &Path) -> String {
path.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "file".into())
}
fn content_type(path: &Path) -> Option<Mime> {
mime_guess::from_path(path).first()
}
#[cfg(test)]
mod tests {
use super::*;
#[async_std::test]
async fn test_stream_and_reader_same_output() {
let value = "This is a test value";
let part_for_stream = Part::text("test_field", value);
let part_for_reader = Part::text("test_field", value);
let mut stream = part_for_stream.into_stream(Some(8));
let mut stream_output = Vec::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk.expect("stream chunk error");
stream_output.extend(chunk);
}
let mut reader = part_for_reader.into_reader(Some(8));
let mut reader_output = Vec::new();
reader
.read_to_end(&mut reader_output)
.await
.expect("reader error");
assert_eq!(stream_output, reader_output);
}
}