1 Month Learning Rust - My First CLI Application

1 Month Learning Rust - My First CLI Application

Daily short news for you
  • For over a week now, I haven't posted anything, not because I have nothing to write about, but because I'm looking for ways to distribute more valuable content in this rapidly exploding AI era.

    As I shared earlier this year, the number of visitors to my blog is gradually declining. When I looked at the statistics, the number of users in the first six months of 2025 has dropped by 30% compared to the same period last year, and by 15% compared to the last six months of 2024. This indicates a reality that users are gradually leaving. What is the reason for this?

    I think the biggest reason is that user habits have changed. They primarily discover the blog through search engines, with Google being the largest. Almost half of the users return to the blog without going through the search step. This is a positive signal, but it's still not enough to increase the number of new users. Not to mention that now, Google has launched the AI Search Labs feature, which means AI displays summarized content when users search, further reducing the likelihood of users accessing the website. Interestingly, when Search Labs was introduced, English articles have taken over the rankings for the most accessed content.

    My articles are usually very long, sometimes reaching up to 2000 words. Writing such an article takes a lot of time. It's normal for many articles to go unread. I know and accept this because not everyone encounters the issues being discussed. For me, writing is a way to cultivate patience and thoughtfulness. Being able to help someone through my writing is a wonderful thing.

    Therefore, I am thinking of focusing on shorter and medium-length content to be able to write more. Long content will only be used when I want to write in detail or delve deeply into a particular topic. So, I am looking for ways to redesign the blog. Everyone, please stay tuned! 😄

    » Read more
  • CloudFlare has introduced the pay per crawl feature to charge for each time AI "crawls" data from your website. What does that mean 🤔?

    The purpose of SEO is to help search engines see the website. When users search for relevant content, your website appears in the search results. This is almost a win-win situation where Google helps more people discover your site, and in return, Google gets more users.

    Now, the game with AI Agents is different. AI Agents have to actively seek out information sources and conveniently "crawl" your data, then mix it up or do something with it that we can't even know. So this is almost a game that benefits only one side 🤔!?

    CloudFlare's move is to make AI Agents pay for each time they retrieve data from your website. If they don’t pay, then I won’t let them read my data. Something like that. Let’s wait a bit longer and see 🤓.

    » Read more
  • Continuing to update on the lawsuit between the Deno group and Oracle over the name JavaScript: It seems that Deno is at a disadvantage as the court has dismissed the Deno group's complaint. However, in August, they (Oracle) must be held accountable for each reason, acknowledging or denying the allegations presented by the Deno group in the lawsuit.

    JavaScript™ Trademark Update

    » 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

Me & the desire to "play with words"

Have you tried writing? And then failed or not satisfied? At 2coffee.dev we have had a hard time with writing. Don't be discouraged, because now we have a way to help you. Click to become a member now!

Have you tried writing? And then failed or not satisfied? At 2coffee.dev we have had a hard time with writing. Don't be discouraged, because now we have a way to help you. Click to become a member 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...