v0.2.0 (9/N): pre-migration auto-backup of var/data.sqlite
PLAN.md §12 *Migrations on schema change* flagged this as a v1.0 prereq. SQLite has no transactional DDL — a half-applied migration can corrupt the user's data with no rollback path. Cheapest defence is a copy-aside before each migrate. backupDatabase() runs at the head of runMigrations() in bundled mode: - skipped on first launch (no data.sqlite yet) - copies var/data.sqlite to var/data.sqlite.<unix-timestamp>.bak - trims to kMaxDatabaseBackups=5 most recent (mtime sort, oldest go first) - copy failure logs a warning and continues; a missing safety-net is not a reason to refuse to boot Dev mode is unaffected — developers own their var/data.sqlite lifecycle and don't want a backup written every time `make dev` restarts. Integration test: bundled-supervisor.sh gained an assertion after the 2nd-launch /healthz check that at least one data.sqlite.*.bak file appears under the user data dir. Verified locally — backup landed at the expected path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
#include "BackendConnection.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QLoggingCategory>
|
||||
#include <QNetworkAccessManager>
|
||||
@@ -262,8 +264,47 @@ QString BackendConnection::databaseUrl() const
|
||||
return QStringLiteral("sqlite:///%1/var/data.sqlite").arg(m_dataDir);
|
||||
}
|
||||
|
||||
void BackendConnection::backupDatabase()
|
||||
{
|
||||
// Cheap insurance against a bad migration corrupting the user's
|
||||
// data. Doctrine offers no rollback for a half-applied migration
|
||||
// against SQLite (no transactional DDL), so the only safe undo is
|
||||
// restoring a copy. PLAN.md §12 *Migrations on schema change*.
|
||||
const QString sqlitePath = m_dataDir + QStringLiteral("/var/data.sqlite");
|
||||
if (!QFileInfo::exists(sqlitePath)) {
|
||||
return; // first launch — nothing to back up yet.
|
||||
}
|
||||
|
||||
const QString varDir = m_dataDir + QStringLiteral("/var");
|
||||
const qint64 stamp = QDateTime::currentSecsSinceEpoch();
|
||||
const QString backupPath = QStringLiteral("%1/data.sqlite.%2.bak").arg(varDir).arg(stamp);
|
||||
|
||||
if (!QFile::copy(sqlitePath, backupPath)) {
|
||||
// Don't fail the launch on a backup miss — log and continue.
|
||||
// The user has a working DB; a missing safety-net is not a
|
||||
// reason to refuse to boot the app.
|
||||
qCWarning(lcBundled) << "pre-migration backup failed (continuing without):"
|
||||
<< "could not copy" << sqlitePath << "→" << backupPath;
|
||||
return;
|
||||
}
|
||||
qCInfo(lcBundled) << "pre-migration backup written to" << backupPath;
|
||||
|
||||
// Trim to N most recent. QDir::Time sort is mtime descending.
|
||||
const QStringList existing = QDir(varDir)
|
||||
.entryList(QStringList{QStringLiteral("data.sqlite.*.bak")},
|
||||
QDir::Files, QDir::Time);
|
||||
for (int i = kMaxDatabaseBackups; i < existing.size(); ++i) {
|
||||
const QString stale = varDir + QLatin1Char('/') + existing.at(i);
|
||||
if (!QFile::remove(stale)) {
|
||||
qCWarning(lcBundled) << "could not trim stale backup" << stale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool BackendConnection::runMigrations()
|
||||
{
|
||||
backupDatabase();
|
||||
|
||||
QProcess proc;
|
||||
proc.setProgram(resolveFrankenphpBin());
|
||||
proc.setArguments({
|
||||
|
||||
@@ -118,6 +118,7 @@ private:
|
||||
void initDevMode();
|
||||
void initBundledMode();
|
||||
bool runMigrations();
|
||||
void backupDatabase();
|
||||
bool spawnChild(QString* errorOut = nullptr);
|
||||
void teardownChild();
|
||||
QString resolveFrankenphpBin() const;
|
||||
@@ -154,6 +155,10 @@ private:
|
||||
QQueue<QString> m_childLog;
|
||||
QString m_childLogBuffer;
|
||||
static constexpr int kChildLogMax = 500;
|
||||
/// Pre-migration auto-backup keeps the N most recent .bak files so
|
||||
/// the user can roll back a bad migration without losing earlier
|
||||
/// safety nets. PLAN.md §12 *Migrations on schema change*.
|
||||
static constexpr int kMaxDatabaseBackups = 5;
|
||||
|
||||
QProcess* m_updateCheck = nullptr;
|
||||
QProcess* m_updateApply = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user