mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
[Client+Server] Auto deck sync with change polling
This commit is contained in:
parent
dcccb0f332
commit
e73fcd602c
18 changed files with 448 additions and 18 deletions
|
|
@ -8,9 +8,12 @@
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QFileSystemWatcher>
|
||||||
#include <QSaveFile>
|
#include <QSaveFile>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include <QTimer>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/deck_list/deck_diff.h>
|
#include <libcockatrice/deck_list/deck_diff.h>
|
||||||
#include <libcockatrice/deck_list/deck_file_format.h>
|
#include <libcockatrice/deck_list/deck_file_format.h>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
|
|
@ -19,12 +22,15 @@
|
||||||
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_list.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_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/command_deck_upload.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_download.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_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/response_deck_upload.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/serverinfo_deckstorage.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/protocol/pending_command.h>
|
||||||
#include <libcockatrice/utility/string_limits.h>
|
#include <libcockatrice/utility/string_limits.h>
|
||||||
|
|
||||||
|
|
@ -32,6 +38,7 @@ namespace
|
||||||
{
|
{
|
||||||
constexpr const char *SYNC_META_DIR = ".sync";
|
constexpr const char *SYNC_META_DIR = ".sync";
|
||||||
constexpr const char *BACKUP_SUFFIX = ".sync-backup-";
|
constexpr const char *BACKUP_SUFFIX = ".sync-backup-";
|
||||||
|
constexpr int WATCH_DEBOUNCE_MS = 2000;
|
||||||
|
|
||||||
QString serverPathFor(const QString &relativePath)
|
QString serverPathFor(const QString &relativePath)
|
||||||
{
|
{
|
||||||
|
|
@ -74,21 +81,145 @@ QString localPathFor(const QString &dir, const QString &name)
|
||||||
|
|
||||||
DeckSyncManager::DeckSyncManager(QObject *parent) : QObject(parent)
|
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)
|
DeckSyncManager::DeckSyncManager(AbstractClient *_client, ColorIdentityProvider provider, QObject *parent)
|
||||||
: QObject(parent), client(_client), colorIdentityProvider(std::move(provider))
|
: 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)
|
void DeckSyncManager::setDeckPath(const QString &path)
|
||||||
{
|
{
|
||||||
deckPath = 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)
|
void DeckSyncManager::setConflictDefault(int _conflictDefault)
|
||||||
|
|
@ -96,11 +227,6 @@ void DeckSyncManager::setConflictDefault(int _conflictDefault)
|
||||||
conflictDefault = _conflictDefault;
|
conflictDefault = _conflictDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckSyncManager::setSyncMode(DeckSyncMode mode)
|
|
||||||
{
|
|
||||||
syncMode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DeckSyncManager::isSyncing() const
|
bool DeckSyncManager::isSyncing() const
|
||||||
{
|
{
|
||||||
return syncing;
|
return syncing;
|
||||||
|
|
@ -111,7 +237,8 @@ void DeckSyncManager::syncNow()
|
||||||
if (syncing) {
|
if (syncing) {
|
||||||
return;
|
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."));
|
emit syncFinished(SyncSummary{}, tr("Not connected to a server."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -138,10 +265,11 @@ void DeckSyncManager::syncNow()
|
||||||
sessionDefault = -1;
|
sessionDefault = -1;
|
||||||
syncMode = DeckSyncMode::TwoWay;
|
syncMode = DeckSyncMode::TwoWay;
|
||||||
firstSyncRun = false;
|
firstSyncRun = false;
|
||||||
|
pendingServerTime = -1;
|
||||||
emit syncStarted();
|
emit syncStarted();
|
||||||
emit statusMessage(tr("Scanning local decks..."));
|
emit statusMessage(tr("Scanning local decks..."));
|
||||||
|
|
||||||
sidecarPath = DeckSyncSidecar::pathForDeckPath(deckPath, profile.key());
|
sidecarPath = DeckSyncSidecar::pathForDeckPath(deckPath, runProfile.key());
|
||||||
DeckSyncSidecar sidecar(sidecarPath);
|
DeckSyncSidecar sidecar(sidecarPath);
|
||||||
if (!sidecar.load()) {
|
if (!sidecar.load()) {
|
||||||
failSync(tr("Could not read sync state."));
|
failSync(tr("Could not read sync state."));
|
||||||
|
|
@ -171,12 +299,80 @@ void DeckSyncManager::syncNow()
|
||||||
.modified = QFileInfo(absolutePath).lastModified()});
|
.modified = QFileInfo(absolutePath).lastModified()});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!firstSyncRun && sidecar.lastSyncSecs > 0) {
|
||||||
|
sendSyncCheck(sidecar.lastSyncSecs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchFullList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckSyncManager::fetchFullList()
|
||||||
|
{
|
||||||
Command_DeckList cmd;
|
Command_DeckList cmd;
|
||||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
||||||
connect(pend, &PendingCommand::finished, this, &DeckSyncManager::onListFinished);
|
connect(pend, &PendingCommand::finished, this, &DeckSyncManager::onListFinished);
|
||||||
client->sendCommand(pend);
|
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,
|
void DeckSyncManager::collectServerFiles(const ServerInfo_DeckStorage_Folder &folder,
|
||||||
const QString &prefix,
|
const QString &prefix,
|
||||||
QMap<int, ServerFile> &files,
|
QMap<int, ServerFile> &files,
|
||||||
|
|
@ -1035,6 +1231,13 @@ void DeckSyncManager::failSync(const QString &error)
|
||||||
void DeckSyncManager::finishSync()
|
void DeckSyncManager::finishSync()
|
||||||
{
|
{
|
||||||
syncing = false;
|
syncing = false;
|
||||||
|
DeckSyncSidecar sidecar(sidecarPath);
|
||||||
|
if (sidecar.load()) {
|
||||||
|
sidecar.lastSyncSecs =
|
||||||
|
pendingServerTime >= 0 ? pendingServerTime : QDateTime::currentDateTime().toSecsSinceEpoch();
|
||||||
|
sidecar.save();
|
||||||
|
}
|
||||||
|
pendingServerTime = -1;
|
||||||
const SyncSummary summary = currentSummary();
|
const SyncSummary summary = currentSummary();
|
||||||
emit statusMessage(tr("Sync complete: %1 uploaded, %2 downloaded, %3 unchanged, %4 skipped.")
|
emit statusMessage(tr("Sync complete: %1 uploaded, %2 downloaded, %3 unchanged, %4 skipped.")
|
||||||
.arg(summary.uploaded)
|
.arg(summary.uploaded)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@
|
||||||
class AbstractClient;
|
class AbstractClient;
|
||||||
class CommandContainer;
|
class CommandContainer;
|
||||||
class DeckList;
|
class DeckList;
|
||||||
|
class QFileSystemWatcher;
|
||||||
|
class QTimer;
|
||||||
class Response;
|
class Response;
|
||||||
class ServerInfo_DeckStorage_Folder;
|
class ServerInfo_DeckStorage_Folder;
|
||||||
|
|
||||||
|
|
@ -67,10 +69,11 @@ public:
|
||||||
explicit DeckSyncManager(QObject *parent = nullptr);
|
explicit DeckSyncManager(QObject *parent = nullptr);
|
||||||
DeckSyncManager(AbstractClient *client, ColorIdentityProvider provider, QObject *parent = nullptr);
|
DeckSyncManager(AbstractClient *client, ColorIdentityProvider provider, QObject *parent = nullptr);
|
||||||
|
|
||||||
void setProfile(const DeckSyncProfile &_profile);
|
void setServerIdentity(const QString &host, int port, const QString &username);
|
||||||
void setDeckPath(const QString &path);
|
void setDeckPath(const QString &path);
|
||||||
void setConflictDefault(int conflictDefault);
|
void setConflictDefault(int conflictDefault);
|
||||||
void setSyncMode(DeckSyncMode mode);
|
void setAutoSyncEnabled(bool enabled);
|
||||||
|
void setSyncIntervalMinutes(int minutes);
|
||||||
[[nodiscard]] bool isSyncing() const;
|
[[nodiscard]] bool isSyncing() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
|
@ -93,6 +96,7 @@ signals:
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onListFinished(const Response &response, const CommandContainer &commandContainer);
|
void onListFinished(const Response &response, const CommandContainer &commandContainer);
|
||||||
|
void onCheckFinished(const Response &response, const CommandContainer &commandContainer);
|
||||||
void
|
void
|
||||||
onEnsureDirFinished(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData);
|
onEnsureDirFinished(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData);
|
||||||
void
|
void
|
||||||
|
|
@ -158,6 +162,15 @@ private:
|
||||||
void failSync(const QString &error);
|
void failSync(const QString &error);
|
||||||
void finishSync();
|
void finishSync();
|
||||||
[[nodiscard]] SyncSummary currentSummary() const;
|
[[nodiscard]] SyncSummary currentSummary() const;
|
||||||
|
void fetchFullList();
|
||||||
|
void sendSyncCheck(qint64 lastSyncSecs);
|
||||||
|
void autoSyncSoon();
|
||||||
|
[[nodiscard]] bool canAutoRun() const;
|
||||||
|
void onDebounceTimeout();
|
||||||
|
void onWatchedPathChanged(const QString &path);
|
||||||
|
void refreshWatcher();
|
||||||
|
void refreshInterval();
|
||||||
|
[[nodiscard]] DeckSyncProfile buildProfile() const;
|
||||||
void collectServerFiles(const ServerInfo_DeckStorage_Folder &folder,
|
void collectServerFiles(const ServerInfo_DeckStorage_Folder &folder,
|
||||||
const QString &prefix,
|
const QString &prefix,
|
||||||
QMap<int, ServerFile> &files,
|
QMap<int, ServerFile> &files,
|
||||||
|
|
@ -195,7 +208,9 @@ private:
|
||||||
QString uniqueLocalPath(const QString &relativePath) const;
|
QString uniqueLocalPath(const QString &relativePath) const;
|
||||||
|
|
||||||
AbstractClient *client = nullptr;
|
AbstractClient *client = nullptr;
|
||||||
DeckSyncProfile profile;
|
QString serverHost;
|
||||||
|
int serverPort = 0;
|
||||||
|
QString serverUsername;
|
||||||
QString deckPath;
|
QString deckPath;
|
||||||
ColorIdentityProvider colorIdentityProvider;
|
ColorIdentityProvider colorIdentityProvider;
|
||||||
bool syncing = false;
|
bool syncing = false;
|
||||||
|
|
@ -211,6 +226,13 @@ private:
|
||||||
QMap<int, ServerFile> stashedServerFiles;
|
QMap<int, ServerFile> stashedServerFiles;
|
||||||
ActiveConflict activeConflict;
|
ActiveConflict activeConflict;
|
||||||
DeckSyncMode syncMode = DeckSyncMode::TwoWay;
|
DeckSyncMode syncMode = DeckSyncMode::TwoWay;
|
||||||
|
QFileSystemWatcher *watcher = nullptr;
|
||||||
|
QTimer *debounceTimer = nullptr;
|
||||||
|
QTimer *intervalTimer = nullptr;
|
||||||
|
bool autoSyncEnabled = true;
|
||||||
|
int syncIntervalMinutes = 5;
|
||||||
|
bool offline = false;
|
||||||
|
qint64 pendingServerTime = -1;
|
||||||
int conflictDefault = DeckSyncConflictAsk;
|
int conflictDefault = DeckSyncConflictAsk;
|
||||||
int sessionDefault = -1;
|
int sessionDefault = -1;
|
||||||
bool firstSyncRun = false;
|
bool firstSyncRun = false;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ bool DeckSyncSidecar::load()
|
||||||
{
|
{
|
||||||
recordMap.clear();
|
recordMap.clear();
|
||||||
mode = MODE_LINKED;
|
mode = MODE_LINKED;
|
||||||
|
lastSyncSecs = 0;
|
||||||
|
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
|
|
@ -39,6 +40,7 @@ bool DeckSyncSidecar::load()
|
||||||
}
|
}
|
||||||
const QJsonValue modeValue = document.object().value("mode");
|
const QJsonValue modeValue = document.object().value("mode");
|
||||||
mode = modeValue.isString() ? modeValue.toString() : MODE_LINKED;
|
mode = modeValue.isString() ? modeValue.toString() : MODE_LINKED;
|
||||||
|
lastSyncSecs = static_cast<qint64>(document.object().value("lastSync").toDouble());
|
||||||
const QJsonObject records = document.object().value("records").toObject();
|
const QJsonObject records = document.object().value("records").toObject();
|
||||||
for (auto it = records.begin(); it != records.end(); ++it) {
|
for (auto it = records.begin(); it != records.end(); ++it) {
|
||||||
if (!it.value().isObject()) {
|
if (!it.value().isObject()) {
|
||||||
|
|
@ -71,6 +73,7 @@ bool DeckSyncSidecar::save() const
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
root.insert("version", SIDECAR_FORMAT_VERSION);
|
root.insert("version", SIDECAR_FORMAT_VERSION);
|
||||||
root.insert("mode", mode);
|
root.insert("mode", mode);
|
||||||
|
root.insert("lastSync", static_cast<double>(lastSyncSecs));
|
||||||
root.insert("records", records);
|
root.insert("records", records);
|
||||||
|
|
||||||
QDir().mkpath(QFileInfo(filePath).absolutePath());
|
QDir().mkpath(QFileInfo(filePath).absolutePath());
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ public:
|
||||||
|
|
||||||
QString filePath; ///< Full path of the sidecar JSON file.
|
QString filePath; ///< Full path of the sidecar JSON file.
|
||||||
QString mode = MODE_LINKED; ///< Linked normally; separate keeps this profile out of sync.
|
QString mode = MODE_LINKED; ///< Linked normally; separate keeps this profile out of sync.
|
||||||
|
qint64 lastSyncSecs = 0; ///< Last completed sync, seconds since epoch; 0 means never.
|
||||||
|
|
||||||
explicit DeckSyncSidecar(const QString &_filePath);
|
explicit DeckSyncSidecar(const QString &_filePath);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -373,9 +373,22 @@ GeneralSettingsPage::GeneralSettingsPage()
|
||||||
SettingsCache::instance().deckSync().setConflictDefault(conflictDefaultSelector.itemData(index).toInt());
|
SettingsCache::instance().deckSync().setConflictDefault(conflictDefaultSelector.itemData(index).toInt());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
autoSyncCheckBox.setChecked(settings.deckSync().getAutoSync());
|
||||||
|
connect(&autoSyncCheckBox, &QCheckBox::toggled, &SettingsCache::instance().deckSync(),
|
||||||
|
&DeckSyncSettings::setAutoSync);
|
||||||
|
|
||||||
|
syncIntervalEdit.setMinimum(1);
|
||||||
|
syncIntervalEdit.setMaximum(60);
|
||||||
|
syncIntervalEdit.setValue(settings.deckSync().getSyncIntervalMinutes());
|
||||||
|
connect(&syncIntervalEdit, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance().deckSync(),
|
||||||
|
&DeckSyncSettings::setSyncIntervalMinutes);
|
||||||
|
|
||||||
auto *deckSyncGrid = new QGridLayout;
|
auto *deckSyncGrid = new QGridLayout;
|
||||||
deckSyncGrid->addWidget(&conflictDefaultLabel, 0, 0);
|
deckSyncGrid->addWidget(&conflictDefaultLabel, 0, 0);
|
||||||
deckSyncGrid->addWidget(&conflictDefaultSelector, 0, 1);
|
deckSyncGrid->addWidget(&conflictDefaultSelector, 0, 1);
|
||||||
|
deckSyncGrid->addWidget(&autoSyncCheckBox, 1, 0, 1, 2);
|
||||||
|
deckSyncGrid->addWidget(&syncIntervalLabel, 2, 0);
|
||||||
|
deckSyncGrid->addWidget(&syncIntervalEdit, 2, 1);
|
||||||
deckSyncGroupBox = new QGroupBox;
|
deckSyncGroupBox = new QGroupBox;
|
||||||
deckSyncGroupBox->setLayout(deckSyncGrid);
|
deckSyncGroupBox->setLayout(deckSyncGrid);
|
||||||
|
|
||||||
|
|
@ -676,5 +689,8 @@ void GeneralSettingsPage::retranslateUi()
|
||||||
tr("Keep the server deck"));
|
tr("Keep the server deck"));
|
||||||
conflictDefaultSelector.setItemText(conflictDefaultSelector.findData(DeckSyncConflictKeepNewer),
|
conflictDefaultSelector.setItemText(conflictDefaultSelector.findData(DeckSyncConflictKeepNewer),
|
||||||
tr("Keep the newer deck"));
|
tr("Keep the newer deck"));
|
||||||
|
autoSyncCheckBox.setText(tr("Sync decks automatically"));
|
||||||
|
syncIntervalLabel.setText(tr("Check every:"));
|
||||||
|
syncIntervalEdit.setSuffix(" " + tr("min"));
|
||||||
resetAllPathsButton->setText(tr("Reset all paths"));
|
resetAllPathsButton->setText(tr("Reset all paths"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,9 @@ private:
|
||||||
|
|
||||||
QLabel conflictDefaultLabel;
|
QLabel conflictDefaultLabel;
|
||||||
QComboBox conflictDefaultSelector;
|
QComboBox conflictDefaultSelector;
|
||||||
|
QCheckBox autoSyncCheckBox;
|
||||||
|
QLabel syncIntervalLabel;
|
||||||
|
QSpinBox syncIntervalEdit;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H
|
#endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
#include "../../deck_loader/deck_loader.h"
|
#include "../../deck_loader/deck_loader.h"
|
||||||
#include "../../deck_sync/deck_sync_conflict.h"
|
#include "../../deck_sync/deck_sync_conflict.h"
|
||||||
#include "../../deck_sync/deck_sync_manager.h"
|
#include "../../deck_sync/deck_sync_manager.h"
|
||||||
#include "../../deck_sync/deck_sync_profile.h"
|
|
||||||
#include "../../pixel_map_generator.h"
|
#include "../../pixel_map_generator.h"
|
||||||
#include "../cards/additional_info/deck_color_identity.h"
|
#include "../cards/additional_info/deck_color_identity.h"
|
||||||
#include "../deck_share/deck_share_utils.h"
|
#include "../deck_share/deck_share_utils.h"
|
||||||
|
|
@ -262,8 +261,14 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
||||||
syncManager = new DeckSyncManager(
|
syncManager = new DeckSyncManager(
|
||||||
client, [](const DeckList &deck) { return getDeckColorIdentity(deck, CardDatabaseManager::query()); }, this);
|
client, [](const DeckList &deck) { return getDeckColorIdentity(deck, CardDatabaseManager::query()); }, this);
|
||||||
syncManager->setConflictDefault(SettingsCache::instance().deckSync().getConflictDefault());
|
syncManager->setConflictDefault(SettingsCache::instance().deckSync().getConflictDefault());
|
||||||
|
syncManager->setAutoSyncEnabled(SettingsCache::instance().deckSync().getAutoSync());
|
||||||
|
syncManager->setSyncIntervalMinutes(SettingsCache::instance().deckSync().getSyncIntervalMinutes());
|
||||||
connect(&SettingsCache::instance().deckSync(), &DeckSyncSettings::conflictDefaultChanged, syncManager,
|
connect(&SettingsCache::instance().deckSync(), &DeckSyncSettings::conflictDefaultChanged, syncManager,
|
||||||
&DeckSyncManager::setConflictDefault);
|
&DeckSyncManager::setConflictDefault);
|
||||||
|
connect(&SettingsCache::instance().deckSync(), &DeckSyncSettings::autoSyncChanged, syncManager,
|
||||||
|
&DeckSyncManager::setAutoSyncEnabled);
|
||||||
|
connect(&SettingsCache::instance().deckSync(), &DeckSyncSettings::syncIntervalMinutesChanged, syncManager,
|
||||||
|
&DeckSyncManager::setSyncIntervalMinutes);
|
||||||
connect(syncManager, &DeckSyncManager::statusMessage, this, &TabDeckStorage::onSyncStatusMessage);
|
connect(syncManager, &DeckSyncManager::statusMessage, this, &TabDeckStorage::onSyncStatusMessage);
|
||||||
connect(syncManager, &DeckSyncManager::syncFinished, this, &TabDeckStorage::onSyncFinished);
|
connect(syncManager, &DeckSyncManager::syncFinished, this, &TabDeckStorage::onSyncFinished);
|
||||||
connect(syncManager, &DeckSyncManager::firstSyncDetected, this, &TabDeckStorage::onFirstSyncDetected);
|
connect(syncManager, &DeckSyncManager::firstSyncDetected, this, &TabDeckStorage::onFirstSyncDetected);
|
||||||
|
|
@ -271,6 +276,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
||||||
if (currentUserInfo != nullptr) {
|
if (currentUserInfo != nullptr) {
|
||||||
syncUsername = QString::fromStdString(currentUserInfo->name());
|
syncUsername = QString::fromStdString(currentUserInfo->name());
|
||||||
}
|
}
|
||||||
|
syncManager->setServerIdentity(client->serverName(), client->serverPort(), syncUsername);
|
||||||
|
|
||||||
retranslateUi();
|
retranslateUi();
|
||||||
|
|
||||||
|
|
@ -330,6 +336,7 @@ QString TabDeckStorage::getTargetPath() const
|
||||||
void TabDeckStorage::handleConnected(const ServerInfo_User &userInfo)
|
void TabDeckStorage::handleConnected(const ServerInfo_User &userInfo)
|
||||||
{
|
{
|
||||||
syncUsername = QString::fromStdString(userInfo.name());
|
syncUsername = QString::fromStdString(userInfo.name());
|
||||||
|
syncManager->setServerIdentity(client->serverName(), client->serverPort(), syncUsername);
|
||||||
setRemoteEnabled(userInfo.user_level() & ServerInfo_User::IsRegistered);
|
setRemoteEnabled(userInfo.user_level() & ServerInfo_User::IsRegistered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -561,8 +568,6 @@ void TabDeckStorage::actOpenDecksFolder()
|
||||||
|
|
||||||
void TabDeckStorage::actSyncNow()
|
void TabDeckStorage::actSyncNow()
|
||||||
{
|
{
|
||||||
DeckSyncProfile profile(client->serverName(), client->serverPort(), syncUsername);
|
|
||||||
syncManager->setProfile(profile);
|
|
||||||
syncManager->setDeckPath(SettingsCache::instance().paths().getDeckPath());
|
syncManager->setDeckPath(SettingsCache::instance().paths().getDeckPath());
|
||||||
aSyncNow->setEnabled(false);
|
aSyncNow->setEnabled(false);
|
||||||
syncManager->syncNow();
|
syncManager->syncNow();
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ set(PROTO_FILES
|
||||||
command_deck_share_list.proto
|
command_deck_share_list.proto
|
||||||
command_deck_share_list_mine.proto
|
command_deck_share_list_mine.proto
|
||||||
command_deck_share_remove.proto
|
command_deck_share_remove.proto
|
||||||
|
command_deck_sync_check.proto
|
||||||
command_deck_upload.proto
|
command_deck_upload.proto
|
||||||
command_del_counter.proto
|
command_del_counter.proto
|
||||||
command_delete_arrow.proto
|
command_delete_arrow.proto
|
||||||
|
|
@ -151,6 +152,7 @@ set(PROTO_FILES
|
||||||
response_deck_share_download.proto
|
response_deck_share_download.proto
|
||||||
response_deck_share_list.proto
|
response_deck_share_list.proto
|
||||||
response_deck_share_list_mine.proto
|
response_deck_share_list_mine.proto
|
||||||
|
response_deck_sync_status.proto
|
||||||
response_deck_upload.proto
|
response_deck_upload.proto
|
||||||
response_dump_zone.proto
|
response_dump_zone.proto
|
||||||
response_forgotpasswordrequest.proto
|
response_forgotpasswordrequest.proto
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "session_commands.proto";
|
||||||
|
|
||||||
|
message Command_DeckSyncCheck {
|
||||||
|
extend SessionCommand {
|
||||||
|
optional Command_DeckSyncCheck ext = 1034;
|
||||||
|
}
|
||||||
|
// Seconds since Unix epoch of the client's last completed sync.
|
||||||
|
// 0 means "never synced": the server reports every stored row.
|
||||||
|
optional uint64 last_sync_time = 1 [default = 0];
|
||||||
|
}
|
||||||
|
|
@ -85,6 +85,7 @@ message Response {
|
||||||
DECK_SHARE_LIST = 1104; // Response listing shared decks
|
DECK_SHARE_LIST = 1104; // Response listing shared decks
|
||||||
DECK_SHARE_DOWNLOAD = 1105; // Response for shared deck download
|
DECK_SHARE_DOWNLOAD = 1105; // Response for shared deck download
|
||||||
DECK_SHARE_LIST_MINE = 1106; // Response listing the caller's own shares
|
DECK_SHARE_LIST_MINE = 1106; // Response listing the caller's own shares
|
||||||
|
DECK_SYNC_STATUS = 1107; // Response with changed decks since a timestamp
|
||||||
CARD_ART_RULE_LIST = 1200; // Response containing a list of card art rules
|
CARD_ART_RULE_LIST = 1200; // Response containing a list of card art rules
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "response.proto";
|
||||||
|
|
||||||
|
message DeckSyncChangedFile {
|
||||||
|
// Server-relative folder path without file extension, e.g. "Modern/Affinity".
|
||||||
|
optional string path = 1;
|
||||||
|
optional uint32 deck_id = 2;
|
||||||
|
optional string guid = 3;
|
||||||
|
optional string hash = 4;
|
||||||
|
optional uint64 last_modified = 5;
|
||||||
|
optional bool deleted = 6 [default = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response_DeckSyncStatus {
|
||||||
|
extend Response {
|
||||||
|
optional Response_DeckSyncStatus ext = 1107;
|
||||||
|
}
|
||||||
|
repeated DeckSyncChangedFile changed_files = 1;
|
||||||
|
// Server clock at query time; the client adopts it as its sync baseline.
|
||||||
|
optional uint64 server_time = 2;
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ message SessionCommand {
|
||||||
DECK_SHARE_LIST_MINE = 1032;
|
DECK_SHARE_LIST_MINE = 1032;
|
||||||
DECK_SHARE_REMOVE = 1033;
|
DECK_SHARE_REMOVE = 1033;
|
||||||
SUBMIT_PASSWORD_VERIFIER = 1034;
|
SUBMIT_PASSWORD_VERIFIER = 1034;
|
||||||
|
DECK_SYNC_CHECK = 1035;
|
||||||
REPLAY_LIST = 1100;
|
REPLAY_LIST = 1100;
|
||||||
REPLAY_DOWNLOAD = 1101;
|
REPLAY_DOWNLOAD = 1101;
|
||||||
REPLAY_MODIFY_MATCH = 1102;
|
REPLAY_MODIFY_MATCH = 1102;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "libcockatrice/settings/settings_manager.h"
|
#include "libcockatrice/settings/settings_manager.h"
|
||||||
|
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
DeckSyncSettings::DeckSyncSettings(const QString &settingPath, QObject *parent)
|
DeckSyncSettings::DeckSyncSettings(const QString &settingPath, QObject *parent)
|
||||||
: SettingsManager(settingPath + "deck_sync.ini", "decksync", QString(), parent)
|
: SettingsManager(settingPath + "deck_sync.ini", "decksync", QString(), parent)
|
||||||
|
|
@ -19,3 +20,25 @@ void DeckSyncSettings::setConflictDefault(int _conflictDefault)
|
||||||
setValue(_conflictDefault, "conflictDefault");
|
setValue(_conflictDefault, "conflictDefault");
|
||||||
emit conflictDefaultChanged(_conflictDefault);
|
emit conflictDefaultChanged(_conflictDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DeckSyncSettings::getAutoSync() const
|
||||||
|
{
|
||||||
|
return getValue("autoSync", QString(), QString(), true).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckSyncSettings::setAutoSync(bool _autoSync)
|
||||||
|
{
|
||||||
|
setValue(_autoSync, "autoSync");
|
||||||
|
emit autoSyncChanged(_autoSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
int DeckSyncSettings::getSyncIntervalMinutes() const
|
||||||
|
{
|
||||||
|
return qBound(1, getValue("syncIntervalMinutes", QString(), QString(), 5).toInt(), 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckSyncSettings::setSyncIntervalMinutes(int _syncIntervalMinutes)
|
||||||
|
{
|
||||||
|
setValue(_syncIntervalMinutes, "syncIntervalMinutes");
|
||||||
|
emit syncIntervalMinutesChanged(_syncIntervalMinutes);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,19 @@ class DeckSyncSettings : public SettingsManager
|
||||||
friend class SettingsCache;
|
friend class SettingsCache;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
explicit DeckSyncSettings(const QString &settingPath, QObject *parent = nullptr);
|
||||||
|
|
||||||
[[nodiscard]] int getConflictDefault() const;
|
[[nodiscard]] int getConflictDefault() const;
|
||||||
void setConflictDefault(int _conflictDefault);
|
void setConflictDefault(int _conflictDefault);
|
||||||
|
[[nodiscard]] bool getAutoSync() const;
|
||||||
|
void setAutoSync(bool _autoSync);
|
||||||
|
[[nodiscard]] int getSyncIntervalMinutes() const;
|
||||||
|
void setSyncIntervalMinutes(int _syncIntervalMinutes);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void conflictDefaultChanged(int conflictDefault);
|
void conflictDefaultChanged(int conflictDefault);
|
||||||
|
void autoSyncChanged(bool autoSync);
|
||||||
public:
|
void syncIntervalMinutesChanged(int syncIntervalMinutes);
|
||||||
explicit DeckSyncSettings(const QString &settingPath, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
DeckSyncSettings(const DeckSyncSettings & /*other*/);
|
DeckSyncSettings(const DeckSyncSettings & /*other*/);
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@
|
||||||
#include <libcockatrice/protocol/pb/command_deck_share_list.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_share_list.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_share_list_mine.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_share_list_mine.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_share_remove.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_share_remove.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/command_deck_upload.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_get_server_stats.pb.h>
|
#include <libcockatrice/protocol/pb/command_get_server_stats.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_replay_delete_match.pb.h>
|
#include <libcockatrice/protocol/pb/command_replay_delete_match.pb.h>
|
||||||
|
|
@ -134,6 +135,7 @@
|
||||||
#include <libcockatrice/protocol/pb/response_deck_share_download.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_share_download.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_share_list.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_share_list.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_share_list_mine.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_share_list_mine.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/response_deck_upload.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_forgotpasswordrequest.pb.h>
|
#include <libcockatrice/protocol/pb/response_forgotpasswordrequest.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_get_admin_notes.pb.h>
|
#include <libcockatrice/protocol/pb/response_get_admin_notes.pb.h>
|
||||||
|
|
@ -371,6 +373,8 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm
|
||||||
return cmdDeckUpload(cmd.GetExtension(Command_DeckUpload::ext), rc);
|
return cmdDeckUpload(cmd.GetExtension(Command_DeckUpload::ext), rc);
|
||||||
case SessionCommand::DECK_DOWNLOAD:
|
case SessionCommand::DECK_DOWNLOAD:
|
||||||
return cmdDeckDownload(cmd.GetExtension(Command_DeckDownload::ext), rc);
|
return cmdDeckDownload(cmd.GetExtension(Command_DeckDownload::ext), rc);
|
||||||
|
case SessionCommand::DECK_SYNC_CHECK:
|
||||||
|
return cmdDeckSyncCheck(cmd.GetExtension(Command_DeckSyncCheck::ext), rc);
|
||||||
case SessionCommand::REPLAY_LIST:
|
case SessionCommand::REPLAY_LIST:
|
||||||
return cmdReplayList(cmd.GetExtension(Command_ReplayList::ext), rc);
|
return cmdReplayList(cmd.GetExtension(Command_ReplayList::ext), rc);
|
||||||
case SessionCommand::REPLAY_DOWNLOAD:
|
case SessionCommand::REPLAY_DOWNLOAD:
|
||||||
|
|
@ -894,6 +898,80 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckSetVisibility(const
|
||||||
return Response::RespOk;
|
return Response::RespOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AbstractServerSocketInterface::syncCheckHelper(int folderId,
|
||||||
|
const QString &prefix,
|
||||||
|
int userId,
|
||||||
|
qint64 sinceSecs,
|
||||||
|
Response_DeckSyncStatus *status)
|
||||||
|
{
|
||||||
|
QSqlQuery *query = sqlInterface->prepareQuery("select id, name from {prefix}_decklist_folders where id_parent = "
|
||||||
|
":id_parent and id_user = :id_user");
|
||||||
|
query->bindValue(":id_parent", folderId);
|
||||||
|
query->bindValue(":id_user", userId);
|
||||||
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QList<std::pair<int, QString>> subfolders;
|
||||||
|
while (query->next()) {
|
||||||
|
subfolders.append({query->value(0).toInt(), query->value(1).toString()});
|
||||||
|
}
|
||||||
|
for (const auto &[subfolderId, subfolderName] : subfolders) {
|
||||||
|
const QString path = prefix.isEmpty() ? subfolderName : prefix + "/" + subfolderName;
|
||||||
|
if (!syncCheckHelper(subfolderId, path, userId, sinceSecs, status)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query = sqlInterface->prepareQuery("select id, name, guid, hash, last_modified, deleted from "
|
||||||
|
"{prefix}_decklist_files where id_folder = :id_folder and id_user = :id_user "
|
||||||
|
"and (last_modified is null or last_modified > FROM_UNIXTIME(:since))");
|
||||||
|
query->bindValue(":id_folder", folderId);
|
||||||
|
query->bindValue(":id_user", userId);
|
||||||
|
query->bindValue(":since", sinceSecs);
|
||||||
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
while (query->next()) {
|
||||||
|
const QString name = query->value(1).toString();
|
||||||
|
DeckSyncChangedFile *changed = status->add_changed_files();
|
||||||
|
changed->set_path((prefix.isEmpty() ? name : prefix + "/" + name).toStdString());
|
||||||
|
changed->set_deck_id(query->value(0).toUInt());
|
||||||
|
changed->set_guid(query->value(2).toString().toStdString());
|
||||||
|
changed->set_hash(query->value(3).toString().toStdString());
|
||||||
|
const QDateTime lastModified = query->value(4).toDateTime();
|
||||||
|
if (lastModified.isValid()) {
|
||||||
|
changed->set_last_modified(lastModified.toSecsSinceEpoch());
|
||||||
|
}
|
||||||
|
changed->set_deleted(query->value(5).toBool());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckSyncCheck(const Command_DeckSyncCheck &cmd,
|
||||||
|
ResponseContainer &rc)
|
||||||
|
{
|
||||||
|
if (authState != PasswordRight) {
|
||||||
|
return Response::RespFunctionNotAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlInterface->checkSql();
|
||||||
|
|
||||||
|
Response_DeckSyncStatus *re = new Response_DeckSyncStatus;
|
||||||
|
if (!syncCheckHelper(0, QString(), userInfo->id(), static_cast<qint64>(cmd.last_sync_time()), re)) {
|
||||||
|
delete re;
|
||||||
|
return Response::RespContextError;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery *clockQuery = sqlInterface->prepareQuery("select unix_timestamp(now())");
|
||||||
|
if (!sqlInterface->execSqlQuery(clockQuery) || !clockQuery->next()) {
|
||||||
|
delete re;
|
||||||
|
return Response::RespContextError;
|
||||||
|
}
|
||||||
|
re->set_server_time(clockQuery->value(0).toULongLong());
|
||||||
|
rc.setResponseExtension(re);
|
||||||
|
return Response::RespOk;
|
||||||
|
}
|
||||||
|
|
||||||
Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd,
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd,
|
||||||
ResponseContainer &rc)
|
ResponseContainer &rc)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ class Command_DeckShareList;
|
||||||
class Command_DeckShareListMine;
|
class Command_DeckShareListMine;
|
||||||
class Command_DeckShareRemove;
|
class Command_DeckShareRemove;
|
||||||
class Command_DeckShareDownload;
|
class Command_DeckShareDownload;
|
||||||
|
class Command_DeckSyncCheck;
|
||||||
class Command_ReplayList;
|
class Command_ReplayList;
|
||||||
class Command_ReplayDownload;
|
class Command_ReplayDownload;
|
||||||
class Command_ReplayModifyMatch;
|
class Command_ReplayModifyMatch;
|
||||||
|
|
@ -111,6 +112,7 @@ class Command_ViewLogHistory;
|
||||||
class Command_WarnUser;
|
class Command_WarnUser;
|
||||||
class QObject;
|
class QObject;
|
||||||
class ResponseContainer;
|
class ResponseContainer;
|
||||||
|
class Response_DeckSyncStatus;
|
||||||
|
|
||||||
class AbstractServerSocketInterface : public Server_ProtocolHandler
|
class AbstractServerSocketInterface : public Server_ProtocolHandler
|
||||||
{
|
{
|
||||||
|
|
@ -163,6 +165,9 @@ private:
|
||||||
bool storeDeckVersion(int deckId, const QString &content, const QString &hash);
|
bool storeDeckVersion(int deckId, const QString &content, const QString &hash);
|
||||||
DeckList *getDeckFromDatabase(int deckId);
|
DeckList *getDeckFromDatabase(int deckId);
|
||||||
Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc);
|
||||||
|
bool
|
||||||
|
syncCheckHelper(int folderId, const QString &prefix, int userId, qint64 sinceSecs, Response_DeckSyncStatus *status);
|
||||||
|
Response::ResponseCode cmdDeckSyncCheck(const Command_DeckSyncCheck &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdDeckShareCreate(const Command_DeckShareCreate &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdDeckShareCreate(const Command_DeckShareCreate &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdDeckShareList(const Command_DeckShareList &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdDeckShareList(const Command_DeckShareList &cmd, ResponseContainer &rc);
|
||||||
|
|
|
||||||
|
|
@ -125,12 +125,15 @@ TEST(DeckSyncSidecarTest, ModeRoundTripsAndDefaultsToLinked)
|
||||||
ASSERT_TRUE(dir.isValid());
|
ASSERT_TRUE(dir.isValid());
|
||||||
DeckSyncSidecar sidecar(sidecarPath(dir));
|
DeckSyncSidecar sidecar(sidecarPath(dir));
|
||||||
EXPECT_EQ(sidecar.mode, DeckSyncSidecar::MODE_LINKED);
|
EXPECT_EQ(sidecar.mode, DeckSyncSidecar::MODE_LINKED);
|
||||||
|
EXPECT_EQ(sidecar.lastSyncSecs, 0);
|
||||||
sidecar.mode = DeckSyncSidecar::MODE_SEPARATE;
|
sidecar.mode = DeckSyncSidecar::MODE_SEPARATE;
|
||||||
|
sidecar.lastSyncSecs = 1791739200;
|
||||||
EXPECT_TRUE(sidecar.save());
|
EXPECT_TRUE(sidecar.save());
|
||||||
|
|
||||||
DeckSyncSidecar reloaded(sidecarPath(dir));
|
DeckSyncSidecar reloaded(sidecarPath(dir));
|
||||||
EXPECT_TRUE(reloaded.load());
|
EXPECT_TRUE(reloaded.load());
|
||||||
EXPECT_EQ(reloaded.mode, DeckSyncSidecar::MODE_SEPARATE);
|
EXPECT_EQ(reloaded.mode, DeckSyncSidecar::MODE_SEPARATE);
|
||||||
|
EXPECT_EQ(reloaded.lastSyncSecs, 1791739200);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DeckSyncSidecarTest, PathForDeckPathBuildsSidecarPath)
|
TEST(DeckSyncSidecarTest, PathForDeckPathBuildsSidecarPath)
|
||||||
|
|
|
||||||
|
|
@ -606,6 +606,33 @@ TEST_F(SettingsDefaultsTest, DeckSync_ConflictDefault_SetAndGet)
|
||||||
ASSERT_EQ(s.getConflictDefault(), DeckSyncConflictAsk);
|
ASSERT_EQ(s.getConflictDefault(), DeckSyncConflictAsk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(SettingsDefaultsTest, DeckSync_AutoSync_Default)
|
||||||
|
{
|
||||||
|
DeckSyncSettings s(settingsPath, nullptr);
|
||||||
|
ASSERT_EQ(s.getAutoSync(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SettingsDefaultsTest, DeckSync_AutoSync_SetAndGet)
|
||||||
|
{
|
||||||
|
DeckSyncSettings s(settingsPath, nullptr);
|
||||||
|
s.setAutoSync(false);
|
||||||
|
ASSERT_EQ(s.getAutoSync(), false);
|
||||||
|
s.setAutoSync(true);
|
||||||
|
ASSERT_EQ(s.getAutoSync(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SettingsDefaultsTest, DeckSync_SyncIntervalMinutes_DefaultAndBounds)
|
||||||
|
{
|
||||||
|
DeckSyncSettings s(settingsPath, nullptr);
|
||||||
|
ASSERT_EQ(s.getSyncIntervalMinutes(), 5);
|
||||||
|
s.setSyncIntervalMinutes(15);
|
||||||
|
ASSERT_EQ(s.getSyncIntervalMinutes(), 15);
|
||||||
|
s.setSyncIntervalMinutes(0);
|
||||||
|
ASSERT_EQ(s.getSyncIntervalMinutes(), 1);
|
||||||
|
s.setSyncIntervalMinutes(600);
|
||||||
|
ASSERT_EQ(s.getSyncIntervalMinutes(), 60);
|
||||||
|
}
|
||||||
|
|
||||||
// --- NetworkSettings ---
|
// --- NetworkSettings ---
|
||||||
|
|
||||||
TEST_F(SettingsDefaultsTest, Network_ClientID_Default)
|
TEST_F(SettingsDefaultsTest, Network_ClientID_Default)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue