CodeGym /Kursy /JAVA 25 SELF /Archiwa/kompresja: java.util.zip

Archiwa/kompresja: java.util.zip

JAVA 25 SELF
Poziom 41 , Lekcja 4
Dostępny

1. Wprowadzenie: po co są potrzebne archiwa i kompresja w Javie

We współczesnym świecie praca z archiwami i skompresowanymi plikami to codzienne zadanie: kopie zapasowe, wymiana plików, logowanie, przechowywanie dużych danych. Java zapewnia standardowe narzędzia do pracy z archiwami w formacie ZIP i plikami GZIP poprzez pakiet java.util.zip.

Co potrafi Java:

  • Czytać i tworzyć archiwa ZIP (wieloplikowe kontenery).
  • Czytać i tworzyć pliki GZIP (kompresja jednego pliku).
  • Zarządzać zawartością archiwów, filtrować pliki według maski.
  • Kontrolować poziom kompresji.
  • Weryfikować bezpieczeństwo przy rozpakowywaniu (walka z zip slip i zip bomb).

2. Podstawowe klasy

ZipInputStream i ZipOutputStream

To klasy strumieniowe do sekwencyjnego odczytu/zapisu archiwów ZIP. Kiedy używać: gdy trzeba czytać lub tworzyć archiwum „w locie”, bez dostępu swobodnego do poszczególnych plików.

Przykład: odczyt archiwum

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("Plik: " + entry.getName());
        // Można czytać zawartość entry przez zis.read(...)
        zis.closeEntry();
    }
}

Przykład: tworzenie archiwum

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("Cześć, archiwum!".getBytes());
    zos.closeEntry();
}

ZipFile

Klasa do dostępu swobodnego do zawartości archiwum ZIP: można szybko uzyskać listę plików, otworzyć dowolny plik po nazwie, odczytać jego zawartość.

Przykład:

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)) {
    // Czytamy zawartość pliku
}
zipFile.close();

Kiedy używać ZipFile?

  • Gdy trzeba szybko uzyskać listę plików, metadane, rozmiar, datę.
  • Gdy trzeba odczytywać pojedyncze pliki bez sekwencyjnego przechodzenia całego archiwum.

ZipEntry

Obiekt reprezentujący pojedynczy plik lub folder wewnątrz archiwum. Zawiera nazwę, rozmiar, datę, flagi, poziom kompresji itd.

import java.util.zip.ZipEntry;

ZipEntry entry = new ZipEntry("docs/readme.txt");
entry.setComment("Opis pliku");
entry.setTime(System.currentTimeMillis());

Poziomy kompresji (Deflater)

Przy tworzeniu archiwum można sterować stopniem kompresji (od 0 – bez kompresji, do 9 – maksymalna kompresja):

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

Zasada: im wyższy poziom – tym wolniejsze działanie, ale większa kompresja.

3. Kompresja pojedynczego pliku

GZIP to format do kompresji jednego pliku (nie archiwum!). Stosowany do logów, plików tymczasowych, transmisji przez sieć.

Przykład: skompresować plik

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

Przykład: zdekompresować plik

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

Pamiętaj: GZIP działa tylko na jednym pliku – nie zachowuje struktury folderów ani dodatkowych metadanych. W przeciwieństwie do niego ZIP pozwala pakować wiele plików i całe foldery, zachowując ich strukturę oraz informacje o każdym elemencie.

4. Pakowanie/rozpakowywanie katalogu, filtry przez PathMatcher

Pakowanie katalogu do ZIP

Aby spakować folder z plikami i podfolderami, rekurencyjnie przechodzimy drzewo plików i dodajemy każdy plik do archiwum z poprawną ścieżką względną (separatory w ZIP to zawsze "/").

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

Rozpakowywanie archiwum do katalogu

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

Filtrowanie plików według maski (PathMatcher)

Można filtrować pliki do pakowania/rozpakowywania według maski, np. tylko "*.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. Bezpieczeństwo: Zip Slip, zip bomb, weryfikacja normalizacji ścieżek

Zip Slip (atak przez ścieżkę)

Problem: Atakujący może stworzyć archiwum z plikiem, którego nazwa to "../../../../etc/passwd". Przy rozpakowywaniu bez weryfikacji taki plik może nadpisać pliki systemowe!

Rozwiązanie: przed zapisem pliku znormalizuj ścieżkę i upewnij się, że nie wychodzi ona poza katalog docelowy.

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: próba zapisu poza katalogiem docelowym!");
        }
        if (entry.isDirectory()) {
            Files.createDirectories(outPath);
        } else {
            Files.createDirectories(outPath.getParent());
            try (OutputStream os = Files.newOutputStream(outPath)) {
                zis.transferTo(os);
            }
        }
        zis.closeEntry();
    }
}

Zip bomb (bomba ZIP)

Problem: archiwum może zawierać plik, który po rozpakowaniu zajmuje gigabajty, choć samo archiwum waży kilka kilobajtów. To może „zabić” serwer lub dysk.

Rozwiązanie: ograniczaj maksymalny rozmiar rozpakowywanych plików i łączny wolumen rozpakowywania, przerywając proces po przekroczeniu limitu.

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. Praktyka: narzędzie CLI „zip/unzip” z maskami

Napiszemy proste narzędzie konsolowe do pakowania i rozpakowywania plików z obsługą masek.

Przykład: pakowanie

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

Przykład: rozpakowywanie z ochroną przed 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: próba zapisu poza katalogiem docelowym!");
            }
            if (entry.isDirectory()) {
                Files.createDirectories(outPath);
            } else {
                Files.createDirectories(outPath.getParent());
                try (OutputStream os = Files.newOutputStream(outPath)) {
                    zis.transferTo(os);
                }
            }
            zis.closeEntry();
        }
    }
}

Przykład uruchomienia:

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

W pierwszym przykładzie polecenie java ZipUtil zip myfolder archive.zip "*.txt" pakuje wszystkie pliki .txt z folderu myfolder do archiwum archive.zip. W drugim przykładzie java ZipUtil unzip archive.zip output rozpakowuje archiwum do folderu output, przy czym weryfikowane jest, aby żaden plik nie został zapisany poza katalogiem docelowym – to właśnie ochrona przed Zip Slip.

1
Ankieta/quiz
Optymalizacja IO, poziom 41, lekcja 4
Niedostępny
Optymalizacja IO
Optymalizacja IO
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION