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.