Qtの応用 - rsync

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

librsyncライブラリは、ネットワーク効率的なファイル同期を実現するためのC言語ライブラリである。
Qtフレームワーク環境でもlibrsyncライブラリを使用することができ、効率的なデータ同期機能を実装する場合に役立つ。

librsyncライブラリの主な特徴は、rsyncアルゴリズムを実装していることである。
このアルゴリズムにより、ファイルの変更された部分のみを転送することができるため、大幅な帯域幅の節約が可能になる。

librsyncライブラリを使用する場合の基本的な流れを以下に示す。

  1. まず、元のファイル (ディレクトリ) のシグネチャを生成する。
  2. 次に、更新されたファイルとそのシグネチャを比較してデルタを作成する。
  3. 最後に、元のファイルにデルタを適用して更新を完了する。


librsyncライブラリの主要な関数には、rs_sig_begin関数やrs_sig_file等のシグネチャ生成関数、
rs_delta_begin関数やrs_delta_file関数等のデルタ生成関数、
そして、rs_patch_begin関数やrs_patch_file関数等のパッチ適用関数がある。

これらの関数を適切に組み合わせることにより、効率的なファイル同期機能を実装することができる。

エラーハンドリングも重要な要素である。
librsyncライブラリの関数は、一般的に、rs_result型の値を返す。
これを確認することにより、操作が成功したかどうかを判断することができる。

メモリ管理にも注意が必要である。
librsyncライブラリは、一部の関数でメモリを動的に割り当てるため、使用後は適切に解放する必要がある。

librsyncライブラリはマルチスレッド環境での使用を想定していないため、Qtのマルチスレッド機能と併用する場合は注意が必要となる。
必要に応じて、ミューテックスやロック等の同期プリミティブを使用して、スレッドセーフな実装を行うことが重要である。


libRsyncのインストール

libRsyncの概要

librsyncは、rsyncと同様、効率的にファイルを転送する他のプログラムを構築するためのものである。
バックアップおよびプログラムにバイナリパッチを配布したり、ディレクトリをサーバやピア間で同期するために記述したプログラムである。

librsyncは、ネットワーク差分を計算し適用するためのライブラリであり、多様なネットワークソフトウェアに統合できるように設計されたインターフェイスを備えている。

librsyncは、rsyncプロトコルのコアアルゴリズムをカプセル化して、2つのファイルの差分を効率的に計算するのに役立つ。
rsyncアルゴリズムは、差分を計算するために2つのファイルが存在する必要が無いため、ほとんどの差分アルゴリズムとは異なる。

その代わり、一方のファイルの各ブロックのチェックサムのセットを必要とし、それらが一緒になってそのファイルの署名を形成する。
もう一方のファイルのどの位置のブロックでも、チェックサムが同じであれば、同一である可能性が高く、残ったものが差分となる。

このアルゴリズムは、同じシステム上で両方のファイルを必要とすることなく、2つのファイル間の差分を転送する。

librsyncは、元々、HTTPのデルタ圧縮におけるrproxy実験のために作成された。
現在では、Dropbox、rdiff-backup、Duplicity等で使用されている。

※注意
librsyncライブラリは、rsyncワイヤプロトコルを実装していない。
ファイルを転送するためにrsyncサーバと対話する場合は、rsyncにシェルアウトする必要がある。

librsyncライブラリは、ファイル名、パーミッション、ディレクトリ等のファイルのメタデータや構造を扱わない。
このライブラリにとって、ファイルは単なるバイトのストリームである。

librsyncライブラリは、SSHやその他のサーバと通信するためのネットワーク関数も含まれていない。
そのため、リモートファイルシステムにアクセスするには、独自のコードを提供する、または、他の仮想ファイルシステムレイヤーを使用する必要がある。

libRsyncライブラリのライセンスは、LGPL v2.1に準拠している。

libRsyncライブラリの詳細な使用方法は、公式WebサイトまたはGithubを参照すること。


ソースコードからインストール

ビルドに必要な依存関係のライブラリをインストールする。

sudo zypper install libb2-devel


libRsyncのGitHubからソースコードをダウンロードする。
ダウンロードしたファイルを解凍する。

tar xf librsync-<バージョン>.tar.gz
cd librsync-<バージョン>


または、Githubからソースコードをクローンする。

git clone https://github.com/librsync/librsync.git
cd librsync


libRsyncをビルドおよびインストールする。

mkdir build && cd build

cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=<libRsyncインストールディレクトリ> ..
make -j $(nproc)
make install


~/.profileファイル等に、環境変数を追記する。

