Mittwoch, 20. Oktober 2010

Komprimierung mit C#, Teil 3 - Archive erstellen und wieder entpacken

Im 3. und (vorerst) letzten Teil zur Reihe "Komprimierung in C#" möchte ich euch eine Möglichkeit zeigen, mehrere Dateien auf einmal in einem Archiv zu komprimieren und dieses wieder zu entpacken.
Um diesen Post hier zu verstehen, ist ein Verständnis der Techniken aus Teil 1 und 2 hilfreich.
Mehrere Dateien in ein Archiv zu verpacken, geht in C# nur über einen kleinen Umweg, denn das verwendete gzip - Format unterstützt standardmäßig nur die Komprimierung von einzelnen Dateien.
Sammlungen von mehreren Dateien werden so meistens zuerst mit tar und dann mit gzip komprimiert, sie erhalten dann die Endung .tar.gz.
Obwohl uns das tar - Format in .Net nicht zur Verfügung steht, lässt sich eine Komprimierung von mehreren Dateien doch umsetzen: Wir fassen die einzelnen Dateien einfach zu einer großen Datei zusammen, in welcher die Dateien durch besondere Zeichen voneinander getrennt sind. Diese große Datei wird dann komprimiert und beim Dekomprimieren wieder anhand der Sonderzeichen in die einzelnen Dateien aufgeteilt.

Die Technik:
Als Kennzeichnung zwischen den Dateien schrieb ich folgenden Header vor jede Datei in den Stream:
|*START*OF*HEADER*|*||GRÖSSE_DER_DATEI||NAME_DER_DATEI*|*END*OF*HEADER*|
Probleme könnte es also in dem unwahrscheinlichen Fall geben, wenn der Inhalt einer Datei der Struktur gleichen würde.

Die Komprimierung / Erstellung eines Archivs:
Der Methode zur Komprimierung werden die zu komprimierenden Dateien als String - Array übergeben sowie der Name und Pfad des Archivs.
Die Dateien werden nun durchlaufen und der jeweilige Header sowie anschließend der Inhalt der Datei wird zuerst in einen MemoryStream geschrieben.
Die Daten aus diesem werden anschließend in ein byte - Array kopiert und dieses wird dann mit einem GZipStream in die Archivdatei geschrieben.
Dadurch, dass die Dateien erst zusammengefasst und dann mit dem GZipStream geschrieben werden und nicht jede Datei nacheinander in das Archiv geschrieben wird, kann das Archiv viel stärker komprimiert werden, da auch Redundanzen zwischen den einzelnen Dateien ausgenutzt werden können.
Der Code:

        private void CreateArchive(string[] files, string archiv)
        {
            GZipStream CompressStream = new GZipStream(new FileStream(archiv, FileMode.Create), CompressionMode.Compress);
            FileStream NormalFileStream;
            byte[] Content;

            ASCIIEncoding encoder = new ASCIIEncoding();
            byte[] HeaderStart = encoder.GetBytes("|*START*OF*HEADER*|*");
            byte[] HeaderEnd = encoder.GetBytes("*|*END*OF*HEADER*|");
            byte[] FileSize; // Größe der aktuellen Datei
            byte[] Separator = encoder.GetBytes("||");
            byte[] FileName; // Name der aktuellen Datei

            MemoryStream TempStream = new MemoryStream();

            foreach (string file in files)
            {
                NormalFileStream = new FileStream(file, FileMode.Open);
                FileSize = encoder.GetBytes(NormalFileStream.Length.ToString());
                FileName = encoder.GetBytes(file.Substring(file.LastIndexOf('\\') + 1));

                TempStream.Write(HeaderStart, 0, HeaderStart.Length);
                TempStream.Write(Separator, 0, Separator.Length);
                TempStream.Write(FileSize, 0, FileSize.Length);
                TempStream.Write(Separator, 0, Separator.Length);
                TempStream.Write(FileName, 0, FileName.Length);
                TempStream.Write(HeaderEnd, 0, HeaderEnd.Length);

                Content = new byte[NormalFileStream.Length];
                NormalFileStream.Read(Content, 0, Content.Length);
                NormalFileStream.Close();
                TempStream.Write(Content, 0, Content.Length);
            }

            byte[] BigFileContent = new byte[TempStream.Length];
            TempStream.Position = 0;
            TempStream.Read(BigFileContent, 0, BigFileContent.Length);
            CompressStream.Write(BigFileContent, 0, BigFileContent.Length);
            CompressStream.Close();
        }


