diff --git a/Cargo.toml b/Cargo.toml index acd0c95..e4d19da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "cloudinary" description = "A Rust library for the Cloudinary API" license = "MIT OR Apache-2.0" keywords = ["cloudinary", "api", "image", "video", "upload"] -version = "0.5.1" +version = "0.5.2" edition = "2021" rust-version = "1.65.0" # due to let-else @@ -25,6 +25,6 @@ url = "2.5.2" dotenv = "0.15.0" pretty_assertions = "1.4.1" -# for minimal-versions +# for minimal-versions [target.'cfg(any())'.dependencies] openssl = { version = "0.10.59", optional = true } # needed to allow foo to build with -Zminimal-versions diff --git a/readme.md b/readme.md index a362a6e..a388ebe 100644 --- a/readme.md +++ b/readme.md @@ -15,7 +15,6 @@ Upload can be done from different sources: - remote file - data url [rfc2397](https://datatracker.ietf.org/doc/html/rfc2397) - ### Local file ```rust @@ -32,7 +31,7 @@ use cloudinary::upload::{UploadOptions, Source, Upload}; let image_url = "https://upload.wikimedia.org/wikipedia/commons/c/ca/1x1.png"; let options = UploadOptions::new().set_public_id("1x1.png".to_string()); let upload = Upload::new("api_key".to_string(), "cloud_name".to_string(), "api_secret".to_string() ); -let result = upload.image(Source::Url(image_url.try_into().unwrap(), &options); +let result = upload.image(Source::Url(image_url.try_into().unwrap()), &options); ``` ### Data url @@ -45,6 +44,12 @@ let upload = Upload::new("api_key".to_string(), "cloud_name".to_string(), "api_s let result = upload.image(Source::DataUrl(data_url.into()), &options); ``` +## Destroy an asset by publicID +```rust +use cloudinary::upload::Upload; +let upload = Upload::new("api_key".to_string(), "cloud_name".to_string(), "api_secret".to_string() ); +let result = upload.destroy("publicID"); +``` ## Transform an image diff --git a/src/lib.rs b/src/lib.rs index 8a15d11..3699722 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,14 @@ //! //! # Upload an image //! +//! Upload can be done from different sources: +//! +//! - local file +//! - remote file +//! - data url [rfc2397](https://datatracker.ietf.org/doc/html/rfc2397) +//! +//! ## Local file +//! //! ```rust //! use cloudinary::upload::{UploadOptions, Source, Upload}; //! let options = UploadOptions::new().set_public_id("file.jpg".to_string()); @@ -14,6 +22,33 @@ //! let result = upload.image(Source::Path("./image.jpg".into()), &options); //! ``` //! +//! ## Remote file +//! +//! ```rust +//! use cloudinary::upload::{UploadOptions, Source, Upload}; +//! let image_url = "https://upload.wikimedia.org/wikipedia/commons/c/ca/1x1.png"; +//! let options = UploadOptions::new().set_public_id("1x1.png".to_string()); +//! let upload = Upload::new("api_key".to_string(), "cloud_name".to_string(), "api_secret".to_string() ); +//! let result = upload.image(Source::Url(image_url.try_into().unwrap()), &options); +//! ``` +//! +//! ## Data url +//! +//! ```rust +//! use cloudinary::upload::{UploadOptions, Source, Upload}; +//! let data_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; +//! let options = UploadOptions::new().set_public_id("1x1.png".to_string()); +//! let upload = Upload::new("api_key".to_string(), "cloud_name".to_string(), "api_secret".to_string() ); +//! let result = upload.image(Source::DataUrl(data_url.into()), &options); +//! ``` +//! +//! # Destroy an asset by publicID +//! ```rust +//! use cloudinary::upload::Upload; +//! let upload = Upload::new("api_key".to_string(), "cloud_name".to_string(), "api_secret".to_string() ); +//! let result = upload.destroy("publicID"); +//! ``` +//! //! # Transform an image //! //! Currently supported transformations: diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 1ba0bb1..80aacfc 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,4 +1,5 @@ use dotenv::dotenv; +use pretty_assertions::assert_eq; use std::env::var; use crate::{ @@ -80,3 +81,38 @@ async fn test_image_upload_from_path() { Error(err) => panic!("{}", err.error.message), } } + +#[tokio::test] +async fn test_destroy_non_existing_asset() { + let (api_key, cloud_name, api_secret) = env(); + let cloudinary = Upload::new(api_key, cloud_name, api_secret); + let public_id = "random-1239290r29-does-it-exists-3we97pcsdlncdsa"; + + let res = cloudinary.destroy(public_id).await.unwrap(); + + assert_eq!(res.result, "not found") +} + +#[tokio::test] +async fn test_destroy_existing_asset() { + let (api_key, cloud_name, api_secret) = env(); + let cloudinary = Upload::new(api_key, cloud_name, api_secret); + let image_path = "./assets/1x1.png"; + let public_id = format!("asset_to_destroy_{}", chrono::Utc::now().timestamp_micros()); + + let options = UploadOptions::new() + .set_public_id(public_id.clone()) + .set_overwrite(true); + let res = cloudinary + .image(Source::Path(image_path.into()), &options) + .await + .unwrap(); + + match res { + Success(_) => { + let res = cloudinary.destroy(public_id).await.unwrap(); + assert_eq!(res.result, "ok") + } + Error(err) => panic!("{}", err.error.message), + } +} diff --git a/src/upload/mod.rs b/src/upload/mod.rs index b1a0327..c93e437 100644 --- a/src/upload/mod.rs +++ b/src/upload/mod.rs @@ -14,6 +14,7 @@ use chrono::Utc; use itertools::Itertools; use reqwest::multipart::{Form, Part}; use reqwest::{Body, Client, Url}; +use result::DestroyResult; use sha1::{Digest, Sha1}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::PathBuf; @@ -50,7 +51,8 @@ impl Upload { } } - /// uploads an image + /// Uploads an image + /// /// ```rust /// use cloudinary::upload::{UploadOptions, Source, Upload}; /// let options = UploadOptions::new().set_public_id("file.jpg".to_string()); @@ -80,33 +82,69 @@ impl Upload { Ok(json) } + /// Destroy the asset by public ID. + /// + /// ```rust + /// use cloudinary::upload::{UploadOptions, Source, Upload}; + /// let upload = Upload::new("api_key".to_string(), "cloud_name".to_string(), "api_secret".to_string() ); + /// let result = upload.destroy("image"); + /// ``` + pub async fn destroy(&self, public_id: IS) -> Result + where + IS: Into + Clone, + { + let client = Client::new(); + + let mut options = UploadOptions::new() + .set_public_id(public_id.clone().into()) + .get_map(); + + self.sign(&mut options); + + let url = format!( + "https://api.cloudinary.com/v1_1/{}/image/destroy", + self.cloud_name + ); + let response = client + .post(&url) + .form(&options) + .send() + .await + .context(format!("destroy {}", public_id.into()))?; + let text = response.text().await?; + let json = serde_json::from_str(&text).context(format!("failed to parse:\n\n {}", text))?; + Ok(json) + } + fn build_form_data(&self, options: &UploadOptions) -> Form { let mut map = options.get_map(); - let resource_type = map.remove("resource_type"); - let timestamp = Utc::now().timestamp_millis().to_string(); + self.sign(&mut map); - let mut form = Form::new() - .text("api_key", self.api_key.clone()) - .text("timestamp", timestamp.clone()); + let mut form = Form::new(); - if let Some(value) = resource_type { - form = form.text("resource_type", value); + for (k, v) in map.iter() { + form = form.text(k.clone(), v.clone()); } + form + } + fn sign(&self, map: &mut BTreeMap) { + let resource_type = map.remove("resource_type"); let str = map.iter().map(|(k, v)| format!("{k}={v}")).join("&"); let mut hasher = Sha1::new(); if !str.is_empty() { hasher.update(str); hasher.update("&"); } + let timestamp = Utc::now().timestamp_millis().to_string(); hasher.update(format!("timestamp={}{}", timestamp, self.api_secret)); - let signature = hasher.finalize(); + map.insert("signature".to_string(), format!("{:x}", hasher.finalize())); + map.insert("api_key".to_string(), self.api_key.clone()); + map.insert("timestamp".to_string(), timestamp); - form = form.text("signature", format!("{:x}", signature)); - for (k, v) in map.iter() { - form = form.text(k.clone(), v.clone()); + if let Some(resource_type) = resource_type { + map.insert("resource_type".to_string(), resource_type); } - form } } diff --git a/src/upload/result.rs b/src/upload/result.rs index c736953..3e195ae 100644 --- a/src/upload/result.rs +++ b/src/upload/result.rs @@ -55,3 +55,8 @@ pub struct Response { pub original_extension: Option, pub api_key: String, } + +#[derive(Clone, Deserialize, Debug)] +pub struct DestroyResult { + pub result: String, +}