# HG changeset patch # User Matti Hamalainen # Date 1503578673 -10800 # Node ID 36c9cb7593265089c6b0f46d7626dbb7d1be1672 # Parent d0943d41f3913f94c2c7655c75b6b508fbc0faa3 Implement simple SQLite database backup at program exit using Qt HTTP/HTTPS and a PHP script on the remote server. Needs more work, testing and better error handling. diff -r d0943d41f391 -r 36c9cb759326 Makefile --- a/Makefile Thu Aug 24 13:16:09 2017 +0300 +++ b/Makefile Thu Aug 24 15:44:33 2017 +0300 @@ -4,7 +4,7 @@ ### # Miscellaneous -QT5_MODULES = Core Gui Widgets Sql PrintSupport +QT5_MODULES = Core Gui Widgets Sql PrintSupport Network QT5_PREFIX = BINTOOL_PREFIX = diff -r d0943d41f391 -r 36c9cb759326 Makefile.cross-mingw-win32 --- a/Makefile.cross-mingw-win32 Thu Aug 24 13:16:09 2017 +0300 +++ b/Makefile.cross-mingw-win32 Thu Aug 24 15:44:33 2017 +0300 @@ -3,7 +3,7 @@ ### # Miscellaneous -QT5_MODULES = Core Gui Widgets Sql PrintSupport +QT5_MODULES = Core Gui Widgets Sql PrintSupport Network QT5_PREFIX ?= /misc/packages/qt5-src QT5_BASE ?= $(QT5_PREFIX)/qtbase BINTOOL_PREFIX ?= i686-w64-mingw32- diff -r d0943d41f391 -r 36c9cb759326 build-win32.sh --- a/build-win32.sh Thu Aug 24 13:16:09 2017 +0300 +++ b/build-win32.sh Thu Aug 24 15:44:33 2017 +0300 @@ -35,7 +35,7 @@ do_cpinstall "$QT5_BASE/plugins/" "$TARGET" "sqldrivers" "qsqlite.dll" do_cpinstall "$QT5_BASE/plugins/" "$TARGET" "printsupport" "windowsprintersupport.dll" - for i in Core Gui Sql Widgets PrintSupport; do + for i in Core Gui Sql Widgets PrintSupport Network; do cp -f "$QT5_BASE/lib/Qt5$i.dll" "$TARGET" done diff -r d0943d41f391 -r 36c9cb759326 slbackup.cfg.example --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/slbackup.cfg.example Thu Aug 24 15:44:33 2017 +0300 @@ -0,0 +1,21 @@ + diff -r d0943d41f391 -r 36c9cb759326 slbackup.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/slbackup.php Thu Aug 24 15:44:33 2017 +0300 @@ -0,0 +1,142 @@ + +// (C) Copyright 2017 Tecnic Software productions (TNSP) +// +// Distributed under 3-clause BSD style license, refer to +// included file "COPYING" for exact terms. +// + +// +// Settings, etc. +// +$dataName = "backup"; +$dataSuffix = ".sqlite3"; +$dataMaxSize = 1024 * 1024; +$dataBackups = 24; +$configFile = "slbackup.cfg"; + +if (file_exists($configFile)) + require_once $configFile; + + +// +// Helper functions +// +function stError($msg) +{ + global $errorSet, $errorMsg; + $errorSet = TRUE; + $errorMsg = $msg; +} + + +function stGetBackupFilenameFilename($base, $suffix, $num) +{ + return sprintf("%s%02d%s", $base, $num, $suffix); +} + + +function stRotateBackups($base, $suffix, $num) +{ + for ($i = $num; $i > 0; $i--) + { + $srcFilename = stGetBackupFilenameFilename($base, $suffix, $i - 1); + if (file_exists($srcFilename)) + { + $dstFilename = stGetBackupFilenameFilename($base, $suffix, $i); + if (file_exists($dstFilename)) + unlink($dstFilename); + + if (@rename($srcFilename, $dstFilename) === FALSE) + return FALSE; + } + } + return TRUE; +} + + +// +// Actual main code begins here +// +$errorMsg = ""; +$errorSet = FALSE; +$index = "file"; + +if (!isset($dataSecret) || !isset($dataPath) || + $dataSecret == "" || $dataPath == "") +{ + error_log("SyntilistaBackup: Invalid configuration."); + exit; +} + +// Basic check for credentials .. +if (isset($_REQUEST["secret"]) && isset($_FILES[$index]) && + ($secret = $_REQUEST["secret"]) == $dataSecret) +{ + $fileName = $_FILES[$index]["tmp_name"]; + $fileSize = $_FILES[$index]["size"]; + $fileError = $_FILES[$index]["error"]; + + switch ($fileError) + { + case UPLOAD_ERR_INI_SIZE: + stError("File size exceeds PHP's max upload size."); + break; + + case UPLOAD_ERR_PARTIAL: + stError("File only partially uploaded."); + break; + + case UPLOAD_ERR_NO_FILE: + stError("No file data received!"); + break; + + case UPLOAD_ERR_NO_TMP_DIR: + stError("Internal error: Temporary file directory not available!"); + break; + + case UPLOAD_ERR_CANT_WRITE: + stError("Internal error: PHP could not write the file to disk."); + break; + + case UPLOAD_ERR_OK: + break; + + default: + stError("Unknown PHP file error occured."); + break; + } + + if (!$errorSet && $fileSize > $dataMaxSize) + { + stError("File is too large (".$fileSize." > ".$dataMaxSize." bytes)."); + } + + if (!$errorSet) + { + $path = $dataPath."/".$dataName; + stRotateBackups($path, $dataSuffix, $dataBackups); + + $dstFilename = stGetBackupFilenameFilename($path, $dataSuffix, 1); + if (@move_uploaded_file($fileName, $dstFilename) === false) + { + stError("Could not move the uploaded file to proper directory."); + } + } + + if ($errorSet) + { + header("Status: 500 Internal error"); + header("Content-Type: text/plain"); + echo $errorMsg; + } +} +else +{ + header("Status: 403 Forbidden"); + header("Content-Type: text/plain"); +} +?> \ No newline at end of file diff -r d0943d41f391 -r 36c9cb759326 src/main.cpp --- a/src/main.cpp Thu Aug 24 13:16:09 2017 +0300 +++ b/src/main.cpp Thu Aug 24 15:44:33 2017 +0300 @@ -11,7 +11,6 @@ #include #include #include -#include #include #include "main.h" #include "ui_mainwindow.h" @@ -246,8 +245,15 @@ settings.uiPos = tmpst.value("pos", QPoint(100, 100)).toPoint(); settings.uiSize = tmpst.value("size", QSize(1000, 600)).toSize(); settings.uiScale = tmpst.value("scale", 1.0f).toDouble(); - settings.dbBackupURL = tmpst.value("dbBackupURL", "").toString(); - settings.dbBackupSecret = tmpst.value("dbBackupSecret", "").toString(); + settings.dbBackupURL = tmpst.value("dbBackupURL", QString()).toString(); + settings.dbBackupSecret = tmpst.value("dbBackupSecret", QString()).toString(); + + // Check commandline arguments for configuring backup settings + if (argc >= 4 && strcmp(argv[1], "config") == 0) + { + settings.dbBackupURL = QString(argv[2]); + settings.dbBackupSecret = QString(argv[3]); + } // // Create logfile and data directory @@ -319,7 +325,6 @@ { // Setup UI ui->setupUi(this); - backupDialog = NULL; // Restore window size and position move(settings.uiPos); @@ -431,6 +436,139 @@ // Commit and close database QSqlDatabase::database().commit(); QSqlDatabase::database().close(); + + // Back up the database + backupDatabase(); +} + + +void SyntilistaMainWindow::backupDatabase() +{ + QString dbFilename = settings.dataPath + QDir::separator() + APP_SQLITE_FILE; + QString backupFilename = APP_SQLITE_FILE; + backupReply = NULL; + backupDialog = NULL; + + if (settings.dbBackupURL == QString() || settings.dbBackupURL == "") + { + slLog("ERROR", QStringLiteral("Database backup URL not set in configuration.")); + return; + } + + if (settings.dbBackupSecret == QString() || settings.dbBackupSecret == "") + { + slLog("ERROR", QStringLiteral("Database backup secret key not set in configuration.")); + return; + } + + // Check for network access + QNetworkAccessManager *manager = new QNetworkAccessManager(); + if (manager->networkAccessible() != QNetworkAccessManager::Accessible) + { + slLog("ERROR", QStringLiteral("Network not available, cannot backup the database.")); + return; + } + + // Attempt to open the database file + QFile *file = new QFile(dbFilename); + if (!file->open(QIODevice::ReadOnly)) + { + slLog("ERROR", QStringLiteral("Failed to open database file '%1' for backup.").arg(dbFilename)); + return; + } + + // Okay, we seem to be "go" .. + slLog("INFO", + QStringLiteral("Attempting database backup from '%1' to '%2'."). + arg(dbFilename).arg(settings.dbBackupURL)); + + // Create the HTTP POST request + QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + // The "secret" key as POST parameter + QHttpPart postPart; + postPart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"secret\";")); + postPart.setBody(QByteArray(settings.dbBackupSecret.toUtf8())); + + // Actual data as binary octet-stream + QHttpPart dataPart; + dataPart.setHeader(QNetworkRequest::ContentTypeHeader, + QVariant("binary/octet-stream")); + + dataPart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"file\"; filename=\""+ backupFilename +"\"")); + + dataPart.setBodyDevice(file); + file->setParent(multiPart); // we cannot delete the QFile object now, so delete it with the multiPart + + multiPart->append(postPart); + multiPart->append(dataPart); + + // Attempt to POST the whole thing + QUrl url(settings.dbBackupURL); + QNetworkRequest request(url); + backupReply = manager->post(request, multiPart); + multiPart->setParent(backupReply); + + connect( + backupReply, + SIGNAL(finished()), + this, + SLOT(backupFinished())); + + connect( + backupReply, + SIGNAL(uploadProgress(qint64, qint64)), + this, + SLOT(backupProgress(qint64, qint64))); + + // Create progress dialog + backupDialog = new QProgressDialog( + tr("Varmuuskopioidaan tietokantaa ..."), + QString(), + 0, + 100, + this); + + backupDialog->setAttribute(Qt::WA_DeleteOnClose); + backupDialog->setAutoClose(false); + backupDialog->setWindowModality(Qt::ApplicationModal); + backupDialog->exec(); +} + + +void SyntilistaMainWindow::backupProgress(qint64 bytesSent, qint64 bytesTotal) +{ + if (bytesTotal > 0) + { + slLog("INFO", + QStringLiteral("Backup sent %1 / %2 bytes."). + arg(bytesSent). + arg(bytesTotal)); + + backupDialog->setValue((bytesSent * 100) / bytesTotal); + } +} + + +void SyntilistaMainWindow::backupError(QNetworkReply::NetworkError code) +{ + slLog("ERROR", + QStringLiteral("Backup failed with network error %1.\n"). + arg(code) + ); +} + + +void SyntilistaMainWindow::backupFinished() +{ + if (backupReply) + { + slLog("PAF", QString::fromUtf8(backupReply->readAll())); + } + slLog("INFO", "Backup finished."); + backupDialog->close(); } diff -r d0943d41f391 -r 36c9cb759326 src/main.h --- a/src/main.h Thu Aug 24 13:16:09 2017 +0300 +++ b/src/main.h Thu Aug 24 15:44:33 2017 +0300 @@ -16,6 +16,11 @@ #include #include #include +#include +#include +#include +#include +#include // @@ -132,6 +137,7 @@ int addTransactionGUI(qint64 id, bool debt, double value); void updatePersonList(); void updateTotalBalance(); + void backupDatabase(); bool printDocumentPage(SLPageInfo &pinfo, const bool getPageInfo, const int page, QPainter *pt, QPrinter *printer); @@ -171,10 +177,17 @@ void printDocument(QPrinter *printer); + void backupProgress(qint64 bytesSent, qint64 bytesTotal); + void backupFinished(); + void backupError(QNetworkReply::NetworkError code); + private: Ui::SyntilistaMainWindow *ui; + QProgressDialog *backupDialog; + QNetworkReply *backupReply; + TransactionSQLModel *model_Latest; PersonInfo currPerson;