Die Dekomprimierung / Entpackung eines Archivs:
Die Komprimierung war der einfache Part, die Dekomprimierung gestaltet sich etwas schwieriger.
Die Methode zur Dekomprimierung erhält den Pfad und Namen des Archivs sowie den Pfad, in den die Dateien anhand ihrer ursprünglichen Namen entpackt werden sollen.
Zuerst müssen die Daten aus der Archivdatei dekomprimiert werden, hierzu wird diese mit einem GZipStream ausgelesen.
Der Inhalt dieses wird anschließend in einen MemoryStream kopiert und dieser schreibt seinen Inhalt in ein byte - Array (ist leichter über einen MemoryStream, deswegen der Umweg).
Das byte - Array wird mit einer Instanz der Klasse ASCIIEncoder in einen String umgewandelt.
Das Auswerten des Inhalts erfolgt in einer Endlosschleife, in jedem Durchlauf wird eine Datei behandelt.
Es gibt 2 Zeiger, die auf eine Position im String zeigen.
Der erste speichert die aktuelle Position, der zweite die aktuelle Suchposition.
Der erste steht immer auf der Position, an der der Header der aktuellen Datei anfängt, der zweite auf eine um 22 höhere Position (da wo die Dateigröße anfängt).
Da die Struktur des Headers bekannt ist (z.B. die Größe der ersten Datei beginnt ab Position 22 und geht bis zum ersten Vorkommen von "||" - deshalb steht die Suchposition auf einer um 22 höheren Position als die aktuelle Position, nach der Suchposition markiert das erste Vorkommen von "||" das Ende der Dateigröße, davor taucht noch ein "||" vor der Dateigröße aus.), können Dateigröße und Name ausgelesen werden.
Die aktuelle Position wird nun um die Länge des Headers inkrementiert.
Mit einem FileStream wird nun das byte - Array, welches den Inhalt der großen Datei speichert, von der aktuellen Position bis zur Position aktuelle Position zuzüglich aktueller Dateilänge, in eine neue Datei geschrieben, diese wird im übergebenen Verzeichnis angelegt und trägt den Namen der ursprünglichen Datei.
Die Positionszeiger werden anschließend um die Größe der aktuellen Datei erhöht.
Der Code:
        private void OpenArchive(string archiv, string decompressPath)
        {
            GZipStream DecompressStream = new GZipStream(new FileStream(archiv, FileMode.Open), CompressionMode.Decompress);
            FileStream NormalFileStream;
            MemoryStream TempStream = new MemoryStream();

            ASCIIEncoding decoder = new ASCIIEncoding();
            ASCIIEncoding Encoder = new ASCIIEncoding();

            string StringFromBytes; // String - Darstellung der eingelesenen Bytes
            int EndSize; // Position im Header, an welcher die Bezeichnung der Dateigröße zu Ende ist
            long FileLength; // Größe der aktuellen Datei
            int StartFileName; // Position im Header, an welcher die Bezeichnung des Dateinamens anfängt
            int EndFileName; // Position im Header, an welcher die Bezeichnung des Dateinamens aufhört
            string FileName; // Name der aktuellen Datei
            string EmptyHeader = "|*START*OF*HEADER*|*||||*|*END*OF*HEADER*|"; // "Prototyp" des Headers
            byte[] EmptyHeaderBytes = Encoder.GetBytes(EmptyHeader); // Prototyp als Bytes

            long CurrentPosition = 0; // aktuelle Position im Inhalt der Datei
            long CurrentSearchPosition = 22; // aktuelle Suchposition im Inhalt der Datei

            DecompressStream.CopyTo(TempStream);
            byte[] BigFileContent = new byte[TempStream.Length];
            TempStream.Position = 0;
            TempStream.Read(BigFileContent, 0, BigFileContent.Length);
        
            StringFromBytes = decoder.GetString(BigFileContent);

            while (true)
            {
                EndSize = StringFromBytes.IndexOf("||", (int)CurrentSearchPosition);
                FileLength = long.Parse(StringFromBytes.Substring((int)CurrentSearchPosition, EndSize - (int)CurrentSearchPosition)); // die Bezeichnung der Dateigröße geht von Position 22 im Header bis Position EndSize

                StartFileName = EndSize + 2;
                EndFileName = StringFromBytes.IndexOf("*|*", StartFileName);
                FileName = StringFromBytes.Substring(StartFileName, EndFileName - StartFileName); // Dateinamen auslesen
              
                CurrentPosition += EmptyHeaderBytes.Length + Encoder.GetBytes(FileLength.ToString()).Length + Encoder.GetBytes(FileName).Length;
                
                NormalFileStream = new FileStream(decompressPath + "\\" + FileName, FileMode.Create);
                NormalFileStream.Write(BigFileContent, (int)CurrentPosition, (int)FileLength);

                CurrentPosition += FileLength;
                CurrentSearchPosition = CurrentPosition + 22;
                NormalFileStream.Close();

                if (CurrentSearchPosition > BigFileContent.Length)
                    break;
            }

            DecompressStream.Close();
        }

Keine Kommentare:

Kommentar veröffentlichen