Wie kann man den Inhalt eines Verzeichnisses so in einem Zip-Archiv speichern, dass die ursprünglichen Benennungen und die Struktur des Dateibaums erhalten bleiben?

Die Lösung liegt im rekursiven Durchwandern des zu packenden Quellverzeichnisses und dem Anpassen der verwendeten Dateipfade.

Im Konstruktor der Beispielklasse werden zunächst der Pfad des zu packenden Verzeichnisses und derjenige des Zielarchivs deklariert. Um zu einem späteren Zeitpunkt die Pfade der gepackten Dateien zu ermitteln, wird der Pfad der Zip-Datei zunächst ohne Dateiendung angegeben.
Die darauf folgenden Operationen sind in einem try-catch -Block gekapselt, um Fehler beim Anlegen des Zip-Archivs abzufangen. Dies geschieht durch einen ZipOutputStream . Er erzeugt das noch leere Zip-Archiv, indem er einen FileOutputStream mit dem gewünschten Dateinamen übergeben bekommt.
Der dann folgende Methodenaufruf erfolgt mir vier Parametern:

Die auf den ersten Blick überflüssig erscheinende Ähnlichkeit des zweiten und dritten Parameters hat seinen Sinn darin, dass die Methode rekursiv mit unterschiedlichen File -Objekten aufgerufen werden muss, der Pfad des Quellverzeichnisses jedoch konstant gehalten werden muss, um die relativen Pfade innerhalb des Archivs bestimmen zu können. Der Methodenaufruf wird nur noch gefolgt vom Schließen des ZipOutputStreams .

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class VerzeichnisZippen {

    public VerzeichnisZippen() {
        
        String dirToZip = "/Users/Willi/Desktop/test";
        String zipName = "/Users/Willi/Desktop/abrakadabra";
        ZipOutputStream zos = null;
        try {
            File f = new File(zipName + ".zip");
            System.out.println("Erzeuge Archiv " + f.getCanonicalPath());
            zos = new ZipOutputStream(new FileOutputStream(
                    f.getCanonicalPath()));
            zipDir(zipName, dirToZip, new File(dirToZip), zos);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (zos != null) zos.close();
            } catch (IOException ioe) {}
        }
    }

    private void zipDir(String zipName, String dirToZip, File dirToZipFile,
            ZipOutputStream zos) {
        if (zipName == null || dirToZip == null || dirToZipFile == null
                || zos == null || !dirToZipFile.isDirectory())
            return;

        FileInputStream fis = null;
        try {
            File[] fileArr = dirToZipFile.listFiles();
            String path;
            for (File f : fileArr) {
                if (f.isDirectory()) {
                    zipDir(zipName, dirToZip, f, zos);
                    continue;
                }
                fis = new FileInputStream(f);
                path = f.getCanonicalPath();
                String name = path.substring(dirToZip.length(), path.length());
                System.out.println("Packe " + name);
                zos.putNextEntry(new ZipEntry(name));
                int len;
                byte[] buffer = new byte[2048];
                while ((len = fis.read(buffer, 0, buffer.length)) > 0) {
                    zos.write(buffer, 0, len);
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if(fis != null) fis.close();
            } catch (IOException ioe) {}
        }
    }

    public static void main(String[] args) {
        new VerzeichnisZippen();
    }
}

Das eigentliche Packen geschieht in der Methode zipDir() , die die o.a. Parameter übergeben bekommt. Nach einer Sicherheitsabfrage, die die Verwendbarkeit der Parameter sicherstellen soll, wird wiederum ein try-catch -Block gebildet, um Fehler beim Zippen abzufangen.
In diesem wird das übergebene Verzeichnis ausgelesen und alle enthaltenen Dateien und Verzeichnisse als File -Objekte in einem Array gespeichert. In einer for-Schleife wird das Array durchlaufen und jedes File -Objekt zunächst darauf hin geprüft, ob es sich um ein Verzeichnis handelt. Ist dies der Fall, so wird die Methode rekursiv mit diesem Verzeichnis als drittem Parameter aufgerufen, sodass eine Verarbeitung dessen Inhalts sichergestellt wird.
Ist dies nicht der Fall, handelt es sich also um eine Datei, so wird ein FileInputStream erzeugt, der später zum Auslesen des Dateiinhaltes dient. Zunächst muss jedoch der ursprünglich Pfad der Datei relativ zum Quellverzeichnis ermittelt und dieser dann innerhalb des Zip-Archivs erzeugt werden. Hierzu dient der zweite Parameter. Vom absoluten Pfad der zu packenden Datei wird der erste Teil, derjenige also, der den Pfad zum Quellverzeichnis ausmacht, entfernt. Es verbleibt der angesprochene relative Pfad.
Die Methode putNextEntry() des ZipOutputStreams nimmt ein ZipEntry -Objekt entgegen. Dies erhält den ermittelten relativen Pfad als Parameter. Die Methode erzeugt einen neuen Eintrag in der Archivdatei, der zunächst jedoch noch leer bleibt.

Das Übertragen der Daten erfolgt über einen Array-Puffer. Der FileInputStream liest mit seiner Methode read() die zu speichernde Datei aus. Dabei wird der Inhalt so lange in einem Byte-Array zwischengespeichert, wie weiterer Inhalt vorhanden ist. Die Größe der ausgelesenen 'Portionen' wird hierbei durch die Länge des Arrays bestimmt. Ist das Array gefüllt wird es an den ZipOutputStream übergeben, dessen Methode write() für eine Speicherung im Archiv sorgt. Der Inhalt des Arrays wird dann wieder überschrieben, wieder ausgelesen, etc. bis der Inhalt der Datei vollständig erfasst wurde.
Nach Schließen des FileInputStream beginnet der Zyklus von vorne.