mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 11:03:25 -08:00
Address review findings
This commit is contained in:
parent
f8c2e1105a
commit
26ffd1cfae
3 changed files with 77 additions and 37 deletions
|
|
@ -73,6 +73,38 @@ QString localPathFor(const QString &dir, const QString &name)
|
||||||
const QString path = segments.join('/');
|
const QString path = segments.join('/');
|
||||||
return path.endsWith(".cod", Qt::CaseInsensitive) ? path : path + QStringLiteral(".cod");
|
return path.endsWith(".cod", Qt::CaseInsensitive) ? path : path + QStringLiteral(".cod");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The sidecar stores the first-sync verdict so the choice outlives the run that
|
||||||
|
// asked for it. An absent or unknown value means the default two-way sync, which
|
||||||
|
// keeps profiles written before the verdict was persisted usable.
|
||||||
|
DeckSyncMode modeForSidecar(const QString &mode)
|
||||||
|
{
|
||||||
|
if (mode == DeckSyncSidecar::MODE_UPLOAD_ONLY) {
|
||||||
|
return DeckSyncMode::UploadOnly;
|
||||||
|
}
|
||||||
|
if (mode == DeckSyncSidecar::MODE_DOWNLOAD_ONLY) {
|
||||||
|
return DeckSyncMode::DownloadOnly;
|
||||||
|
}
|
||||||
|
if (mode == DeckSyncSidecar::MODE_SEPARATE) {
|
||||||
|
return DeckSyncMode::Separate;
|
||||||
|
}
|
||||||
|
return DeckSyncMode::TwoWay;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sidecarModeFor(DeckSyncMode mode)
|
||||||
|
{
|
||||||
|
switch (mode) {
|
||||||
|
case DeckSyncMode::UploadOnly:
|
||||||
|
return DeckSyncSidecar::MODE_UPLOAD_ONLY;
|
||||||
|
case DeckSyncMode::DownloadOnly:
|
||||||
|
return DeckSyncSidecar::MODE_DOWNLOAD_ONLY;
|
||||||
|
case DeckSyncMode::Separate:
|
||||||
|
return DeckSyncSidecar::MODE_SEPARATE;
|
||||||
|
case DeckSyncMode::TwoWay:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return DeckSyncSidecar::MODE_LINKED;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
DeckSyncManager::DeckSyncManager(QObject *parent) : QObject(parent)
|
DeckSyncManager::DeckSyncManager(QObject *parent) : QObject(parent)
|
||||||
|
|
@ -101,11 +133,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;
|
||||||
|
|
@ -144,7 +171,6 @@ void DeckSyncManager::syncNow()
|
||||||
ensuredDirs.insert(QString());
|
ensuredDirs.insert(QString());
|
||||||
localFiles.clear();
|
localFiles.clear();
|
||||||
sessionDefault = -1;
|
sessionDefault = -1;
|
||||||
syncMode = DeckSyncMode::TwoWay;
|
|
||||||
firstSyncRun = false;
|
firstSyncRun = false;
|
||||||
emit syncStarted();
|
emit syncStarted();
|
||||||
emit statusMessage(tr("Scanning local decks..."));
|
emit statusMessage(tr("Scanning local decks..."));
|
||||||
|
|
@ -155,7 +181,10 @@ void DeckSyncManager::syncNow()
|
||||||
failSync(tr("Could not read sync state."));
|
failSync(tr("Could not read sync state."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (sidecar.mode == DeckSyncSidecar::MODE_SEPARATE) {
|
// Apply the persisted first-sync verdict: the onboarding choice lasts beyond
|
||||||
|
// the run that asked for it, and a profile that never made one syncs both ways.
|
||||||
|
syncMode = modeForSidecar(sidecar.mode);
|
||||||
|
if (syncMode == DeckSyncMode::Separate) {
|
||||||
failSync(tr("Sync is disabled for this server."));
|
failSync(tr("Sync is disabled for this server."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -249,11 +278,16 @@ void DeckSyncManager::continueFirstSync(DeckSyncMode mode)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
firstSyncRun = false;
|
firstSyncRun = false;
|
||||||
if (mode == DeckSyncMode::Separate) {
|
|
||||||
DeckSyncSidecar sidecar(sidecarPath);
|
DeckSyncSidecar sidecar(sidecarPath);
|
||||||
sidecar.load();
|
if (!sidecar.load()) {
|
||||||
sidecar.mode = DeckSyncSidecar::MODE_SEPARATE;
|
failSync(tr("Could not read sync state."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Persist the verdict next to the separate marker so a later syncNow() uses
|
||||||
|
// the same direction instead of silently reverting to two-way.
|
||||||
|
sidecar.mode = sidecarModeFor(mode);
|
||||||
sidecar.save();
|
sidecar.save();
|
||||||
|
if (mode == DeckSyncMode::Separate) {
|
||||||
stashedServerFiles.clear();
|
stashedServerFiles.clear();
|
||||||
failSync(tr("Sync is disabled for this server."));
|
failSync(tr("Sync is disabled for this server."));
|
||||||
return;
|
return;
|
||||||
|
|
@ -444,6 +478,14 @@ void DeckSyncManager::classify(const QMap<int, ServerFile> &serverFiles)
|
||||||
applyDownload(it.key(), server.id, server.guid, server.hash, server.lastModifiedSecs);
|
applyDownload(it.key(), server.id, server.guid, server.hash, server.lastModifiedSecs);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Legacy row without a hash and an untouched local copy: upload once
|
||||||
|
// so the server stamps hash and last_modified.
|
||||||
|
if (!localChanged && !serverChanged) {
|
||||||
|
if (syncMode != DeckSyncMode::DownloadOnly) {
|
||||||
|
applyUpload(it.key(), server.path, server.id, server.guid);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const qint64 localSecs = it.value().modified.toSecsSinceEpoch();
|
const qint64 localSecs = it.value().modified.toSecsSinceEpoch();
|
||||||
const int effective = sessionDefault >= 0 ? sessionDefault : conflictDefault;
|
const int effective = sessionDefault >= 0 ? sessionDefault : conflictDefault;
|
||||||
// A server row without last_modified predates sync: the local file wins.
|
// A server row without last_modified predates sync: the local file wins.
|
||||||
|
|
@ -589,19 +631,9 @@ void DeckSyncManager::queueConflict(const QString &relativePath,
|
||||||
const QString &serverHash,
|
const QString &serverHash,
|
||||||
qint64 serverLastModifiedSecs)
|
qint64 serverLastModifiedSecs)
|
||||||
{
|
{
|
||||||
DeckSyncSidecar sidecar(sidecarPath);
|
// No base snapshot (the usual first two-way sync) is not a reason to pick a
|
||||||
sidecar.load();
|
// winner silently: the dialog handles hasBase == false by disabling merge,
|
||||||
const QString guid = !serverGuid.isEmpty() ? serverGuid : sidecar.get(relativePath).guid;
|
// and Keep both still works, so queue the conflict like any other.
|
||||||
if (readBaseSnapshot(guid).isEmpty()) {
|
|
||||||
emit statusMessage(tr("No base snapshot for %1, keeping the newer version.").arg(relativePath));
|
|
||||||
const qint64 localSecs = localFiles.value(relativePath).modified.toSecsSinceEpoch();
|
|
||||||
if (serverLastModifiedSecs == 0 || localSecs > serverLastModifiedSecs) {
|
|
||||||
applyUpload(relativePath, serverPath, deckId, serverGuid);
|
|
||||||
} else {
|
|
||||||
applyDownload(relativePath, deckId, serverGuid, serverHash, serverLastModifiedSecs);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ConflictDownload ctx{.serverPath = serverPath,
|
const ConflictDownload ctx{.serverPath = serverPath,
|
||||||
.deckId = deckId,
|
.deckId = deckId,
|
||||||
.guid = serverGuid,
|
.guid = serverGuid,
|
||||||
|
|
@ -662,7 +694,10 @@ void DeckSyncManager::resolveConflict(const DeckSyncResolution &resolution)
|
||||||
if (!syncing || !activeConflict.active) {
|
if (!syncing || !activeConflict.active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (resolution.rememberSession && (resolution.action == DeckSyncResolutionAction::KeepMine ||
|
// "Always use this rule" also settles the conflicts already queued for this
|
||||||
|
// run, not just the setting for future ones.
|
||||||
|
const bool remember = resolution.rememberSession || resolution.rememberAlways;
|
||||||
|
if (remember && (resolution.action == DeckSyncResolutionAction::KeepMine ||
|
||||||
resolution.action == DeckSyncResolutionAction::KeepServer)) {
|
resolution.action == DeckSyncResolutionAction::KeepServer)) {
|
||||||
sessionDefault = resolution.action == DeckSyncResolutionAction::KeepMine ? DeckSyncConflictKeepMine
|
sessionDefault = resolution.action == DeckSyncResolutionAction::KeepMine ? DeckSyncConflictKeepMine
|
||||||
: DeckSyncConflictKeepServer;
|
: DeckSyncConflictKeepServer;
|
||||||
|
|
|
||||||
|
|
@ -31,18 +31,19 @@ class ServerInfo_DeckStorage_Folder;
|
||||||
*
|
*
|
||||||
* Each syncNow() run loads the profile sidecar, scans the deck folder, fetches
|
* Each syncNow() run loads the profile sidecar, scans the deck folder, fetches
|
||||||
* the server tree and mirrors both sides: unknown local decks upload, unknown
|
* the server tree and mirrors both sides: unknown local decks upload, unknown
|
||||||
* server decks download, and decks changed on both sides resolve newer-wins
|
* server decks download, and a deck changed on both sides pauses for a verdict,
|
||||||
* (ties go to the server). A local file is matched to its server row by the
|
* so nothing is overwritten silently. A local file is matched to its server row
|
||||||
* sidecar's row id, then guid, then the server path, and finally by content, so
|
* by the sidecar's row id, then guid, then the server path, and finally by
|
||||||
* renaming either side does not duplicate a deck. Before a download overwrites a
|
* content, so renaming either side does not duplicate a deck. Before a download
|
||||||
* locally modified file, the local content is copied to a .sync-backup file next
|
* overwrites a locally modified file, the local content is copied to a
|
||||||
* to it.
|
* .sync-backup file next to it.
|
||||||
*
|
*
|
||||||
* Ownership: fire-and-forget per run, driven by PendingCommand round trips.
|
* Ownership: fire-and-forget per run, driven by PendingCommand round trips.
|
||||||
* Both-sides-changed decks pause the queue for a verdict via conflictRequest /
|
* Both-sides-changed decks pause the queue for a verdict via conflictRequest /
|
||||||
* resolveConflict (or the conflict default setting); the first sync of an
|
* resolveConflict (or the conflict default setting); the first sync of an
|
||||||
* unknown profile pauses for an onboarding verdict via firstSyncDetected /
|
* unknown profile pauses for an onboarding verdict via firstSyncDetected /
|
||||||
* continueFirstSync. No watcher and no auto triggers — those arrive later.
|
* continueFirstSync, and that verdict is stored in the sidecar so later runs keep
|
||||||
|
* the chosen direction. No watcher and no auto triggers — those arrive later.
|
||||||
*
|
*
|
||||||
* Deletes are tombstones, never silent: a locally deleted deck is pushed with
|
* Deletes are tombstones, never silent: a locally deleted deck is pushed with
|
||||||
* Command_DeckDel, a server-side delete is applied locally only after copying
|
* Command_DeckDel, a server-side delete is applied locally only after copying
|
||||||
|
|
@ -75,7 +76,6 @@ public:
|
||||||
void setProfile(const DeckSyncProfile &_profile);
|
void setProfile(const DeckSyncProfile &_profile);
|
||||||
void setDeckPath(const QString &path);
|
void setDeckPath(const QString &path);
|
||||||
void setConflictDefault(int conflictDefault);
|
void setConflictDefault(int conflictDefault);
|
||||||
void setSyncMode(DeckSyncMode mode);
|
|
||||||
[[nodiscard]] bool isSyncing() const;
|
[[nodiscard]] bool isSyncing() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
|
@ -217,7 +217,7 @@ private:
|
||||||
QMap<QString, ConflictDownload> pendingConflicts;
|
QMap<QString, ConflictDownload> pendingConflicts;
|
||||||
QMap<int, ServerFile> stashedServerFiles;
|
QMap<int, ServerFile> stashedServerFiles;
|
||||||
ActiveConflict activeConflict;
|
ActiveConflict activeConflict;
|
||||||
DeckSyncMode syncMode = DeckSyncMode::TwoWay;
|
DeckSyncMode syncMode = DeckSyncMode::TwoWay; ///< This run's direction, loaded from the sidecar.
|
||||||
int conflictDefault = DeckSyncConflictAsk;
|
int conflictDefault = DeckSyncConflictAsk;
|
||||||
int sessionDefault = -1;
|
int sessionDefault = -1;
|
||||||
bool firstSyncRun = false;
|
bool firstSyncRun = false;
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,14 @@ class DeckSyncSidecar
|
||||||
public:
|
public:
|
||||||
static constexpr const char *MODE_LINKED = "linked";
|
static constexpr const char *MODE_LINKED = "linked";
|
||||||
static constexpr const char *MODE_SEPARATE = "separate";
|
static constexpr const char *MODE_SEPARATE = "separate";
|
||||||
|
static constexpr const char *MODE_UPLOAD_ONLY = "uploadOnly";
|
||||||
|
static constexpr const char *MODE_DOWNLOAD_ONLY = "downloadOnly";
|
||||||
|
|
||||||
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.
|
/// How this profile syncs: linked is the default two-way mode, separate
|
||||||
|
/// keeps it out of sync, and the first-sync verdicts narrow it to one
|
||||||
|
/// direction. An unrecognised value falls back to the two-way sync.
|
||||||
|
QString mode = MODE_LINKED;
|
||||||
|
|
||||||
explicit DeckSyncSidecar(const QString &_filePath);
|
explicit DeckSyncSidecar(const QString &_filePath);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue