mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
Merge de3468cc33 into 28706b5b8d
This commit is contained in:
commit
cd6f73888f
8 changed files with 286 additions and 46 deletions
|
|
@ -600,3 +600,16 @@ max_shares_per_day=50
|
|||
; Minimum enforced: 1.
|
||||
; Default: 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);
|
||||
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, SIGNAL(timeout()), this, SLOT(purgeDeckTombstones()));
|
||||
const int deckSyncPurgeInterval = getDeckSyncPurgeInterval();
|
||||
if (deckSyncPurgeInterval > 0) {
|
||||
qDebug() << "Starting deck tombstone purge clock, interval" << deckSyncPurgeInterval << "ms";
|
||||
deckTombstoneClock->start(deckSyncPurgeInterval);
|
||||
}
|
||||
|
||||
// SOCKET SERVER
|
||||
if (getNumberOfTCPPools() > 0) {
|
||||
gameServer =
|
||||
|
|
@ -863,6 +873,11 @@ void Servatrice::cleanupExpiredDeckShares()
|
|||
servatriceDatabaseInterface->cleanupExpiredDeckShares();
|
||||
}
|
||||
|
||||
void Servatrice::purgeDeckTombstones()
|
||||
{
|
||||
servatriceDatabaseInterface->purgeDeckTombstones(getDeckSyncTombstoneDays());
|
||||
}
|
||||
|
||||
void Servatrice::statusUpdate()
|
||||
{
|
||||
if (!servatriceDatabaseInterface->checkSql()) {
|
||||
|
|
@ -1402,6 +1417,17 @@ int Servatrice::getDeckSyncMaxVersions() const
|
|||
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
|
||||
{
|
||||
return settingsCache->value("server/number_pools", 1).toInt();
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ private slots:
|
|||
void sweepLegacyBackups();
|
||||
void shutdownTimeout();
|
||||
void cleanupExpiredDeckShares();
|
||||
void purgeDeckTombstones();
|
||||
|
||||
protected:
|
||||
void doSendIslMessage(const IslMessage &msg, int _serverId) override;
|
||||
|
|
@ -181,6 +182,7 @@ private:
|
|||
QTimer *pingClock, *statusUpdateClock;
|
||||
QTimer *deckShareCleanupClock;
|
||||
QTimer *legacyBackupSweepClock;
|
||||
QTimer *deckTombstoneClock;
|
||||
Servatrice_GameServer *gameServer;
|
||||
Servatrice_WebsocketGameServer *websocketGameServer;
|
||||
Servatrice_IslServer *islServer;
|
||||
|
|
@ -321,6 +323,8 @@ public:
|
|||
int getDeckShareMaxDecksPerShare() const;
|
||||
int getDeckShareMaxSharesPerDay() const;
|
||||
int getDeckSyncMaxVersions() const;
|
||||
int getDeckSyncTombstoneDays() const;
|
||||
int getDeckSyncPurgeInterval() const;
|
||||
int getMaxUsersPerAddress() const;
|
||||
/// Raw security/trusted_sources list; match addresses against it with isTrustedSource().
|
||||
QString getTrustedSources() const;
|
||||
|
|
|
|||
|
|
@ -1384,8 +1384,8 @@ DeckList *Servatrice_DatabaseInterface::getDeckFromDatabase(int deckId, int user
|
|||
{
|
||||
checkSql();
|
||||
|
||||
QSqlQuery *query =
|
||||
prepareQuery("select content from {prefix}_decklist_files where id = :id and id_user = :id_user");
|
||||
QSqlQuery *query = prepareQuery(
|
||||
"select content from {prefix}_decklist_files where id = :id and id_user = :id_user and deleted = 0");
|
||||
query->bindValue(":id", deckId);
|
||||
query->bindValue(":id_user", userId);
|
||||
execSqlQuery(query);
|
||||
|
|
@ -1536,6 +1536,19 @@ void Servatrice_DatabaseInterface::cleanupExpiredDeckShares()
|
|||
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)
|
||||
{
|
||||
checkSql();
|
||||
|
|
|
|||
|
|
@ -155,6 +155,11 @@ public:
|
|||
bool getDeckShareItem(const QString &token, int itemId, QString &content);
|
||||
/** @brief Deletes all expired share bundles (cascades to their items). */
|
||||
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 getNextReplayId() override;
|
||||
|
|
|
|||
|
|
@ -710,7 +710,8 @@ bool AbstractServerSocketInterface::deckListHelper(int folderId,
|
|||
|
||||
query = sqlInterface->prepareQuery("select id, name, upload_time, is_public, banner_card_name, "
|
||||
"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_user", userId);
|
||||
if (!sqlInterface->execSqlQuery(query)) {
|
||||
|
|
@ -855,8 +856,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckSetVisibility(const
|
|||
sqlInterface->checkSql();
|
||||
|
||||
if (cmd.has_deck_id()) {
|
||||
QSqlQuery *query =
|
||||
sqlInterface->prepareQuery("select 1 from {prefix}_decklist_files where id = :id and id_user = :id_user");
|
||||
QSqlQuery *query = sqlInterface->prepareQuery(
|
||||
"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_user", userInfo->id());
|
||||
sqlInterface->execSqlQuery(query);
|
||||
|
|
@ -966,7 +967,8 @@ void AbstractServerSocketInterface::deckDelDirHelper(int basePathId)
|
|||
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);
|
||||
sqlInterface->execSqlQuery(query);
|
||||
|
||||
|
|
@ -1017,18 +1019,17 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckDel(const Command_D
|
|||
|
||||
sqlInterface->checkSql();
|
||||
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_user", userInfo->id());
|
||||
sqlInterface->execSqlQuery(query);
|
||||
if (!query->next()) {
|
||||
if (!sqlInterface->execSqlQuery(query)) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
if (query->numRowsAffected() == 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1129,31 +1130,71 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman
|
|||
return Response::RespNameNotFound;
|
||||
}
|
||||
|
||||
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)) {
|
||||
// A deck that was deleted elsewhere leaves a tombstone behind. Revive it
|
||||
// instead of inserting a second row with the same guid, which would split
|
||||
// the deck's version history and leave the tombstone to be purged.
|
||||
int deckId = -1;
|
||||
bool inserted = false;
|
||||
QSqlQuery *tombstoneQuery =
|
||||
sqlInterface->prepareQuery("select id from {prefix}_decklist_files where id_user = :id_user and guid = "
|
||||
":guid and deleted = 1 order by last_modified desc limit 1");
|
||||
tombstoneQuery->bindValue(":id_user", userInfo->id());
|
||||
tombstoneQuery->bindValue(":guid", guid);
|
||||
if (!sqlInterface->execSqlQuery(tombstoneQuery)) {
|
||||
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)) {
|
||||
QSqlQuery *cleanupQuery = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id");
|
||||
cleanupQuery->bindValue(":id", deckId);
|
||||
sqlInterface->execSqlQuery(cleanupQuery);
|
||||
if (inserted) {
|
||||
QSqlQuery *cleanupQuery =
|
||||
sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id");
|
||||
cleanupQuery->bindValue(":id", deckId);
|
||||
sqlInterface->execSqlQuery(cleanupQuery);
|
||||
}
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -1163,9 +1204,12 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman
|
|||
stampQuery->bindValue(":id", deckId);
|
||||
stampQuery->bindValue(":id_user", userInfo->id());
|
||||
if (!sqlInterface->execSqlQuery(stampQuery) || !stampQuery->next()) {
|
||||
QSqlQuery *cleanupQuery = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id");
|
||||
cleanupQuery->bindValue(":id", deckId);
|
||||
sqlInterface->execSqlQuery(cleanupQuery);
|
||||
if (inserted) {
|
||||
QSqlQuery *cleanupQuery =
|
||||
sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id");
|
||||
cleanupQuery->bindValue(":id", deckId);
|
||||
sqlInterface->execSqlQuery(cleanupQuery);
|
||||
}
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
|
|
@ -1198,9 +1242,11 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman
|
|||
}
|
||||
const QString oldHash = currentQuery->value(1).toString();
|
||||
|
||||
QString updateQuery = "update {prefix}_decklist_files set name=:name, upload_time=NOW(), content=:content, "
|
||||
"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()";
|
||||
QString updateQuery =
|
||||
"update {prefix}_decklist_files set name=:name, upload_time=NOW(), content=:content, "
|
||||
"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()) {
|
||||
updateQuery += ", is_public=:is_public";
|
||||
}
|
||||
|
|
@ -1402,7 +1448,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareCreate(const C
|
|||
// issues its own query on the same cached statement set.
|
||||
QSqlQuery *query =
|
||||
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_user", userInfo->id());
|
||||
if (!sqlInterface->execSqlQuery(query)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue