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 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 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}