Compare commits

..

4 commits

9 changed files with 68 additions and 198 deletions

View file

@ -274,9 +274,11 @@ void DeckSyncManager::syncNow()
localFiles.insert(relativePath, file);
}
// Always ask the server first: its server_time becomes the sync baseline,
// so uploads racing the DeckList response cannot fall outside the window.
sendSyncCheck(sidecar.lastSyncSecs);
if (!firstSyncRun && sidecar.lastSyncSecs > 0) {
sendSyncCheck(sidecar.lastSyncSecs);
return;
}
fetchFullList();
}
void DeckSyncManager::fetchFullList()
@ -307,9 +309,6 @@ void DeckSyncManager::onCheckFinished(const Response &response, const CommandCon
return;
}
const Response_DeckSyncStatus &status = response.GetExtension(Response_DeckSyncStatus::ext);
if (status.has_server_time()) {
pendingServerTime = static_cast<qint64>(status.server_time());
}
DeckSyncSidecar sidecar(sidecarPath);
if (!sidecar.load()) {
fetchFullList();
@ -356,9 +355,6 @@ void DeckSyncManager::collectServerFiles(const ServerInfo_DeckStorage_Folder &fo
{
for (const auto &item : folder.items()) {
const QString name = QString::fromStdString(item.name());
if (name.isEmpty() || name == "." || name == ".." || name.contains('/') || name.contains('\\')) {
continue;
}
if (item.has_folder()) {
const QString path = prefix.isEmpty() ? name : prefix + "/" + name;
dirs.insert(path);
@ -438,37 +434,8 @@ void DeckSyncManager::classify(const QMap<QString, ServerFile> &serverFiles)
sidecar.load();
const qint64 nowSecs = QDateTime::currentDateTime().toSecsSinceEpoch();
QMap<int, QString> serverPathById;
QMap<QString, QString> serverPathByGuid;
for (auto sit = serverFiles.begin(); sit != serverFiles.end(); ++sit) {
serverPathById.insert(sit.value().id, sit.key());
if (!sit.value().guid.isEmpty()) {
serverPathByGuid.insert(sit.value().guid, sit.key());
}
}
QSet<QString> matchedServerPaths;
for (auto it = localFiles.begin(); it != localFiles.end(); ++it) {
const DeckSyncRecord sidecarRecord = sidecar.get(it.key());
QString serverPath;
if (sidecarRecord.remoteId != -1 && serverPathById.contains(sidecarRecord.remoteId)) {
serverPath = serverPathById.value(sidecarRecord.remoteId);
} else if (!sidecarRecord.guid.isEmpty() && serverPathByGuid.contains(sidecarRecord.guid)) {
serverPath = serverPathByGuid.value(sidecarRecord.guid);
} else {
serverPath = serverPathFor(it.key());
if (!serverFiles.contains(serverPath)) {
UploadOp op;
op.relativePath = it.key();
op.serverPath = serverPath;
op.deckId = -1;
op.guid = sidecar.contains(it.key()) && !sidecar.get(it.key()).guid.isEmpty()
? sidecar.get(it.key()).guid
: QUuid::createUuid().toString(QUuid::WithoutBraces);
pendingUploads.append(op);
continue;
}
}
const QString serverPath = serverPathFor(it.key());
auto serverIt = serverFiles.find(serverPath);
if (serverIt == serverFiles.end()) {
const DeckSyncRecord known = sidecar.get(it.key());
@ -500,7 +467,6 @@ void DeckSyncManager::classify(const QMap<QString, ServerFile> &serverFiles)
pendingUploads.append(op);
continue;
}
matchedServerPaths.insert(serverIt.key());
if (it.value().hash == serverIt.value().hash) {
DeckSyncRecord record = sidecar.get(it.key());
if (record.guid.isEmpty()) {
@ -515,24 +481,6 @@ void DeckSyncManager::classify(const QMap<QString, ServerFile> &serverFiles)
++unchangedCount;
continue;
}
const DeckSyncRecord baseRecord = sidecar.get(it.key());
const bool localChanged = baseRecord.baseHash.isEmpty() || baseRecord.baseHash != it.value().hash;
const bool serverChanged = baseRecord.baseHash.isEmpty() || baseRecord.baseHash != serverIt.value().hash;
if (localChanged && !serverChanged) {
if (syncMode == DeckSyncMode::DownloadOnly) {
continue;
}
applyUpload(it.key(), serverPath, serverIt.value().id, serverIt.value().guid);
continue;
}
if (!localChanged && serverChanged) {
if (syncMode == DeckSyncMode::UploadOnly) {
continue;
}
applyDownload(it.key(), serverIt.value().id, serverIt.value().guid, serverIt.value().hash,
serverIt.value().lastModifiedSecs);
continue;
}
const qint64 localSecs = it.value().modified.toSecsSinceEpoch();
const int effective = sessionDefault >= 0 ? sessionDefault : conflictDefault;
// A server row without last_modified predates sync: the local file wins.
@ -575,39 +523,12 @@ void DeckSyncManager::classify(const QMap<QString, ServerFile> &serverFiles)
}
}
< if (syncMode != DeckSyncMode::UploadOnly) {
for (auto it = serverFiles.begin(); it != serverFiles.end(); ++it) {
if (matchedServerPaths.contains(it.key())) {
continue;
}
const QString wantedPath = it.key().endsWith(".cod", Qt::CaseInsensitive) ? it.key() : it.key() + ".cod";
if (localFiles.contains(wantedPath) || QFileInfo(deckPath + "/" + wantedPath).exists()) {
continue;
}
const QString relativePath = uniqueLocalPath(wantedPath);
if (relativePath.isEmpty()) {
++skippedCount;
continue;
}
DownloadOp op;
op.relativePath = relativePath;
op.deckId = it.value().id;
op.guid = it.value().guid;
op.hash = it.value().hash;
op.lastModifiedSecs = it.value().lastModifiedSecs;
pendingDownloads.append(op);
}
}
if (syncMode != DeckSyncMode::DownloadOnly) {
const QMap<QString, DeckSyncRecord> knownRecords = sidecar.records();
for (auto it = knownRecords.begin(); it != knownRecords.end(); ++it) {
if (localFiles.contains(it.key())) {
continue;
}
if (QFileInfo::exists(deckPath + "/" + it.key())) {
continue;
}
if (it.value().remoteId == -1) {
sidecar.remove(it.key());
continue;
@ -615,7 +536,6 @@ void DeckSyncManager::classify(const QMap<QString, ServerFile> &serverFiles)
pendingDeletes.append(it.key());
}
}
}
sidecar.save();
processNext();
@ -1091,13 +1011,7 @@ void DeckSyncManager::applyDownloadedDeck(const QString &relativePath,
return;
}
const QString cleanDeckPath = QDir::cleanPath(deckPath);
const QString absolutePath = QDir::cleanPath(deckPath + "/" + relativePath);
if (absolutePath != cleanDeckPath && !absolutePath.startsWith(cleanDeckPath + "/")) {
++skippedCount;
processNext();
return;
}
const QString absolutePath = deckPath + "/" + relativePath;
if (localFiles.contains(relativePath)) {
const QString backupPath =
absolutePath + BACKUP_SUFFIX + QString::number(QDateTime::currentDateTime().toSecsSinceEpoch());

View file

@ -5,7 +5,6 @@
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSaveFile>
namespace
{
@ -77,12 +76,12 @@ bool DeckSyncSidecar::save() const
root.insert("records", records);
QDir().mkpath(QFileInfo(filePath).absolutePath());
QSaveFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return false;
}
file.write(QJsonDocument(root).toJson(QJsonDocument::Compact));
return file.commit();
return true;
}
bool DeckSyncSidecar::contains(const QString &relativePath) const

View file

@ -261,13 +261,6 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
syncManager = new DeckSyncManager(this);
syncManager->setClient(client);
syncManager->setDeckPath(SettingsCache::instance().paths().getDeckPath());
connect(&SettingsCache::instance().paths(), &PathsSettings::deckPathChanged, this, [this] {
const QString deckPath = SettingsCache::instance().paths().getDeckPath();
localDirModel->setRootPath(deckPath);
localDirView->setRootIndex(localDirModel->index(localDirModel->rootPath(), 0));
syncManager->setDeckPath(deckPath);
});
syncManager->setColorIdentityProvider(
[](const DeckList &deck) { return getDeckColorIdentity(deck, CardDatabaseManager::query()); });
syncManager->setConflictDefault(SettingsCache::instance().deckSync().getConflictDefault());

View file

@ -13,36 +13,12 @@ namespace
{
using ZoneCardKey = QPair<QString, QString>;
struct ZoneCardInfo
{
int count = 0;
QString cardSetShortName;
QString cardSetNumber;
QString cardProviderId;
};
QMap<ZoneCardKey, ZoneCardInfo> zoneCardInfos(const DeckList &deck)
{
QMap<ZoneCardKey, ZoneCardInfo> infos;
deck.forEachCard([&infos](InnerDecklistNode *zone, DecklistCardNode *card) {
ZoneCardInfo &info = infos[{zone->getName(), card->getName()}];
if (info.count == 0) {
info.cardSetShortName = card->getCardSetShortName();
info.cardSetNumber = card->getCardCollectorNumber();
info.cardProviderId = card->getCardProviderId();
}
info.count += card->getNumber();
});
return infos;
}
QMap<ZoneCardKey, int> zoneCardCounts(const DeckList &deck)
{
QMap<ZoneCardKey, int> counts;
const QMap<ZoneCardKey, ZoneCardInfo> infos = zoneCardInfos(deck);
for (auto it = infos.begin(); it != infos.end(); ++it) {
counts.insert(it.key(), it.value().count);
}
deck.forEachCard([&counts](InnerDecklistNode *zone, DecklistCardNode *card) {
counts[{zone->getName(), card->getName()}] += card->getNumber();
});
return counts;
}
@ -58,25 +34,17 @@ bool changeLessThan(const DeckCardChange &a, const DeckCardChange &b)
DeckDiffResult DeckDiff::diff(const DeckList &oldDeck, const DeckList &newDeck)
{
const QMap<ZoneCardKey, int> oldCounts = zoneCardCounts(oldDeck);
const QMap<ZoneCardKey, ZoneCardInfo> newInfos = zoneCardInfos(newDeck);
const QMap<ZoneCardKey, int> newCounts = zoneCardCounts(newDeck);
QSet<ZoneCardKey> keys(oldCounts.keyBegin(), oldCounts.keyEnd());
keys.unite(QSet<ZoneCardKey>(newInfos.keyBegin(), newInfos.keyEnd()));
keys.unite(QSet<ZoneCardKey>(newCounts.keyBegin(), newCounts.keyEnd()));
DeckDiffResult result;
for (const ZoneCardKey &key : keys) {
const int oldCount = oldCounts.value(key, 0);
const ZoneCardInfo newInfo = newInfos.value(key, ZoneCardInfo());
if (oldCount != newInfo.count) {
DeckCardChange change;
change.zone = key.first;
change.cardName = key.second;
change.oldCount = oldCount;
change.newCount = newInfo.count;
change.cardSetShortName = newInfo.cardSetShortName;
change.cardSetNumber = newInfo.cardSetNumber;
change.cardProviderId = newInfo.cardProviderId;
result.changes.append(change);
const int newCount = newCounts.value(key, 0);
if (oldCount != newCount) {
result.changes.append({key.first, key.second, oldCount, newCount});
}
}
std::sort(result.changes.begin(), result.changes.end(), changeLessThan);
@ -101,41 +69,21 @@ DeckList
DeckDiff::merge(const DeckList &baseDeck, const DeckDiffResult &localChanges, const DeckDiffResult &remoteChanges)
{
QMap<ZoneCardKey, int> counts = zoneCardCounts(baseDeck);
QMap<ZoneCardKey, DeckCardChange> printings;
for (const DeckCardChange &change : localChanges.changes) {
counts[{change.zone, change.cardName}] = change.newCount;
printings[{change.zone, change.cardName}] = change;
}
for (const DeckCardChange &change : remoteChanges.changes) {
counts[{change.zone, change.cardName}] = change.newCount;
printings[{change.zone, change.cardName}] = change;
}
DeckList merged = baseDeck;
QMap<ZoneCardKey, QList<DecklistCardNode *>> nodesByKey;
merged.forEachCard([&nodesByKey](InnerDecklistNode *zone, DecklistCardNode *card) {
nodesByKey[{zone->getName(), card->getName()}].append(card);
});
for (auto it = counts.begin(); it != counts.end(); ++it) {
const ZoneCardKey &key = it.key();
const int target = qMax(0, it.value());
QList<DecklistCardNode *> nodes = nodesByKey.value(key);
if (target == 0) {
for (DecklistCardNode *node : nodes) {
merged.getTree()->deleteNode(node);
}
} else if (nodes.isEmpty()) {
const DeckCardChange printing = printings.value(key);
merged.getTree()->addCard(printing.cardName, target, printing.zone, -1, printing.cardSetShortName,
printing.cardSetNumber, printing.cardProviderId, true);
} else {
nodes.first()->setNumber(target);
for (int i = 1; i < nodes.size(); ++i) {
merged.getTree()->deleteNode(nodes.at(i));
}
DeckList merged(baseDeck.getMetadata(), DecklistNodeTree(), baseDeck.getSideboardPlans());
QList<ZoneCardKey> keys = counts.keys();
std::sort(keys.begin(), keys.end());
for (const ZoneCardKey &key : keys) {
const int count = qMax(0, counts.value(key));
for (int i = 0; i < count; ++i) {
merged.addCard(key.second, key.first);
}
}
merged.refreshDeckHash();
return merged;
}

View file

@ -14,13 +14,10 @@ class DeckList;
/** @brief A single card-count change within one zone. */
struct DeckCardChange
{
QString zone; ///< Zone name, e.g. "main".
QString cardName; ///< Card name.
int oldCount = 0; ///< Copies in the old deck.
int newCount = 0; ///< Copies in the new deck.
QString cardSetShortName; ///< Printing set carried from the new side, empty when unchanged.
QString cardSetNumber; ///< Printing collector number carried from the new side.
QString cardProviderId; ///< Printing provider id carried from the new side.
QString zone; ///< Zone name, e.g. "main".
QString cardName; ///< Card name.
int oldCount = 0; ///< Copies in the old deck.
int newCount = 0; ///< Copies in the new deck.
/** @brief Copies added (positive) or removed (negative). */
[[nodiscard]] int delta() const
{
@ -43,9 +40,9 @@ struct DeckDiffResult
* @class DeckDiff
* @brief Pure card-count diff and three-way merge for deck sync.
*
* Compares card names and copy counts per zone only; a merged deck keeps
* base printings for existing cards and carries the changed side's printing
* for newly added cards.
* Compares card names and copy counts per zone only; printing information
* (set, collector number, provider) is not tracked, so a merged deck keeps
* base printings with merged counts.
*/
class DeckDiff
{

View file

@ -69,7 +69,6 @@ QString PathsSettings::getRedirectCachePath() const
void PathsSettings::setDeckPath(const QString &_deckPath)
{
setValue(_deckPath, "decks");
emit deckPathChanged();
}
void PathsSettings::setFiltersPath(const QString &_filtersPath)

View file

@ -39,7 +39,6 @@ public:
void setSpoilerDatabasePath(const QString &_spoilerDatabasePath);
signals:
void deckPathChanged();
void cardDatabasePathChanged();
void picsPathChanged();
void themeChanged();

View file

@ -872,31 +872,51 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckSetVisibility(const
return Response::RespOk;
}
bool AbstractServerSocketInterface::syncCheckHelper(int userId, qint64 sinceSecs, Response_DeckSyncStatus *status)
bool AbstractServerSocketInterface::syncCheckHelper(int folderId,
const QString &prefix,
int userId,
qint64 sinceSecs,
Response_DeckSyncStatus *status)
{
// Single query over the user's files: folder rows are hard-deleted while
// their files linger as tombstones, so walking the live folder tree would
// miss deletes pointing at nonexistent folders. The client only checks
// changed_files_size(), so folder paths are not reconstructed here.
QSqlQuery *query = sqlInterface->prepareQuery("select id, guid, hash, last_modified, deleted from "
"{prefix}_decklist_files where id_user = :id_user "
"and (last_modified is null or last_modified >= "
"FROM_UNIXTIME(:since))");
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(1).toString().toStdString());
changed->set_hash(query->value(2).toString().toStdString());
const QDateTime lastModified = query->value(3).toDateTime();
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(4).toBool());
changed->set_deleted(query->value(5).toBool());
}
return true;
}
@ -911,7 +931,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckSyncCheck(const Com
sqlInterface->checkSql();
Response_DeckSyncStatus *re = new Response_DeckSyncStatus;
if (!syncCheckHelper(userInfo->id(), static_cast<qint64>(cmd.last_sync_time()), re)) {
if (!syncCheckHelper(0, QString(), userInfo->id(), static_cast<qint64>(cmd.last_sync_time()), re)) {
delete re;
return Response::RespContextError;
}

View file

@ -159,7 +159,8 @@ private:
bool storeDeckVersion(int deckId, const QString &content, const QString &hash);
DeckList *getDeckFromDatabase(int deckId);
Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc);
bool syncCheckHelper(int userId, qint64 sinceSecs, Response_DeckSyncStatus *status);
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 cmdDeckShareCreate(const Command_DeckShareCreate &cmd, ResponseContainer &rc);