JavaRush /Курси /JAVA 25 SELF /Архіви/стиснення: java.util.zip

Архіви/стиснення: java.util.zip

JAVA 25 SELF
Рівень 41 , Лекція 4
Відкрита

1. Вступ: навіщо потрібні архіви та стиснення в Java

У сучасному світі робота з архівами й стиснутими файлами — звичне завдання: резервні копії, обмін файлами, логування, зберігання великих даних. Java надає стандартні засоби для роботи з архівами формату ZIP і стиснутими файлами GZIP через пакет java.util.zip.

Що вміє Java:

  • Читати й створювати ZIP-архіви (багатофайлові контейнери).
  • Читати й створювати GZIP-файли (стиснення одного файлу).
  • Керувати вмістом архівів, фільтрувати файли за маскою.
  • Контролювати рівень стиснення.
  • Перевіряти безпеку під час розпакування (боротьба з Zip Slip і Zip Bomb).

2. Основні класи

ZipInputStream і ZipOutputStream

Це потокові класи для послідовного читання/запису ZIP-архівів. Коли використовувати: якщо потрібно читати або створювати архів «на льоту», без довільного доступу до окремих файлів.

Приклад: читання архіву

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("Файл: " + entry.getName());
        // Можна читати вміст entry через zis.read(...)
        zis.closeEntry();
    }
}

Приклад: створення архіву

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("Привіт, архів!".getBytes());
    zos.closeEntry();
}

ZipFile

Клас для довільного доступу до вмісту ZIP-архіву: можна швидко отримати список файлів, відкрити будь-який файл за ім’ям, читати його вміст.

Приклад:

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)) {
    // Читаємо вміст файлу
}
zipFile.close();

Коли використовувати ZipFile?

  • Якщо потрібно швидко отримати список файлів, метадані, розмір, дату.
  • Якщо потрібно читати окремі файли без послідовного перебору всього архіву.

ZipEntry

Об’єкт, що представляє окремий файл або папку всередині архіву. Містить ім’я, розмір, дату, прапорці, рівень стиснення тощо.

import java.util.zip.ZipEntry;

ZipEntry entry = new ZipEntry("docs/readme.txt");
entry.setComment("Опис файлу");
entry.setTime(System.currentTimeMillis());

Рівні стиснення (Deflater)

Під час створення архіву можна керувати ступенем стиснення (від 0 — без стиснення, до 9 — максимальне стиснення):

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); // або 0..9
    // ...
}
  • Deflater.NO_COMPRESSION (0)
  • Deflater.BEST_SPEED (1)
  • Deflater.BEST_COMPRESSION (9)
  • Deflater.DEFAULT_COMPRESSION (-1)

Правило: що вищий рівень — то повільніше, але сильніше стискає.

3. Стиснення одного файлу

GZIP — це формат для стиснення одного файлу (не архів!). Застосовується для логів, тимчасових файлів, передавання мережею.

Приклад: стиснути файл

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);
}

Приклад: розпакувати файл

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);
}

Пам’ятайте: GZIP працює тільки з одним файлом — він не зберігає структуру папок і додаткові метадані. На відміну від нього, ZIP дозволяє пакувати одразу кілька файлів і цілі папки, зберігаючи їхню структуру та інформацію про кожен елемент.

4. Пакування/розпакування директорії, фільтри за PathMatcher

Пакування директорії у ZIP

Щоб запакувати папку з файлами та підпапками, рекурсивно обходимо дерево файлів і додаємо кожен файл до архіву з правильним відносним шляхом (розділювачі в ZIP — завжди "/").

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(); }
        });
}

Розпакування архіву до директорії

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();
    }
}

Фільтрація файлів за маскою (PathMatcher)

Можна фільтрувати файли для пакування/розпакування за маскою, наприклад лише "*.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. Безпека: Zip Slip, Zip Bomb, перевірка нормалізації шляху

Zip Slip (атака через шлях)

Проблема: зловмисник може створити архів із файлом, у якого ім’я — "../../../../etc/passwd". Під час розпакування без перевірки такий файл може перезаписати системні файли!

Рішення: перед записом файлу нормалізуйте шлях і переконайтеся, що він не виходить за межі цільової директорії.

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: спроба запису поза цільовою папкою!");
        }
        if (entry.isDirectory()) {
            Files.createDirectories(outPath);
        } else {
            Files.createDirectories(outPath.getParent());
            try (OutputStream os = Files.newOutputStream(outPath)) {
                zis.transferTo(os);
            }
        }
        zis.closeEntry();
    }
}

Zip Bomb (архів-бомба)

Проблема: архів може містити файл, який після розпакування займає гігабайти, хоча сам архів важить кілька кілобайтів. Це може «вбити» сервер або диск.

Рішення: обмежуйте максимальний розмір розпаковуваних файлів і загальний обсяг розпакування, перериваючи процес у разі перевищення ліміту.

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 МБ
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. Практика: CLI-утиліта «zip/unzip» з масками

Напишемо просту консольну утиліту для пакування та розпакування файлів із підтримкою масок.

Приклад: пакування

// 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(); }
            });
    }
}

Приклад: розпакування із захистом від 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: спроба запису поза цільовою папкою!");
            }
            if (entry.isDirectory()) {
                Files.createDirectories(outPath);
            } else {
                Files.createDirectories(outPath.getParent());
                try (OutputStream os = Files.newOutputStream(outPath)) {
                    zis.transferTo(os);
                }
            }
            zis.closeEntry();
        }
    }
}

Приклад запуску:

java ZipUtil zip myfolder archive.zip "*.txt"
java ZipUtil unzip archive.zip output

У першому прикладі команда java ZipUtil zip myfolder archive.zip "*.txt" пакує усі .txt файли з папки myfolder до архіву archive.zip. У другому прикладі java ZipUtil unzip archive.zip output розпаковує архів у папку output, водночас перевіряється, щоб жоден файл не був записаний за межі цільової директорії — це і є захист від Zip Slip.

1
Опитування
Оптимізація IO, рівень 41, лекція 4
Недоступний
Оптимізація IO
Оптимізація IO
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