Uploading videos is a common requirement for modern web applications, and handling this functionality efficiently on the server side is important. Rust‘s performance and safety features are an excellent choice for backend development, while Cloudinary offers powerful cloud-based media management solutions.
This blog post will guide you through uploading a video in a Rust backend with Cloudinary. You’ll learn how to set up your Rust environment, configure Cloudinary, and implement video upload functionality in a few steps.
Cloudinary provides a secure and comprehensive API for quickly uploading media files from server-side code, directly from the browser or a mobile application.
The Github repository is here.
To get the most out of this article, you should have:
- Rust and Cargo installed.
- Basic understanding of Rust.
- Postman or any API testing application installed.
- A free Cloudinary account.
To upload a video to Cloudinary using a Rust backend, create a new Rust project and install the required dependencies. Use the following command to initialize a new project and navigate to the project:
cargo new cloudinary-rust && cd cloudinary-rust
Code language: JavaScript (javascript)
Update the [dependencies]
section of the cargo.toml
file in the project’s root directory to include all the dependencies you’ll use in this tutorial. The dependencies include:
actix-web
. A Rust-based framework for building web applications.actix-multipart
. A library used alongsideactix-web
to handle form data.dotenv
. A library for loading environment variables.reqwest = { version = "0.11", features = ["multipart"] }
. A library for making HTTP requests with support for multipart form data.serde = { version = "1.0.195", features = ["derive"] }
. A framework for serializing and deserializing Rust data structures.serde_json
. A library for parsingJSON
data.tokio = { version = "1", features = ["full"] }
. A runtime for handling asynchronous programming in Rust.futures-util
. A utility and combinator for handling future and asynchronous programming in Rust.mime
. A library for handlingMIME
types in Rust.sha1
. A library for hashing in Rust.hex
. A library for encoding and decoding hexadecimal data.chrono
. A Rust library for handling date and time.tempfile
. A library for creating temporary files and directories.
Next, update the cargo.toml
file with the following dependencies:
[package]
name = "cloudinary-rust"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "4.5"
actix-multipart = "0.6"
dotenv = "0.15.0"
reqwest = { version = "0.11", features = ["multipart"] }
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
tokio = { version = "1", features = ["full"] }
futures-util = "0.3"
mime = "0.3"
sha1 = "0.10"
hex = "0.4"
chrono = "0.4"
tempfile = "3.3"
Code language: PHP (php)
Next, run the command below to install all the dependencies listed in the cargo.toml
file:
cargo run
Cloudinary allows developers to upload, store, manage, optimize, and deliver images and videos. When uploading media assets, Cloudinary provides two major ways to upload:
- Unsigned uploads. Allow uploads to Cloudinary without requiring the generation of authentication signatures.
- Signed uploads. Require a secure signature to authorize each upload request to Cloudinary.
While both upload methods let you upload your media assets to Cloudinary, they differ in use cases and implementation. For this tutorial, we’ll use signed uploads.
Next, retrieve your Cloudinary Cloud name, API key, and API secret from the Cloudinary dashboard.
Log in to your Cloudinary dashboard to get product environment credentials such as the Cloud name, API key, and API secret.
Then, create a .env
file in the root folder of the project using the following command:
For macOS and Linux:
touch .env
Code language: CSS (css)
For Windows(Command Prompt):
type NUL > .env
Code language: CSS (css)
For Windows(PowerShell):
New-Item -Path .env -ItemType File
Code language: CSS (css)
Add your product credentials to the environment file:
CLOUDINARY_CLOUD_NAME="*********************"
CLOUDINARY_API_KEY="*********************"
CLOUDINARY_API_SECRET="*********************"
Code language: JavaScript (javascript)
Replace *********************
with your credentials.
Replace ********************* with your credentials.
You must create an API model for the responses to help define the data structures representing and handling the data exchanged between the backend API and Cloudinary.
Create a file named models.rs
in the src
folder and create CloudinaryResponse
, APIResponse
, and APIErrorResponse
structs to describe the response body from Cloudinary and API responses.
// src/models.rs
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct CloudinaryResponse {
pub public_id: String,
pub secure_url: String,
}
#[derive(Serialize)]
pub struct APIResponse<T> {
pub status: u16,
pub message: String,
pub data: Option<T>,
}
#[derive(Serialize)]
pub struct APIErrorResponse {
pub status: u16,
pub message: String,
pub data: Option<String>,
}
Code language: PHP (php)
Create the API Service
A best practice when creating APIs is to separate the application logic from the API handlers to make your code maintainable, testable, scalable, and reusable.
In this API service, you’ll:
- Import the required dependencies.
- Create a
MAX_SIZE
constant andParamValue enum
to define the maximum size of an uploadable video and parameter types. - Create a
VideoService
struct. - Create an implementation block that includes
env_loader
andgenerate_signature
methods. These methods load environment variables and generate a signature to authorize each upload request. The signature is generated by concatenating the parameters and appending the API secrets.
Create a video_service.rs
file in the src
directory and add the following code for the API service below:
// src/video_service.rs
use crate::models::CloudinaryResponse;
use actix_multipart::Multipart;
use actix_web::Error;
use dotenv::dotenv;
use futures_util::StreamExt;
use reqwest::{
multipart::{self, Part},
Client,
};
use sha1::{Digest, Sha1};
use std::{collections::HashMap, env, io::Write};
use tempfile::NamedTempFile;
use tokio::io::AsyncReadExt;
const MAX_SIZE: usize = 10 * 1024 * 1024; // 10MB
enum ParamValue {
Str(String),
Int(i64),
}
pub struct VideoService;
impl VideoService {
fn env_loader(key: &str) -> String {
dotenv().ok();
match env::var(key) {
Ok(v) => v.to_string(),
Err(_) => format!("Error loading env variable"),
}
}
fn generate_signature(params: HashMap<&str, ParamValue>, api_secret: &str) -> String {
// Step 1: Sort the parameters by keys and concatenate them
let mut sorted_keys: Vec<&&str> = params.keys().collect();
sorted_keys.sort();
let mut sorted_params = String::new();
for key in sorted_keys {
if !sorted_params.is_empty() {
sorted_params.push('&');
}
let value = match ¶ms[key] {
ParamValue::Str(s) => s.clone(),
ParamValue::Int(i) => i.to_string(),
};
sorted_params.push_str(&format!("{}={}", key, value));
}
// Step 2: Concatenate the sorted parameters and the API secret
let string_to_sign = format!("{}{}", sorted_params, api_secret);
// Step 3: Generate an SHA-1 hash of the concatenated string
let mut hasher = Sha1::new();
hasher.update(string_to_sign.as_bytes());
// Step 4: Return the hex-encoded result
hex::encode(hasher.finalize())
}
}
Code language: PHP (php)
Before uploading a video to Cloudinary, handle the file upload in your Rust backend. Create a function that saves the uploaded video to a temporary file, ensuring it is indeed a video file and doesn’t exceed a specified size limit. This step is crucial for validating and preparing the file before it’s processed or uploaded to Cloudinary.
The backend system can handle various video file formats, such as MP4, WebM, and others supported by the MIME type validation. The video file is temporarily saved for validation and then uploaded to Cloudinary.
Add the following code to implement video file validating and saving to a temporary file:
// src/video_service.rs
//...
impl VideoService {
//...
pub async fn save_file(mut payload: Multipart) -> Result<NamedTempFile, Error> {
let mut total_size = 0;
let mut temp_file = NamedTempFile::new()?; // Create a temporary file
// Iterate over multipart stream
while let Some(field) = payload.next().await {
let mut field = field?;
let content_type = field.content_type(); // Get the MIME type of the file
// Ensure content_type is present and it is a video
if let Some(content_type) = content_type {
if content_type.type_() != mime::VIDEO {
return Err(actix_web::error::ErrorBadRequest(
"Only video files are allowed",
));
}
} else {
return Err(actix_web::error::ErrorBadRequest("Missing content type"));
}
// Write the file content to the temporary file synchronously
while let Some(chunk) = field.next().await {
let data = chunk?;
total_size += data.len();
if total_size > MAX_SIZE {
return Err(actix_web::error::ErrorBadRequest(
"File size limit exceeded",
));
}
temp_file.write_all(&data)?;
}
}
Ok(temp_file)
}
}
Code language: JavaScript (javascript)
After saving the video file locally in the prior step, the next task is to upload it to Cloudinary. Create a function that manages the upload process by generating a signed request to Cloudinary’s API. This function should read the file, create the required authentication parameters, and then send the video file to Cloudinary.
Once the upload is complete, Cloudinary’s response will include the video URL, which you can store in a database or pass on to another service for further processing.
Add the following code to implement the upload functionality:
// src/video_service.rs
//...
impl VideoService {
//...
pub async fn upload_to_cloudinary(
temp_file: &NamedTempFile,
) -> Result<CloudinaryResponse, Box<dyn std::error::Error>> {
let client = Client::new();
let cloud_name = VideoService::env_loader("CLOUDINARY_CLOUD_NAME");
let api_secret = VideoService::env_loader("CLOUDINARY_API_SECRET");
let api_key = VideoService::env_loader("CLOUDINARY_API_KEY");
let timestamp = chrono::Utc::now().timestamp();
let public_id = temp_file
.path()
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("file")
.to_string();
// Include only public_id and timestamp in the signature
let mut params = HashMap::new();
params.insert("public_id", ParamValue::Str(public_id.to_string()));
params.insert("timestamp", ParamValue::Int(timestamp));
let signature = VideoService::generate_signature(params, &api_secret);
let mut file = tokio::fs::File::open(temp_file.path()).await?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await?;
let part = Part::bytes(buffer).file_name(public_id.clone());
let form = multipart::Form::new()
.text("public_id", public_id.clone())
.text("timestamp", timestamp.to_string())
.text("signature", signature)
.text("api_key", api_key)
.part("file", part);
let res = client
.post(format!(
"https://api.cloudinary.com/v1_1/{}/video/upload",
cloud_name
))
.multipart(form)
.send()
.await?;
let result = res.text().await?;
let cloudinary_response: CloudinaryResponse = serde_json::from_str(&result)?;
Ok(cloudinary_response)
}
}
Code language: PHP (php)
In the previous steps, you successfully implemented the API logic. Next, you need to create the handler, which is the endpoint that can receive requests from any client.
Import the required dependencies in the handler file and create an upload_video
function with a corresponding API route. This handler will use the services to perform the necessary actions and return the appropriate response using the APIResponse
and APIErrorResponse
. Create a file named handler.rs
in the src
folder and add the following code snippet:
// src/handler.rs
use actix_multipart::Multipart;
use actix_web::{post, Error, HttpResponse};
use reqwest::StatusCode;
use crate::{
models::{APIErrorResponse, APIResponse, CloudinaryResponse},
video_service::VideoService,
};
#[post("/upload")]
pub async fn upload_video(multipart: Multipart) -> Result<HttpResponse, Error> {
let file_path = VideoService::save_file(multipart).await?;
let upload_details = VideoService::upload_to_cloudinary(&file_path).await;
match upload_details {
Ok(data) => Ok(HttpResponse::Created().json(APIResponse::<CloudinaryResponse> {
status: StatusCode::CREATED.as_u16(),
message: "success".to_string(),
data: Some(data),
})),
Err(error) => Ok(HttpResponse::InternalServerError().json(APIErrorResponse {
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
message: "failure".to_string(),
data: Some(error.to_string()),
})),
}
}
Code language: PHP (php)
Update the main.rs file to include your application entry point and use the handler to set up the server with the following code.
// src/main.rs
use actix_web::{App, HttpServer};
use handler::upload_video;
mod handler;
mod models;
mod video_service;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(upload_video))
.bind("127.0.0.1:8080")?
.run()
.await
}
Code language: PHP (php)
Next, start the development server using the command below:
cargo run
After uploading the video, you should receive a response indicating a successful upload, along with details about the video file. This confirms that your Rust backend and Cloudinary integration are working correctly.
This blog post shows you how to upload a video in the Rust backend with Cloudinary. You can also use the same approach for image uploads but with different specifications from video uploads. To learn more about how Cloudinary allows developers to upload, store, manage, optimize, and deliver images and videos, contact us today.
If you found this blog post helpful and want to discuss it more, head over to the Cloudinary Community forum and its associated Discord. We’d love to hear about your projects, experiences, and challenges.