Skip to content

Commit

Permalink
upload from url (#8)
Browse files Browse the repository at this point in the history
Upload from URL.
  • Loading branch information
Lurk authored Oct 15, 2023
1 parent af13785 commit f9c3351
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 32 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/scheduled.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ jobs:
if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile
- name: cargo test --locked
env:
CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }}
CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}
CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }}
run: cargo test --locked --all-features --all-targets
# https://twitter.com/alcuadrado/status/1571291687837732873
update:
Expand All @@ -50,3 +54,6 @@ jobs:
run: cargo test --locked --all-features --all-targets
env:
RUSTFLAGS: -D deprecated
CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }}
CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}
CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }}
20 changes: 20 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,17 @@ jobs:
run: cargo generate-lockfile
# https://twitter.com/jonhoo/status/1571290371124260865
- name: cargo test --locked
env:
CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }}
CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}
CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }}
run: cargo test --locked --all-features --all-targets
# https://github.com/rust-lang/cargo/issues/6669
- name: cargo test --doc
env:
CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }}
CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}
CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }}
run: cargo test --locked --all-features --doc
minimal:
runs-on: ubuntu-latest
Expand All @@ -49,6 +57,10 @@ jobs:
- name: cargo update -Zminimal-versions
run: cargo +nightly update -Zminimal-versions
- name: cargo test
env:
CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }}
CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}
CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }}
run: cargo test --locked --all-features --all-targets
os-check:
runs-on: ${{ matrix.os }}
Expand All @@ -67,6 +79,10 @@ jobs:
if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile
- name: cargo test
env:
CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }}
CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}
CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }}
run: cargo test --locked --all-features --all-targets
coverage:
runs-on: ubuntu-latest
Expand All @@ -85,6 +101,10 @@ jobs:
if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile
- name: cargo llvm-cov
env:
CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }}
CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}
CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }}
run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target
/Cargo.lock
.env
34 changes: 19 additions & 15 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@
name = "cloudinary"
description = "A Rust library for the Cloudinary API"
license = "MIT OR Apache-2.0"
keywords = ["cloudinary", "api", "image", "video", "upload" ]
version = "0.1.0"
keywords = ["cloudinary", "api", "image", "video", "upload"]
version = "0.2.0"
edition = "2021"
rust-version = "1.63.0" # due to openssl = { version = "0.10", features = ["vendored"] }
rust-version = "1.63.0" # due to openssl = { version = "0.10", features = ["vendored"] }

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.66"
chrono = "0.4.22"
anyhow = "1.0.75"
chrono = "0.4.31"
itertools = "0.11.0"
mime = "0.3.16"
paste = "1.0.9"
reqwest = { version = "0.11.18", features=["json", "multipart", "stream"]}
serde = { version = "1.0.147", features = ["derive"] }
serde_json = "1.0.87"
sha1 = "0.10.5"
tokio = {version = "1.28.2", features=["full"]}
tokio-util = "0.7.4"
log = "0.4.14"
openssl = { version = "0.10", features = ["vendored"] }
mime = "0.3.17"
paste = "1.0.14"
reqwest = { version = "0.11.22", features = ["json", "multipart", "stream"] }
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107"
sha1 = "0.10.6"
tokio = { version = "1.33.0", features = ["rt", "macros"] }
tokio-util = "0.7.9"
log = "0.4.20"
openssl = { version = "0.10.57", features = ["vendored"] }
openssl-src = "111.28.0+1.1.1w" # due to minimal version fail

[dev-dependencies]
dotenv = "0.15.0"
Binary file added assets/1x1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 27 additions & 17 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ use anyhow::{Context, Result};
use chrono::Utc;
use itertools::Itertools;
use reqwest::multipart::{Form, Part};
use reqwest::{Body, Client};
use reqwest::{Body, Client, Url};
use sha1::{Digest, Sha1};
use std::path::Path;
use std::path::PathBuf;
use tokio::fs::File;
use tokio_util::codec::{BytesCodec, FramedRead};
use upload::{result::UploadResult, UploadOptions};
Expand All @@ -17,6 +17,11 @@ pub struct Cloudinary {
api_secret: String,
}

pub enum Source {
Path(PathBuf),
Url(Url),
}

