mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-07 02:23:25 -08:00
[Client+Server] Auto deck sync with change polling
This commit is contained in:
parent
7390c89827
commit
5026e9a956
18 changed files with 448 additions and 18 deletions
|
|
@ -8,10 +8,13 @@
|
|||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QLoggingCategory>
|
||||
#include <QSaveFile>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
#include <QUuid>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/deck_list/deck_diff.h>
|
||||
#include <libcockatrice/deck_list/deck_file_format.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
|
@ -20,12 +23,15 @@
|
|||
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_new_dir.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_sync_check.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_sync_status.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_upload.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_deckstorage.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
|
|
@ -35,6 +41,7 @@ namespace
|
|||
{
|
||||
constexpr const char *SYNC_META_DIR = ".sync";
|
||||
constexpr const char *BACKUP_SUFFIX = ".sync-backup-";
|
||||
constexpr int WATCH_DEBOUNCE_MS = 2000;
|
||||
|
||||
QString serverPathFor(const QString &relativePath)
|
||||
{
|
||||
|
|
@ -77,21 +84,145 @@ QString localPathFor(const QString &dir, const QString &name)
|
|||
|
||||
DeckSyncManager::DeckSyncManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
watcher = new QFileSystemWatcher(this);
|
||||
debounceTimer = new QTimer(this);
|
||||
debounceTimer->setSingleShot(true);
|
||||
debounceTimer->setInterval(WATCH_DEBOUNCE_MS);
|
||||
intervalTimer = new QTimer(this);
|
||||
connect(watcher, &QFileSystemWatcher::fileChanged, this,
|
||||
[this](const QString &path) { onWatchedPathChanged(path); });
|
||||
connect(watcher, &QFileSystemWatcher::directoryChanged, this, [this](const QString &path) {
|
||||
refreshWatcher();
|
||||
onWatchedPathChanged(path);
|
||||
});
|
||||
connect(debounceTimer, &QTimer::timeout, this, &DeckSyncManager::onDebounceTimeout);
|
||||
connect(intervalTimer, &QTimer::timeout, this, [this] { autoSyncSoon(); });
|
||||
refreshInterval();
|
||||
}
|
||||
|
||||
DeckSyncManager::DeckSyncManager(AbstractClient *_client, ColorIdentityProvider provider, QObject *parent)
|
||||
: QObject(parent), client(_client), colorIdentityProvider(std::move(provider))
|
||||
{
|
||||
if (client == nullptr) {
|
||||
return;
|
||||
}
|
||||
connect(
|
||||
client, &AbstractClient::statusChanged, this,
|
||||
[this](ClientStatus status) {
|
||||
if (status == StatusDisconnected) {
|
||||
offline = true;
|
||||
debounceTimer->stop();
|
||||
}
|
||||
},
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
client, &AbstractClient::userInfoChanged, this,
|
||||
[this](const ServerInfo_User &userInfo) {
|
||||
offline = false;
|
||||
serverUsername = QString::fromStdString(userInfo.name());
|
||||
autoSyncSoon();
|
||||
},
|
||||
Qt::UniqueConnection);
|
||||
}
|
||||
|
||||
void DeckSyncManager::setProfile(const DeckSyncProfile &_profile)
|
||||
void DeckSyncManager::setServerIdentity(const QString &host, int port, const QString &username)
|
||||
{
|
||||
profile = _profile;
|
||||
serverHost = host;
|
||||
serverPort = port;
|
||||
serverUsername = username;
|
||||
}
|
||||
|
||||
void DeckSyncManager::setDeckPath(const QString &path)
|
||||
{
|
||||
deckPath = path;
|
||||
refreshWatcher();
|
||||
}
|
||||
|
||||
void DeckSyncManager::setAutoSyncEnabled(bool enabled)
|
||||
{
|
||||
autoSyncEnabled = enabled;
|
||||
refreshInterval();
|
||||
}
|
||||
|
||||
void DeckSyncManager::setSyncIntervalMinutes(int minutes)
|
||||
{
|
||||
syncIntervalMinutes = qBound(1, minutes, 60);
|
||||
refreshInterval();
|
||||
}
|
||||
|
||||
DeckSyncProfile DeckSyncManager::buildProfile() const
|
||||
{
|
||||
return DeckSyncProfile(serverHost, serverPort, serverUsername);
|
||||
}
|
||||
|
||||
void DeckSyncManager::refreshInterval()
|
||||
{
|
||||
intervalTimer->stop();
|
||||
if (autoSyncEnabled) {
|
||||
intervalTimer->start(syncIntervalMinutes * 60000);
|
||||
}
|
||||
}
|
||||
|
||||
void DeckSyncManager::refreshWatcher()
|
||||
{
|
||||
watcher->removePaths(watcher->files() + watcher->directories());
|
||||
if (deckPath.isEmpty() || !QDir(deckPath).exists()) {
|
||||
return;
|
||||
}
|
||||
QStringList watched;
|
||||
watched.append(deckPath);
|
||||
QDirIterator it(deckPath, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
const QString path = it.next();
|
||||
if (!path.contains("/" + QString(SYNC_META_DIR) + "/") && !path.endsWith("/" + QString(SYNC_META_DIR))) {
|
||||
watched.append(path);
|
||||
}
|
||||
}
|
||||
watcher->addPaths(watched);
|
||||
}
|
||||
|
||||
void DeckSyncManager::onWatchedPathChanged(const QString &path)
|
||||
{
|
||||
if (path.contains("/" + QString(SYNC_META_DIR) + "/")) {
|
||||
return;
|
||||
}
|
||||
autoSyncSoon();
|
||||
}
|
||||
|
||||
void DeckSyncManager::autoSyncSoon()
|
||||
{
|
||||
if (!autoSyncEnabled) {
|
||||
return;
|
||||
}
|
||||
debounceTimer->start(WATCH_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
void DeckSyncManager::onDebounceTimeout()
|
||||
{
|
||||
if (syncing) {
|
||||
debounceTimer->start(WATCH_DEBOUNCE_MS);
|
||||
return;
|
||||
}
|
||||
if (canAutoRun()) {
|
||||
syncNow();
|
||||
}
|
||||
}
|
||||
|
||||
bool DeckSyncManager::canAutoRun() const
|
||||
{
|
||||
if (!autoSyncEnabled || offline || client == nullptr || deckPath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const DeckSyncProfile autoProfile = buildProfile();
|
||||
if (!autoProfile.isValid()) {
|
||||
return false;
|
||||
}
|
||||
const QString autoSidecarPath = DeckSyncSidecar::pathForDeckPath(deckPath, autoProfile.key());
|
||||
if (!QFileInfo::exists(autoSidecarPath)) {
|
||||
return false;
|
||||
}
|
||||
DeckSyncSidecar sidecar(autoSidecarPath);
|
||||
return sidecar.load() && sidecar.mode == DeckSyncSidecar::MODE_LINKED;
|
||||
}
|
||||
|
||||
void DeckSyncManager::setConflictDefault(int _conflictDefault)
|
||||
|
|
@ -99,11 +230,6 @@ void DeckSyncManager::setConflictDefault(int _conflictDefault)
|
|||
conflictDefault = _conflictDefault;
|
||||
}
|
||||
|
||||
void DeckSyncManager::setSyncMode(DeckSyncMode mode)
|
||||
{
|
||||
syncMode = mode;
|
||||
}
|
||||
|
||||
bool DeckSyncManager::isSyncing() const
|
||||
{
|
||||
return syncing;
|
||||
|
|
@ -114,7 +240,8 @@ void DeckSyncManager::syncNow()
|
|||
if (syncing) {
|
||||
return;
|
||||
}
|
||||
if (client == nullptr || !profile.isValid() || deckPath.isEmpty()) {
|
||||
const DeckSyncProfile runProfile = buildProfile();
|
||||
if (client == nullptr || !runProfile.isValid() || deckPath.isEmpty()) {
|
||||
emit syncFinished(SyncSummary{}, tr("Not connected to a server."));
|
||||
return;
|
||||
}
|
||||
|
|
@ -141,10 +268,11 @@ void DeckSyncManager::syncNow()
|
|||
sessionDefault = -1;
|
||||
syncMode = DeckSyncMode::TwoWay;
|
||||
firstSyncRun = false;
|
||||
pendingServerTime = -1;
|
||||
emit syncStarted();
|
||||
emit statusMessage(tr("Scanning local decks..."));
|
||||
|
||||
sidecarPath = DeckSyncSidecar::pathForDeckPath(deckPath, profile.key());
|
||||
sidecarPath = DeckSyncSidecar::pathForDeckPath(deckPath, runProfile.key());
|
||||
DeckSyncSidecar sidecar(sidecarPath);
|
||||
if (!sidecar.load()) {
|
||||
failSync(tr("Could not read sync state."));
|
||||
|
|
@ -174,12 +302,80 @@ void DeckSyncManager::syncNow()
|
|||
.modified = QFileInfo(absolutePath).lastModified()});
|
||||
}
|
||||
|
||||
if (!firstSyncRun && sidecar.lastSyncSecs > 0) {
|
||||
sendSyncCheck(sidecar.lastSyncSecs);
|
||||
return;
|
||||
}
|
||||
fetchFullList();
|
||||
}
|
||||
|
||||
void DeckSyncManager::fetchFullList()
|
||||
{
|
||||
Command_DeckList cmd;
|
||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DeckSyncManager::onListFinished);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void DeckSyncManager::sendSyncCheck(qint64 lastSyncSecs)
|
||||
{
|
||||
emit statusMessage(tr("Checking for server changes..."));
|
||||
Command_DeckSyncCheck cmd;
|
||||
cmd.set_last_sync_time(static_cast<google::protobuf::uint64>(lastSyncSecs));
|
||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DeckSyncManager::onCheckFinished);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void DeckSyncManager::onCheckFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
if (!syncing) {
|
||||
return;
|
||||
}
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
fetchFullList();
|
||||
return;
|
||||
}
|
||||
const Response_DeckSyncStatus &status = response.GetExtension(Response_DeckSyncStatus::ext);
|
||||
DeckSyncSidecar sidecar(sidecarPath);
|
||||
if (!sidecar.load()) {
|
||||
fetchFullList();
|
||||
return;
|
||||
}
|
||||
|
||||
bool localClean = status.changed_files_size() == 0;
|
||||
if (localClean) {
|
||||
for (auto it = localFiles.begin(); it != localFiles.end(); ++it) {
|
||||
const DeckSyncRecord record = sidecar.get(it.key());
|
||||
if (record.remoteId == -1 || record.baseHash != it.value().hash) {
|
||||
localClean = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (localClean) {
|
||||
const QMap<QString, DeckSyncRecord> knownRecords = sidecar.records();
|
||||
for (auto it = knownRecords.begin(); it != knownRecords.end(); ++it) {
|
||||
if (!localFiles.contains(it.key())) {
|
||||
localClean = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!localClean) {
|
||||
if (status.changed_files_size() > 0) {
|
||||
emit statusMessage(tr("%1 server changes, syncing...").arg(status.changed_files_size()));
|
||||
}
|
||||
fetchFullList();
|
||||
return;
|
||||
}
|
||||
|
||||
pendingServerTime = status.has_server_time() ? static_cast<qint64>(status.server_time()) : -1;
|
||||
unchangedCount = localFiles.size();
|
||||
emit statusMessage(tr("Decks are already in sync."));
|
||||
finishSync();
|
||||
}
|
||||
|
||||
void DeckSyncManager::collectServerFiles(const ServerInfo_DeckStorage_Folder &folder,
|
||||
const QString &prefix,
|
||||
QMap<int, ServerFile> &files,
|
||||
|
|
@ -1034,6 +1230,13 @@ void DeckSyncManager::failSync(const QString &error)
|
|||
void DeckSyncManager::finishSync()
|
||||
{
|
||||
syncing = false;
|
||||
DeckSyncSidecar sidecar(sidecarPath);
|
||||
if (sidecar.load()) {
|
||||
sidecar.lastSyncSecs =
|
||||
pendingServerTime >= 0 ? pendingServerTime : QDateTime::currentDateTime().toSecsSinceEpoch();
|
||||
sidecar.save();
|
||||
}
|
||||
pendingServerTime = -1;
|
||||
const SyncSummary summary = currentSummary();
|
||||
emit statusMessage(tr("Sync complete: %1 uploaded, %2 downloaded, %3 unchanged, %4 skipped.")
|
||||
.arg(summary.uploaded)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue