changeset 142:36c9cb759326

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.
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 24 Aug 2017 15:44:33 +0300
parents d0943d41f391
children 3b904b49ce57
files Makefile Makefile.cross-mingw-win32 build-win32.sh slbackup.cfg.example slbackup.php src/main.cpp src/main.h
diffstat 7 files changed, 321 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- 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 =
 
--- 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-
--- 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
 
--- /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 @@
+<?php
+// "secret" used for authentication
+$dataSecret = "pretendsecretpassword";
+
+// Absolute path for storing the backup files
+$dataPath = "/some/path/syntilista_backup/";
+
+// Backup filename prefix
+//$dataName = "backup";
+
+// Backup filename suffix/extension
+//$dataSuffix = ".sqlite3";
+
+// Maximum accepted size of file(s) to backup
+// NOTE! PHP upload size has to be at least this!
+//$dataMaxSize = 1024 * 1024;
+
+// How many backups to keep?
+//$dataBackups = 24;
+
+?>
--- /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 @@
+<?php
+//
+// Syntilista - debt list/management database
+// SQLite3 database backup backend PHP blurb
+// Programmed and designed by Matti Hämäläinen <ccr@tnsp.org>
+// (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
--- 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 <QSettings>
 #include <QPrintDialog>
 #include <QPrintPreviewDialog>
-#include <QProgressDialog>
 #include <QStandardPaths>
 #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();
 }
 
 
--- 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 <QSqlQueryModel>
 #include <QPainter>
 #include <QPrinter>
+#include <QProgressDialog>
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QHttpMultiPart>
 
 
 //
@@ -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;