Oki doki, dzisiaj postaram się pokazać jak w prosty sposób zrobić plik podobny do archiwum np. zipa czy rara. Wprawdzie w naszym formacie nie będziemy stosowali kompresji (aby niepotrzebnie na początek nie komplikować) ale pokaże jedno z możliwych podejść do tego zagadnienia. Na początek może napisze po krótce do czego może się przydać taki plik, otóż można w nim trzymać np. spakowane levele do gry, save gamy, mapy oraz inne zasoby do programów. Ok, koniec marudzenia, co będziemy potrzebowali do szczęścia ?
1. Podstawy programowania obiektowego.
2. Podstawy wiadomości o strumieniach.
3. kilka minut czasu aby przeczytać i zrozumieć.
Na początek, trzeba się zastanowić jaka będzie wewnętrzna struktura naszego archiwum ? Ja wybrałem bardzo prosty układ, pliki w archiwum są przechowywane kolejno w następującej postaci:
Rozmiar nazwy[int], nazwa[tablica bajtów], rozmiar danych[int], dane plik[tablica bajtów]
Jakie są główne plusy:
1. Prosta budowa pliku.
2. Łatwość dodawania kolejnych danych
minusy:
1. Czasochłonne usuwanie plików z archiwum.
2. Czasochłonna zmiana kolejności plików w archiwum.
3. Aby uzyskać listę plików trzeba przeskanować całe archiwum.

Jak widać jest więcej minusów jak plusów, ale w moim przypadku przeważyła dla mnie prostota pliku, poza tym kasowanie/reorganizacja archiwum nie są zbyt częstymi operacjami więc nie były dla mnie dużym minusem.
Ok. wiemy już, co chcemy oraz jakimi środkami to uzyskamy, teraz wypisze metody, które zaimplementowałem w naszej klasie obsługującej archiwum, i opisze z grubsza (szczegóły można zrozumieć analizując dołączony kod) idee działania tych metod:

Tworzymy nasze archiwum, w zależności od trybu mode (patrz TFileStream + F1), tworzymy nowe archiwum lub otwieramy już istniejące. Inicjujemy wewnętrznego streama który będzie obsługiwał nasz plik.

Bez komentarza 😉

Głównym zadaniem tej procedury jest przeskanowanie archiwum i odnalezienie nazw oraz rozmiarów plików przechowywanych w archiwum. Ponieważ układ w naszym arch. jest sekwencyjny wystarczy abyśmy pobrali nazwę i rozmiar pliku na początku którego obecnie znajduje się nasz wewnętrzny stream, zapisujemy te dane, a następnie przeskakujemy o wielkość właśnie zbadanego pliku i trafiamy na początek następnego. Czynność tą powtarzamy aż dojdziemy do końca.

Dzięki tej procce dodajemy nowe dane do naszego archiwum, jak widać dane mogą być pobierane z dowolnego streama (lub pliku), który zwróci nam swój rozmiar. Metoda na początku przeskakuje na koniec naszego archiwum, a później dopisuje do niego przekazane dane. Zanim jednak to zrobi sprawdza czy w naszym archiwum nie ma już pliku o podanej nazwie. W tej implementacji możliwe jest przekazanie całej ścieżki jako nazwy pliku. Nazwa jest Case sensitive !

Ponieważ jak wiemy usuwanie pliku jest czasochłonne ta metoda ma dodatkowy parametr imediate, który mówi czy usunięcie ma zostać wykonane zaraz po wywołaniu tej metody. Po co takie coś ? Ano jeśli ustawimy imediate na false, każde wywołanie tej procedury zapisze tylko nazwę pliku, który ma zostać usunięty. Na sam koniec, gdy już wybierzemy wszystkie pliki do skasowania wywołujemy ExecutePendingDeletion i całe kasowanie następuje dużo szybciej (w jednym przebiegu).

Funkcja dokonuje właściwego kasowania z archiwum (polecam przyjrzeć się metodzie DeleteFile od środka). Idea jest bardzo prosta:
1. Tworzymy tymczasowy stream, do którego będziemy kopiowali dane.
2. Ustawiamy się na początku naszego archiwum
3. Pobieramy nazwę pliku z archiwum, jeśli ta nazwa jest na liście plików do skasowania przeskakujemy do punktu 5
4. Kopiujemy plik (nazwa + dane) do tymczasowego streama
5. Przechodzimy do kolejnego pliku w naszym archiwum, i wykonujemy punkt 3. Całość powtarzamy tak długo aż skończą się dane w archiwum.
6. Zamykamy tymczasowy stream, zamykamy nasze archiwum.
7. Kasujemy nasze archiwum, dokonujemy rename tymczasowego staremu (pliku) do nazwy jaką miało nasze stare archiwum i ponownie je otwieramy

Funkcja zapamiętuje aktualna pozycje w streamie, następnie pobiera nagłówek pliku na początku którego ustawiony jest stream po czym powraca do pozycji zapamiętanej na początku. Jako rezultat zwraca pobraną nazwę pliku.

Funkcja pobiera rozmiar pliku, na początku którego ustawiony jest stream po czym przeskakuje o tą wartość dalej, w rezultacie trafia na początek kolejnego pliku.

Funkcja zwraca pointer do streama z którego można pobrać plik na początku którego znajduje się stream. Jeśli skipHeader jest ustawiony na true, zwrócony stream będzie pokazywał na początek danych pliku. Jeśli zaś skipHeader = false stream będzie pokazywał na nagłówek pliku. W zmiennej FileSize zostanie zwrócona wielkość pliku. UWAGA: przeczytanie mniejszej lub większej ilości danych ze zwróconego streama niż FileSize powoduje, że nasze wewnętrzny stream jest gdzieś poza nagłówkami plików. Aby móc ponownie z niego korzystać należy wywołać procedure FindFileNamed, procedury które mają w nazwie NextFile nie będą poprawnie działać !

Działanie jest identyczne jak w GetNextFile, jedyna różnica polega na tym, że zwrócony stream będzie ustawiony na początku pliku o zadanej nazwie.

Zasada działania identyczna jak w GetNextFileName, jedyna różnica polega na tym, że zwracane są inne dane z nagłówka.

Funkcja zwraca true jeśli nie jesteśmy na końcu archiwum. Aby to sprawdzić wystarczy porównać czy pozycja w naszym wewnętrznym streamie jest mniejsza od wielkości pliku.

Procedura zmienia kolejność plików w archiwum. Zasada jest zbliżona do kasowania pliku, czyli tworzymy tymczasowy stream, do którego kopiowane są kolejno dane z naszego archiwum. Różnica polega na tym, że kolejne dane które będą skopiowane są wyszukiwane metodą FindFileNamed na podstawie zmiennej OrderedNames.

Ok. To tyle, trochę zwięźle i może lakonicznie, ale kod jest naprawdę prosty, proponuje poeksperymentować i zobaczyć jak to wygląda w praktyce. Wszelakie pytania mile widziane.

Autor: Toster