vi ~/.profile


# ~/.profileファイル

export PATH="/<libRsyncのインストールディレクトリ>/bin:$PATH"
export LD_LIBRARY_PATH="/<libRsyncのインストールディレクトリ>/lib64:$LD_LIBRARY_PATH"


パッケージ管理システムからインストール

sudo zypper install librsync2



ライブラリのリンク

Pkg-configを使用してlibrsyncライブラリを検索して、プロジェクトにリンクする。
システムにPkg-configとlibrsyncライブラリがインストールされていることを確認すること。

  • Qtプロジェクトファイル (.pro) の場合
 # Pkg-configを使用
 CONFIG += link_pkgconfig
 PKGCONFIG += librsync


  • CMakeLists.txtファイルの場合
 # librsyncの依存関係を確認
 pkg_check_modules(LIBRSYNC REQUIRED librsync)
 
 # 実行可能ファイルを追加
 add_executable(<ターゲット名>
    main.cpp
 )
 
 # インクルードディレクトリを追加
 target_include_directories(<ターゲット名> PRIVATE ${LIBRSYNC_INCLUDE_DIRS})
 
 # リンクオプションを追加 (必要な場合)
 target_link_options(<ターゲット名> PRIVATE
    ${LIBRSYNC_LDFLAGS}
 )
 
 # pkg_check_modulesコマンドがLIBRSYNC_LDFLAGSを提供していない場合や追加のカスタムフラグが必要な場合
 #target_link_options(<ターゲット名> PRIVATE
 #   "-L${LIBRSYNC_LIBRARY_DIRS}"
 #)
 
 # ライブラリをリンク
 target_link_libraries(<ターゲット名> PRIVATE
    ${LIBRSYNC_LIBRARIES}
 )



ローカルディレクトリとの同期

以下の例では、librsyncライブラリを使用して、同一PC上のファイルおよびディレクトリを非同期でファイルを差分転送している。

  • 非同期処理
    QtConcurrent::runメソッドを実行して、ファイル同期を非同期で実行する。
    QFutureクラスを使用して、非同期処理の結果を管理する。
  • ストリーミング処理
    librsyncライブラリを使用して、ファイルをストリーミング方式で読み込み、差分を計算する。
    大きなファイルでもメモリ効率良く処理することができる。
  • 進捗報告
    progressUpdatedシグナルを使用して、処理の進捗を報告する。
  • ディレクトリ構造の維持
    ソースディレクトリの構造を宛先ディレクトリに複製する。


 // FileSyncWorker.hファイル
 
 #include <QCoreApplication>
 #include <QFileInfo>
 #include <QDir>
 #include <QDirIterator>
 #include <QFuture>
 #include <QtConcurrent>
 #include <librsync.h>
 #include <QDebug>
 
 class FileSyncWorker : public QObject
 {
    Q_OBJECT
 
 public:
    explicit FileSyncWorker(QObject *parent = nullptr) : QObject(parent) {}
 
    // ディレクトリの同期を非同期で実行
    QFuture<void> syncFiles(const QString &sourcePath, const QString &destPath)
    {
       return QtConcurrent::run([this, sourcePath, destPath]() {
          QDirIterator it(sourcePath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
          while (it.hasNext()) {
             QString sourceFilePath   = it.next();
             QString relativeFilePath = QDir(sourcePath).relativeFilePath(sourceFilePath);
             QString destFilePath     = QDir(destPath).filePath(relativeFilePath);
 
             QFileInfo sourceInfo(sourceFilePath);
             if (sourceInfo.isDir()) {
                QDir().mkpath(destFilePath);
             }
             else {
                syncFile(sourceFilePath, destFilePath);
             }
          }
       });
    }
 
 signals:
    void progressUpdated(int progress);
    void errorOccurred(const QString &error);
 
 private:
    // 個別のファイルを同期するメソッド
    void syncFile(const QString &sourcePath, const QString &destPath)
    {
       QFile sourceFile(sourcePath);
       QFile destFile(destPath);
 
       if (!sourceFile.open(QIODevice::ReadOnly)) {
          emit errorOccurred(QString("ソースファイルを開けません: %1").arg(sourcePath));
          return;
       }
 
       if (!destFile.open(QIODevice::ReadWrite)) {
          emit errorOccurred(QString("宛先ファイルを開けません: %1").arg(destPath));
          sourceFile.close();
 
          return;
       }
 
       // librsyncライブラリを使用して、ファイルの差分を計算して適用
       rs_result result;
       rs_buffers_t buf;
 
       // 入力バッファの設定
       char inbuf[8192] = {};
       buf.next_in      = inbuf;
       buf.avail_in     = 0;
 
       // 出力バッファの設定
       char outbuf[8192] = {};
       buf.next_out      = outbuf;
       buf.avail_out     = sizeof(outbuf);
 
       // シグネチャの生成
       rs_job_t *job = rs_sig_begin(RS_DEFAULT_BLOCK_LEN, 0, RS_MD4_SIG_MAGIC);
       if (!job) {
          emit errorOccurred("シグネチャジョブの作成に失敗しました");
          sourceFile.close();
          destFile.close();
 
          return;
       }
 
       do {
          if (buf.avail_in == 0) {
             buf.avail_in = sourceFile.read(inbuf, sizeof(inbuf));
             buf.next_in = inbuf;
          }
 
          result = rs_job_iter(job, &buf);
 
          if (buf.avail_out == 0) {
             destFile.write(outbuf, sizeof(outbuf));
             buf.next_out = outbuf;
             buf.avail_out = sizeof(outbuf);
          }
       } while (result == RS_BLOCKED);
 
       if (result != RS_DONE) {
          emit errorOccurred(QString("シグネチャの生成に失敗しました: %1").arg(rs_strerror(result)));
          rs_job_free(job);
          sourceFile.close();
          destFile.close();
 
          return;
       }
 
       if (buf.avail_out < sizeof(outbuf)) {
          destFile.write(outbuf, sizeof(outbuf) - buf.avail_out);
       }
 
       rs_job_free(job);
       sourceFile.close();
       destFile.close();
 
       // 進捗の更新 (各ファイルを1単位としている)
       emit progressUpdated(1);
    }
 };


 // main.cppファイル
 
 #inlcude "FileSyncWorker.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    QString sourcePath = "<同期元ディレクトリ>";
    QString destPath   = "<同期先ディレクトリ>";
 
    FileSyncWorker worker;
    QObject::connect(&worker, &FileSyncWorker::progressUpdated, [](int progress) {
       qDebug() << "進捗: " << progress;
    });
 
    QObject::connect(&worker, &FileSyncWorker::errorOccurred, [](const QString &error) {
       qDebug() << "エラー: " << error;
    });
 
    // 同期完了まで待機
    QFuture<void> future = worker.syncFiles(sourcePath, destPath);
    future.waitForFinished();
 
    return a.exec();
 }



