1. Giới thiệu: tại sao cần lưu trữ và nén trong Java
Trong thế giới hiện đại, làm việc với lưu trữ và tệp nén — là nhiệm vụ thường gặp: sao lưu, trao đổi tệp, ghi log, lưu trữ dữ liệu lớn. Java cung cấp các công cụ chuẩn để làm việc với lưu trữ định dạng ZIP và tệp nén GZIP thông qua gói java.util.zip.
Java có thể làm gì:
- Đọc và tạo lưu trữ ZIP (container nhiều tệp).
- Đọc và tạo tệp GZIP (nén một tệp).
- Quản lý nội dung lưu trữ, lọc tệp theo mẫu.
- Điều khiển mức nén.
- Kiểm tra an toàn khi giải nén (chống zip slip và zip bomb).
2. Các lớp chính
ZipInputStream và ZipOutputStream
Đây là các lớp luồng để đọc/ghi tuần tự lưu trữ ZIP. Khi nào nên dùng: khi cần đọc hoặc tạo lưu trữ “on-the-fly”, không cần truy cập ngẫu nhiên tới từng tệp riêng lẻ.
Ví dụ: đọc lưu trữ
import java.io.*;
import java.util.zip.*;
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
System.out.println("Tệp: " + entry.getName());
// Có thể đọc nội dung entry qua zis.read(...)
zis.closeEntry();
}
}
Ví dụ: tạo lưu trữ
import java.io.FileOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
ZipEntry entry = new ZipEntry("hello.txt");
zos.putNextEntry(entry);
zos.write("Xin chào, lưu trữ!".getBytes());
zos.closeEntry();
}
ZipFile
Lớp dành cho truy cập ngẫu nhiên tới nội dung lưu trữ ZIP: có thể nhanh chóng lấy danh sách tệp, mở bất kỳ tệp nào theo tên, đọc nội dung của nó.
Ví dụ:
import java.util.zip.*;
import java.io.*;
ZipFile zipFile = new ZipFile("archive.zip");
zipFile.stream().forEach(entry -> System.out.println(entry.getName()));
ZipEntry entry = zipFile.getEntry("hello.txt");
try (InputStream is = zipFile.getInputStream(entry)) {
// Đọc nội dung tệp
}
zipFile.close();
Khi nào nên dùng ZipFile?
- Khi cần nhanh chóng lấy danh sách tệp, metadata, kích thước, ngày tháng.
- Khi cần đọc các tệp riêng lẻ mà không phải duyệt tuần tự toàn bộ lưu trữ.
ZipEntry
Đối tượng đại diện cho một tệp hoặc thư mục bên trong lưu trữ. Chứa tên, kích thước, ngày tháng, cờ, mức nén, v.v.
import java.util.zip.ZipEntry;
ZipEntry entry = new ZipEntry("docs/readme.txt");
entry.setComment("Mô tả tệp");
entry.setTime(System.currentTimeMillis());
Mức nén (Deflater)
Khi tạo lưu trữ, có thể điều khiển mức nén (từ 0 — không nén, đến 9 — nén tối đa):
import java.io.FileOutputStream;
import java.util.zip.Deflater;
import java.util.zip.ZipOutputStream;
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
zos.setLevel(Deflater.BEST_COMPRESSION); // hoặc 0..9
// ...
}
- Deflater.NO_COMPRESSION (0)
- Deflater.BEST_SPEED (1)
- Deflater.BEST_COMPRESSION (9)
- Deflater.DEFAULT_COMPRESSION (-1)
Quy tắc: mức càng cao — càng chậm, nhưng nén mạnh hơn.
3. Nén một tệp
GZIP là định dạng để nén một tệp (không phải lưu trữ!). Dùng cho log, tệp tạm thời, truyền qua mạng.
Ví dụ: nén tệp
import java.util.zip.*;
import java.io.*;
try (GZIPOutputStream gos = new GZIPOutputStream(new FileOutputStream("file.txt.gz"));
FileInputStream fis = new FileInputStream("file.txt")) {
fis.transferTo(gos);
}
Ví dụ: giải nén tệp
import java.util.zip.GZIPInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
try (GZIPInputStream gis = new GZIPInputStream(new FileInputStream("file.txt.gz"));
FileOutputStream fos = new FileOutputStream("file.txt")) {
gis.transferTo(fos);
}
Hãy nhớ: GZIP chỉ làm việc với một tệp — nó không lưu cấu trúc thư mục hay metadata bổ sung. Trái lại, ZIP cho phép đóng gói nhiều tệp và cả thư mục, giữ nguyên cấu trúc và thông tin của từng phần tử.
4. Đóng gói/giải nén thư mục, bộ lọc theo PathMatcher
Đóng gói thư mục vào ZIP
Để đóng gói một thư mục với các tệp và thư mục con, duyệt đệ quy cây tệp và thêm từng tệp vào lưu trữ với đường dẫn tương đối đúng (dấu phân tách trong ZIP luôn là "/").
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
Path sourceDir = Paths.get("myfolder");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
Files.walk(sourceDir)
.filter(Files::isRegularFile)
.forEach(path -> {
String entryName = sourceDir.relativize(path).toString().replace("\\", "/");
try (InputStream is = Files.newInputStream(path)) {
zos.putNextEntry(new ZipEntry(entryName));
is.transferTo(zos);
zos.closeEntry();
} catch (IOException e) { e.printStackTrace(); }
});
}
Giải nén lưu trữ vào thư mục
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path outPath = Paths.get("output", entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(outPath);
} else {
Files.createDirectories(outPath.getParent());
try (OutputStream os = Files.newOutputStream(outPath)) {
zis.transferTo(os);
}
}
zis.closeEntry();
}
}
Lọc tệp theo mẫu (PathMatcher)
Có thể lọc tệp để đóng gói/giải nén theo mẫu, ví dụ chỉ "*.txt":
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.PathMatcher;
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*.txt");
Files.walk(sourceDir)
.filter(matcher::matches)
.forEach(/* ... */);
5. Bảo mật: Zip Slip, zip bomb, kiểm tra chuẩn hóa đường dẫn
Zip Slip (tấn công qua đường dẫn)
Vấn đề: Kẻ tấn công có thể tạo lưu trữ với tệp có tên — "../../../../etc/passwd". Khi giải nén mà không kiểm tra, tệp như vậy có thể ghi đè tệp hệ thống!
Giải pháp: trước khi ghi tệp, hãy chuẩn hóa đường dẫn và đảm bảo nó không thoát ra ngoài thư mục đích.
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
Path targetDir = Paths.get("output").toAbsolutePath();
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path outPath = targetDir.resolve(entry.getName()).normalize();
if (!outPath.startsWith(targetDir)) {
throw new IOException("Zip Slip: cố gắng ghi ra ngoài thư mục đích!");
}
if (entry.isDirectory()) {
Files.createDirectories(outPath);
} else {
Files.createDirectories(outPath.getParent());
try (OutputStream os = Files.newOutputStream(outPath)) {
zis.transferTo(os);
}
}
zis.closeEntry();
}
}
Zip bomb (bom nén)
Vấn đề: lưu trữ có thể chứa một tệp mà sau khi giải nén chiếm hàng gigabyte, dù bản thân lưu trữ chỉ vài kilobyte. Điều này có thể “hạ gục” server hoặc ổ đĩa.
Giải pháp: giới hạn kích thước tối đa của từng tệp được giải nén và tổng dung lượng giải nén, dừng quá trình khi vượt quá giới hạn.
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
long maxSize = 100 * 1024 * 1024; // 100 MB
long totalUnzipped = 0;
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path outPath = Paths.get("output", entry.getName()).normalize();
Files.createDirectories(outPath.getParent());
long written = 0;
try (OutputStream os = Files.newOutputStream(outPath)) {
byte[] buf = new byte[8192];
int len;
while ((len = zis.read(buf)) > 0) {
os.write(buf, 0, len);
written += len;
totalUnzipped += len;
if (written > maxSize || totalUnzipped > maxSize) {
throw new IOException("Zip bomb detected!");
}
}
}
zis.closeEntry();
}
}
6. Thực hành: tiện ích CLI “zip/unzip” với mẫu
Hãy viết một tiện ích console đơn giản để đóng gói và giải nén tệp với hỗ trợ mẫu.
Ví dụ: đóng gói
// java ZipUtil zip myfolder archive.zip *.txt
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public static void zip(String sourceDir, String zipFile, String glob) throws IOException {
Path src = Paths.get(sourceDir);
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + glob);
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
Files.walk(src)
.filter(Files::isRegularFile)
.filter(matcher::matches)
.forEach(path -> {
String entryName = src.relativize(path).toString().replace("\\", "/");
try (InputStream is = Files.newInputStream(path)) {
zos.putNextEntry(new ZipEntry(entryName));
is.transferTo(zos);
zos.closeEntry();
} catch (IOException e) { e.printStackTrace(); }
});
}
}
Ví dụ: giải nén có bảo vệ chống Zip Slip
// java ZipUtil unzip archive.zip output
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public static void unzip(String zipFile, String outDir) throws IOException {
Path targetDir = Paths.get(outDir).toAbsolutePath();
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path outPath = targetDir.resolve(entry.getName()).normalize();
if (!outPath.startsWith(targetDir)) {
throw new IOException("Zip Slip: cố gắng ghi ra ngoài thư mục đích!");
}
if (entry.isDirectory()) {
Files.createDirectories(outPath);
} else {
Files.createDirectories(outPath.getParent());
try (OutputStream os = Files.newOutputStream(outPath)) {
zis.transferTo(os);
}
}
zis.closeEntry();
}
}
}
Ví dụ chạy:
java ZipUtil zip myfolder archive.zip "*.txt"
java ZipUtil unzip archive.zip output
Trong ví dụ đầu tiên, lệnh java ZipUtil zip myfolder archive.zip "*.txt" đóng gói tất cả các tệp .txt từ thư mục myfolder vào lưu trữ archive.zip. Ở ví dụ thứ hai, java ZipUtil unzip archive.zip output giải nén lưu trữ vào thư mục output, đồng thời kiểm tra để không có tệp nào được ghi ra ngoài thư mục đích — đây chính là bảo vệ khỏi Zip Slip.
GO TO FULL VERSION