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
Задача
JAVA 25 SELF, 41 уровень, 4 лекция
Недоступна
Отправка секретного доклада в космос: Операция "Сжатие" 🚀
Отправка секретного доклада в космос: Операция "Сжатие" 🚀
1
Задача
JAVA 25 SELF, 41 уровень, 4 лекция
Недоступна
Великое архивирование: Сбор текстовых свитков Цифровой Библиотеки 📚
Великое архивирование: Сбор текстовых свитков Цифровой Библиотеки 📚
1
Опрос
Оптимизация IO, 41 уровень, 4 лекция
Недоступен
Оптимизация IO
Оптимизация IO
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