dingtalk_stream/client/stream_/
download_resources.rs1use crate::frames::down_message::callback_message::{PayloadAudio, PayloadFile, PayloadPicture, PayloadVideo};
2use crate::DingTalkStream;
3use anyhow::anyhow;
4use async_trait::async_trait;
5use log::info;
6use serde_json::json;
7use std::ops::Deref;
8use std::path::PathBuf;
9use url::Url;
10
11#[async_trait]
12pub trait DingtalkResource {
13 type T;
14 async fn fetch(
15 &self,
16 dingtalk: &DingTalkStream,
17 save_to: PathBuf,
18 ) -> crate::Result<(PathBuf, Self::T)>;
19}
20
21#[cfg(feature = "image")]
22type Picture = image::DynamicImage;
23
24#[cfg(not(feature = "image"))]
25type Picture = Vec<u8>;
26
27#[async_trait]
28impl DingtalkResource for PayloadPicture {
29 type T = Picture;
30
31 async fn fetch(
32 &self,
33 dingtalk: &DingTalkStream,
34 save_to_dir: PathBuf,
35 ) -> crate::Result<(PathBuf, Self::T)> {
36 if !save_to_dir.exists() {
37 tokio::fs::create_dir_all(&save_to_dir).await?;
38 }
39 if save_to_dir.is_file() {
40 return Err(anyhow!("save_to_dir is a file"));
41 }
42 let filepath = save_to_dir.join(format!(
43 "{}.png",
44 format!("{:x}", md5::compute(&self.download_code))
45 ));
46 if filepath.exists() {
47 let bytes = tokio::fs::read(&filepath).await?;
48 #[cfg(feature = "image")]
49 return Ok((filepath, image::load_from_memory(&bytes)?));
50 #[cfg(not(feature = "image"))]
51 return Ok((filepath, bytes));
52 }
53 let download_url = fetch_download_url(dingtalk, &self.download_code).await?;
54 let bytes = dingtalk
55 .http_client
56 .get(download_url)
57 .send()
58 .await?
59 .bytes()
60 .await?;
61
62 #[cfg(feature = "image")]
63 let image = {
64 use std::io::Cursor;
65 let image = image::load_from_memory(&bytes)?;
66 let mut cursor = Cursor::new(vec![]);
67 image.write_to(&mut cursor, image::ImageFormat::Png)?;
68 tokio::fs::write(&filepath, cursor.into_inner()).await?;
69 image
70 };
71 #[cfg(not(feature = "image"))]
72 let image = {
73 tokio::fs::write(&filepath, bytes.as_ref()).await?;
74 bytes.to_vec()
75 };
76 info!("Downloaded image to {}", filepath.display());
77 Ok((filepath, image))
78 }
79}
80
81#[async_trait]
82impl DingtalkResource for PayloadVideo {
83 type T = Vec<u8>;
84
85 async fn fetch(
86 &self,
87 dingtalk: &DingTalkStream,
88 save_to_dir: PathBuf,
89 ) -> crate::Result<(PathBuf, Self::T)> {
90 if !save_to_dir.exists() {
91 tokio::fs::create_dir_all(&save_to_dir).await?;
92 }
93 if save_to_dir.is_file() {
94 return Err(anyhow!("save_to_dir is a file"));
95 }
96 let filepath = save_to_dir.join(format!(
97 "{}.{}",
98 format!("{:x}", md5::compute(&self.download_code)),
99 self.video_type
100 ));
101 if filepath.exists() {
102 let bytes = tokio::fs::read(&filepath).await?;
103 return Ok((filepath, bytes));
104 }
105 let download_url = fetch_download_url(dingtalk, &self.download_code).await?;
106 let bytes = dingtalk
107 .http_client
108 .get(download_url)
109 .send()
110 .await?
111 .bytes()
112 .await?;
113 tokio::fs::write(&filepath, bytes.as_ref()).await?;
114 info!(
115 "Downloaded {} video to {}",
116 self.download_code,
117 filepath.display()
118 );
119 Ok((filepath, bytes.to_vec()))
120 }
121}
122
123#[async_trait]
124impl DingtalkResource for PayloadAudio {
125 type T = Vec<u8>;
126
127 async fn fetch(
128 &self,
129 dingtalk: &DingTalkStream,
130 save_to_dir: PathBuf,
131 ) -> crate::Result<(PathBuf, Self::T)> {
132 if !save_to_dir.exists() {
133 tokio::fs::create_dir_all(&save_to_dir).await?;
134 }
135 if save_to_dir.is_file() {
136 return Err(anyhow!("save_to_dir is a file"));
137 }
138 let filepath = save_to_dir.join(format!(
139 "{}.mp3",
140 format!("{:x}", md5::compute(&self.download_code)),
141 ));
142 if filepath.exists() {
143 let bytes = tokio::fs::read(&filepath).await?;
144 return Ok((filepath, bytes));
145 }
146 let download_url = fetch_download_url(dingtalk, &self.download_code).await?;
147 let bytes = dingtalk
148 .http_client
149 .get(download_url)
150 .send()
151 .await?
152 .bytes()
153 .await?;
154 tokio::fs::write(&filepath, bytes.as_ref()).await?;
155 info!(
156 "Downloaded {} audio to {}",
157 self.download_code,
158 filepath.display()
159 );
160 Ok((filepath, bytes.to_vec()))
161 }
162}
163
164#[async_trait]
165impl DingtalkResource for PayloadFile {
166 type T = Vec<u8>;
167
168 async fn fetch(
169 &self,
170 dingtalk: &DingTalkStream,
171 save_to_dir: PathBuf,
172 ) -> crate::Result<(PathBuf, Self::T)> {
173 if !save_to_dir.exists() {
174 tokio::fs::create_dir_all(&save_to_dir).await?;
175 }
176 if save_to_dir.is_file() {
177 return Err(anyhow!("save_to_dir is a file"));
178 }
179 let filepath = save_to_dir.join(format!(
180 "{}_{}",
181 format!("{:x}", md5::compute(&self.download_code)),
182 self.file_name
183 ));
184 if filepath.exists() {
185 let bytes = tokio::fs::read(&filepath).await?;
186 return Ok((filepath, bytes));
187 }
188 let download_url = fetch_download_url(dingtalk, &self.download_code).await?;
189 let bytes = dingtalk
190 .http_client
191 .get(download_url)
192 .send()
193 .await?
194 .bytes()
195 .await?;
196 tokio::fs::write(&filepath, bytes.as_ref()).await?;
197 info!(
198 "Downloaded {} file to {}",
199 self.download_code,
200 filepath.display()
201 );
202 Ok((filepath, bytes.to_vec()))
203 }
204}
205
206async fn fetch_download_url(dingtalk: &DingTalkStream, download_code: &str) -> crate::Result<Url> {
207 let access_token = dingtalk.get_access_token().await?;
208 let response = dingtalk
209 .http_client
210 .post(crate::MESSAGE_FILES_DOWNLOAD_URL)
211 .header("x-acs-dingtalk-access-token", access_token.deref())
212 .header("Content-Type", "application/json")
213 .json(&json!({
214 "robotCode": &dingtalk.credential.client_id,
215 "downloadCode": download_code,
216 }))
217 .send()
218 .await?;
219 let code = response.status();
220 if response.status().is_success() {
221 let json = response.json::<serde_json::Value>().await?;
222 let download_url = json.get("downloadUrl").and_then(|it| it.as_str());
223 info!(
224 "Get download url by download_code: {download_code}, download_url: {}",
225 download_url.unwrap_or("None")
226 );
227 Ok(download_url
228 .ok_or(anyhow!("download_url is not found"))?
229 .try_into()?)
230 } else {
231 Err(anyhow!(
232 "Failed to download file with unexpected http-code: {}, download_code: {}",
233 code,
234 download_code
235 ))
236 }
237}