Qua các bài viết về concept cơ bản trong Rust trước đó. Hôm nay mình sẽ làm 1 tool giải nén file zip bằng thư viện zip trong Rust.

Cách sử dụng tool này khá đơn giản, chỉ cần nhập dòng lệnh sau

cargo run <file_name>

Ok, bắt đầu thôi nào.

Tạo project với Cargo

Sử dụng cargo new để tạo 1 project mới

cargo new extract-zip-file

Mình sẽ có cấu trúc thư mục cơ bản như sau:

extract-zip-file/
├── Cargo.toml
├── Cargo.lock
└── src
    └── main.rs

Trước hết mình sẽ install thư viện zip vào bên trong project.

Vào bên trong file Cargo.toml, thêm thư viện như sau:

[package]
//...

[dependencies]
zip = "0.6.4"

Ngay sau khi thêm đoạn zip=... vào phần dependencies, project sẽ tự động install thư viện zip về.

Bắt đầu dòng code đầu tiên

Đầu tiên, mình sẽ đi vào src/main.rs để khai báo thư viện

use std::fs;
use std::io;
use std::path::Path;
use zip::ZipArchive;

Như ở trên, mình sử dụng 4 thư viện ghi/đọc dữ liệu từ file và phục vụ việc tạo ra những file mới sau khi extract thành công.

Kế tiếp, tại hàm main mình sẽ kiểm tra đầu vào của file

fn main() {
  std::process::exit(real_main());
}

Tất nhiên, dòng code ở trên vẫn chưa chạy được vì mình chưa khởi tạo hàm real_main(). Sau đây mình sẽ khởi tạo hàm real_main() và thực hiện lệnh thao tác extract zip file.

Tạo hàm real_main() và kiểm tra đầu vào

Bên dưới hàm main, mình tạo 1 hàm real_main để xử lý dữ liệu đầu vào

fn real_main() -> i32 {

}

Ở đây mình khai báo fn real_main() -> i32 là do mình muốn hàm này sẽ trả về 0 hoặc 1. Vì tại hàm main, dòng lệnh std::process::exit() chỉ chạy khi đầu vào là 1 hoặc 0 tương đương với 0 là đầu vào hợp lệ và bắt đầu xử lý, nếu trả về 1, tức đầu vào không hợp lệ, và sẽ exit khỏi chương trình.

Như đã nói ở trên, mình phải kiểm tra đầu vào có hợp lệ hay không.

Trước khi kiểm tra mình cần phân tích một chút về đầu vào. Câu lệnh để thực thi của mình là

cargo run <file_name>

Tại đây, cargo run là lệnh thực thi project của Rust với run là đối số đầu tiên, và <file_name> là đối số thứ 2. Công việc kiểm tra của mình là phải check xem đầu vào có thực sự đúng với yêu cầu hay không.

Bước đầu kiểm tra, mình sẽ tạo 1 biến args là collection bằng Vec, không định dạng kiểu dữ liệu bằng _

let args: Vec<_> = std::env::args().collect();

if args.len() < 2 {
  println!("Usage: {} <filename>", args[0]);
  return 1;
}

Lệnh if args.len() < 2 để kiểm tra xem đầu vào dữ liệu có chứa file zip hay không. Nếu câu lệnh thực thi không đúng với điều kiện mong muốn để thực thi thì chương trình sẽ chạy lệnh println!("Usage: {} <filename>", args[0]) để hướng dẫn người dùng nhập đúng điều kiện và sau đó trả về 1, tức là chạy lệnh exit tại hàm main, và thoát chương trình.

Trong trường hợp điều kiện đã đúng, mình sẽ tạo 1 đường dẫn cho file được giải nén và ghi dữ liệu vào bên trong file với thư viện Path và fs

let fname = Path::new(&*args[1]);
let file = fs::File::open(&fname).unwrap();

Đoạn code trên sau khi mình mở file bằng fs::File::open() thì nó trả về 1 đối tượng Result<fs::File, std::io::Error> đại diện cho việc mở file thành công hoặc thất bại. Mình sử dụng phương thức .unwrap() để xử lý kết quả đấy, trong trường hợp mở file thất bại chương trình sẽ panic!() lỗi và dừng chương trình, ngược lại, sẽ tạo ra file và gán nó cho biến file sẵn sàng cho việc đọc/ghi dữ liệu.

Bắt đầu xử lý file

Qua công đoạn kiểm tra, mình sẽ bắt đầu giải nén file với phương thức ZipArchive thông qua thư viện zip.

