/// <summary> /// Nejsložitější metoda v celé třídě. Má na starost uložení změn provedených v archívu. /// </summary> public void SaveArchieve() { // Metoda nemá smysl, pokud není otevřen nějaký soubor if (!IsOpened) throw new ArchieverException("Nepracujete s žádným souborem!"); // Do tohoto MemoryStreamu načteme všechny staré věci které mají v archívu zůstat (nebyly odstraněny) a zároveň do něj přidáme nové věci. using (MemoryStream MyMemory = new MemoryStream()) { // Musíme tedy projít všechny soubory které chceme zachovat a "přeuložit" je do streamu MyMemory int ToJump = 0, CurrentSize = 0; for (int i = 0; i < FilesIn.Count; i++) { // CurrentSize používám jen jako pomocnou proměnnou abych pořád nemusel dělat toto nepěkné přetypování CurrentSize = ((FileHeader)FilesIn[i]).Size; // Jesliže je momentální index v poli indexů pro odstranění z archívu, vynecháme jej, ale musíme s ním počítat i při přeskoku pomocí Seek if (FilesToRemove.IndexOf(i) > -1) { ToJump += CurrentSize; continue; // Jak jistě víte, příkaz continue ukončí momentální iteraci, ale ne samotný cyklus. } // Přeskoč na daný soubor MyArchieve.Seek((long)(FilesBegin + ToJump), SeekOrigin.Begin); // Opět se zde vědomě dopouštím stejné chyby jako v indexeru, viz. komentáře v kódu indexeru byte[] buffer = new byte[CurrentSize]; MyArchieve.Read(buffer, 0, CurrentSize); MyMemory.Write(buffer, 0, CurrentSize); ToJump += CurrentSize; } // Teď už můžeme z FilesIn skutečně bezpečně odstranit všechny ty indexy, které jsou v poli FilesToRemove foreach (int index in FilesToRemove) FilesIn.RemoveAt(index); // Další krok bude přidání nových souborů do archívu... byte[] buf = new byte[1024]; // V BytesRead bude počet bajtů který se přečtl při jedné blokové operaci.V TotalSize nasčítáme počet všech přečtěných bajtů, což je totéž jako velikost souboru int BytesRead = 0, TotalSize = 0; foreach (string PathToFile in FilesToAdd) { try // Zde by mohla nastat spousta špatností, protože pracujeme s filesystémem { // Toto je již velmi známá konstrukce čtení dat po pevných blocích - netřeba dál komentovat. using (FileStream MyNewFile = File.OpenRead(PathToFile)) { TotalSize = 0; while ((BytesRead = MyNewFile.Read(buf, 0, 1024)) > 0) { MyMemory.Write(buf, 0, BytesRead); TotalSize += BytesRead; } } } catch (Exception e) // Chytej jakoukoliv vyjímku { // Vyhoď vyjímku ArchieverException s textem původní vyjímky která nastala throw new ArchieverException(e.Message); } // Všimněte si, že díky bloku using() nemusíme používat finally a v něm stream zavírat! // Když všechno prošlo bez chyby, tak vytvoříme pro nový soubor hlavičku a přidáme ji do pole FilesIn FileHeader MyNewHeader = new FileHeader(); MyNewHeader.Name = Path.GetFileName(PathToFile); MyNewHeader.Size = TotalSize; FilesIn.Add(MyNewHeader); } // Vyčistíme kolekce FilesToAdd a FilesToRemove FilesToAdd.Clear(); FilesToRemove.Clear(); // V posledním kroku prostě přepíšeme původní stream MyArchieve novou hlavičkou a tím co máme v MyMemory streamu MyArchieve.Seek(0, SeekOrigin.Begin); MyArchieve.SetLength(0); // Toto zničí všechny data ve streamu (zkrátí délku souboru na 0) // První čtyři bajty archívu je počet souborů v něm (opět používáme BitConverter) MyArchieve.Write(BitConverter.GetBytes(FilesIn.Count), 0, 4); // Teď zapíšeme názvy souborů (s ohledem na 150 bajtový limit jeho délky) a hned za něj jeho velikost, celkem tedy 154 bajtů (v případě že někdo nezmění NameMaxLength na jinou hodnotu než 150) foreach (FileHeader item in FilesIn) { // Toto pole bajtů má přesně takovou velikost jako je délka názvu souboru v kódování systému byte[] bytesOfName = Encoding.Default.GetBytes(item.Name); // Musíme velikost pole bytesOfName zarovnat na NameMaxLength, což je v našem případě 150 byte[] normalizedName = new byte[NameMaxLength]; // Pozorně prosím prostudujte jak pracuje tento cyklus (kopíruje z pole s názvem souboru do normalizovaného pole od délce NameMaxLength, když je delší - oseká se) for (int i = 0; i < (bytesOfName.Length > NameMaxLength ? NameMaxLength : bytesOfName.Length);i++) normalizedName[i] = bytesOfName[i]; MyArchieve.Write(normalizedName, 0,NameMaxLength); MyArchieve.Write(BitConverter.GetBytes(item.Size), 0, 4); } // ... a úplně nakonec zapíšeme všechny soubory, které již máme připraveny v MyMemory - tam jsou ve stejném pořadí tak jako v hlavičce. MyArchieve.Write(MyMemory.ToArray(), 0, (int)MyMemory.Length); } // A HOTOVO! (Všimněte si, že celý kód metody byl v postatě "zabalen" v bloku od using(MemoryStream MyMemory ...) takže ať se stalo cokoliv bude vždycky zavřen! }
/// <summary> /// Otevři archív, přečti názvy souborů v něm uložených do interní kolekce FilesIn /// </summary> /// <param name="Where">Cesta k archívu</param> public void OpenArchieve(string Where) { // Je-li už nějaký archív otevřen, zavři jej if (IsOpened) Close(); // Kdyby se cokoliv stalo, použij try-catch try { // Otevři soubor s archívem (metoda File.Open vyhodí FileNotFoundException vyjímku když soubor neexistuje) MyArchieve = File.Open(Where,FileMode.Open); // Přečti první 4 bajty, v nich je uložený počet souborů v archívu. (Vidíte, že je dobré si pamatovat kolik který datový typ zabírá v paměti bajtů) byte[] NumFiles = new byte[4]; MyArchieve.Read(NumFiles, 0, 4); // Statická třída BitConverter slouží k převedení pole bajtů na nějaký primitivní datový typ (v tomto případě Int32 - což je obyčejný int) int CountFiles = BitConverter.ToInt32(NumFiles, 0); /* Teď budeme číst názvy souborů a jejich velikosti v archívu. * Vždycky 150 bajtů obsahuje název souboru a další 4 bajty hned za ním obsahují velikost tohoto souboru, takže budeme číst po 154 bajtových blocích. * Je zřejmé že 150 bajtů je maximální velikost názvu souboru - tento limit jsem si zvolil. Záleží na kódování sytému kolik je to znaků * - v systémech s Unicode kódováním to bude 75 znaků, v ASCII kodování to je 150 znaků. * Maximální délku názvu souboru jsem nadefinoval nahoře jako konstantu s názvem NameMaxLegth pro případ, že by někomu 150 bajtů nestačilo. */ byte[] buffer = new byte[NameMaxLength+4]; // Čtecí buffer for (int i = 0; i < CountFiles; i++) // Čti přesně tolikrát kolik jsme přečetli že je počet souborů v archívu { MyArchieve.Read(buffer, 0, NameMaxLength+4); // Sestavíme strukturu FileHeader z přečtených dat FileHeader Header = new FileHeader(); // Metoda Encoding.Default.GetString() zajistí převod pole bajtů na řetězec podle kódování systému na kterém program běží. // Je jasné, že toto není nejvhodnější pokud by program používali v různých částech světa - jím vytvořené archívy by nebyly kompatibilní. // Pak by bylo nutné použít nějaké pevné kódování - nejlépe UTF8 (což je Unicode), takže by se volala metoda Encoding.UTF8.GetString() Header.Name = Encoding.Default.GetString(buffer, 0, buffer.Length - 4).TrimEnd('\0'); // Metoda TrimEnd() oseká z konce daného řetězce netisknutelné znaky - což jsou znaky \0 v našem případě, ty vznikly při zarovnání jména souboru na NameMaxLength při vytváření archívu. // Opět dekóduj poslední 4 bajty z bloku jako velikost souboru Header.Size = BitConverter.ToInt32(buffer, buffer.Length - 4); // Přidej právě vytvořenou strukturu do kolekce FilesIn FilesIn.Add(Header); } // Po skončení cyklu bude samozřejmě kurzor uvnitř streamu nastaven na první bajt prvního souboru v archívu. Jako pomůcku tuto hodnotu ulož. FilesBegin = MyArchieve.Position; } catch (Exception e) // Zachytni všechno { // Něco selhalo, zavři stream <- soubor s archívem se prostě nepodařilo přečíst MyArchieve.Close(); MyArchieve = null; // ..a vyhoď vyjímku s textem o tom co selhalo throw new ArchieverException(e.Message); } }