cloudinary/upload/
mod.rs

1mod access_mode;
2mod allowed_headers;
3mod background_removal;
4mod categorizations;
5mod delivery_type;
6pub mod moderation;
7mod options;
8mod raw_convert;
9mod resource_type;
10mod responsive_breakpoints;
11pub mod result;
12
13use anyhow::{Context, Result};
14use chrono::Utc;
15use reqwest::multipart::{Form, Part};
16use reqwest::{Body, Client, Url};
17use result::DestroyResult;
18use sha1::{Digest, Sha1};
19use std::collections::BTreeSet;
20use std::path::PathBuf;
21use tokio::fs::File;
22use tokio_util::codec::{BytesCodec, FramedRead};
23
24pub use self::result::UploadResult;
25pub use self::{
26    access_mode::AccessModes, allowed_headers::AllowedHeaders,
27    background_removal::BackgroundRemoval, categorizations::Categorizations,
28    delivery_type::DeliveryType, moderation::Moderation, options::OptionalParameters,
29    raw_convert::RawConvert, resource_type::ResourceTypes,
30    responsive_breakpoints::ResponsiveBreakpoints,
31};
32
33pub struct Upload {
34    cloud_name: String,
35    api_key: String,
36    api_secret: String,
37}
38
39pub enum Source {
40    Path(PathBuf),
41    Url(Url),
42    DataUrl(String),
43}
44
45impl Upload {
46    pub fn new(api_key: String, cloud_name: String, api_secret: String) -> Self {
47        Upload {
48            api_key,
49            api_secret,
50            cloud_name,
51        }
52    }
53
54    /// Uploads an image
55    ///
56    /// ```rust
57    /// use std::collections::BTreeSet;
58    /// use cloudinary::upload::{Source, Upload, OptionalParameters};
59    ///
60    /// let upload = Upload::new("api_key".to_string(), "cloud_name".to_string(), "api_secret".to_string() );
61    /// let options = BTreeSet::from([OptionalParameters::PublicId("file.jpg".to_string())]);
62    /// let result = upload.image(Source::Path("./image.jpg".into()), &options);
63    /// ```
64    pub async fn image(
65        &self,
66        src: Source,
67        options: &BTreeSet<OptionalParameters>,
68    ) -> Result<UploadResult> {
69        let client = Client::new();
70        let file = match src {
71            Source::Path(path) => prepare_file(&path).await?,
72            Source::Url(url) => Part::text(url.as_str().to_string()),
73            Source::DataUrl(base64) => Part::text(base64),
74        };
75        let multipart = self.build_form(options).part("file", file);
76        let url = format!(
77            "https://api.cloudinary.com/v1_1/{}/image/upload",
78            self.cloud_name
79        );
80        let response = client
81            .post(&url)
82            .multipart(multipart)
83            .send()
84            .await
85            .context(format!("upload to {}", url))?;
86        let text = response.text().await?;
87        let json = serde_json::from_str(&text).context(format!("failed to parse:\n\n {}", text))?;
88        Ok(json)
89    }
90
91    /// destroy the asset by public id.
92    ///
93    /// ```rust
94    /// use cloudinary::upload::{Source, Upload};
95    /// let upload = Upload::new("api_key".to_string(), "cloud_name".to_string(), "api_secret".to_string() );
96    /// let result = upload.destroy("image");
97    /// ```
98    pub async fn destroy<IS>(&self, public_id: IS) -> Result<DestroyResult>
99    where
100        IS: Into<String> + Clone,
101    {
102        let client = Client::new();
103
104        let url = format!(
105            "https://api.cloudinary.com/v1_1/{}/image/destroy",
106            self.cloud_name
107        );
108        let response = client
109            .post(&url)
110            .multipart(
111                self.build_form(&BTreeSet::from([OptionalParameters::PublicId(
112                    public_id.clone().into(),
113                )])),
114            )
115            .send()
116            .await
117            .context(format!("destroy {}", public_id.into()))?;
118        let text = response.text().await?;
119        let json = serde_json::from_str(&text).context(format!("failed to parse:\n\n {}", text))?;
120        Ok(json)
121    }
122
123    fn build_form(&self, options: &BTreeSet<OptionalParameters>) -> Form {
124        let mut form = Form::new();
125        let mut hasher = Sha1::new();
126        let timestamp = Utc::now().timestamp_millis().to_string();
127
128        for option in options {
129            let (key, value) = option.get_pair();
130            if key != "resource_type" {
131                hasher.update(option.to_string());
132                hasher.update("&");
133            };
134
135            form = form.text(key, value);
136        }
137
138        hasher.update(format!("timestamp={}{}", timestamp, self.api_secret));
139
140        form = form.text("signature", format!("{:x}", hasher.finalize()));
141        form = form.text("api_key", self.api_key.clone());
142        form = form.text("timestamp", timestamp.clone());
143
144        form
145    }
146}
147
148async fn prepare_file(src: &PathBuf) -> Result<Part> {
149    let file = File::open(&src).await?;
150
151    let filename = src.file_name().unwrap().to_string_lossy().into_owned();
152
153    let stream = FramedRead::new(file, BytesCodec::new());
154    let file_body = Body::wrap_stream(stream);
155    Ok(Part::stream(file_body)
156        .file_name(filename)
157        .mime_str("image/*")?)
158}