Mình sẽ khai báo 1 biến archive là mutable để chứa các file cần giải nén và tiện cho việc sửa đổi file.

*Có thể tìm hiểu thêm về mutable và immutable nếu không hiểu.

let mut archive = ZipArchive::new(file).unwrap();

Cú pháp ZipArchive::new(file) giúp mình mở file nén và đọc các dữ liệu bên trong.

Kế tiếp, mình sẽ sử dụng vòng lặp (for loop) để đọc các file bên trong.

for i in 0..archive.len() {

}

Tại đây, mình sẽ cho vòng lặp chạy qua các phần tử (các file/folder bên trong file .zip) bắt đầu từ index = 0.

Bên trong vòng lặp, mình sẽ kiểm tra file có rỗng hay không, và gán nó vào 1 biến outpath.

let mut file = archive.by_index(i).unwrap();

let outpath = match file.enclosed_name() {
  Some(path) => path.to_owned(),
  None => continue;
}

Biến archive hỗ trợ phương thức .by_index(i) để truy cập vào bên trong file tại vị trí i, nó sẽ trả về Result<ZipFile, ZipError> trong đó ZipFile là thể hiện cho file bên trong biến archive.

Sau đó mình tạo file outpath bằng cú pháp match và phương thức enclosed_name() để check file có rỗng hay không. Trường hợp file không rỗng mình sẽ dùng phương thức .to_owned() để tạo 1 bản sao của biến path và gán vào biến outpath. Ngược lại, nó sẽ đi đến file tiếp theo.

Tiếp theo, mình kiểm tra xem file nén có ghi chú nào hay không, nếu có mình sẽ hiển thị ra màn hình. Vì trường hợp ghi chú này tuỳ vào mục đích của tác giả, nên đôi khi sẽ không có, mình sẽ handle nó bên trong 1 block code cụ thể.

*Có thể tìm hiểu về scope nếu không hiểu.

{
  let comment = file.comment();
  if !comment.is_empty() {
    println!("File {} comment: {}", i, comment);
  }
}

Cuối cùng, mình sẽ xử lý ghi dữ liệu từ file nén ra 1 file mới.

Khi giải nén, sẽ có 1 số trường hợp bên trong file .zip là file hoặc folder. Mình sẽ kiểm tra phần tử đấy có phải là folder hay không bằng cách sử dụng phương thức .ends_with("/") vì các folder thường sẽ được khai báo như extra/out.txt

if (*file.name()).ends_with('/') {

}

Vì biến outpath hiện tại mình đang mong muốn đầu ra là file, nên nếu là folder, thì mình cần phải tạo 1 thư mục cha của outpath để chứa các file bên trong. Bên trong if mình sẽ sử dụng phương thức .create_dir_all() để tạo thư mục cha cho outpath

println!("File {} extracted to \"{}\"", i, outpath.display());
fs::create_dir_all(&outpath).unwrap();

Ngược lại, nếu phần tử là 1 file, thì mình sẽ đọc dữ liệu tại file nén và ghi vào 1 biến mới outfile cùng với tên file và đường dẫn đã được chỉ định trước đó.

println!("File {} extracted to \"{}\" ({} bytes)", i, outpath.display(), file.size());

if let Some(p) = outpath.parent() {
  if !p.exists() {
    fs::create_dir_all(&p).unwrap();
  }
}

let mut outfile = fs::File::create(&outpath).unwrap();
io::copy(&mut file, &mut outfile).unwrap();

Sau khi mình hiển thị dữ liệu được nén ra ngoài màn hình với println!() mình kiểm tra xem folder cha của outpath có tồn tại hay không. nếu nó không tồn tại, mình sẽ tạo 1 folder mới bằng create_dir_all()

Kế tiếp, mình tạo 1 biến outfile với đường dẫn và tên file được chỉ định bằng biến outpath trước đó.

Cuối cùng, mình sử dụng phương thức copy() để ghi dữ liệu từ file nén sang outfile. Cả 2 trường hợp mình đều sử dụng unwrap() để handle khi việc tạo và đọc file sang outfile xảy ra lỗi thì chương trình sẽ panic!() lỗi và dừng chương trình.

Khi kết thúc vòng for, mình trả về giá trị 0 để thể hiện việc project được thực thi tại hàm main.

fn real_main() -> i32 {
  //do extracted file
  0
}

Kết

Như vậy, là mình đã tạo ra được 1 dự án có thể giải nén file .zip bằng thư viện zip của Rust rồi. Cùng xem lại kết quả tại đây.


Kaiz