1. Einführung: Wozu Archive und Komprimierung in Java
In der heutigen Welt ist der Umgang mit Archiven und komprimierten Dateien eine alltägliche Aufgabe: Backups, Dateiaustausch, Logging, Speicherung großer Datenmengen. Java stellt Standardwerkzeuge für die Arbeit mit ZIP-Archiven und GZIP-komprimierten Dateien über das Paket java.util.zip bereit.
Was Java kann:
- ZIP-Archive lesen und erstellen (Mehrdatei-Container).
- GZIP-Dateien lesen und erstellen (Komprimierung einer einzelnen Datei).
- Archivinhalt verwalten, Dateien nach Muster filtern.
- Den Komprimierungsgrad steuern.
- Sicherheit beim Entpacken prüfen (Schutz vor Zip Slip und Zip-Bomb).
2. Grundlegende Klassen
ZipInputStream und ZipOutputStream
Das sind Stream-Klassen für sequentielles Lesen/Schreiben von ZIP-Archiven. Wann verwenden: wenn ein Archiv „on the fly“ gelesen oder erstellt werden soll, ohne Random-Access zu einzelnen Dateien.
Beispiel: Archiv lesen
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("Datei: " + entry.getName());
// Der Inhalt des Eintrags kann über zis.read(...) gelesen werden
zis.closeEntry();
}
}
Beispiel: Archiv erstellen
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("Hallo, Archiv!".getBytes());
zos.closeEntry();
}
ZipFile
Klasse für Random-Access zum Inhalt eines ZIP-Archivs: Man kann schnell die Dateiliste erhalten, jede Datei per Namen öffnen und ihren Inhalt lesen.
Beispiel:
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)) {
// Dateiinhalt lesen
}
zipFile.close();
Wann ZipFile verwenden?
- Wenn man schnell die Dateiliste, Metadaten, Größe und Datum erhalten möchte.
- Wenn man einzelne Dateien lesen will, ohne das gesamte Archiv sequentiell zu durchlaufen.
ZipEntry
Ein Objekt, das eine einzelne Datei oder einen Ordner innerhalb des Archivs repräsentiert. Enthält Name, Größe, Datum, Flags, Komprimierungsstufe usw.
import java.util.zip.ZipEntry;
ZipEntry entry = new ZipEntry("docs/readme.txt");
entry.setComment("Dateibeschreibung");
entry.setTime(System.currentTimeMillis());
Komprimierungsstufen (Deflater)
Beim Erstellen eines Archivs kann man den Komprimierungsgrad steuern (von 0 – ohne Komprimierung, bis 9 – maximale Komprimierung):
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); // oder 0..9
// ...
}
- Deflater.NO_COMPRESSION (0)
- Deflater.BEST_SPEED (1)
- Deflater.BEST_COMPRESSION (9)
- Deflater.DEFAULT_COMPRESSION (-1)
Faustregel: je höher die Stufe, desto langsamer, aber desto stärker die Komprimierung.
3. Komprimierung einer einzelnen Datei
GZIP ist ein Format zur Komprimierung einer einzigen Datei (kein Archiv!). Wird für Logs, temporäre Dateien und die Übertragung über das Netzwerk verwendet.
Beispiel: Datei komprimieren
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);
}
Beispiel: Datei entpacken
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);
}
Merken Sie: GZIP arbeitet nur mit einer Datei – es speichert weder Ordnerstruktur noch zusätzliche Metadaten. Im Gegensatz dazu ermöglicht ZIP das Packen mehrerer Dateien und ganzer Ordner und bewahrt deren Struktur sowie Informationen zu jedem Element.
4. Packen/Entpacken eines Verzeichnisses, Filter mit PathMatcher
Ein Verzeichnis in ein ZIP packen
Um einen Ordner mit Dateien und Unterordnern zu packen, durchlaufen wir den Dateibaum rekursiv und fügen jede Datei mit dem richtigen relativen Pfad hinzu (Pfadtrenner im ZIP sind immer "/").
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(); }
});
}
Ein Archiv in ein Verzeichnis entpacken
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();
}
}
Dateien nach Muster filtern (PathMatcher)
Dateien lassen sich für das Packen/Entpacken nach einem Muster filtern, z. B. nur "*.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. Sicherheit: Zip Slip, Zip-Bomb, Prüfung der Pfadnormalisierung
Zip Slip (Angriff über Pfadangaben)
Problem: Ein Angreifer kann ein Archiv mit einer Datei erstellen, deren Name — "../../../../etc/passwd" — lautet. Beim Entpacken ohne Prüfung kann eine solche Datei Systemdateien überschreiben!
Lösung: Vor dem Schreiben den Pfad normalisieren und sicherstellen, dass er das Zielverzeichnis nicht verlässt.
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: Versuch, außerhalb des Zielordners zu schreiben!");
}
if (entry.isDirectory()) {
Files.createDirectories(outPath);
} else {
Files.createDirectories(outPath.getParent());
try (OutputStream os = Files.newOutputStream(outPath)) {
zis.transferTo(os);
}
}
zis.closeEntry();
}
}
Zip-Bomb (Archivbombe)
Problem: Ein Archiv kann eine Datei enthalten, die nach dem Entpacken Gigabytes belegt, obwohl das Archiv selbst nur wenige Kilobyte groß ist. Das kann Server oder Festplatte lahmlegen.
Lösung: Beschränken Sie die maximale Größe entpackter Dateien und das Gesamtvolumen der Entpackung und brechen Sie den Prozess bei Überschreitung ab.
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. Praxis: CLI-Tool „zip/unzip“ mit Mustern
Wir schreiben ein einfaches Konsolen-Tool zum Packen und Entpacken von Dateien mit Musterunterstützung.
Beispiel: Packen
// 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(); }
});
}
}
Beispiel: Entpacken mit Schutz vor 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: Versuch, außerhalb des Zielordners zu schreiben!");
}
if (entry.isDirectory()) {
Files.createDirectories(outPath);
} else {
Files.createDirectories(outPath.getParent());
try (OutputStream os = Files.newOutputStream(outPath)) {
zis.transferTo(os);
}
}
zis.closeEntry();
}
}
}
Beispielaufruf:
java ZipUtil zip myfolder archive.zip "*.txt"
java ZipUtil unzip archive.zip output
Im ersten Beispiel packt der Befehl java ZipUtil zip myfolder archive.zip "*.txt" alle .txt-Dateien aus dem Ordner myfolder in das Archiv archive.zip. Im zweiten Beispiel entpackt java ZipUtil unzip archive.zip output das Archiv in den Ordner output, wobei sichergestellt wird, dass keine Datei außerhalb des Zielverzeichnisses geschrieben wird – genau das ist der Schutz vor Zip Slip.
GO TO FULL VERSION