use std::{fmt, path::Path};
use mime::{APPLICATION_OCTET_STREAM, Mime};
use tokio::{
fs,
io::{AsyncRead, Result as IoResult},
};
use tokio_util::codec::{BytesCodec, FramedRead};
use crate::api::{Form, FormValue, WriteForm};
pub struct InputFileReader {
file_name: Option<String>,
mime_type: Option<Mime>,
reader: FramedRead<Box<dyn AsyncRead + Send + Sync + Unpin>, BytesCodec>,
}
impl InputFileReader {
pub fn new<T>(reader: T) -> Self
where
T: AsyncRead + Send + Sync + Unpin + 'static,
{
InputFileReader {
reader: FramedRead::new(Box::new(reader), BytesCodec::new()),
file_name: None,
mime_type: None,
}
}
pub fn with_file_name<T>(mut self, value: T) -> Self
where
T: Into<String>,
{
self.file_name = Some(value.into());
self
}
pub fn file_name(&self) -> Option<&str> {
self.file_name.as_deref()
}
pub fn with_mime_type(mut self, value: Mime) -> Self {
self.mime_type = Some(value);
self
}
pub fn mime_type(&self) -> Option<&Mime> {
self.mime_type.as_ref()
}
}
impl<T> From<T> for InputFileReader
where
T: AsyncRead + Send + Sync + Unpin + 'static,
{
fn from(reader: T) -> Self {
InputFileReader::new(reader)
}
}
impl PartialEq for InputFileReader {
fn eq(&self, other: &Self) -> bool {
self.file_name.eq(&other.file_name) && self.mime_type.eq(&other.mime_type)
}
}
impl fmt::Debug for InputFileReader {
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
out.debug_struct("InputFileReader")
.field("file_name", &self.file_name)
.field("mime_type", &self.mime_type)
.finish()
}
}
impl WriteForm for InputFileReader {
type Output = String;
fn write(self, form: &mut Form) -> Self::Output {
let Self {
file_name,
mime_type,
reader,
} = self;
let idx = form.len() + 1;
let id = format!("tgbot_file_{idx}");
form.insert_field(
&id,
FormValue::File {
name: file_name,
mime_type,
reader,
},
);
format!("attach://{id}")
}
}
#[derive(Debug, PartialEq)]
pub enum InputFile {
Id(String),
Url(String),
Reader(InputFileReader),
}
impl InputFile {
pub fn file_id<T>(file_id: T) -> Self
where
T: Into<String>,
{
Self::Id(file_id.into())
}
pub fn url<T>(url: T) -> Self
where
T: Into<String>,
{
Self::Url(url.into())
}
pub async fn path(path: impl AsRef<Path>) -> IoResult<Self> {
let path = path.as_ref();
let file = fs::File::open(path).await?;
let mut reader = InputFileReader::new(file);
if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
let mime_type = path
.extension()
.and_then(|x| x.to_str())
.and_then(|x| mime_guess::from_ext(x).first())
.unwrap_or(APPLICATION_OCTET_STREAM);
reader = reader.with_file_name(file_name).with_mime_type(mime_type);
}
Ok(reader.into())
}
}
impl<T> From<T> for InputFile
where
T: Into<InputFileReader>,
{
fn from(reader: T) -> Self {
InputFile::Reader(reader.into())
}
}
impl WriteForm for InputFile {
type Output = String;
fn write(self, form: &mut Form) -> Self::Output {
match self {
Self::Id(text) | Self::Url(text) => text,
Self::Reader(reader) => reader.write(form),
}
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::*;
#[tokio::test]
async fn input_file() {
let id = InputFile::file_id("file-id");
assert_eq!(format!("{id:?}"), r#"Id("file-id")"#);
let url = InputFile::url("http://example.com/archive.zip");
assert_eq!(format!("{url:?}"), r#"Url("http://example.com/archive.zip")"#);
let path = InputFile::path("LICENSE").await.unwrap();
assert_eq!(
format!("{path:?}"),
r#"Reader(InputFileReader { file_name: Some("LICENSE"), mime_type: Some("application/octet-stream") })"#,
);
let reader = InputFileReader::from(Cursor::new(b"data"))
.with_file_name("name")
.with_mime_type(mime::TEXT_PLAIN);
assert_eq!(reader.file_name().unwrap(), "name");
assert_eq!(reader.mime_type().unwrap(), &mime::TEXT_PLAIN);
let reader = InputFile::from(reader);
assert_eq!(
format!("{reader:?}"),
r#"Reader(InputFileReader { file_name: Some("name"), mime_type: Some("text/plain") })"#,
);
let reader = InputFile::from(Cursor::new(b"data"));
assert_eq!(
format!("{reader:?}"),
"Reader(InputFileReader { file_name: None, mime_type: None })",
);
}
}