mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-11 12:33:25 -08:00
Merge d0cd13fce7 into b26ec833cb
This commit is contained in:
commit
10f19040c7
9 changed files with 375 additions and 57 deletions
|
|
@ -3,15 +3,19 @@
|
||||||
#include "../deck_loader/deck_loader.h"
|
#include "../deck_loader/deck_loader.h"
|
||||||
#include "deck_sync_sidecar.h"
|
#include "deck_sync_sidecar.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QSaveFile>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
#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>
|
||||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_deck_del.pb.h>
|
||||||
#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>
|
||||||
|
|
@ -24,6 +28,8 @@
|
||||||
#include <libcockatrice/protocol/pending_command.h>
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
#include <libcockatrice/utility/string_limits.h>
|
#include <libcockatrice/utility/string_limits.h>
|
||||||
|
|
||||||
|
inline Q_LOGGING_CATEGORY(DeckSyncLog, "deck_sync");
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr const char *SYNC_META_DIR = ".sync";
|
constexpr const char *SYNC_META_DIR = ".sync";
|
||||||
|
|
@ -112,10 +118,13 @@ void DeckSyncManager::syncNow()
|
||||||
syncing = true;
|
syncing = true;
|
||||||
uploadedCount = 0;
|
uploadedCount = 0;
|
||||||
downloadedCount = 0;
|
downloadedCount = 0;
|
||||||
|
removedCount = 0;
|
||||||
unchangedCount = 0;
|
unchangedCount = 0;
|
||||||
skippedCount = 0;
|
skippedCount = 0;
|
||||||
|
syncNote.clear();
|
||||||
pendingUploads.clear();
|
pendingUploads.clear();
|
||||||
pendingDownloads.clear();
|
pendingDownloads.clear();
|
||||||
|
pendingDeletes.clear();
|
||||||
plannedLocalPaths.clear();
|
plannedLocalPaths.clear();
|
||||||
ensuredDirs.clear();
|
ensuredDirs.clear();
|
||||||
ensuredDirs.insert(QString());
|
ensuredDirs.insert(QString());
|
||||||
|
|
@ -208,6 +217,22 @@ void DeckSyncManager::onListFinished(const Response &response, const CommandCont
|
||||||
DeckSyncSidecar sidecar(sidecarPath);
|
DeckSyncSidecar sidecar(sidecarPath);
|
||||||
sidecar.load();
|
sidecar.load();
|
||||||
const qint64 nowSecs = QDateTime::currentDateTime().toSecsSinceEpoch();
|
const qint64 nowSecs = QDateTime::currentDateTime().toSecsSinceEpoch();
|
||||||
|
// A listing that comes back empty while the sidecar still remembers rows that
|
||||||
|
// were uploaded means the server lost its deck data (a purge, or an account
|
||||||
|
// that was re-created), not that every deck was just deleted. Obeying that
|
||||||
|
// listing would remove the whole collection, so removals are held back.
|
||||||
|
bool sidecarHasRemoteRows = false;
|
||||||
|
// The records as they were before this run starts mutating them; only used to
|
||||||
|
// judge whether an empty listing is credible.
|
||||||
|
const QMap<QString, DeckSyncRecord> sidecarAtStart = sidecar.records();
|
||||||
|
for (auto rit = sidecarAtStart.begin(); rit != sidecarAtStart.end(); ++rit) {
|
||||||
|
if (rit.value().remoteId != -1) {
|
||||||
|
sidecarHasRemoteRows = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bool remoteStateLost = serverFiles.isEmpty() && sidecarHasRemoteRows;
|
||||||
|
bool heldBackRemovals = false;
|
||||||
// Reuse the sidecar's guid so the server keeps recognizing the row, and mint
|
// Reuse the sidecar's guid so the server keeps recognizing the row, and mint
|
||||||
// one only for a deck that has never been synced.
|
// one only for a deck that has never been synced.
|
||||||
auto guidFor = [&sidecar](const QString &relativePath) {
|
auto guidFor = [&sidecar](const QString &relativePath) {
|
||||||
|
|
@ -275,8 +300,36 @@ void DeckSyncManager::onListFinished(const Response &response, const CommandCont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverId == -1) {
|
if (serverId == -1) {
|
||||||
|
const DeckSyncRecord sidecarRecord = sidecar.get(it.key());
|
||||||
|
// The sidecar knows a server row that the listing no longer has: it was
|
||||||
|
// deleted elsewhere. Only mirror that delete when the local file is
|
||||||
|
// still exactly what was synced - an edit made after the remote delete
|
||||||
|
// is newer than the delete and gets uploaded again.
|
||||||
|
const bool knownToServer = sidecarRecord.remoteId != -1 || !sidecarRecord.guid.isEmpty();
|
||||||
|
const bool unchangedSinceSync =
|
||||||
|
!sidecarRecord.baseHash.isEmpty() && it.value().hash == sidecarRecord.baseHash;
|
||||||
|
if (knownToServer && unchangedSinceSync) {
|
||||||
|
if (remoteStateLost) {
|
||||||
|
// The listing is empty although this deck was uploaded, so the
|
||||||
|
// row is not known to be deleted, only missing. Keep the file
|
||||||
|
// and the record; re-uploading it blind could duplicate the
|
||||||
|
// deck once the real listing returns.
|
||||||
|
heldBackRemovals = true;
|
||||||
|
++skippedCount;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString absolutePath = deckPath + "/" + it.key();
|
||||||
|
const QString backupPath =
|
||||||
|
absolutePath + BACKUP_SUFFIX + QString::number(QDateTime::currentDateTime().toSecsSinceEpoch());
|
||||||
|
if (!QFile::copy(absolutePath, backupPath) || !QFile::remove(absolutePath)) {
|
||||||
|
++skippedCount;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sidecar.remove(it.key());
|
||||||
|
++removedCount;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const QString serverPath = serverPathFor(it.key());
|
const QString serverPath = serverPathFor(it.key());
|
||||||
pendingUploads.append(UploadOp{.relativePath = it.key(),
|
pendingUploads.append(UploadOp{.relativePath = it.key(),
|
||||||
.serverPath = serverPath,
|
.serverPath = serverPath,
|
||||||
|
|
@ -298,6 +351,7 @@ void DeckSyncManager::onListFinished(const Response &response, const CommandCont
|
||||||
record.lastSyncSecs = nowSecs;
|
record.lastSyncSecs = nowSecs;
|
||||||
record.deleted = false;
|
record.deleted = false;
|
||||||
sidecar.set(it.key(), record);
|
sidecar.set(it.key(), record);
|
||||||
|
snapshotLocalFile(it.key(), record.guid);
|
||||||
++unchangedCount;
|
++unchangedCount;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -337,6 +391,39 @@ void DeckSyncManager::onListFinished(const Response &response, const CommandCont
|
||||||
.lastModifiedSecs = it.value().lastModifiedSecs});
|
.lastModifiedSecs = it.value().lastModifiedSecs});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Never uploaded, so there is no server row to remove.
|
||||||
|
sidecar.remove(it.key());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (matchedServerIds.contains(it.value().remoteId)) {
|
||||||
|
// Another local file (a rename or a copy) now owns this row: drop the
|
||||||
|
// stale record, but never tombstone the row it took over.
|
||||||
|
sidecar.remove(it.key());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (remoteStateLost) {
|
||||||
|
// The whole listing is missing, not this one deck: pushing the delete
|
||||||
|
// would purge rows that are probably still there.
|
||||||
|
heldBackRemovals = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pendingDeletes.append(it.key());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heldBackRemovals) {
|
||||||
|
syncNote = tr("The server listed no decks although this collection was synced before. "
|
||||||
|
"No deletions were applied this run; the decks were kept.");
|
||||||
|
}
|
||||||
|
|
||||||
sidecar.save();
|
sidecar.save();
|
||||||
processNext();
|
processNext();
|
||||||
}
|
}
|
||||||
|
|
@ -352,7 +439,12 @@ void DeckSyncManager::processNext()
|
||||||
if (!syncing) {
|
if (!syncing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!pendingUploads.isEmpty()) {
|
if (!pendingDeletes.isEmpty()) {
|
||||||
|
const QString relativePath = pendingDeletes.first();
|
||||||
|
DeckSyncSidecar sidecar(sidecarPath);
|
||||||
|
sidecar.load();
|
||||||
|
sendDelete(relativePath, sidecar.get(relativePath).remoteId);
|
||||||
|
} else if (!pendingUploads.isEmpty()) {
|
||||||
processNextUpload();
|
processNextUpload();
|
||||||
} else if (!pendingDownloads.isEmpty()) {
|
} else if (!pendingDownloads.isEmpty()) {
|
||||||
sendDownload(pendingDownloads.first());
|
sendDownload(pendingDownloads.first());
|
||||||
|
|
@ -453,7 +545,12 @@ void DeckSyncManager::sendUpload(const UploadOp &op)
|
||||||
cmd.set_color_identity(colorIdentityProvider(loaded->deckList).toStdString());
|
cmd.set_color_identity(colorIdentityProvider(loaded->deckList).toStdString());
|
||||||
}
|
}
|
||||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
||||||
pend->setExtraData(op.relativePath);
|
QVariantMap context;
|
||||||
|
context.insert("path", op.relativePath);
|
||||||
|
context.insert("serverPath", op.serverPath);
|
||||||
|
context.insert("deckId", op.deckId);
|
||||||
|
context.insert("guid", op.guid);
|
||||||
|
pend->setExtraData(context);
|
||||||
connect(pend, &PendingCommand::finished, this, &DeckSyncManager::onUploadFinished);
|
connect(pend, &PendingCommand::finished, this, &DeckSyncManager::onUploadFinished);
|
||||||
client->sendCommand(pend);
|
client->sendCommand(pend);
|
||||||
}
|
}
|
||||||
|
|
@ -469,8 +566,22 @@ void DeckSyncManager::onUploadFinished(const Response &response,
|
||||||
failSync(tr("Server connection lost."));
|
failSync(tr("Server connection lost."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QString relativePath = extraData.toString();
|
const QVariantMap context = extraData.toMap();
|
||||||
|
const QString relativePath = context.value("path").toString();
|
||||||
|
const int deckId = context.value("deckId", -1).toInt();
|
||||||
if (response.response_code() != Response::RespOk) {
|
if (response.response_code() != Response::RespOk) {
|
||||||
|
if (response.response_code() == Response::RespNameNotFound && deckId != -1) {
|
||||||
|
// The server row is gone (purged tombstone or removed elsewhere):
|
||||||
|
// upload the deck as new instead of updating a dead id.
|
||||||
|
const UploadOp retry{.relativePath = relativePath,
|
||||||
|
.serverPath = context.value("serverPath").toString(),
|
||||||
|
.serverDir = parentDir(context.value("serverPath").toString()),
|
||||||
|
.deckId = -1,
|
||||||
|
.guid = context.value("guid").toString()};
|
||||||
|
pendingUploads.prepend(retry);
|
||||||
|
processNext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
++skippedCount;
|
++skippedCount;
|
||||||
processNext();
|
processNext();
|
||||||
return;
|
return;
|
||||||
|
|
@ -501,6 +612,7 @@ void DeckSyncManager::onUploadFinished(const Response &response,
|
||||||
record.deleted = false;
|
record.deleted = false;
|
||||||
sidecar.set(relativePath, record);
|
sidecar.set(relativePath, record);
|
||||||
sidecar.save();
|
sidecar.save();
|
||||||
|
snapshotLocalFile(relativePath, record.guid);
|
||||||
++uploadedCount;
|
++uploadedCount;
|
||||||
processNext();
|
processNext();
|
||||||
}
|
}
|
||||||
|
|
@ -615,10 +727,80 @@ void DeckSyncManager::onDownloadFinished(const Response &response,
|
||||||
record.deleted = false;
|
record.deleted = false;
|
||||||
sidecar.set(relativePath, record);
|
sidecar.set(relativePath, record);
|
||||||
sidecar.save();
|
sidecar.save();
|
||||||
|
snapshotLocalFile(relativePath, record.guid);
|
||||||
++downloadedCount;
|
++downloadedCount;
|
||||||
processNext();
|
processNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeckSyncManager::sendDelete(const QString &relativePath, int deckId)
|
||||||
|
{
|
||||||
|
emit statusMessage(tr("Deleting %1 from server...").arg(relativePath));
|
||||||
|
Command_DeckDel cmd;
|
||||||
|
cmd.set_deck_id(deckId);
|
||||||
|
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
||||||
|
pend->setExtraData(relativePath);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &DeckSyncManager::onDeleteFinished);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckSyncManager::onDeleteFinished(const Response &response,
|
||||||
|
const CommandContainer & /*commandContainer*/,
|
||||||
|
const QVariant &extraData)
|
||||||
|
{
|
||||||
|
if (!syncing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString relativePath = extraData.toString();
|
||||||
|
for (int i = 0; i < pendingDeletes.size(); ++i) {
|
||||||
|
if (pendingDeletes.at(i) == relativePath) {
|
||||||
|
pendingDeletes.removeAt(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Both outcomes converge the record: pushed deletes and server-side
|
||||||
|
// deletes (already tombstoned, answering NameNotFound) are done.
|
||||||
|
if (response.response_code() == Response::RespOk || response.response_code() == Response::RespNameNotFound) {
|
||||||
|
DeckSyncSidecar sidecar(sidecarPath);
|
||||||
|
sidecar.load();
|
||||||
|
const int deletedDeckId = sidecar.get(relativePath).remoteId;
|
||||||
|
sidecar.remove(relativePath);
|
||||||
|
sidecar.save();
|
||||||
|
if (deletedDeckId != -1) {
|
||||||
|
// The unmatched-server-rows pass queued a download for this row before
|
||||||
|
// the delete ran. The delete won, so drop the download instead of
|
||||||
|
// letting it fail with NameNotFound and count as a skipped deck. A
|
||||||
|
// failed delete leaves the download in place as the fallback.
|
||||||
|
pendingDownloads.removeIf([deletedDeckId](const DownloadOp &op) { return op.deckId == deletedDeckId; });
|
||||||
|
}
|
||||||
|
++removedCount;
|
||||||
|
} else {
|
||||||
|
++skippedCount;
|
||||||
|
}
|
||||||
|
processNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckSyncManager::snapshotLocalFile(const QString &relativePath, const QString &guid)
|
||||||
|
{
|
||||||
|
if (guid.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QFile source(deckPath + "/" + relativePath);
|
||||||
|
if (!source.open(QIODevice::ReadOnly)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QDir().mkpath(deckPath + "/" + SYNC_META_DIR + "/bases");
|
||||||
|
// QSaveFile: a crash or a full disk must not leave a truncated base behind,
|
||||||
|
// because DeckDiff::merge() against a partial base re-adds or drops cards.
|
||||||
|
QSaveFile snapshot(deckPath + "/" + SYNC_META_DIR + "/bases/" + guid + ".cod");
|
||||||
|
if (!snapshot.open(QIODevice::WriteOnly)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
snapshot.write(source.readAll());
|
||||||
|
if (!snapshot.commit()) {
|
||||||
|
qCWarning(DeckSyncLog) << "DeckSync: could not write the base snapshot for" << relativePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DeckSyncManager::failSync(const QString &error)
|
void DeckSyncManager::failSync(const QString &error)
|
||||||
{
|
{
|
||||||
syncing = false;
|
syncing = false;
|
||||||
|
|
@ -629,11 +811,16 @@ void DeckSyncManager::finishSync()
|
||||||
{
|
{
|
||||||
syncing = false;
|
syncing = false;
|
||||||
const SyncSummary summary = currentSummary();
|
const SyncSummary summary = currentSummary();
|
||||||
emit statusMessage(tr("Sync complete: %1 uploaded, %2 downloaded, %3 unchanged, %4 skipped.")
|
QString message = tr("Sync complete: %1 uploaded, %2 downloaded, %3 removed, %4 unchanged, %5 skipped.")
|
||||||
.arg(summary.uploaded)
|
.arg(summary.uploaded)
|
||||||
.arg(summary.downloaded)
|
.arg(summary.downloaded)
|
||||||
.arg(summary.unchanged)
|
.arg(summary.removed)
|
||||||
.arg(summary.skipped));
|
.arg(summary.unchanged)
|
||||||
|
.arg(summary.skipped);
|
||||||
|
if (!summary.note.isEmpty()) {
|
||||||
|
message += QStringLiteral(" ") + summary.note;
|
||||||
|
}
|
||||||
|
emit statusMessage(message);
|
||||||
emit syncFinished(summary, QString());
|
emit syncFinished(summary, QString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -642,7 +829,9 @@ DeckSyncManager::SyncSummary DeckSyncManager::currentSummary() const
|
||||||
return SyncSummary{
|
return SyncSummary{
|
||||||
.uploaded = uploadedCount,
|
.uploaded = uploadedCount,
|
||||||
.downloaded = downloadedCount,
|
.downloaded = downloadedCount,
|
||||||
|
.removed = removedCount,
|
||||||
.unchanged = unchangedCount,
|
.unchanged = unchangedCount,
|
||||||
.skipped = skippedCount,
|
.skipped = skippedCount,
|
||||||
|
.note = syncNote,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,13 @@ class ServerInfo_DeckStorage_Folder;
|
||||||
*
|
*
|
||||||
* Ownership: fire-and-forget per run, driven by PendingCommand round trips.
|
* Ownership: fire-and-forget per run, driven by PendingCommand round trips.
|
||||||
* No watcher, no auto triggers, no conflict UI — those arrive in later layers.
|
* No watcher, no auto triggers, no conflict UI — those arrive in later layers.
|
||||||
|
*
|
||||||
|
* Deletes are tombstones, never silent: a locally deleted deck is pushed with
|
||||||
|
* Command_DeckDel, a server-side delete is applied locally only after copying
|
||||||
|
* the file to a .sync-backup, and resurrection reuses the stored deck id. A
|
||||||
|
* listing that comes back empty while the sidecar still remembers uploaded rows
|
||||||
|
* is treated as lost server state rather than a collection-wide delete, so that
|
||||||
|
* run applies no removals and says so in its summary.
|
||||||
*/
|
*/
|
||||||
class DeckSyncManager : public QObject
|
class DeckSyncManager : public QObject
|
||||||
{
|
{
|
||||||
|
|
@ -51,8 +58,10 @@ public:
|
||||||
{
|
{
|
||||||
int uploaded = 0;
|
int uploaded = 0;
|
||||||
int downloaded = 0;
|
int downloaded = 0;
|
||||||
|
int removed = 0; ///< Decks deleted on the server, or locally to mirror a server delete.
|
||||||
int unchanged = 0;
|
int unchanged = 0;
|
||||||
int skipped = 0;
|
int skipped = 0;
|
||||||
|
QString note; ///< Non-error caveat about this run, empty when there is none.
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit DeckSyncManager(QObject *parent = nullptr);
|
explicit DeckSyncManager(QObject *parent = nullptr);
|
||||||
|
|
@ -78,6 +87,8 @@ private slots:
|
||||||
onUploadFinished(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData);
|
onUploadFinished(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData);
|
||||||
void
|
void
|
||||||
onDownloadFinished(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData);
|
onDownloadFinished(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData);
|
||||||
|
void
|
||||||
|
onDeleteFinished(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct ServerFile
|
struct ServerFile
|
||||||
|
|
@ -125,6 +136,8 @@ private:
|
||||||
void sendMkDirFor(const QString &dir);
|
void sendMkDirFor(const QString &dir);
|
||||||
void sendUpload(const UploadOp &op);
|
void sendUpload(const UploadOp &op);
|
||||||
void sendDownload(const DownloadOp &op);
|
void sendDownload(const DownloadOp &op);
|
||||||
|
void sendDelete(const QString &relativePath, int deckId);
|
||||||
|
void snapshotLocalFile(const QString &relativePath, const QString &guid);
|
||||||
static QString parentDir(const QString &path);
|
static QString parentDir(const QString &path);
|
||||||
QString uniqueLocalPath(const QString &relativePath) const;
|
QString uniqueLocalPath(const QString &relativePath) const;
|
||||||
|
|
||||||
|
|
@ -140,8 +153,11 @@ private:
|
||||||
QSet<QString> plannedLocalPaths; ///< Download targets reserved during this run, not yet on disk.
|
QSet<QString> plannedLocalPaths; ///< Download targets reserved during this run, not yet on disk.
|
||||||
QList<UploadOp> pendingUploads;
|
QList<UploadOp> pendingUploads;
|
||||||
QList<DownloadOp> pendingDownloads;
|
QList<DownloadOp> pendingDownloads;
|
||||||
|
QList<QString> pendingDeletes;
|
||||||
|
QString syncNote;
|
||||||
int uploadedCount = 0;
|
int uploadedCount = 0;
|
||||||
int downloadedCount = 0;
|
int downloadedCount = 0;
|
||||||
|
int removedCount = 0;
|
||||||
int unchangedCount = 0;
|
int unchangedCount = 0;
|
||||||
int skippedCount = 0;
|
int skippedCount = 0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -593,13 +593,19 @@ void TabDeckStorage::renderSyncStatus()
|
||||||
case SyncStatusType::Failed:
|
case SyncStatusType::Failed:
|
||||||
syncStatusLabel->setText(tr("Sync failed: %1").arg(syncStatusDetail));
|
syncStatusLabel->setText(tr("Sync failed: %1").arg(syncStatusDetail));
|
||||||
break;
|
break;
|
||||||
case SyncStatusType::Finished:
|
case SyncStatusType::Finished: {
|
||||||
syncStatusLabel->setText(tr("Sync complete: %1 uploaded, %2 downloaded, %3 unchanged, %4 skipped.")
|
QString text = tr("Sync complete: %1 uploaded, %2 downloaded, %3 removed, %4 unchanged, %5 skipped.")
|
||||||
.arg(lastSyncSummary.uploaded)
|
.arg(lastSyncSummary.uploaded)
|
||||||
.arg(lastSyncSummary.downloaded)
|
.arg(lastSyncSummary.downloaded)
|
||||||
.arg(lastSyncSummary.unchanged)
|
.arg(lastSyncSummary.removed)
|
||||||
.arg(lastSyncSummary.skipped));
|
.arg(lastSyncSummary.unchanged)
|
||||||
|
.arg(lastSyncSummary.skipped);
|
||||||
|
if (!lastSyncSummary.note.isEmpty()) {
|
||||||
|
text += QStringLiteral("\n") + lastSyncSummary.note;
|
||||||
|
}
|
||||||
|
syncStatusLabel->setText(text);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case SyncStatusType::Idle:
|
case SyncStatusType::Idle:
|
||||||
syncStatusLabel->setText(tr("Not synced yet"));
|
syncStatusLabel->setText(tr("Not synced yet"));
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -604,3 +604,16 @@ max_shares_per_day=50
|
||||||
; Minimum enforced: 1.
|
; Minimum enforced: 1.
|
||||||
; Default: 20
|
; Default: 20
|
||||||
max_versions=20
|
max_versions=20
|
||||||
|
|
||||||
|
; How many days a deleted deck stays restorable as a tombstone before the
|
||||||
|
; purge clock removes it (with its versions) for good. Re-uploading the
|
||||||
|
; same deck within this window revives the tombstone instead of creating a copy.
|
||||||
|
; A value of 0 purges tombstones on the next purge run.
|
||||||
|
; Default: 30
|
||||||
|
tombstone_days=30
|
||||||
|
|
||||||
|
; How often, in minutes, the tombstone purge runs. This is independent of
|
||||||
|
; [deck_share] cleanup_interval, so disabling share cleanup does not disable
|
||||||
|
; purging. A value of 0 disables purging entirely.
|
||||||
|
; Default: 60
|
||||||
|
purge_interval=60
|
||||||
|
|
|
||||||
|
|
@ -639,6 +639,16 @@ bool Servatrice::initServer()
|
||||||
connect(legacyBackupSweepClock, &QTimer::timeout, this, &Servatrice::sweepLegacyBackups);
|
connect(legacyBackupSweepClock, &QTimer::timeout, this, &Servatrice::sweepLegacyBackups);
|
||||||
legacyBackupSweepClock->start(kLegacyBackupSweepIntervalMs);
|
legacyBackupSweepClock->start(kLegacyBackupSweepIntervalMs);
|
||||||
|
|
||||||
|
// Tombstone purging runs on its own clock: turning share cleanup off must not
|
||||||
|
// silently disable it as well.
|
||||||
|
deckTombstoneClock = new QTimer(this);
|
||||||
|
connect(deckTombstoneClock, &QTimer::timeout, this, &Servatrice::purgeDeckTombstones);
|
||||||
|
const int deckSyncPurgeInterval = getDeckSyncPurgeInterval();
|
||||||
|
if (deckSyncPurgeInterval > 0) {
|
||||||
|
qDebug() << "Starting deck tombstone purge clock, interval" << deckSyncPurgeInterval << "ms";
|
||||||
|
deckTombstoneClock->start(deckSyncPurgeInterval);
|
||||||
|
}
|
||||||
|
|
||||||
// SOCKET SERVER
|
// SOCKET SERVER
|
||||||
if (getNumberOfTCPPools() > 0) {
|
if (getNumberOfTCPPools() > 0) {
|
||||||
gameServer =
|
gameServer =
|
||||||
|
|
@ -863,6 +873,11 @@ void Servatrice::cleanupExpiredDeckShares()
|
||||||
servatriceDatabaseInterface->cleanupExpiredDeckShares();
|
servatriceDatabaseInterface->cleanupExpiredDeckShares();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Servatrice::purgeDeckTombstones()
|
||||||
|
{
|
||||||
|
servatriceDatabaseInterface->purgeDeckTombstones(getDeckSyncTombstoneDays());
|
||||||
|
}
|
||||||
|
|
||||||
void Servatrice::statusUpdate()
|
void Servatrice::statusUpdate()
|
||||||
{
|
{
|
||||||
if (!servatriceDatabaseInterface->checkSql()) {
|
if (!servatriceDatabaseInterface->checkSql()) {
|
||||||
|
|
@ -1402,6 +1417,17 @@ int Servatrice::getDeckSyncMaxVersions() const
|
||||||
return qMax(1, settingsCache->value("deck_sync/max_versions", 20).toInt());
|
return qMax(1, settingsCache->value("deck_sync/max_versions", 20).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Servatrice::getDeckSyncTombstoneDays() const
|
||||||
|
{
|
||||||
|
return qMax(0, settingsCache->value("deck_sync/tombstone_days", 30).toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
int Servatrice::getDeckSyncPurgeInterval() const
|
||||||
|
{
|
||||||
|
// default: every 60 minutes, independent of deck_share/cleanup_interval
|
||||||
|
return qMax(0, settingsCache->value("deck_sync/purge_interval", 60).toInt()) * 60000;
|
||||||
|
}
|
||||||
|
|
||||||
int Servatrice::getNumberOfTCPPools() const
|
int Servatrice::getNumberOfTCPPools() const
|
||||||
{
|
{
|
||||||
return settingsCache->value("server/number_pools", 1).toInt();
|
return settingsCache->value("server/number_pools", 1).toInt();
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@ private slots:
|
||||||
void sweepLegacyBackups();
|
void sweepLegacyBackups();
|
||||||
void shutdownTimeout();
|
void shutdownTimeout();
|
||||||
void cleanupExpiredDeckShares();
|
void cleanupExpiredDeckShares();
|
||||||
|
void purgeDeckTombstones();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void doSendIslMessage(const IslMessage &msg, int _serverId) override;
|
void doSendIslMessage(const IslMessage &msg, int _serverId) override;
|
||||||
|
|
@ -181,6 +182,7 @@ private:
|
||||||
QTimer *pingClock, *statusUpdateClock;
|
QTimer *pingClock, *statusUpdateClock;
|
||||||
QTimer *deckShareCleanupClock;
|
QTimer *deckShareCleanupClock;
|
||||||
QTimer *legacyBackupSweepClock;
|
QTimer *legacyBackupSweepClock;
|
||||||
|
QTimer *deckTombstoneClock;
|
||||||
Servatrice_GameServer *gameServer;
|
Servatrice_GameServer *gameServer;
|
||||||
Servatrice_WebsocketGameServer *websocketGameServer;
|
Servatrice_WebsocketGameServer *websocketGameServer;
|
||||||
Servatrice_IslServer *islServer;
|
Servatrice_IslServer *islServer;
|
||||||
|
|
@ -321,6 +323,8 @@ public:
|
||||||
int getDeckShareMaxDecksPerShare() const;
|
int getDeckShareMaxDecksPerShare() const;
|
||||||
int getDeckShareMaxSharesPerDay() const;
|
int getDeckShareMaxSharesPerDay() const;
|
||||||
int getDeckSyncMaxVersions() const;
|
int getDeckSyncMaxVersions() const;
|
||||||
|
int getDeckSyncTombstoneDays() const;
|
||||||
|
int getDeckSyncPurgeInterval() const;
|
||||||
int getMaxUsersPerAddress() const;
|
int getMaxUsersPerAddress() const;
|
||||||
/// Raw security/trusted_sources list; match addresses against it with isTrustedSource().
|
/// Raw security/trusted_sources list; match addresses against it with isTrustedSource().
|
||||||
QString getTrustedSources() const;
|
QString getTrustedSources() const;
|
||||||
|
|
|
||||||
|
|
@ -1386,8 +1386,8 @@ DeckList *Servatrice_DatabaseInterface::getDeckFromDatabase(int deckId, int user
|
||||||
{
|
{
|
||||||
checkSql();
|
checkSql();
|
||||||
|
|
||||||
QSqlQuery *query =
|
QSqlQuery *query = prepareQuery(
|
||||||
prepareQuery("select content from {prefix}_decklist_files where id = :id and id_user = :id_user");
|
"select content from {prefix}_decklist_files where id = :id and id_user = :id_user and deleted = 0");
|
||||||
query->bindValue(":id", deckId);
|
query->bindValue(":id", deckId);
|
||||||
query->bindValue(":id_user", userId);
|
query->bindValue(":id_user", userId);
|
||||||
execSqlQuery(query);
|
execSqlQuery(query);
|
||||||
|
|
@ -1538,6 +1538,19 @@ void Servatrice_DatabaseInterface::cleanupExpiredDeckShares()
|
||||||
execSqlQuery(query);
|
execSqlQuery(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Servatrice_DatabaseInterface::purgeDeckTombstones(int olderThanDays)
|
||||||
|
{
|
||||||
|
checkSql();
|
||||||
|
|
||||||
|
// DB-native clock: tombstones are stamped with NOW() on this same
|
||||||
|
// database, so the grace window is immune to app/db clock skew.
|
||||||
|
// The null arm only catches manually flagged rows.
|
||||||
|
QSqlQuery *query = prepareQuery("delete from {prefix}_decklist_files where deleted = 1 and (last_modified < "
|
||||||
|
"DATE_SUB(NOW(), INTERVAL :days DAY) or last_modified is null)");
|
||||||
|
query->bindValue(":days", qMax(0, olderThanDays));
|
||||||
|
execSqlQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
bool Servatrice_DatabaseInterface::getDeckSharesForUser(int userId, QList<DeckShareSummaryRecord> &shares)
|
bool Servatrice_DatabaseInterface::getDeckSharesForUser(int userId, QList<DeckShareSummaryRecord> &shares)
|
||||||
{
|
{
|
||||||
checkSql();
|
checkSql();
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,11 @@ public:
|
||||||
bool getDeckShareItem(const QString &token, int itemId, QString &content);
|
bool getDeckShareItem(const QString &token, int itemId, QString &content);
|
||||||
/** @brief Deletes all expired share bundles (cascades to their items). */
|
/** @brief Deletes all expired share bundles (cascades to their items). */
|
||||||
void cleanupExpiredDeckShares();
|
void cleanupExpiredDeckShares();
|
||||||
|
/**
|
||||||
|
* @brief Hard-deletes tombstoned decks older than the grace period (cascades to versions).
|
||||||
|
* @param olderThanDays Tombstones older than this many days are purged; 0 purges all tombstones.
|
||||||
|
*/
|
||||||
|
void purgeDeckTombstones(int olderThanDays);
|
||||||
|
|
||||||
int getNextGameId() override;
|
int getNextGameId() override;
|
||||||
int getNextReplayId() override;
|
int getNextReplayId() override;
|
||||||
|
|
|
||||||
|
|
@ -714,7 +714,8 @@ bool AbstractServerSocketInterface::deckListHelper(int folderId,
|
||||||
|
|
||||||
query = sqlInterface->prepareQuery("select id, name, upload_time, is_public, banner_card_name, "
|
query = sqlInterface->prepareQuery("select id, name, upload_time, is_public, banner_card_name, "
|
||||||
"banner_card_provider, color_identity, tags, guid, hash, last_modified from "
|
"banner_card_provider, color_identity, tags, guid, hash, last_modified from "
|
||||||
"{prefix}_decklist_files where id_folder = :id_folder and id_user = :id_user");
|
"{prefix}_decklist_files where id_folder = :id_folder and id_user = :id_user "
|
||||||
|
"and deleted = 0");
|
||||||
query->bindValue(":id_folder", folderId);
|
query->bindValue(":id_folder", folderId);
|
||||||
query->bindValue(":id_user", userId);
|
query->bindValue(":id_user", userId);
|
||||||
if (!sqlInterface->execSqlQuery(query)) {
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
|
|
@ -859,8 +860,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckSetVisibility(const
|
||||||
sqlInterface->checkSql();
|
sqlInterface->checkSql();
|
||||||
|
|
||||||
if (cmd.has_deck_id()) {
|
if (cmd.has_deck_id()) {
|
||||||
QSqlQuery *query =
|
QSqlQuery *query = sqlInterface->prepareQuery(
|
||||||
sqlInterface->prepareQuery("select 1 from {prefix}_decklist_files where id = :id and id_user = :id_user");
|
"select 1 from {prefix}_decklist_files where id = :id and id_user = :id_user and deleted = 0");
|
||||||
query->bindValue(":id", cmd.deck_id());
|
query->bindValue(":id", cmd.deck_id());
|
||||||
query->bindValue(":id_user", userInfo->id());
|
query->bindValue(":id_user", userInfo->id());
|
||||||
sqlInterface->execSqlQuery(query);
|
sqlInterface->execSqlQuery(query);
|
||||||
|
|
@ -970,7 +971,8 @@ void AbstractServerSocketInterface::deckDelDirHelper(int basePathId)
|
||||||
deckDelDirHelper(query->value(0).toInt());
|
deckDelDirHelper(query->value(0).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
query = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id_folder = :id_folder");
|
query = sqlInterface->prepareQuery("update {prefix}_decklist_files set deleted = 1, last_modified = NOW() "
|
||||||
|
"where id_folder = :id_folder and deleted = 0");
|
||||||
query->bindValue(":id_folder", basePathId);
|
query->bindValue(":id_folder", basePathId);
|
||||||
sqlInterface->execSqlQuery(query);
|
sqlInterface->execSqlQuery(query);
|
||||||
|
|
||||||
|
|
@ -1021,18 +1023,17 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckDel(const Command_D
|
||||||
|
|
||||||
sqlInterface->checkSql();
|
sqlInterface->checkSql();
|
||||||
QSqlQuery *query =
|
QSqlQuery *query =
|
||||||
sqlInterface->prepareQuery("select id from {prefix}_decklist_files where id = :id and id_user = :id_user");
|
sqlInterface->prepareQuery("update {prefix}_decklist_files set deleted = 1, last_modified = NOW() where id = "
|
||||||
|
":id and id_user = :id_user and deleted = 0");
|
||||||
query->bindValue(":id", cmd.deck_id());
|
query->bindValue(":id", cmd.deck_id());
|
||||||
query->bindValue(":id_user", userInfo->id());
|
query->bindValue(":id_user", userInfo->id());
|
||||||
sqlInterface->execSqlQuery(query);
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
if (!query->next()) {
|
return Response::RespContextError;
|
||||||
|
}
|
||||||
|
if (query->numRowsAffected() == 0) {
|
||||||
return Response::RespNameNotFound;
|
return Response::RespNameNotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
query = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id");
|
|
||||||
query->bindValue(":id", cmd.deck_id());
|
|
||||||
sqlInterface->execSqlQuery(query);
|
|
||||||
|
|
||||||
return Response::RespOk;
|
return Response::RespOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1133,31 +1134,71 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman
|
||||||
return Response::RespNameNotFound;
|
return Response::RespNameNotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
QSqlQuery *query =
|
// A deck that was deleted elsewhere leaves a tombstone behind. Revive it
|
||||||
sqlInterface->prepareQuery("insert into {prefix}_decklist_files (id_folder, id_user, name, upload_time, "
|
// instead of inserting a second row with the same guid, which would split
|
||||||
"content, is_public, banner_card_name, banner_card_provider, color_identity, "
|
// the deck's version history and leave the tombstone to be purged.
|
||||||
"tags, guid, hash, last_modified) values(:id_folder, :id_user, :name, NOW(), "
|
int deckId = -1;
|
||||||
":content, :is_public, :banner_card_name, :banner_card_provider, "
|
bool inserted = false;
|
||||||
":color_identity, :tags, :guid, :hash, NOW())");
|
QSqlQuery *tombstoneQuery =
|
||||||
query->bindValue(":id_folder", folderId);
|
sqlInterface->prepareQuery("select id from {prefix}_decklist_files where id_user = :id_user and guid = "
|
||||||
query->bindValue(":id_user", userInfo->id());
|
":guid and deleted = 1 order by last_modified desc limit 1");
|
||||||
query->bindValue(":name", deckName);
|
tombstoneQuery->bindValue(":id_user", userInfo->id());
|
||||||
query->bindValue(":content", deckStr);
|
tombstoneQuery->bindValue(":guid", guid);
|
||||||
query->bindValue(":is_public", cmd.has_is_public() && cmd.is_public() ? 1 : 0);
|
if (!sqlInterface->execSqlQuery(tombstoneQuery)) {
|
||||||
query->bindValue(":banner_card_name", bannerCardName);
|
|
||||||
query->bindValue(":banner_card_provider", bannerCardProvider);
|
|
||||||
query->bindValue(":color_identity", colorIdentity);
|
|
||||||
query->bindValue(":tags", tagsJson);
|
|
||||||
query->bindValue(":guid", guid);
|
|
||||||
query->bindValue(":hash", deckHash);
|
|
||||||
if (!sqlInterface->execSqlQuery(query)) {
|
|
||||||
return Response::RespContextError;
|
return Response::RespContextError;
|
||||||
}
|
}
|
||||||
const int deckId = query->lastInsertId().toInt();
|
if (tombstoneQuery->next()) {
|
||||||
|
deckId = tombstoneQuery->value(0).toInt();
|
||||||
|
QSqlQuery *reviveQuery = sqlInterface->prepareQuery(
|
||||||
|
"update {prefix}_decklist_files set id_folder=:id_folder, name=:name, upload_time=NOW(), "
|
||||||
|
"content=:content, is_public=:is_public, banner_card_name=:banner_card_name, "
|
||||||
|
"banner_card_provider=:banner_card_provider, color_identity=:color_identity, tags=:tags, "
|
||||||
|
"hash=:hash, last_modified=NOW(), deleted=0 where id=:id and id_user=:id_user");
|
||||||
|
reviveQuery->bindValue(":id_folder", folderId);
|
||||||
|
reviveQuery->bindValue(":id_user", userInfo->id());
|
||||||
|
reviveQuery->bindValue(":id", deckId);
|
||||||
|
reviveQuery->bindValue(":name", deckName);
|
||||||
|
reviveQuery->bindValue(":content", deckStr);
|
||||||
|
reviveQuery->bindValue(":is_public", cmd.has_is_public() && cmd.is_public() ? 1 : 0);
|
||||||
|
reviveQuery->bindValue(":banner_card_name", bannerCardName);
|
||||||
|
reviveQuery->bindValue(":banner_card_provider", bannerCardProvider);
|
||||||
|
reviveQuery->bindValue(":color_identity", colorIdentity);
|
||||||
|
reviveQuery->bindValue(":tags", tagsJson);
|
||||||
|
reviveQuery->bindValue(":hash", deckHash);
|
||||||
|
if (!sqlInterface->execSqlQuery(reviveQuery)) {
|
||||||
|
return Response::RespContextError;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QSqlQuery *query = sqlInterface->prepareQuery(
|
||||||
|
"insert into {prefix}_decklist_files (id_folder, id_user, name, upload_time, "
|
||||||
|
"content, is_public, banner_card_name, banner_card_provider, color_identity, "
|
||||||
|
"tags, guid, hash, last_modified) values(:id_folder, :id_user, :name, NOW(), "
|
||||||
|
":content, :is_public, :banner_card_name, :banner_card_provider, "
|
||||||
|
":color_identity, :tags, :guid, :hash, NOW())");
|
||||||
|
query->bindValue(":id_folder", folderId);
|
||||||
|
query->bindValue(":id_user", userInfo->id());
|
||||||
|
query->bindValue(":name", deckName);
|
||||||
|
query->bindValue(":content", deckStr);
|
||||||
|
query->bindValue(":is_public", cmd.has_is_public() && cmd.is_public() ? 1 : 0);
|
||||||
|
query->bindValue(":banner_card_name", bannerCardName);
|
||||||
|
query->bindValue(":banner_card_provider", bannerCardProvider);
|
||||||
|
query->bindValue(":color_identity", colorIdentity);
|
||||||
|
query->bindValue(":tags", tagsJson);
|
||||||
|
query->bindValue(":guid", guid);
|
||||||
|
query->bindValue(":hash", deckHash);
|
||||||
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
|
return Response::RespContextError;
|
||||||
|
}
|
||||||
|
deckId = query->lastInsertId().toInt();
|
||||||
|
inserted = true;
|
||||||
|
}
|
||||||
if (!storeDeckVersion(deckId, deckStr, deckHash)) {
|
if (!storeDeckVersion(deckId, deckStr, deckHash)) {
|
||||||
QSqlQuery *cleanupQuery = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id");
|
if (inserted) {
|
||||||
cleanupQuery->bindValue(":id", deckId);
|
QSqlQuery *cleanupQuery =
|
||||||
sqlInterface->execSqlQuery(cleanupQuery);
|
sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id");
|
||||||
|
cleanupQuery->bindValue(":id", deckId);
|
||||||
|
sqlInterface->execSqlQuery(cleanupQuery);
|
||||||
|
}
|
||||||
return Response::RespContextError;
|
return Response::RespContextError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1167,9 +1208,12 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman
|
||||||
stampQuery->bindValue(":id", deckId);
|
stampQuery->bindValue(":id", deckId);
|
||||||
stampQuery->bindValue(":id_user", userInfo->id());
|
stampQuery->bindValue(":id_user", userInfo->id());
|
||||||
if (!sqlInterface->execSqlQuery(stampQuery) || !stampQuery->next()) {
|
if (!sqlInterface->execSqlQuery(stampQuery) || !stampQuery->next()) {
|
||||||
QSqlQuery *cleanupQuery = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id");
|
if (inserted) {
|
||||||
cleanupQuery->bindValue(":id", deckId);
|
QSqlQuery *cleanupQuery =
|
||||||
sqlInterface->execSqlQuery(cleanupQuery);
|
sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id");
|
||||||
|
cleanupQuery->bindValue(":id", deckId);
|
||||||
|
sqlInterface->execSqlQuery(cleanupQuery);
|
||||||
|
}
|
||||||
return Response::RespContextError;
|
return Response::RespContextError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1202,9 +1246,11 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman
|
||||||
}
|
}
|
||||||
const QString oldHash = currentQuery->value(1).toString();
|
const QString oldHash = currentQuery->value(1).toString();
|
||||||
|
|
||||||
QString updateQuery = "update {prefix}_decklist_files set name=:name, upload_time=NOW(), content=:content, "
|
QString updateQuery =
|
||||||
"banner_card_name=:banner_card_name, banner_card_provider=:banner_card_provider, "
|
"update {prefix}_decklist_files set name=:name, upload_time=NOW(), content=:content, "
|
||||||
"color_identity=:color_identity, tags=:tags, guid=:guid, hash=:hash, last_modified=NOW()";
|
"banner_card_name=:banner_card_name, banner_card_provider=:banner_card_provider, "
|
||||||
|
"color_identity=:color_identity, tags=:tags, guid=:guid, hash=:hash, last_modified=NOW(), "
|
||||||
|
"deleted=0";
|
||||||
if (cmd.has_is_public()) {
|
if (cmd.has_is_public()) {
|
||||||
updateQuery += ", is_public=:is_public";
|
updateQuery += ", is_public=:is_public";
|
||||||
}
|
}
|
||||||
|
|
@ -1406,7 +1452,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareCreate(const C
|
||||||
// issues its own query on the same cached statement set.
|
// issues its own query on the same cached statement set.
|
||||||
QSqlQuery *query =
|
QSqlQuery *query =
|
||||||
sqlInterface->prepareQuery("select id, color_identity from {prefix}_decklist_files where id_folder = "
|
sqlInterface->prepareQuery("select id, color_identity from {prefix}_decklist_files where id_folder = "
|
||||||
":id_folder and id_user = :id_user");
|
":id_folder and id_user = :id_user and deleted = 0");
|
||||||
query->bindValue(":id_folder", folderId);
|
query->bindValue(":id_folder", folderId);
|
||||||
query->bindValue(":id_user", userInfo->id());
|
query->bindValue(":id_user", userInfo->id());
|
||||||
if (!sqlInterface->execSqlQuery(query)) {
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue