Skip to content

Commit

Permalink
Merge pull request #36 from Lurk/delete
Browse files Browse the repository at this point in the history
destroy asset by public ID
  • Loading branch information
Lurk authored Oct 7, 2024
2 parents d6f682c + 5e1b50d commit 39f3a86
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 17 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
9 changes: 7 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
35 changes: 35 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,48 @@
//!
//! # 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());
//! let upload = Upload::new("api_key".to_string(), "cloud_name".to_string(), "api_secret".to_string() );
//! 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:
Expand Down
36 changes: 36 additions & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use dotenv::dotenv;
use pretty_assertions::assert_eq;
use std::env::var;

use crate::{
Expand Down Expand Up @@ -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),
}
}
64 changes: 51 additions & 13 deletions src/upload/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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<IS>(&self, public_id: IS) -> Result<DestroyResult>
where
IS: Into<String> + 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<String, String>) {
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
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/upload/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@ pub struct Response {
pub original_extension: Option<String>,
pub api_key: String,
}

#[derive(Clone, Deserialize, Debug)]
pub struct DestroyResult {
pub result: String,
}

0 comments on commit 39f3a86

Please sign in to comment.