1 Month Learning Rust - My First CLI Application

1 Month Learning Rust - My First CLI Application

Daily short news for you
  • "MongoDB also applies Eating Our Own Dog Food in their product development process. If you are not yet familiar with what that is, you can read more in the article Eating your own dog food - Using your own product."

    » Read more
  • swapy is a library that helps you create drag-and-drop actions to easily swap the positions of components.

    The library supports a wide range of platforms, making it perfect for building something personalized like a user’s Dashboard.

    » Read more
  • After waking up, I saw the news feed flooded with articles about Microsoft rewriting the TypeScript compiler - tsc in Go, achieving performance that is 10 times faster than the current one. Wow!

    But when I saw this news, the question immediately popped into my head: "Why not Rust?" You know, the trend of rewriting everything in Rust is hotter than ever, and it’s not an exaggeration to say that it's sweeping through the rankings of previous legacy tools.

    What’s even more interesting is the choice of Go - which supposedly provides the best performance to date - as they say. Many people expressed disappointment about why it wasn’t C# instead 😆. When something is too famous, everything is scrutinized down to the smallest detail, and not everyone listens to what is said 🥶

    Why Go? #411

    » Read more

The Problem

A year ago, I declared that I would learn Rust within a month. And the result is that the series of articles about the Rust learning process has not ended yet. Can this be considered a failure? I think not.

Programming languages are just tools to solve problems. Learning a new one can broaden our experience and help us solve problems more efficiently. Besides learning, I also have other tasks with higher priority, so I have to work on them first.

Many times, I wanted to give up, but the series of articles about Rust was still well-received by readers. I don't know why, because what I wrote was just a summary or some explanations of my understanding of Rust. It seems that what people want to see is a companion. Ah! At least there is someone learning Rust like me.

In the past year, we have learned most of the basic knowledge of Rust, including variables, data types, and basic data structures. The most difficult part - ownership - I think should be applied to a real project to encounter problems.

So, this is the time to do a practical exercise, applying what we have learned to create a command-line tool (CLI). But then I remembered that I also created a CLI application to create and upload thumbnail images in the article Using CLI to Increase Efficiency in Work.

The previous application solved the problem of processing image sizes and uploading them in bulk to the blog. Imagine you have an image, regardless of the format and size, and when you put it into the CLI, it will create a series of images with different sizes and upload them to R2 of Cloudflare.

But, if we bring all the features of the old CLI to the new one, it will take a lot of time, so in this short article, I will simplify it. I will ignore the image processing step and just upload the image to R2.

Let's start!

First, create a new Rust project using cargo, named img-cli.

$ cargo new img-cli

Run the program.

$ cargo run
   Compiling img-cli v0.1.0 (/Users/hoaitx/src/hoaitx/img-cli)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/img-cli`
Hello, world!

Now, we need to get the argument which is the path to the image to be uploaded. Something like this:

$ img-cli path_to_image

In Rust, the simplest way to get the arguments after the command is args.

fn main() {
    let args: Vec<String> = env::args().collect();
    let file_path = &args[1];
}

The API to upload the image to R2 uses a PUT request, with a binary body containing the image data. One thing to note is that the name of the image uploaded to R2 is extracted from the API path. Additionally, a secret parameter 'X-Custom' in the headers is needed to authenticate the upload.

First, write a function to read the image file data.

fn read_file(path: &str) -> Result<Vec<u8>, std::io::Error> {
    let mut file = File::open(path)?;
    let mut file_bytes = Vec::new();
    file.read_to_end(&mut file_bytes)?;
    Ok(file_bytes)
}

read_file takes a path to the image, returns the data as Vec<u8> if successful, or an error.

Next, write a function to upload the image to R2. I use the reqwest library to call the API. This library usually comes with tokio to handle asynchronous operations. For simplicity, we will use the blocking API of reqwest.

Install reqwest:

$ cargo add reqwest --features blocking

After calling the API, the result is a JSON containing the link to the uploaded image. We need to extract this link and print it out. To do this, we need to map the response data to a struct. Install serde and serde_json to handle JSON.

$ cargo add serde --features derive
$ cargo add serde_json

Declare a struct Response to map the API response.

#[derive(serde::Deserialize, serde::Serialize)]
struct Response {
    full: String,
}

Put everything together into a function up_file.

#[derive(serde::Deserialize, serde::Serialize)]
struct Response {
    full: String,
}

fn up_file(path: &str) -> Result<String, Box<dyn std::error::Error>> {
    let url = "https://static-img.2coffee.dev";
    let client = reqwest::blocking::Client::new();
    let file_path = path;

    // get the file name from the path
    let file_name = file_path
        .split('/')
        .last()
        .unwrap_or("unknown")
        .to_string();

    let file_bytes = read_file(file_path)?;

    let response = client
        .put(format!("{}/{}", url, file_name))
        .header("Content-Type", "application/x-binary")
        .header("X-Custom-Auth-Key", "XXX")
        .body(file_bytes)
        .send();

    let response_struct: Response =
        serde_json::from_str(response?.text()?.as_str())?;

    Ok(response_struct.full)
}

Put the up_file function into main.

fn main() {
    let args: Vec<String> = env::args().collect();
    let file_path = &args[1];
    let url = up_file(file_path);

    print!("{}", url.unwrap());
}

Run the program.

$ cargo run -- '/Users/hoaitx/Downloads/hoaitx.jpg'
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.20s
     Running `target/debug/rs /Users/hoaitx/Downloads/hoaitx.jpg`
https://static-img.2coffee.dev/hoaitx.jpg

It works!

In this article, we applied basic knowledge and used some libraries to call the API and handle the response. It's not easy when we start working on a real project. In future articles, we will move on to more advanced concepts in Rust.

Premium
Hello

5 profound lessons

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!

View all

Subscribe to receive new article notifications

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.

Comments (0)

Leave a comment...