1 tháng học Rust - ứng dụng CLI đầu tiên

1 tháng học Rust - ứng dụng CLI đầu tiên

Vấn đề

1 năm trước, tôi đã hô hào rằng sẽ quyết tâm học Rust trong vòng 1 tháng. Và kết quả thì như bạn đã thấy, chuỗi bài viết về quá trình học Rust vẫn chưa kết thúc. Như vậy có thể coi đó là một sự thất bại rồi phải không? Không biết bạn đọc nghĩ sao nhưng tôi thì nghĩ không hẳn là như thế.

Ngôn ngữ lập trình suy cho cùng là một công cụ để giải bài toán. Biết thêm được một cái mới, hẳn kinh nghiệm làm bài sẽ nhiều hơn, chưa kể còn giải được theo cách tối ưu. Bên cạnh việc học, tôi cũng có nhiều việc phải làm nữa. Những công việc đó có độ ưu tiên cao hơn nên buộc mình phải bắt tay vào làm sớm hơn.

Nhiều lần tôi có ý thôi không học nữa. Nhưng bằng một lý do nào đó mà chuỗi bài viết về Rust vẫn được bạn đọc yêu thích. Tôi không rõ tại sao, vì gần như những gì viết ra chỉ là các bản tóm tắt hoặc có thêm đôi ba lời giải thích cách hiểu của mình về Rust. Thậm chí còn không được đầy đủ so với tài liệu gốc (dù sao thì tôi vẫn khuyên bạn nên đọc bản gốc 😅). Có vẻ như thứ mà nhiều người muốn thấy là một người đồng hành. À! Ít ra thì đang có người học Rust giống như mình.

Một năm vừa rồi, chúng ta hầu như đã học được gần hết kiến thức cơ bản của Rust, về biến, kiểu dữ liệu và cả một số cấu trúc dữ liệu cơ bản. Riêng phần khó nhất - quyền sở hữu thì tôi tin là phải bắt tay vào làm dự án thực sự mới phát sinh ra nhiều vấn đề, chứ mà lý thuyết thì ai mà chẳng đọc được đúng không.

Vậy thì đây cũng là thời điểm để làm một bài thực hành, ứng dụng những gì đã học được để tạo một công cụ dòng lệnh (CLI). Nhưng đến đây tôi chợt nhớ ra trước đó mình cũng có tạo ra một ứng dụng CLI để tạo và upload hình ảnh thumbnail trong bài viết Ứng dụng CLI để tăng hiệu suất trong công việc. Sẽ thật tuyệt vời nếu viết lại được công cụ này bằng Rust.

Ứng dụng trước đó đang giải quyết công việc xử lý kích thước và tải lên ảnh hàng loạt các bài viết trên blog. Tưởng tượng bạn có một hình ảnh, bất kể định dạng nào, kích thước bao nhiêu thì khi đưa vào CLI này nó sẽ tạo ra một loạt các hình ảnh với kích thước khác nhau rồi tải chúng lên R2 của Cloudflare. Thứ chúng ta nhận được sau đó là tất cả đường dẫn đến hình ảnh ở trên.

Nhưng suy đi cũng phải ngẫm lại, nếu mang hết tính năng của CLI cũ sang thì mất kha khá thời gian đấy, cho nên trong một bài viết ngắn này, tôi sẽ đơn giản đi một chút. Bỏ qua công đoạn xử lý ảnh đi, giờ chỉ cần nhập vào một hình ảnh thì nó tải lên R2 thôi.

Bắt đầu nào!

Đầu tiên tạo một project Rust mới bằng cargo, đặt tên là img-cli.

$ cargo new img-cli

Chạy thử chương trình.

$ 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!

Bây giờ chúng ta cần lấy được tham số là đường dẫn đến hình ảnh cần được tải lên. Một cái gì đó giống như là:

$ img-cli path_to_image

Thì trong Rust cách đơn giản nhất để lấy các tham số đằng sau lệnh là args.

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

API tải ảnh lên R2 gọi qua PUT, với body là binary chứa dữ liệu ảnh cần tải lên. Một điều cần lưu ý là tên của ảnh tải lên R2 được trích xuất từ đường dẫn API. Ngoài ra, cần truyền thêm một tham số bí mật là 'X-Custom' trong headers để xác thực quyền tải lên hình ảnh.

Nhưng trước tiên, viết một hàm để đọc dữ liệu file ảnh.

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 nhận vào một đường dẫn đến hình ảnh, trả về dữ liệu kiểu Vec<u8> nếu thành công. Ngược lại, nó sẽ trả về lỗi. Vec<u8> là kiểu dữ liệu được dùng trong body của API.

Tiếp theo, viết một hàm tải ảnh lên R2. Ở đây tôi sử dụng thư viện reqwest để gọi API. Thư viện này thường đi kèm với tokio để xử lý bất đồng bộ. Tokio là một runtime cung cấp môi trường xử lý bất đồng bộ cho Rust. Để đơn giản, chúng ta sẽ tạm thời không dùng đến mà chỉ sử dụng API blocking của reqwest.

Cài đặt reqwest:

$ cargo add reqwest --features blocking

Lưu ý là sau khi gọi API, kết quả trả về là một JSON có chứa liên kết đến hình ảnh vừa tải lên. Chúng ta cần lấy được liên kết này và in ra màn hình. Để làm được cần phải tiến hành mapping được các trường dữ liệu vào một kiểu dữ liệu struct. Cài thêm 2 thư viện serdeserde_json để xử lý.

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

Khai báo một struct Response để mapping phản hồi của API.

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

Ráp chúng lại với nhau thành một hàm 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;

    // lấy ra tên file chính là chuỗi cuối cùng sau dấu '/'
    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)
}

Đưa các hàm vào main.

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

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

Chạy thử

$ 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

Mọi thứ hoạt động!

Trong bài viết này chúng ta đã vận dụng những kiến thức cơ bản và thêm cả cách sử dụng một số thư viện để gọi API cũng như xử lý kết quả phản hồi. Quả là khi bắt tay vào làm thì mới thấy khó khăn. Các bài viết sau đó, chúng ta sẽ tiến dần tới nhiều khái niệm nâng cao trong Rust. Hy vọng mọi người sẽ tiếp tục ủng hộ series bài viết về Rust này trong tương lai!

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.
Author

Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.

Bạn thấy bài viết này có ích?
Không

Bình luận (0)