impl Cloudinary {
pub fn new(api_key: String, cloud_name: String, api_secret: String) -> Self {
Cloudinary {
Expand All @@ -28,27 +33,33 @@ impl Cloudinary {

/// uploads an image
/// ```rust
/// use cloudinary::{Cloudinary, upload::UploadOptions};
/// use cloudinary::{Source, Cloudinary};
/// use cloudinary::upload::{UploadOptions};
/// let options = UploadOptions::new().set_public_id("file.jpg".to_string());
/// let cloudinary = Cloudinary::new("api_key".to_string(), "cloud_name".to_string(), "api_secret".to_string() );
/// let result = cloudinary.upload_image("./image.jpg".to_string(), &options);
/// let result = cloudinary.upload_image(Source::Path("./image.jpg".into()), &options);
/// ```
pub async fn upload_image(
&self,
src: String,
src: Source,
options: &UploadOptions<'_>,
) -> Result<UploadResult> {
let client = Client::new();
let file = prepare_file(&src).await?;
let file = match src {
Source::Path(path) => prepare_file(&path).await?,
Source::Url(url) => Part::text(url.as_str().to_string()),
};
let multipart = self.build_form_data(options).part("file", file);
let url = format!(
"https://api.cloudinary.com/v1_1/{}/image/upload",
self.cloud_name
);
let response = client
.post(format!(
"https://api.cloudinary.com/v1_1/{}/image/upload",
self.cloud_name
))
.post(&url)
.multipart(multipart)
.send()
.await?;
.await
.context(format!("upload to {}", url))?;
let text = response.text().await?;
let json = serde_json::from_str(&text).context(format!("failed to parse:\n\n {}", text))?;
Ok(json)
Expand Down Expand Up @@ -84,18 +95,17 @@ impl Cloudinary {
}
}

async fn prepare_file(src: &str) -> Result<Part> {
async fn prepare_file(src: &PathBuf) -> Result<Part> {
let file = File::open(&src).await?;

let filename = Path::new(src)
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
let filename = src.file_name().unwrap().to_string_lossy().into_owned();

let stream = FramedRead::new(file, BytesCodec::new());
let file_body = Body::wrap_stream(stream);
Ok(Part::stream(file_body)
.file_name(filename)
.mime_str("image/*")?)
}

#[cfg(test)]
mod tests;
58 changes: 58 additions & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use dotenv::dotenv;
use std::env::var;

use crate::{
upload::result::UploadResult::{Error, Success},
upload::UploadOptions,
Cloudinary, Source,
};

#[tokio::test]
async fn test_image_upload_from_url() {
dotenv().ok();
let api_secret = var("CLOUDINARY_API_SECRET").expect("enviroment variables not set");
let api_key = var("CLOUDINARY_API_KEY").expect("enviroment variables not set");
let cloud_name = var("CLOUDINARY_CLOUD_NAME").expect("enviroment variables not set");

let cloudinary = Cloudinary::new(api_key, cloud_name, api_secret);
let image_url = "https://upload.wikimedia.org/wikipedia/commons/c/ca/1x1.png";
let public_id = "image_upload_from_url";

let options = UploadOptions::new()
.set_public_id(String::from(public_id))
.set_overwrite(true);
let res = cloudinary
.upload_image(Source::Url(image_url.try_into().unwrap()), &options)
.await
.unwrap();

match res {
Success(img) => assert_eq!(img.public_id, public_id),
Error(err) => panic!("{}", err.error.message),
}
}

#[tokio::test]
async fn test_image_upload_from_path() {
dotenv().ok();
let api_secret = var("CLOUDINARY_API_SECRET").expect("enviroment variables not set");
let api_key = var("CLOUDINARY_API_KEY").expect("enviroment variables not set");
let cloud_name = var("CLOUDINARY_CLOUD_NAME").expect("enviroment variables not set");

let cloudinary = Cloudinary::new(api_key, cloud_name, api_secret);
let image_path = "./assets/1x1.png";
let public_id = "image_upload_from_path";

let options = UploadOptions::new()
.set_public_id(String::from(public_id))
.set_overwrite(true);
let res = cloudinary
.upload_image(Source::Path(image_path.try_into().unwrap()), &options)
.await
.unwrap();

match res {
Success(img) => assert_eq!(img.public_id, public_id),
Error(err) => panic!("{}", err.error.message),
}
}

0 comments on commit f9c3351

Please sign in to comment.