diff --git a/settings-example.json b/settings-example.json index 3c724fa69b38c078d270785b50eecd9b115e0f82..f831474a53409e8017fbacc11b26ad8f1057b4e6 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 c25acfd8695a239570e26a3522f73e415371cf63..c4247870deecbac020196550ed09127cf7ff3f43 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 4b84ea2e3d2e4e71851a5b7c9417ea94d6a33c2f..865d9d35adb5bec1145ef2aa2bc337d1fe0ea283 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 764da8cf628ac976e386c3aaa9fb297e9dd75a94..228d8030240e879669bd04ade70f3c4bd2a0f707 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 5994489883192be470f2c07f96acf5e0762255f4..9c3daf8b1ebb3bae600924be6ddc4ba5a1414b54 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 0000000000000000000000000000000000000000..15b2e79fa578f6e37fe60b6d0294eefbaa354373 --- /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 3d112320c40a403f2b1ac8a6934edc51e811a45e..a03afff24ea03d56d75d1321a5108ad3f854e25a 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 01132a3ccd570ee52194ab917b8660ee654ebebc..408c682d23770add60f06285181ae5b924a9f71b 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 d2fed2383dfa91e1efbf23a9ea8107cc2af1a38e..a3eae0d966c020db117a4813c0dc60034945a5f1 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 5f02b844a57bb39b3e44631b34d53b4c09658a43..b5a67553127b03dfdbfa14f927bdd604ffc60776 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 32ad6a41b4892f6704d722309ea0ed30b2737692..5bc19172abdf2f2b7ef9093777fbd250575e2856 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)