Intro - problem do rozwiązania
Jesteś programistą, więc na pewno znasz jakieś narzędzie do porównywania plików np.
narzędzie diff. Możesz chcieć sprawdzić co się zmieniło w danej wersji pliku w stosunku do jego poprzedniej wersji lub jeszcze wcześniejszej. Na pierwszy rzut oka narzuca się pomysł na trzymanie każdej wersji pliku -to rozwiązanie jest bardzo pamięciożerne, gdyż często w dużym pliku (np. dużym pliku
archiwum ZIP) może się zmienić tylko drobiazg (np. komentarz archiwum). Jak widać trzymanie całych wersji jest bardzo nieefektywne, co dopiero wysyłanie całych wersji przez sieć. Ewentualnie możemy trzymać diffy każdego pliku, ale co w przypadku plików binarnych?
Zaproponowane rozwiązanie
Każdy plik jest dzielony na kawałki, dla każdego kawałka tworzona jest checksuma, właśnie te checksumy są porównywane i w razie niezgodności tworzony jest diff (określony w tej bibliotece jako
delta) z kawałków o niezgodnej checksumie (
więcej informacji na temat algorytmu). W odróżnieniu od innych bibliotek generujących różnicę między wersjami pliku zamiast trzymania dwóch wersji pliku wystarczy jeden oraz plik checksum każdego bloku w pliku (określony w tej bibliotece jako
sygnatura).
Właśnie takie rozwiązanie stosuje dość popularny program
rsync służący do synchronizacji plików przez sieć. Do generowania diffów plików i stosowania ich celem doprowadzenia do identyczności plików w różnych wersjach służy biblioteka
librsync używana przez programy
rsync,
dropbox,
rdiff-backup i inne narzędzia.
Nazwa librsync się kojarzy z rsync, ale biblioteka ta nie dostarcza wszystkich mechanizmów programu
rsync takich jak synchronizacja całych katalogów, serwer, synchronizacja przez sieć itp., dostarcza funkcjonalność w zasadniczo trzech operacjach:
W plikach biblioteki poza biblioteką jest dostarczone narzędzie
rdiff, które z poziomu wiersza poleceń dostarcza powyższe operacje.
Jak pobrać
Tutaj pojawia się pewien dylemat -zależy którą wersję biblioteki, a wybór nie jest taki trywialny:
W tym punkcie można dodać, że na
stronie domowej jest na dzień dzisiejszy podana licencja
GNU LGPL v2.1.
Instalacja starej wersji 0.9.7
Budowa tej wersji biblioteki jest bardzo prosta, a zarazem mające więcej funkcji konfiguracyjnych
Budowa Linux
Tutaj opis jest zbędny, wystarczy:
./configure
make
sudo make install
Możemy w skrypcie
./configure
opcjonalnie wskazać miejsce instalacji (
--prefix=...
) lub ustawić, aby były tworzone dynamiczne biblioteki zamiast/oprócz statycznych (szczegóły
./configure --help
).
Budowa Windows z użyciem MSYS
Jeśli ktoś dobrze skonfiguruje sobie
http://mingw.org/wiki/msys (czyli zainstaluje make, dobrze poda ścieżki do g++) to budowa przebiega podobnie jak dla Linuxa.
Budowa Windows bez użycia MSYS
Teraz pytanie, co chcemy zbudować, jeśli tylko bibliotekę librsync to dodajemy w naszym środowisku (np. QT Creatorze) wszystkie pliki języka C dostarczone przez bibliotekę, z wyjątkiem plików zawierających funkcję main() (
isprefix.driver.c oraz
rdiff.c).
Jeśli natomiast chcemy zbudować narzędzie rdiff (co to jest to za chwilę) to jednak musimy dodać do projektu również plik
rdiff.c.
Następnie musimy do ścieżki includów dodać również katalog
PCbuild (jest tam plik
config.h), a jeśli budujemy rdiffa to też katalog
popt.
Niestety są również wymagana jest pewna modyfikacje w kodzie (oczywiście lepiej to poprawiać dopiero w sytuacji, gdy kompilator na to nam zwróci uwagę), a mianowicie w pliku
delta.c należy usunąć przedrostek
inline z deklaracji funkcji (lub zamienić
inline na
static inline).
Do pełnego skompilowania brakuje nam jeszcze dodania stałych czasu kompilacji
HAVE_STDARG_H i
HAVE_STRERROR.
Budowa Windows na Visual Studio
Na VS 2015 musiałem zrobić te same kroki co powyżej, bez modyfikacji pliku
delta.c. Musiałem również dodać dodatkowe stałe czasu kompilacji:
_CRT_SECURE_NO_WARNINGS i
_CRT_NONSTDC_NO_WARNINGS; oczywiście zamiast wyłączać ostrzeżenia można poprawić kod, aby używał bezpieczniejszych funkcji.
Instalacja najnowszej wersji 2.0.1
Niestety w kolejnej wersji jest ciężej to wszystko zbudować na Linuxie, a co dopiero na Windowsie. Bibliotekę w nowej wersji można pobrać
z githuba.
Do zbudowania potrzebujemy mieć
perla (może być wersja portable).
Budowa Linux
Jeśli chcemy zbudować również
rdiff to musimy doinstalować
bibliotekę popt.
Tutaj nie jest dostarczony instalator
configure, zamiast tego mamy
cmake, dlatego budujemy:
cmake .
make
sudo make install
możemy jeszcze zmienić na linkowanie statyczne bibliotek:
sed -i 's/SHARED/STATIC/g' CMakeLists.txt
możemy też ustawić ścieżkę do budowania naszej biblioteki
cmake -DCMAKE_INSTALL_PREFIX:PATH=/gdzie/budujemy/biblioteke/ .
Budowa Windows
Samo zbudowanie biblioteki przy pomocy CMake'a nie jest takie trudne. Wpierw należy poustawiać ścieżki zmiennej środowiskowej
Path (do kompilatora, Perla, CMake'a) i zrestartować komputer. Teraz wystarczy w rozpakowanym katalogu biblioteki wywołać:
PS C:\Users\user\Desktop\librsync\librsync-master> cmake -G "[konfiguracja]" --target=rsync .
PS C:\Users\user\Desktop\librsync\librsync-master> # komenda budowy...
Komenda
cmake dla MinGW z podaniem ścieżki budowy biblioteki może wyglądać tak:
PS C:\Users\user\Desktop\librsync\librsync-master> cmake -G "MinGW Makefiles" -DCMAKE_INSTALL_PREFIX:PATH=./compiled/ --target=rsync .
PS C:\Users\user\Desktop\librsync\librsync-master> mingw32-make.exe
PS C:\Users\user\Desktop\librsync\librsync-master> mingw32-make.exe install
Możemy też zmienić sposób budowania biblioteki, aby mieć skompilowaną bibliotekę statycznie zamiast dynamicznie, w tym celu zmieniamy w pliku
CMakeLists.txt tekst
SHARED na
STATIC.
Uwaga: jeśli chcemy więcej niż raz zawołać w tym samym katalogu komendę cmake to musimy usunąć cache CMake'a (plik CMakeCache.txt) przed kolejnym wywołaniem. |
Jeśli natomiast chcielibyśmy zbudować sobie narzędzie
rdiff to musimy jeszcze pobrać pewne dodatkowe biblioteki.
Narzędzie rdiff
Jest narzędziem dostarczonym wraz z bibliotekę librsync demonstrującym to, co dostarcza owa biblioteka. Opiszę je, gdyż pomoże to w zrozumieniu dostarczonych funkcji, z których to korzysta narzędzie rdiff.
Zasadniczo narzędzie ma trzy funkcje:
Ten opis wygląda zawile, więc podam prosty przykład praktyczny:
Student ma dostarczoną płytkę z pracą magisterską do promotora i sekretariatu, termin obrony za miesiąc, więc wyjechał za granicę. Nagle promotor zauważył plagiat w kodzie programu i w tekście, nie chcąc, aby jego podopieczny miał do końca życia zniszczoną karierę naukową dzwoni do niego i każe podmienić płytę na niezawierającą plagiatów. Niestety student jest za granicą, więc nie podmieni płytki po wprowadzeniu poprawek, promotor się zgodził podmienić za studenta, ale nie da się wysłać całej płytki przez internet, gdyż tam za odległą granicą nie ma kafejek internetowych, internet jest tylko mobilny, ale bardzo drogi i wolny, plików do modyfikacji jest pare (a dokument tekstowy jest bardzo duży). Wydaje się, że student nie jest w stanie obronić swojej przyszłości, ale doświadczony promotor nawiązuje rozmowę: Promotor: A masz u siebie na dysku obraz płyty oddanej do sekretariatu? Student: Mam, mogę do niego coś dodać, ale nie dam rady go przesłać przez internet w całości, nawet nie dam rady przesłać wszystkich zmodyfikowanych plików przez internet Promotor: Mam rozwiązanie - użyj rdiffa - z obrazu płyty oddanej do sekretariatu zrób sygnaturę, następnie wprowadź swoje zmiany do obrazu płyty, następnie wyślij mi deltę zrobioną ze zmienionego obrazu i sygnatury obrazu złożonej płyty. Promotor wtedy mając deltę aktualizuje plik obrazu do najnowszej wersji, nagrywa i podmienia w sekretariacie.
|
Przykład użycia programu rdiff
Zacznę od tego, że poniższe pliki to nie wyżej wymieniona praca magisterska. W poniższych komendach dla pliku w wersji 1.0 utworzę sygnaturę, następnie w oparciu o tę sygnaturę i plik w wersji 2.0 utworzę deltę, która posłuży za odtworzenie pliku w wersji 2.0 z pliku w wersji 1.0:
$ rdiff signature project_v1.0.zip project_v1.0.signature
$ rdiff delta project_v1.0.signature project_v2.0.zip project_v1.0_v2.0.delta
$ rdiff patch project_v1.0.zip project_v1.0_v2.0.delta project_v2.0_recreated.zip
$ ls -lh
-rw-r--r-- 1 grzegorz grzegorz 3,2K lip 11 00:09 project_v1.0.signature
-rw-r--r-- 1 grzegorz grzegorz 215K lip 11 00:10 project_v1.0_v2.0.delta
-rw-r--r-- 1 grzegorz grzegorz 537K lip 10 20:53 project_v1.0.zip
-rw-r--r-- 1 grzegorz grzegorz 751K lip 10 23:56 project_v2.0.zip
-rw-r--r-- 1 grzegorz grzegorz 751K lip 11 00:11 project_v2.0_recreated.zip
Możemy nie podawać nazwy pliku wyjściowego, ale wtedy wyjście zostanie wypisane na ekran w formacie binarnym.
Argumenty uruchomienia programu rdiff
Oto opisane opcje uruchomienia programu rdiff:
$ rdiff -h
Usage: rdiff [OPTIONS] signature [BASIS [SIGNATURE]]
[OPTIONS] delta SIGNATURE [NEWFILE [DELTA]]
[OPTIONS] patch BASIS [DELTA [NEWFILE]]
Options:
-v, --verbose Trace internal processing
-V, --version Show program version
-?, --help Show this help message
-s, --statistics Show performance statistics
Delta-encoding options:
-b, --block-size=BYTES Signature block size
-S, --sum-size=BYTES Set signature strength
--paranoia Verify all rolling checksums
IO options:
-I, --input-size=BYTES Input buffer size
-O, --output-size=BYTES Output buffer size
-z, --gzip[=LEVEL] gzip-compress deltas
-i, --bzip2[=LEVEL] bzip2-compress deltas
jak widać z nich jest możliwość wyświetlenia statystyk, ich włączenie ubogaca nasz output w sposób podobny do poniższego:
$ rdiff -s signature project_v1.0.zip project_v1.0.signature
rdiff: signature statistics: signature[269 blocks, 2048 bytes per block]
$ rdiff -s delta project_v1.0.signature project_v2.0.zip project_v1.0_v2.0.delta
rdiff: loadsig statistics: signature[269 blocks, 2048 bytes per block]
rdiff: delta statistics: literal[16 cmds, 219529 bytes, 48 cmdbytes] copy[3 cmds, 548864 bytes, 0 false, 20 cmdbytes]
$ rdiff -s patch project_v1.0.zip project_v1.0_v2.0.delta project_v2.0_recreated.zip
rdiff: patch statistics: literal[16 cmds, 219529 bytes, 48 cmdbytes] copy[3 cmds, 548864 bytes, 0 false, 20 cmdbytes]
Jeśli chodzi o resztę argumentów urachamiania nie są jeszcze zaimplementowane opcje kompresji.
Delta pliku bez zmian
Jeśli sprawdzimy np:
$ diff plik.txt plik.txt
to nie zostanie nic wyświetlone, sprawdźmy więc rozmiar delty pliku bez zmian:
$ cp project_v2.0.zip project_v3.0.zip
$ rdiff signature project_v2.0.zip project_v2.0.signature
$ rdiff delta project_v2.0.signature project_v3.0.zip project_v2.0_v3.0.delta
$ ll -h
-rw-r--r-- 1 grzegorz grzegorz 4,5K lip 11 00:13 project_v2.0.signature
-rw-r--r-- 1 grzegorz grzegorz 11 lip 11 00:14 project_v2.0_v3.0.delta
-rw-r--r-- 1 grzegorz grzegorz 751K lip 10 23:56 project_v2.0.zip
-rw-r--r-- 1 grzegorz grzegorz 751K lip 11 00:13 project_v3.0.zip
jak widać delta, gdy brak zmian, zajmuje więcej niż 0 bajtów, na szczęście jednak nie jest to dużo.
Użycie biblioteki librsync
Wiemy już co biblioteka robi, w jakiej kolejności należy wykonać pewne kroki, jakie daje korzyści. Więc jeśli komuś nie wystarczy narzędzie rdiff (chce używać tych funkcji w kodzie programu) to czas na ich opisanie.
Funkcje operujące na całych plikach
Biblioteka dostarcza funkcje operujące na całych plikach, opis tych funkcji znajduje się na
odpowiedniej stronie dokumentacji, pozwolę sobie przetłumaczyć i zacytować dokumentację z powyższej strony zmieniając to w bardziej intuicyjną formę:
rs_result rs_sig_file( FILE * plikOtwartyDoOdczytu, FILE * plikDoUtworzeniaSygnaturyOtwartyDoZapisu, size_t wielkoscBlokuPodczasGenerowaniaSygnatury, size_t przycietaDlugoscChecksumWBajtach, rs_stats_t * opcjonalnyWskaznikDoStatystyk );
rs_result rs_sig_file( FILE * plikOtwartyDoOdczytu, FILE * plikDoUtworzeniaSygnaturyOtwartyDoZapisu, size_t wielkoscBlokuPodczasGenerowaniaSygnatury, size_t przycietaDlugoscChecksumWBajtach, rs_magic_number numerOznaczajacyJakiFormatUzyc, rs_stats_t * opcjonalnyWskaznikDoStatystyk );
rs_result rs_loadsig_file( FILE * plikSygnatury, rs_signature_t ** sygnaturaWPamieci, rs_stats_t * statystyki );
rs_result rs_build_hash_table( rs_signature_t * checkSumy );
rs_result rs_delta_file( rs_signature_t * sygnaturaWPamieci, FILE * nowaWersjaPliku, FILE * plikDelty, rs_stats_t * statystyki );
rs_result rs_patch_file( FILE * plikWPoprzedniejWersji, FILE * plikDeltyMiedzyWersjami, FILE * wyjsciowyNowyPlik, rs_stats_t * statystyki );
Funkcje pomocnicze
Zanim przejdę do przykładów pozwolę sobie przedstawić pewne funkcje pomocnicze, z których będę korzystał w następnych przykładach:
#include <memory>
using namespace std;
#include <cstdio>
#include <cstring>
shared_ptr < FILE > openFile( const char * fileName, const char * fileMode )
{
if( nullptr == fileName || 0 == strlen( fileName ) )
{
throw invalid_argument( "File name can't be empty!" );
}
shared_ptr < FILE > file( fopen( fileName, fileMode ), fclose );
if( !file )
{
throw runtime_error( string( "File " ) + fileName + " can't be opened in mode " + fileMode + "!" );
}
return file;
}
Przykład generowania delty
Czas na przykład użycia powyższych funkcji. Wraz z biblioteką dostarczonym przykładem jest kod
rdiff.c, oczywiście nie będę przeklejał tego kodu, ani pisał rdiffa na swój sposób od początku, zamiast tego umieszczę parę przykładów funkcji bardziej na kształt C++.
Zacznijmy od funkcji generujących sygnaturę pliku oraz generującą deltę.
Jednak zanim pojawi się przykład na generowanie sygnatury podkreślam, iż funkcja rs_sig_file() między wersjami różni się ilością argumentów. Mamy też problem taki, iż dla "magicznego numeru" RS_MD4_SIG_MAGIC jedyna akceptowalna STRONG_LEN to 8, tego problemu nie mamy w przypadku RS_BLAKE2_SIG_MAGIC. Innymi słowy dla wersji biblioteki 0.9.7 zadziała nam zawołanie:
rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, RS_DEFAULT_STRONG_LEN, & stats ); Dla wersji 2+ jeśli chcemy kompatybilności z wersją 0.9.7 powinniśmy zawołać:
rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, 8, RS_MD4_SIG_MAGIC, & stats ); Dla wersji 2+ powinniśmy zawołać:
rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, RS_MAX_STRONG_SUM_LENGTH, RS_BLAKE2_SIG_MAGIC, & stats ); Powyższe warunki odzwierciedliłem w postaci wymyślonych przeze mnie makr, których niestety biblioteka nie dostarcza:
#define LIBRSYNC_VERSION 2 #define LIBRSYNC_COMPATIBLE_WITH_OLD_VERSION
|
A oto obiecane przykłady:
#include <iostream>
#include <memory>
using namespace std;
#include <cstdio>
#include <cstring>
#include <librsync.h>
void generateSignatureForFile( const char * fileName, const char * signatureFileName )
{
shared_ptr < FILE > fileToGenerateSignatureFor = openFile( fileName, "rb" );
shared_ptr < FILE > signatureFile = openFile( signatureFileName, "wb" );
rs_stats_t stats;
#if LIBRSYNC_VERSION>=2
#ifdef LIBRSYNC_COMPATIBLE_WITH_OLD_VERSION
#define RS_DEFAULT_STRONG_LEN 8
rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, RS_DEFAULT_STRONG_LEN, RS_MD4_SIG_MAGIC, & stats );
#else
rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, RS_MAX_STRONG_SUM_LENGTH, RS_BLAKE2_SIG_MAGIC, & stats );
#endif
#else
rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, RS_DEFAULT_STRONG_LEN, & stats );
#endif
if( RS_DONE != result )
{
throw runtime_error( "Error during generation of signature! " + string( rs_strerror( result ) ) );
}
rs_log_stats( & stats );
}
shared_ptr < rs_signature_t > loadSumsetIntoMemory( const char * signatureFileName )
{
shared_ptr < FILE > signatureFile = openFile( signatureFileName, "rb" );
rs_signature_t * sumsetRaw;
rs_stats_t stats;
rs_result result = rs_loadsig_file( signatureFile.get(), & sumsetRaw, & stats );
shared_ptr < rs_signature_t > sumset( sumsetRaw, rs_free_sumset );
if( RS_DONE != result )
{
throw runtime_error( "Problem during loading signature file to memory! " + string( rs_strerror( result ) ) );
}
rs_log_stats( & stats );
result = rs_build_hash_table( sumset.get() );
if( RS_DONE != result )
{
throw runtime_error( "Problem during creating hash table! " + string( rs_strerror( result ) ) );
}
return sumset;
}
void generateDeltaFile( const char * signatureFileName, const char * newFileFileName, const char * deltaFileName )
{
shared_ptr < rs_signature_t > sumset = loadSumsetIntoMemory( signatureFileName );
shared_ptr < FILE > newFile = openFile( newFileFileName, "rb" );
shared_ptr < FILE > deltaFile = openFile( deltaFileName, "wb" );
rs_stats_t stats;
rs_result result = rs_delta_file( sumset.get(), newFile.get(), deltaFile.get(), & stats );
if( RS_DONE != result )
{
throw runtime_error( "Problem during creating delta file!" + string( rs_strerror( result ) ) );
}
rs_log_stats( & stats );
}
Mając powyższe funkcje możemy wygenerować deltę między dwoma plikami:
string createDeltaFileReturningItsName( const char * file, const char * fileInDifferentVersion, bool removeSignatureFile = true )
{
const string && signatureFileName = string( file ) + ".signature";
generateSignatureForFile( file, signatureFileName.c_str() );
const string && deltaFileName = string( file ) + '_' + fileInDifferentVersion + ".delta";
generateDeltaFile( signatureFileName.c_str(), fileInDifferentVersion, deltaFileName.c_str() );
if( removeSignatureFile )
remove( signatureFileName.c_str() );
return deltaFileName;
}
W powyższych funkcjach, z powodu użycia wyjątków, a chcąc mimo wszystko zagwarantować zwalnianie zasobów używam
https://pl.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization.
Zostało nam jeszcze odtworzenie pliku w kolejnej wersji mając poprzednią wersję oraz deltę, oto przykładowa funkcja-gotowiec:
#include <memory>
using namespace std;
#include <cstdio>
#include <librsync.h>
void recreateFile( const char * basicFileName, const char * deltaFileName, const char * recreatedNewFileName )
{
shared_ptr < FILE > basicFile = openFile( basicFileName, "rb" );
shared_ptr < FILE > deltaFile = openFile( deltaFileName, "rb" );
shared_ptr < FILE > recreatedNewFile = openFile( recreatedNewFileName, "wb" );
rs_stats_t stats;
rs_result result = rs_patch_file( basicFile.get(), deltaFile.get(), recreatedNewFile.get(), & stats );
if( RS_DONE != result )
{
throw runtime_error( "Problem during patching file '" + string( recreatedNewFileName ) + "'! " + string( rs_strerror( result ) ) );
}
rs_log_stats( & stats );
}
Dodatkowe możliwości biblioteki rsync
Wszystkie publiczne funkcje biblioteki opisane są
w dokumentacji, niemniej jednak skupię się na paru z nich:
Statystyki
Do trzymania statystyk jest struktura
rs_stats_t. Możemy je wyświetlić przy użyciu funkcji:
char * rs_format_stats( rs_stats_t const * stats, char * buf, size_t size );
int rs_log_stats( rs_stats_t const * stats );
obydwie funkcje generują tekst podobny do:
loadsig statistics: signature[269 blocks, 2048 bytes per block]
Jeśli chcielibyśmy inaczej wyświetlać statystyki musimy zagłębić się w to, co zawiera struktura
rs_stats_t, na chwilę obecną w bibliotece nie są dostarczone inne funkcje do wyświetlania statystyk w inny sposób.
Jeśli nie interesują nas ani trochę statystyki, możemy do powyższych funkcji zamiast adresu struktury podawać
nullptr.
Kody błędów i logowanie
Zacznijmy od włączenia logowania pracy biblioteki i błędów, służy do tego funkcja:
void rs_trace_set_level( rs_loglevel level );
Gdzie listę kodów błędów możemy znaleźć
w dokumentacji, np. możemy wyświetlać każde wywołanie funkcji bibliotecznej podając wartość
RS_LOG_DEBUG.
Mamy również możliwość ustawić funkcję zwrotną w razie błędu:
typedef void rs_trace_fn_t( rs_loglevel level, char const * msg );
void rs_trace_to( rs_trace_fn_t * new_impl )
void rs_trace_stderr( rs_loglevel level, char const * msg );
int rs_supports_trace();
Rezultat operacji
Wiele z operacji zwraca:
rs_result, możliwe opcje można zobaczyć
w dokumentacji, natomiast do wyświetlenia błędu biblioteki librsync w formie czytelnej dla człowieka służy funkcja:
char const * rs_strerror( rs_result r );
Wyświetlanie wersji i treści licencji
Co ciekawe biblioteka ta zawiera zmienne globalne posiadające takie informacje:
char const rs_librsync_version[];
char const rs_licence_string[];
Przetwarzanie strumieniowe
Funkcje do operowania na całych plikach są bardzo wygodne w użyciu, natomiast może się zdarzyć, że nie będziemy mieli do czynienia z plikami, a ze strumieniem danych (np. operując na bazie danych czy plikach zdalnych), wtedy nie musimy tworzyć na chwilę pliku, czytać go, itd., gdyż mamy też dostarczone w ramach tej biblioteki
funkcje do obsługi strumieniowej. Zanim ich użyjemy musimy wypełnić strukturę
rs_buffers_t, następnie rozpocząć daną operację, odbywa się to przy użyciu funkcji:
rs_job_t * rs_sig_begin( size_t new_block_len, size_t strong_sum_len, rs_magic_number sig_magic );
rs_job_t * rs_sig_begin( size_t new_block_len, size_t strong_sum_len );
rs_job_t * rs_loadsig_begin( rs_signature_t ** );
rs_job_t * rs_delta_begin( rs_signature_t * );
rs_job_t * rs_patch_begin( rs_copy_cb * copy_cb, void * copy_arg );
Mając zapoczątkowaną operację, czyli wypełnioną
strukturę rs_job_t, możemy iterować po buforach:
rs_result rs_job_iter( rs_job_t *, rs_buffers_t * );
Przykłady operowania na strumieniach
Teraz czas na przykłady, tutaj będę umieszczał jeden przykład dla każdej operacji.
Poniższe przykłady otrzymałem drogą eksperymentów w oparciu o dokumentację i kod źródłowy biblioteki, nie znalazłem w internecie żadnych przykładów na generowanie przy użyciu strumieni, dlatego zastrzegam sobie, że kod poniższych funkcji może zawierać lekkie niedociągnięcia, które nie wyszły podczas moich testów na plikach, na których testowałem.
Zacznę od funkcji, których będę używał w kolejnych przykładach:
#include <string>
#include <fstream>
using namespace std;
#include <librsync.h>
string getFileContext( const char * fileName )
{
if( nullptr == fileName || 0 == strlen( fileName ) )
{
throw invalid_argument( "File name can't be empty!" );
}
ifstream fileToRead( fileName, ios::binary | ios::in );
if( !fileToRead )
{
throw runtime_error( string( "File " ) + fileName + " can't be opened!" );
}
fileToRead.seekg( 0, fileToRead.end );
const int fileLength = fileToRead.tellg();
fileToRead.seekg( 0, fileToRead.beg );
string fileContext( fileLength, '\0' );
fileToRead.read( & fileContext[ 0 ], fileLength );
return fileContext;
}
rs_buffers_t fillBuffers( const string & inputData, string & outputBuffer, bool noMoreDataAfterThat = true )
{
rs_buffers_t buffers;
buffers.next_in = const_cast < char *>( inputData.c_str() );
buffers.avail_in = inputData.size();
buffers.eof_in = noMoreDataAfterThat;
buffers.next_out = const_cast < char *>( outputBuffer.c_str() );
buffers.avail_out = outputBuffer.size();
return buffers;
}
Wiem, że w powyższym przykładzie używam niepopularnego
const_cast, niestety muszę tutaj przypisać na wskaźniki
char*, oczywiście mógłbym samodzielnie alokować taki bufor, ale ze względu na używanie wyjątków, istniałaby możliwość wycieku pamięci, dlatego wolę używania jako bufora obiektów string.
Tworzenie sygnatury dla danych
W tej sytuacji również mamy rozbieżność w funkcji
rs_sig_begin() dla różnych wersji biblioteki, dlatego używam,
wymyślonych przeze mnie, makr analogicznie jak wyżej.
Tutaj również zacznę od przykładu, opis kontrowersyjnych fragmentów poniżej:
#include <string>
#include <memory>
using namespace std;
#include <librsync.h>
string generateSignatureForData( const string & data )
{
string outputBuffer( 2 * data.size(), '\0' );
rs_buffers_t buffers = fillBuffers( data, outputBuffer );
#if LIBRSYNC_VERSION>=2
#ifdef LIBRSYNC_COMPATIBLE_WITH_OLD_VERSION
#define RS_DEFAULT_STRONG_LEN 8
shared_ptr < rs_job_t > job( rs_sig_begin( RS_DEFAULT_BLOCK_LEN, RS_DEFAULT_STRONG_LEN, RS_MD4_SIG_MAGIC ), rs_job_free );
#else
shared_ptr < rs_job_t > job( rs_sig_begin( RS_DEFAULT_BLOCK_LEN, RS_MAX_STRONG_SUM_LENGTH, RS_BLAKE2_SIG_MAGIC ), rs_job_free );
#endif
#else
shared_ptr < rs_job_t > job( rs_sig_begin( RS_DEFAULT_BLOCK_LEN, RS_DEFAULT_STRONG_LEN ), rs_job_free );
#endif
rs_result result = rs_job_iter( job.get(), & buffers );
if( buffers.avail_in )
{
throw runtime_error( "! Not all buffer read! Necessairy greater output buffer!!!" );
}
if( RS_BLOCKED == result )
{
throw runtime_error( "Data stream not ended!" );
}
else if( RS_DONE != result )
{
throw runtime_error( "Error during stream processing: " + string( strerror( result ) ) );
}
outputBuffer.erase( buffers.next_out - outputBuffer.c_str() );
rs_log_stats( rs_job_statistics( job.get() ) );
return outputBuffer;
}
W powyższym przykładzie używam wcześniej przedstawionych funkcji. Sprawdzenia błędów odbywają się zgodnie z dokumentacją. Rozmiar buffora wyjściowego robię w trochę trywialny sposób, który jednak może być podatny na błędy, czyli w razie gdyby buffor był zbyt maly to wejdziemy do pierwszego ifa, w tej sytuacji powinniśmy dostarczyć większy buffor i ponowić operację.
Bufor wyjściowy (nasza sygnatura pliku) jest zawarta w buforze na pozycji od początku buffora wejściowego do
buffers.next_out, dlatego czyszczę wszystko poza tym. Żeby wyświetlić statystyk używam funkcji jak wyżej (statystyki są zawarte w strukturze
rs_job_t).
Tworzenie delty
Tutaj musimy zacząć od odpowiedniego załadowania sygnatury do pamięci (
bez tego będzie segfault), jak widać w pierwszej funkcji bufor wyjściowy nie jest używany.
shared_ptr < rs_signature_t > loadSignatureIntoMemory( const string & signatureBuffer )
{
string outputBuffer( signatureBuffer.size(), '\0' );
rs_buffers_t buffers = fillBuffers( signatureBuffer, outputBuffer );
rs_signature_t * sumsetRaw;
shared_ptr < rs_job_t > job( rs_loadsig_begin( & sumsetRaw ), rs_job_free );
shared_ptr < rs_signature_t > sumset( sumsetRaw, rs_free_sumset );
rs_result result = rs_job_iter( job.get(), & buffers );
if( buffers.avail_in )
{
throw runtime_error( "! Not all buffer read! Necessairy greater output buffer!!!" );
}
if( RS_BLOCKED == result )
{
throw runtime_error( "Data stream not ended!" );
}
else if( RS_DONE != result )
{
throw runtime_error( "Error during stream processing: " + string( strerror( result ) ) );
}
result = rs_build_hash_table( sumset.get() );
if( RS_DONE != result )
{
throw runtime_error( "Problem during creating hash table! " + string( rs_strerror( result ) ) );
}
rs_log_stats( rs_job_statistics( job.get() ) );
return sumset;
}
string generateDeltaForData( const string & signatureBuffer, const string & dataNew )
{
shared_ptr < rs_signature_t > sumset = loadSignatureIntoMemory( signatureBuffer );
shared_ptr < rs_job_t > job( rs_delta_begin( sumset.get() ), rs_job_free );
string outputBuffer( 2 * dataNew.size(), '\0' );
rs_buffers_t buffers = fillBuffers( dataNew, outputBuffer );
rs_result result;
do
{
result = rs_job_iter( job.get(), & buffers );
if( buffers.avail_in )
{
throw runtime_error( "! Not all buffer read! Necessairy greater output buffer!!!" );
}
if( RS_DONE == result )
{
break;
}
else if( RS_BLOCKED != result )
{
throw runtime_error( "Error during stream processing: " + string( strerror( result ) ) );
}
}
while( RS_BLOCKED == result );
outputBuffer.erase( buffers.next_out - outputBuffer.c_str() );
rs_log_stats( rs_job_statistics( job.get() ) );
return outputBuffer;
}
W funkcji generującej deltę jak widać użyłem pętli, z nieznanych mi przyczyn bufor wyjściowy dla plików nie był ukończony (czyli wygenerowany plik delty w stosunku do pliku wygenerowanego przy pomocy funkcji operujących na całych plikach nie dodawał na końcu pliku jednego znaku: '\0', ciekawym jest, że inne funkcje, których używam w tym "rozdziale" nie wymagały takich zabiegów dla zapewnienia poprawności operacji, dla plików których używałem w testach).
Odtwarzanie nowych danych z danych w starszej wersji oraz delty
Tutaj niestety mamy pewien problem, gdyż mamy dwa źródła danych potrzebne równocześnie, funkcja
rs_job_iter() przyjmuje tylko jeden buffor wejściowy, więc potrzebujemy podać drugi buffor do funkcji
rs_patch_begin(), która ma następującą, dziwną formę:
rs_job_t * rs_patch_begin( rs_copy_cb * copy_cb, void * copy_arg );
typedef rs_result rs_copy_cb( void * opaque, rs_long_t pos, size_t * len, void ** buf );
Niestety tylko jedna funkcja pasująca dla definicji
rs_copy_cb jest dostarczona w bibliotece i znajduje się w pliku
buf.c:
rs_result rs_file_copy_cb( void * arg, rs_long_t pos, size_t * len, void ** buf );
implementacja ta operuje na deskryptorach plików języka C (
FILE) ukrytych pod argumentem
*arg, dlatego chcąc pominąć pliki języka C musimy napisać jej własną wersję.
Poniżej dwie przykładowe implementacje -dla strumieni i dla obiektu string:
rs_result copyDataFromString( void * arg, rs_long_t pos, size_t * len, void ** buf )
{
const string & data = * reinterpret_cast < string *>( arg );
if( pos < 0 || pos >= data.size() )
{
cerr << "seek failed!\n";
return RS_IO_ERROR;
}
if( 0 == * len )
{
cerr << "unexpected eof!\n";
return RS_INPUT_ENDED;
}
memcpy( * buf, data.c_str() + pos, * len );
return RS_DONE;
}
rs_result copyDataFromStream( void * arg, rs_long_t pos, size_t * len, void ** buf )
{
istream & stream = * reinterpret_cast < istream *>( arg );
if( !stream.seekg( pos, ios_base::beg ) )
{
cerr << "seek failed! " << endl;
return RS_IO_ERROR;
}
if( !stream.read( static_cast < char *>( * buf ), * len ) )
{
cerr << "Error: " <<( stream.eof() ? "eofbit "
: "" ) <<( stream.bad() ? "badbit "
: "" ) <<( stream.fail() ? "failbit "
: "" ) << endl;
return RS_IO_ERROR;
}
return RS_DONE;
}
Mając odpowiednie implementacje czas na funkcję odtwarzającą dane w nowej wersji w oparciu o dane w starej wersji oraz deltę:
string recreateNeverVersionOfData( const string & oldDataBuffer, const string & deltaBuffer )
{
string outputBuffer( oldDataBuffer.size() + deltaBuffer.size(), '\0' );
rs_buffers_t buffers = fillBuffers( deltaBuffer, outputBuffer );
shared_ptr < rs_job_t > job( rs_patch_begin( copyDataFromString, const_cast < string *>( & oldDataBuffer ) ), rs_job_free );
rs_result result = rs_job_iter( job.get(), & buffers );
if( buffers.avail_in )
{
throw runtime_error( "! Not all buffer read! Necessairy greater output buffer!!!" );
}
if( RS_BLOCKED == result )
{
throw runtime_error( "Data stream not ended!" );
}
else if( RS_DONE != result )
{
throw runtime_error( "Error during stream processing: " + string( strerror( result ) ) );
}
outputBuffer.erase( buffers.next_out - outputBuffer.c_str() );
rs_log_stats( rs_job_statistics( job.get() ) );
return outputBuffer;
}
I tym oto sposobem przebrnęliśmy przez wszystkie funkcje do obsługi strumieni
Pull stream interface
Kolejnym sposobem używanie biblioteki są funkcje, które wywołują dostarczone przez programiste funkcje, a te z kolei pobierające większą ilość danych do bufora oraz wysyłają kolejne porcje przetworzonych danych do bufora wyjściowego, więcej informacji
w dokumentacji, a przykłady użycia tych funkcji są w pliku
whole.c. Podejrzewam, że niemalże wszyscy zainteresowani biblioteką zostaną nasyceni powyższymi przykładami, dlatego pozwolę sobie nie opisywać dodatkowo i tej części biblioteki.
Wielowątkowość
Na oficjalnej stronie poświęconej
api librsync napisali, że biblioteka może być używana w programach wielowątkowych, chociaż nie synchronizuje ona wątków samodzielnie, każde zadanie tej biblioteki powinno być obsługiwane TYLKO przez pojedynczy wątek.
Minusy biblioteki
Nadszedł czas na smutną, aczkolwiek konieczną sekcja w opisie biblioteki. Mimo, iż opisana biblioteka zawiera świetne algorytmy, używane przez wiele bardzo popularnych programów, jest zaniedbana pod względem użytkowym (oczywiście mam nadzieję, że dzięki temu artykułowi będzie łatwiej się nią posługiwać). Dokumentacja jest bardzo słaba, niemalże w ogóle nie ma przykładów użycia, jest trudność w budowaniu na systemie Windows. Mimo tych trudności dostarczone algorytmy są świetne i dlatego warto jej użyć. Fajną informacją jest to iż ostatnimi czasu widać na
githubie, że biblioteką ponownie ktoś się zajmuje.