From b04cc992507a81ef888a0685a46890b530d1e0ea Mon Sep 17 00:00:00 2001
From: Robert Goldmann <deadlocker@gmx.de>
Date: Sun, 4 Oct 2020 18:22:06 +0200
Subject: [PATCH] backup database to owncloud

---
 settings-example.json                   | 11 +++++-
 src/StorageLeaf.py                      |  9 +++--
 src/blueprints/Devices.py               | 15 ++++----
 src/blueprints/Measurements.py          | 15 ++++----
 src/blueprints/Sensors.py               | 17 +++++----
 src/logic/BackupService.py              | 48 +++++++++++++++++++++++++
 src/logic/database/Database.py          | 10 +++---
 src/logic/database/DatabaseAccess.py    |  7 +++-
 src/logic/database/DeviceAccess.py      |  2 +-
 src/logic/database/MeasurementAccess.py |  2 +-
 src/logic/database/SensorAccess.py      |  2 +-
 11 files changed, 106 insertions(+), 32 deletions(-)
 create mode 100644 src/logic/BackupService.py

diff --git a/settings-example.json b/settings-example.json
index 3c724fa..f831474 100644
--- a/settings-example.json
+++ b/settings-example.json
@@ -8,7 +8,16 @@
         "certfile": ""
     },
     "database": {
-        "databasePath": "storageLeaf.db"
+        "databasePath": "storageLeaf.db",
+        "backup":
+        {
+            "enable": true,
+            "maxModifications": 30,
+            "owncloudHost": "https://myowncloud.de",
+            "owncloudUser": "myUser",
+            "owncloudPassword": "",
+            "owncloudDestinationPath": "MyFolder"
+        }
     },
     "api": {
         "url": "http://localhost:10003",
diff --git a/src/StorageLeaf.py b/src/StorageLeaf.py
index c25acfd..c424787 100644
--- a/src/StorageLeaf.py
+++ b/src/StorageLeaf.py
@@ -5,6 +5,7 @@ from TheCodeLabs_FlaskUtils.FlaskBaseApp import FlaskBaseApp
 
 from blueprints import Routes, Devices, Sensors, Measurements
 from logic import Constants
+from logic.BackupService import BackupService
 
 LOGGER = DefaultLogger().create_logger_if_not_exists(Constants.APP_NAME)
 
@@ -12,12 +13,14 @@ LOGGER = DefaultLogger().create_logger_if_not_exists(Constants.APP_NAME)
 class StorageLeaf(FlaskBaseApp):
     def __init__(self, appName: str):
         super().__init__(appName, os.path.dirname(__file__), LOGGER, serveRobotsTxt=False)
+        databaseSettings = self._settings['database']
+        self._backupService = BackupService(databaseSettings['databasePath'], **databaseSettings['backup'])
 
     def _register_blueprints(self, app):
         app.register_blueprint(Routes.construct_blueprint(self._settings, self._version))
-        app.register_blueprint(Devices.construct_blueprint(self._settings))
-        app.register_blueprint(Sensors.construct_blueprint(self._settings))
-        app.register_blueprint(Measurements.construct_blueprint(self._settings))
+        app.register_blueprint(Devices.construct_blueprint(self._settings, self._backupService))
+        app.register_blueprint(Sensors.construct_blueprint(self._settings, self._backupService))
+        app.register_blueprint(Measurements.construct_blueprint(self._settings, self._backupService))
         return app
 
 
diff --git a/src/blueprints/Devices.py b/src/blueprints/Devices.py
index 4b84ea2..865d9d3 100644
--- a/src/blueprints/Devices.py
+++ b/src/blueprints/Devices.py
@@ -1,27 +1,30 @@
+from typing import Dict
+
 from flask import Blueprint, jsonify, request
 
 from logic.AuthenticationWrapper import require_api_key
+from logic.BackupService import BackupService
 from logic.Parameters import DeviceParameters
 from logic.RequestValidator import RequestValidator, ValidationError
 from logic.database.Database import Database
 
 
-def construct_blueprint(settings):
+def construct_blueprint(settings: Dict, backupService: BackupService):
     devices = Blueprint('devices', __name__)
 
     @devices.route('/devices', methods=['GET'])
     def get_all_devices():
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         return jsonify(database.deviceAccess.get_all_devices())
 
     @devices.route('/device/<int:deviceID>', methods=['GET'])
     def get_device(deviceID):
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         return jsonify(database.deviceAccess.get_device(deviceID))
 
     @devices.route('/device/<int:deviceID>/sensors/', methods=['GET'])
     def get_all_sensors_for_device(deviceID):
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         device = database.deviceAccess.get_device(deviceID)
         if not device:
             return jsonify({'success': False, 'msg': f'No device with id "{deviceID}" existing'})
@@ -31,7 +34,7 @@ def construct_blueprint(settings):
     @devices.route('/device/<int:deviceID>', methods=['DELETE'])
     @require_api_key(password=settings['api']['key'])
     def delete_device(deviceID):
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         if not database.deviceAccess.get_device(deviceID):
             return jsonify({'success': False, 'msg': f'No device with id "{deviceID}" existing'})
 
@@ -49,7 +52,7 @@ def construct_blueprint(settings):
     def add_device():
         try:
             parameters = RequestValidator.validate(request, [DeviceParameters.DEVICE.value])
-            database = Database(settings['database']['databasePath'])
+            database = Database(settings['database']['databasePath'], backupService)
 
             deviceName = parameters[DeviceParameters.DEVICE.value]
             existingDevice = database.deviceAccess.get_device_by_name(deviceName)
diff --git a/src/blueprints/Measurements.py b/src/blueprints/Measurements.py
index 764da8c..228d803 100644
--- a/src/blueprints/Measurements.py
+++ b/src/blueprints/Measurements.py
@@ -3,22 +3,23 @@ from typing import Dict
 from flask import Blueprint, jsonify, request
 
 from logic.AuthenticationWrapper import require_api_key
-from logic.database.Database import Database
+from logic.BackupService import BackupService
 from logic.Parameters import DeviceParameters, SensorParameters, MeasurementParameters
 from logic.RequestValidator import RequestValidator, ValidationError
+from logic.database.Database import Database
 
 
-def construct_blueprint(settings):
+def construct_blueprint(settings: Dict, backupService: BackupService):
     measurements = Blueprint('measurements', __name__)
 
     @measurements.route('/measurements', methods=['GET'])
     def get_all_measurements():
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         return jsonify(database.measurementAccess.get_all_measurements())
 
     @measurements.route('/measurement/<int:measurementID>', methods=['GET'])
     def get_measurement(measurementID):
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         return jsonify(database.measurementAccess.get_measurement(measurementID))
 
     @measurements.route('/measurements', methods=['POST'])
@@ -26,7 +27,7 @@ def construct_blueprint(settings):
     def add_multiple_measurements():
         try:
             parameters = RequestValidator.validate(request, DeviceParameters.get_values())
-            database = Database(settings['database']['databasePath'])
+            database = Database(settings['database']['databasePath'], backupService)
 
             deviceName = parameters[DeviceParameters.DEVICE.value]
             if not database.deviceAccess.get_device_by_name(deviceName):
@@ -63,7 +64,7 @@ def construct_blueprint(settings):
     def add_single_measurement():
         try:
             parameters = RequestValidator.validate(request, MeasurementParameters.get_values())
-            database = Database(settings['database']['databasePath'])
+            database = Database(settings['database']['databasePath'], backupService)
 
             sensorID = parameters[MeasurementParameters.SENSOR_ID.value]
             if not database.sensorAccess.get_sensor(sensorID):
@@ -78,7 +79,7 @@ def construct_blueprint(settings):
     @measurements.route('/measurement/<int:measurementID>', methods=['DELETE'])
     @require_api_key(password=settings['api']['key'])
     def delete_measurement(measurementID):
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         if not database.measurementAccess.get_measurement(measurementID):
             return jsonify({'success': False, 'msg': f'No measurement with id "{measurementID}" existing'})
 
diff --git a/src/blueprints/Sensors.py b/src/blueprints/Sensors.py
index 5994489..9c3daf8 100644
--- a/src/blueprints/Sensors.py
+++ b/src/blueprints/Sensors.py
@@ -1,27 +1,30 @@
+from typing import Dict
+
 from flask import Blueprint, jsonify, request
 
 from logic.AuthenticationWrapper import require_api_key
+from logic.BackupService import BackupService
 from logic.Parameters import SensorParameters
 from logic.RequestValidator import RequestValidator, ValidationError
 from logic.database.Database import Database
 
 
-def construct_blueprint(settings):
+def construct_blueprint(settings: Dict, backupService: BackupService):
     sensors = Blueprint('sensors', __name__)
 
     @sensors.route('/sensors', methods=['GET'])
     def get_all_sensors():
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         return jsonify(database.sensorAccess.get_all_sensors())
 
     @sensors.route('/sensor/<int:sensorID>', methods=['GET'])
     def get_sensor(sensorID):
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         return jsonify(database.sensorAccess.get_sensor(sensorID))
 
     @sensors.route('/sensor/<int:sensorID>/measurements', methods=['GET'])
     def get_all_measurements_for_sensor(sensorID):
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         sensor = database.sensorAccess.get_sensor(sensorID)
         if not sensor:
             return jsonify({'success': False, 'msg': f'No sensor with id "{sensorID}" existing'})
@@ -30,7 +33,7 @@ def construct_blueprint(settings):
 
     @sensors.route('/sensor/<int:sensorID>/measurements/latest', methods=['GET'])
     def get_latest_measurements_for_sensor(sensorID):
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         sensor = database.sensorAccess.get_sensor(sensorID)
         if not sensor:
             return jsonify({'success': False, 'msg': f'No sensor with id "{sensorID}" existing'})
@@ -40,7 +43,7 @@ def construct_blueprint(settings):
     @sensors.route('/sensor/<int:sensorID>', methods=['DELETE'])
     @require_api_key(password=settings['api']['key'])
     def delete_sensor(sensorID):
-        database = Database(settings['database']['databasePath'])
+        database = Database(settings['database']['databasePath'], backupService)
         if not database.sensorAccess.get_sensor(sensorID):
             return jsonify({'success': False, 'msg': f'No sensor with id "{sensorID}" existing'})
 
@@ -55,7 +58,7 @@ def construct_blueprint(settings):
             parameters = RequestValidator.validate(request, [SensorParameters.NAME.value,
                                                              SensorParameters.TYPE.value,
                                                              SensorParameters.DEVICE_ID.value])
-            database = Database(settings['database']['databasePath'])
+            database = Database(settings['database']['databasePath'], backupService)
 
             deviceID = parameters[SensorParameters.DEVICE_ID.value]
             sensorName = parameters[SensorParameters.NAME.value]
diff --git a/src/logic/BackupService.py b/src/logic/BackupService.py
new file mode 100644
index 0000000..15b2e79
--- /dev/null
+++ b/src/logic/BackupService.py
@@ -0,0 +1,48 @@
+import logging
+
+from TheCodeLabs_BaseUtils.OwncloudUploader import OwncloudUploader
+
+from logic import Constants
+
+LOGGER = logging.getLogger(Constants.APP_NAME)
+
+
+class BackupService:
+    def __init__(self,
+                 fileToBackup: str,
+                 enable: bool,
+                 maxModifications: int,
+                 owncloudHost: str,
+                 owncloudUser: str,
+                 owncloudPassword: str,
+                 owncloudDestinationPath: str):
+        self._fileToBackup = fileToBackup
+        self._enable = enable
+        self._maxModifications = maxModifications
+        self._owncloudHost = owncloudHost
+        self._owncloudUser = owncloudUser
+        self._owncloudPassword = owncloudPassword
+        self._owncloudDestinationPath = owncloudDestinationPath
+
+        self.__reset()
+
+    def __reset(self):
+        self._numberOfModifications = 0
+
+    def is_backup_needed(self):
+        if not self._enable:
+            return False
+
+        return self._numberOfModifications >= self._maxModifications
+
+    def backup(self):
+        LOGGER.info('Running backup...')
+        uploader = OwncloudUploader(self._owncloudHost, self._owncloudUser, self._owncloudPassword)
+        uploader.upload(self._owncloudDestinationPath, self._fileToBackup)
+        self.__reset()
+
+    def perform_modification(self):
+        self._numberOfModifications += 1
+        LOGGER.debug(f'New Modification ({self._numberOfModifications}/{self._maxModifications})')
+        if self.is_backup_needed():
+            self.backup()
diff --git a/src/logic/database/Database.py b/src/logic/database/Database.py
index 3d11232..a03afff 100644
--- a/src/logic/database/Database.py
+++ b/src/logic/database/Database.py
@@ -1,6 +1,8 @@
 import logging
+from typing import Dict
 
 from logic import Constants
+from logic.BackupService import BackupService
 from logic.database.DeviceAccess import DeviceAccess
 from logic.database.MeasurementAccess import MeasurementAccess
 from logic.database.SensorAccess import SensorAccess
@@ -9,11 +11,11 @@ LOGGER = logging.getLogger(Constants.APP_NAME)
 
 
 class Database:
-    def __init__(self, databasePath):
+    def __init__(self, databasePath: str, backupService: BackupService):
         self._databasePath = databasePath
-        self.deviceAccess = DeviceAccess(databasePath)
-        self.sensorAccess = SensorAccess(databasePath)
-        self.measurementAccess = MeasurementAccess(databasePath)
+        self.deviceAccess = DeviceAccess(databasePath, backupService)
+        self.sensorAccess = SensorAccess(databasePath, backupService)
+        self.measurementAccess = MeasurementAccess(databasePath, backupService)
 
         self.__create_database()
 
diff --git a/src/logic/database/DatabaseAccess.py b/src/logic/database/DatabaseAccess.py
index 01132a3..408c682 100644
--- a/src/logic/database/DatabaseAccess.py
+++ b/src/logic/database/DatabaseAccess.py
@@ -5,6 +5,7 @@ from abc import ABC
 from enum import Enum
 
 from logic import Constants
+from logic.BackupService import BackupService
 
 LOGGER = logging.getLogger(Constants.APP_NAME)
 
@@ -13,6 +14,7 @@ class FetchType(Enum):
     NONE = 1
     ONE = 2
     ALL = 3
+    CREATE = 4
 
 
 class DatabaseAccess(ABC):
@@ -28,8 +30,9 @@ class DatabaseAccess(ABC):
             d[col[0]] = row[idx]
         return d
 
-    def __init__(self, databasePath):
+    def __init__(self, databasePath, backupService: BackupService):
         self._databasePath = databasePath
+        self._backupService = backupService
 
     @abc.abstractmethod
     def create_table(self):
@@ -48,5 +51,7 @@ class DatabaseAccess(ABC):
                     return cursor.fetchone()
                 if fetch_type == FetchType.ALL:
                     return cursor.fetchall()
+                if fetch_type == FetchType.NONE:
+                    self._backupService.perform_modification()
             finally:
                 cursor.close()
diff --git a/src/logic/database/DeviceAccess.py b/src/logic/database/DeviceAccess.py
index d2fed23..a3eae0d 100644
--- a/src/logic/database/DeviceAccess.py
+++ b/src/logic/database/DeviceAccess.py
@@ -13,7 +13,7 @@ class DeviceAccess(DatabaseAccess):
     def create_table(self):
         self._query(f'''CREATE TABLE IF NOT EXISTS {self.TABLE_NAME} (
                             id INTEGER PRIMARY KEY AUTOINCREMENT, 
-                            name TEXT NOT NULL)''', fetch_type=FetchType.NONE)
+                            name TEXT NOT NULL)''', fetch_type=FetchType.CREATE)
 
     def get_all_devices(self) -> List[Dict[str, str]]:
         return self._query(f'SELECT * FROM {self.TABLE_NAME} ORDER BY name', fetch_type=FetchType.ALL)
diff --git a/src/logic/database/MeasurementAccess.py b/src/logic/database/MeasurementAccess.py
index 5f02b84..b5a6755 100644
--- a/src/logic/database/MeasurementAccess.py
+++ b/src/logic/database/MeasurementAccess.py
@@ -16,7 +16,7 @@ class MeasurementAccess(DatabaseAccess):
                          id INTEGER PRIMARY KEY AUTOINCREMENT,
                          sensor_id INTEGER,
                          value TEXT NOT NULL,
-                         timestamp TEXT NOT NULL)''', fetch_type=FetchType.NONE)
+                         timestamp TEXT NOT NULL)''', fetch_type=FetchType.CREATE)
 
     def __get_current_datetime(self):
         return datetime.strftime(datetime.now(), self.DATE_FORMAT)
diff --git a/src/logic/database/SensorAccess.py b/src/logic/database/SensorAccess.py
index 32ad6a4..5bc1917 100644
--- a/src/logic/database/SensorAccess.py
+++ b/src/logic/database/SensorAccess.py
@@ -15,7 +15,7 @@ class SensorAccess(DatabaseAccess):
                          id INTEGER PRIMARY KEY AUTOINCREMENT,
                          device_id INTEGER,
                          name TEXT NOT NULL, 
-                         type TEXT NOT NULL)''', fetch_type=FetchType.NONE)
+                         type TEXT NOT NULL)''', fetch_type=FetchType.CREATE)
 
     def get_all_sensors(self) -> List[Dict[str, str]]:
         return self._query(f'SELECT * FROM {self.TABLE_NAME} ORDER BY device_id, id', fetch_type=FetchType.ALL)
-- 
GitLab