Skip to main content

dingtalk_stream/client/stream_/
download_resources.rs

1use crate::frames::down_message::callback_message::{PayloadFile, PayloadPicture};
2use crate::DingTalkStream;
3use anyhow::anyhow;
4use async_trait::async_trait;
5use serde_json::json;
6use std::ops::Deref;
7use std::path::PathBuf;
8use log::info;
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 PayloadFile {
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.file_name
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 {} file to {}",
116            self.download_code,
117            filepath.display()
118        );
119        Ok((filepath, bytes.to_vec()))
120    }
121}
122
123async fn fetch_download_url(dingtalk: &DingTalkStream, download_code: &str) -> crate::Result<Url> {
124    let access_token = dingtalk.get_access_token().await?;
125    let response = dingtalk
126        .http_client
127        .post(crate::MESSAGE_FILES_DOWNLOAD_URL)
128        .header("x-acs-dingtalk-access-token", access_token.deref())
129        .header("Content-Type", "application/json")
130        .json(&json!({
131            "robotCode": &dingtalk.credential.client_id,
132            "downloadCode": download_code,
133        }))
134        .send()
135        .await?;
136    let code = response.status();
137    if response.status().is_success() {
138        let json = response.json::<serde_json::Value>().await?;
139        let download_url = json.get("downloadUrl").and_then(|it| it.as_str());
140        info!(
141            "Get download url by download_code: {download_code}, download_url: {}",
142            download_url.unwrap_or("None")
143        );
144        Ok(download_url
145            .ok_or(anyhow!("download_url is not found"))?
146            .try_into()?)
147    } else {
148        Err(anyhow!(
149            "Failed to download file with unexpected http-code: {}, download_code: {}",
150            code,
151            download_code
152        ))
153    }
154}