2929#include < QFile>
3030#include < QFileInfo>
3131#include < QSqlError>
32+ #include < QRecursiveMutex>
3233#include < QStandardPaths>
3334#include < QUuid>
3435#include < qsqlquery.h>
3536
37+ namespace {
38+ QRecursiveMutex& maintenanceMutex ()
39+ {
40+ static QRecursiveMutex mutex;
41+ return mutex;
42+ }
43+
44+ class AdoptedMaintenanceLock
45+ {
46+ public:
47+ explicit AdoptedMaintenanceLock (QRecursiveMutex& mutex) : m_mutex(mutex) {}
48+ ~AdoptedMaintenanceLock () { m_mutex.unlock (); }
49+ private:
50+ QRecursiveMutex& m_mutex;
51+ };
52+ }
53+
3654DatabaseManager& DatabaseManager::instance ()
3755{
3856 static DatabaseManager instance;
@@ -108,17 +126,39 @@ QSqlDatabase DatabaseManager::database() const
108126}
109127
110128bool DatabaseManager::backupDatabase (const QString& backupPath, QString* errorMessage)
129+ {
130+ if (!maintenanceMutex ().tryLock ()) {
131+ const QString message = " Another database maintenance operation is already running." ;
132+ if (errorMessage) *errorMessage = message;
133+ qWarning () << " Database backup rejected:" << message;
134+ return false ;
135+ }
136+ AdoptedMaintenanceLock lock (maintenanceMutex ());
137+ return backupDatabaseUnlocked (backupPath, errorMessage);
138+ }
139+
140+ bool DatabaseManager::backupDatabaseUnlocked (const QString& backupPath, QString* errorMessage)
141+ {
142+ return createSnapshot (m_database, backupPath, errorMessage);
143+ }
144+
145+ bool DatabaseManager::createSnapshot (QSqlDatabase database, const QString& backupPath,
146+ QString* errorMessage, BackupFailure* failure)
111147{
112148 auto setError = [errorMessage](const QString& message) {
113149 if (errorMessage)
114150 *errorMessage = message;
115151 };
116152
117- if (!m_database.isOpen ()) {
153+ if (failure)
154+ *failure = BackupFailure::None;
155+
156+ if (!database.isOpen ()) {
118157 const QString message = " The BrickSuite database is not open." ;
119158 setError (message);
120159 qWarning () << " Database backup rejected:" << message;
121160
161+ if (failure) *failure = BackupFailure::Source;
122162 return false ;
123163 }
124164
@@ -128,7 +168,7 @@ bool DatabaseManager::backupDatabase(const QString& backupPath, QString* errorMe
128168
129169 if (trimmedPath.isEmpty ()) {
130170 setError (" No backup file was selected." );
131-
171+ if (failure) *failure = BackupFailure::Destination;
132172 return false ;
133173 }
134174
@@ -145,6 +185,7 @@ bool DatabaseManager::backupDatabase(const QString& backupPath, QString* errorMe
145185 .arg (backupDirectory.absolutePath ()));
146186 qCritical () << " Database backup failed:" << message;
147187
188+ if (failure) *failure = BackupFailure::Destination;
148189 return false ;
149190 }
150191 }
@@ -165,6 +206,7 @@ bool DatabaseManager::backupDatabase(const QString& backupPath, QString* errorMe
165206 " backup file:\n %1" )
166207 .arg (temporaryPath));
167208
209+ if (failure) *failure = BackupFailure::Destination;
168210 return false ;
169211 }
170212 }
@@ -176,7 +218,7 @@ bool DatabaseManager::backupDatabase(const QString& backupPath, QString* errorMe
176218 //
177219 escapedTemporaryPath.replace (" '" , " ''" );
178220
179- QSqlQuery query (m_database );
221+ QSqlQuery query (database );
180222
181223 const QString sql = QString (" VACUUM INTO '%1'" ).arg (escapedTemporaryPath);
182224
@@ -188,6 +230,11 @@ bool DatabaseManager::backupDatabase(const QString& backupPath, QString* errorMe
188230 QString (" Unable to create the database backup.\n\n %1" ).arg (message));
189231 qCritical () << " Database backup VACUUM INTO failed:" << message;
190232
233+ if (failure) {
234+ const QString lower = message.toLower ();
235+ *failure = lower.contains (" busy" ) || lower.contains (" locked" )
236+ ? BackupFailure::Busy : BackupFailure::Snapshot;
237+ }
191238 return false ;
192239 }
193240
@@ -198,6 +245,7 @@ bool DatabaseManager::backupDatabase(const QString& backupPath, QString* errorMe
198245 " the temporary backup file was not created."
199246 << " TemporaryPath:" << temporaryPath;
200247
248+ if (failure) *failure = BackupFailure::Snapshot;
201249 return false ;
202250 }
203251
@@ -213,6 +261,7 @@ bool DatabaseManager::backupDatabase(const QString& backupPath, QString* errorMe
213261 " existing destination file could not be replaced:\n %1" )
214262 .arg (trimmedPath));
215263
264+ if (failure) *failure = BackupFailure::Destination;
216265 return false ;
217266 }
218267 }
@@ -224,6 +273,7 @@ bool DatabaseManager::backupDatabase(const QString& backupPath, QString* errorMe
224273 " but it could not be renamed to:\n %1" )
225274 .arg (trimmedPath));
226275
276+ if (failure) *failure = BackupFailure::Destination;
227277 return false ;
228278 }
229279
@@ -233,6 +283,11 @@ bool DatabaseManager::backupDatabase(const QString& backupPath, QString* errorMe
233283}
234284
235285bool DatabaseManager::verifyDatabaseBackup (const QString& backupPath, QString* errorMessage) const
286+ {
287+ return verifyBackup (backupPath, errorMessage);
288+ }
289+
290+ bool DatabaseManager::verifyBackup (const QString& backupPath, QString* errorMessage)
236291{
237292 auto setError = [errorMessage](const QString& message) {
238293 if (errorMessage)
@@ -336,6 +391,58 @@ bool DatabaseManager::verifyDatabaseBackup(const QString& backupPath, QString* e
336391 return true ;
337392}
338393
394+ DatabaseManager::VerifiedBackupResult DatabaseManager::createVerifiedBackup (
395+ const QString& sourceDatabasePath, const QString& backupPath)
396+ {
397+ VerifiedBackupResult result;
398+ result.backupPath = backupPath;
399+
400+ if (!maintenanceMutex ().tryLock ()) {
401+ result.failure = BackupFailure::Busy;
402+ result.errorMessage = " Another database maintenance operation is already running." ;
403+ return result;
404+ }
405+ AdoptedMaintenanceLock lock (maintenanceMutex ());
406+
407+ const QString connectionName = QString (" BrickSuiteAutomaticBackup_%1" )
408+ .arg (QUuid::createUuid ().toString (QUuid::WithoutBraces));
409+ const QString pendingPath = backupPath + " .pending."
410+ + QUuid::createUuid ().toString (QUuid::WithoutBraces);
411+ {
412+ QSqlDatabase source = QSqlDatabase::addDatabase (" QSQLITE" , connectionName);
413+ source.setDatabaseName (sourceDatabasePath);
414+ source.setConnectOptions (" QSQLITE_OPEN_READONLY" );
415+ if (!source.open ()) {
416+ result.failure = BackupFailure::Source;
417+ result.errorMessage = QString (" Unable to open the BrickSuite database for backup.\n\n %1" )
418+ .arg (source.lastError ().text ());
419+ } else if (createSnapshot (source, pendingPath, &result.errorMessage , &result.failure )) {
420+ source.close ();
421+ QString verificationError;
422+ if (verifyBackup (pendingPath, &verificationError)) {
423+ if (QFile::rename (pendingPath, backupPath)) {
424+ result.success = true ;
425+ result.failure = BackupFailure::None;
426+ } else {
427+ result.failure = BackupFailure::Destination;
428+ result.errorMessage = QString (" The verified backup could not be published as: %1" )
429+ .arg (backupPath);
430+ QFile::remove (pendingPath);
431+ }
432+ } else {
433+ result.failure = BackupFailure::Verification;
434+ result.errorMessage = verificationError;
435+ if (!QFile::remove (pendingPath))
436+ QFile::rename (pendingPath, pendingPath + " .unverified" );
437+ }
438+ }
439+ source.close ();
440+ }
441+ QSqlDatabase::removeDatabase (connectionName);
442+ QFile::remove (pendingPath + " .tmp" );
443+ return result;
444+ }
445+
339446QString DatabaseManager::databasePath () const
340447{
341448 const QString dataPath = QStandardPaths::writableLocation (QStandardPaths::AppLocalDataLocation);
@@ -350,6 +457,14 @@ bool DatabaseManager::restoreDatabase(const QString& backupPath, QString* errorM
350457 *errorMessage = message;
351458 };
352459
460+ if (!maintenanceMutex ().tryLock ()) {
461+ const QString message = " Another database maintenance operation is already running." ;
462+ setError (message);
463+ qWarning () << " Database restore rejected:" << message;
464+ return false ;
465+ }
466+ AdoptedMaintenanceLock lock (maintenanceMutex ());
467+
353468 const QString trimmedBackupPath = backupPath.trimmed ();
354469
355470 qInfo () << " Database restore requested:" << trimmedBackupPath;
@@ -546,4 +661,4 @@ bool DatabaseManager::restoreDatabase(const QString& backupPath, QString* errorM
546661 qInfo () << " Pre-restore safety backup:" << safetyBackupPath;
547662
548663 return true ;
549- }
664+ }
0 commit comments