リモートPCのディレクトリとの同期

以下の例では、librsyncライブラリを使用して、リモートPC上のファイルおよびディレクトリを非同期でファイルを差分転送している。

  • 非同期処理
    QtConcurrent::runメソッドを実行して、ファイル同期を非同期で実行する。
    QFutureクラスを使用して、非同期処理の結果を管理する。
  • ネットワーク通信
    QTcpSocketクラスを使用して、リモートホストと通信する。
  • ストリーミング処理
    ファイルデータとコマンドをストリーミングで送受信する。
  • ディレクトリ構造の維持
    ソースディレクトリの構造を宛先ディレクトリに複製する。


 // RemoteFileSyncWorker.hファイル
 
 #include <QObject>
 #include <QDir>
 #include <QDirIterator>
 #include <QFileInfo>
 #include <QDataStream>
 #include <QFuture>
 #include <QtConcurrent>
 #include <QTcpSocket>
 #include <librsync.h>
 
 class RemoteFileSyncWorker : public QObject
 {
    Q_OBJECT
 
 private:
    QTcpSocket *socket;
 
    void syncFile(const QString &sourcePath, const QString &destPath)
    {
       QFile sourceFile(sourcePath);
       if (!sourceFile.open(QIODevice::ReadOnly)) {
          emit errorOccurred(QString("ソースファイルのオープンに失敗: %1").arg(sourcePath));
          return;
       }
 
       // リモートファイルのシグネチャを要求
       QByteArray command = "SIGNATURE:" + destPath.toUtf8();
       if (!sendData(command)) {
          emit errorOccurred("シグネチャ要求の送信に失敗");
          return;
       }
 
       QByteArray remoteSignature = receiveData();
       if (remoteSignature.isEmpty()) {
          emit errorOccurred("リモートシグネチャの受信に失敗");
          return;
       }
 
       // デルタの計算
       rs_result result;
       rs_buffers_t buf;
       char inbuf[8192];
       char outbuf[8192];
 
       rs_job_t *job = rs_delta_begin(new rs_signature_t());
       if (!job) {
          emit errorOccurred("デルタジョブの作成に失敗");
          return;
       }
 
       do {
          buf.next_in   = inbuf;
          buf.avail_in  = sourceFile.read(inbuf, sizeof(inbuf));
          buf.next_out  = outbuf;
          buf.avail_out = sizeof(outbuf);
 
          result = rs_job_iter(job, &buf);
 
          if (buf.avail_out < sizeof(outbuf)) {
             QByteArray deltaData(outbuf, sizeof(outbuf) - buf.avail_out);
             if (!sendData(deltaData)) {
                emit errorOccurred("デルタデータの送信に失敗");
                rs_job_free(job);
 
                return;
             }
          }
       } while (result == RS_BLOCKED);
 
       rs_job_free(job);
 
       if (result != RS_DONE) {
          emit errorOccurred(QString("デルタの計算に失敗しました: %1").arg(rs_strerror(result)));
          return;
       }
 
       // 完了通知
       if (!sendData("DONE")) {
          emit errorOccurred("完了通知の送信に失敗");
          return;
       }
 
       emit progressUpdated(1);
    }
 
    bool connectToHost(const QString &host, quint16 port)
    {
       socket->connectToHost(host, port);
       return socket->waitForConnected(5000);
    }
 
    void disconnectFromHost()
    {
       if (socket->state() == QAbstractSocket::ConnectedState) {
          socket->disconnectFromHost();
       }
    }
 
    bool sendData(const QByteArray &data)
    {
       QDataStream out(socket);
       out << quint32(data.size());
       out.writeRawData(data.constData(), data.size());
 
       return socket->waitForBytesWritten(5000);
   }
 
    QByteArray receiveData()
    {
       if (!socket->waitForReadyRead(5000)) {
          return QByteArray();
       }
 
       QDataStream in(socket);
       quint32 blockSize;
       in >> blockSize;
 
       while (socket->bytesAvailable() < blockSize) {
          if (!socket->waitForReadyRead(5000)) {
             return QByteArray();
          }
       }
 
       QByteArray data;
       data.resize(blockSize);
       in.readRawData(data.data(), blockSize);
 
       return data;
    }
 
 public:
    explicit RemoteFileSyncWorker(QObject *parent = nullptr) : QObject(parent), socket(new QTcpSocket(this))
    {}
 
    ~RemoteFileSyncWorker()
    {
       disconnectFromHost();
    }
 
    QFuture<void> syncFiles(const QString &sourcePath, const QString &destPath, const QString &host, quint16 port)
    {
       return QtConcurrent::run([this, sourcePath, destPath, host, port]() {
          if (!connectToHost(host, port)) {
             emit errorOccurred("リモートホストへの接続に失敗しました");
             return;
          }
 
          QDirIterator it(sourcePath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
          while (it.hasNext()) {
             QString sourceFilePath = it.next();
             QString relativeFilePath = QDir(sourcePath).relativeFilePath(sourceFilePath);
             QString destFilePath = QDir(destPath).filePath(relativeFilePath);
 
             QFileInfo sourceInfo(sourceFilePath);
             if (sourceInfo.isDir()) {
                // リモートPCでディレクトリを作成するコマンドを送信
                QByteArray command = "MKDIR:" + destFilePath.toUtf8();
                if (!sendData(command)) {
                   emit errorOccurred("ディレクトリ作成コマンドの送信に失敗");
                   return;
                }
             }
             else {
                syncFile(sourceFilePath, destFilePath);
             }
          }
 
          disconnectFromHost();
       });
    }
 
 signals:
    void progressUpdated(int progress);
    void errorOccurred(const QString &error);
 };
 
 #endif // REMOTEFILESYNCWORKER_H


 #include <QCoreApplication>
 #include <QDebug>
 #include "RemoteFileSyncWorker.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    QString sourcePath = "<同期元ディレクトリ>";
    QString destPath   = "<同期先ディレクトリ>";
    QString remoteHost = "<リモートホストのIPアドレス>";
    quint16 remotePort = <リモートホストのポート番号>;
 
    RemoteFileSyncWorker worker;
    QObject::connect(&worker, &RemoteFileSyncWorker::progressUpdated, [](int progress) {
       qDebug() << "進捗: " << progress;
    });
 
    QObject::connect(&worker, &RemoteFileSyncWorker::errorOccurred, [](const QString &error) {
       qDebug() << "エラー: " << error;
    });
 
    // 同期の開始
    QFuture<void> future = worker.syncFiles(sourcePath, destPath, remoteHost, remotePort);
 
    // メインイベントループを開始
    int result = a.exec();
 
    // 同期完了まで待機
    future.waitForFinished();
 
    return result;
 }