1 Month Learning Rust - My First CLI Application

1 Month Learning Rust - My First CLI Application

Daily short news for you
  • Morning news, does everyone remember the lawsuit of Ryan Dahl - or more accurately, the Deno group against Oracle over the name JavaScript?

    Oracle has responded that they are not giving up the name JavaScript 🫣

    https://x.com/deno_land/status/1876728474666217739

    » Read more
  • Are people taking their Tet holidays early or what? Traffic has dropped significantly this whole week 😳. It's a bit sad to talk to myself, so if anyone passes by and reads this, please drop a "comment" for some fun at home. You can say anything since it's anonymous 😇🔥

    » Read more
  • Someone asked me where I get my news so quickly, or how I find so many tools and projects... where do I get all of that? Well, there’s a source far on the horizon but close right in front of you, and that is the Github Trending page.

    This page tracks the repositories that have the most "stars" according to day/week/month. It also allows you to filter by programming language, and each language represents a kind of theme. For example, Python is buzzing about AI, LLMs..., Rust has all the super powerful tools, and Go is... just a continuous plaything 😁. Meanwhile, JavaScript 🫣😑

    » 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

The secret stack of Blog

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, 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...
Scroll or click to go to the next page