From f543cc371f8918159e1ed9a4bacd1b8d7f131feb Mon Sep 17 00:00:00 2001 From: Waffle Date: Fri, 20 Sep 2019 23:28:58 +0300 Subject: [PATCH] Implement file downloading (and also add `Bot::get_file`) --- Cargo.toml | 3 +- src/bot/mod.rs | 69 +++++++++++++++++++++++++++++++++++++++++ src/core/mod.rs | 2 +- src/core/network/mod.rs | 35 +++++++++++++++++++++ src/errors.rs | 17 ++++++++++ src/lib.rs | 3 ++ 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/errors.rs diff --git a/Cargo.toml b/Cargo.toml index d4c2b459..b928b697 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,5 @@ lazy_static = "1.3" apply = "0.2.2" derive_more = "0.15.0" tokio = "0.2.0-alpha.4" -bytes = "0.4.12" \ No newline at end of file +bytes = "0.4.12" +futures-preview = "0.3.0-alpha.18" \ No newline at end of file diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 1a818920..bb55f28a 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -1,5 +1,7 @@ use reqwest::r#async::Client; +use crate::core::network::{download_file, download_file_stream}; +use crate::core::requests::get_file::GetFile; use crate::core::{ requests::{ edit_message_live_location::EditMessageLiveLocation, @@ -11,6 +13,10 @@ use crate::core::{ }, types::{InputFile, InputMedia}, }; +use crate::DownloadError; +use reqwest::r#async::Chunk; +use tokio::io::AsyncWrite; +use tokio::stream::Stream; pub struct Bot { token: String, @@ -42,6 +48,62 @@ impl Bot { /// Telegram functions impl Bot { + /// Download file from telegram into `destination`. + /// `path` can be obtained from [`get_file`] method. + /// + /// For downloading as Stream of Chunks see [`download_file_stream`]. + /// + /// ## Examples + /// + /// ```no_run + /// use async_telegram_bot::{ + /// bot::Bot, + /// core::{requests::Request, types::File as TgFile}, + /// }; + /// use tokio::fs::File; + /// # use async_telegram_bot::core::requests::RequestError; + /// + /// # async fn run() -> Result<(), Box> { + /// let bot = Bot::new("TOKEN"); + /// let mut file = File::create("/home/waffle/Pictures/test.png").await?; + /// + /// let TgFile { file_path, .. } = bot.get_file("*file_id*").send().await?; + /// bot.download_file(&file_path, &mut file).await?; + /// # Ok(()) } + /// ``` + /// + /// [`get_file`]: crate::bot::Bot::get_file + /// [`download_file_stream`]: crate::bot::Bot::download_file_stream + pub async fn download_file( + &self, + path: &str, + destination: &mut D, + ) -> Result<(), DownloadError> + where + D: AsyncWrite + Unpin, + { + download_file(&self.client, &self.token, path, destination).await + } + + /// Download file from telegram. + /// + /// `path` can be obtained from [`get_file`] method. + /// + /// For downloading into [`AsyncWrite`] (e.g. [`tokio::fs::File`]) + /// see [`download_file`]. + /// + /// [`get_file`]: crate::bot::Bot::get_file + /// [`AsyncWrite`]: tokio::io::AsyncWrite + /// [`tokio::fs::File`]: tokio::fs::File + /// [`download_file`]: crate::bot::Bot::download_file + pub async fn download_file_stream( + &self, + path: &str, + ) -> Result>, reqwest::Error> + { + download_file_stream(&self.client, &self.token, path).await + } + pub fn get_me(&self) -> GetMe { GetMe::new(self.ctx()) } @@ -135,4 +197,11 @@ impl Bot { pub fn stop_message_live_location(&self) -> StopMessageLiveLocation { StopMessageLiveLocation::new(self.ctx()) } + + pub fn get_file(&self, file_id: F) -> GetFile + where + F: Into, + { + GetFile::new(self.ctx(), file_id.into()) + } } diff --git a/src/core/mod.rs b/src/core/mod.rs index 8fd20ac7..bf462606 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,3 +1,3 @@ -mod network; +pub(crate) mod network; pub mod requests; pub mod types; diff --git a/src/core/network/mod.rs b/src/core/network/mod.rs index b890a173..a1a482ef 100644 --- a/src/core/network/mod.rs +++ b/src/core/network/mod.rs @@ -3,12 +3,19 @@ use crate::core::{ types::ResponseParameters, }; +use crate::DownloadError; use apply::Apply; +use bytes::Buf; +use futures::StreamExt; +use reqwest::r#async::Chunk; use reqwest::{ r#async::{multipart::Form, Client}, StatusCode, }; use serde::{de::DeserializeOwned, Serialize}; +use tokio::io::AsyncWriteExt; +use tokio::prelude::AsyncWrite; +use tokio::stream::Stream; const TELEGRAM_API_URL: &str = "https://api.telegram.org"; @@ -102,6 +109,34 @@ enum TelegramResponse { }, } +pub async fn download_file( + client: &Client, + token: &str, + path: &str, + destination: &mut D, +) -> Result<(), DownloadError> +where + D: AsyncWrite + Unpin, +{ + let mut stream = download_file_stream(client, token, path).await?; + + while let Some(chunk) = stream.next().await { + destination.write_all(chunk?.bytes()).await?; + } + + Ok(()) +} + +pub(crate) async fn download_file_stream( + client: &Client, + token: &str, + path: &str, +) -> Result>, reqwest::Error> { + let url = file_url(TELEGRAM_API_URL, token, path); + let resp = client.get(&url).send().await?.error_for_status()?; + Ok(resp.into_body()) +} + impl Into> for TelegramResponse { fn into(self) -> Result { match self { diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 00000000..720d2d74 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,17 @@ +#[derive(Debug, Display, From)] +pub enum DownloadError { + #[display(fmt = "Network error: {err}", err = _0)] + NetworkError(reqwest::Error), + + #[display(fmt = "IO Error: {err}", err = _0)] + Io(std::io::Error), +} + +impl std::error::Error for DownloadError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + DownloadError::NetworkError(err) => Some(err), + DownloadError::Io(err) => Some(err), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7425f248..b916367d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,6 @@ extern crate serde; pub mod bot; pub mod core; +pub mod errors; + +pub use errors::DownloadError;