diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 81f748ef..b7a6ac88 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -1003,8 +1003,8 @@ "infoDialog_import_comperror_text": "Sad! Your browser doesn't support importing. Please try again with another browser.", "infoDialog_import_confirm_text": "Are you sure to import \"$1\"? This process can't be reverted!", "infoDialog_import_confirm_title": "Confirm import", - "infoDialog_import_hyperror_text": "The selected configuration file \"$1\" can't be imported. It's not compatible with Hyperion 2.0 and higher!", "infoDialog_import_jsonerror_text": "The selected configuration file \"$1\" is not a .json file, or it's corrupted. Error message: ($2)", + "infoDialog_import_version_error_text": "The selected configuration file \"$1\" can not be imported. It's not compatible with Hyperion 2.0.17 and higher!", "infoDialog_wizrgb_text": "Your RGB Byte Order is already well adjusted.", "infoDialog_writeconf_error_text": "Saving your configuration failed.", "infoDialog_writeimage_error_text": "The selected file \"$1\" is not an image file, or it's corrupted! Please select another image file.", diff --git a/assets/webconfig/js/content_general.js b/assets/webconfig/js/content_general.js index 3a25045a..6f094256 100644 --- a/assets/webconfig/js/content_general.js +++ b/assets/webconfig/js/content_general.js @@ -28,11 +28,6 @@ $(document).ready(function () { // Instance handling function handleInstanceRename(e) { - conf_editor.on('change', function () { - window.readOnlyMode ? $('#btn_cl_save').prop('disabled', true) : $('#btn_submit').prop('disabled', false); - window.readOnlyMode ? $('#btn_ma_save').prop('disabled', true) : $('#btn_submit').prop('disabled', false); - }); - var inst = e.currentTarget.id.split("_")[1]; showInfoDialog('renInst', $.i18n('conf_general_inst_renreq_t'), getInstanceNameByIndex(inst)); @@ -119,14 +114,14 @@ $(document).ready(function () { //check file is json var check = isJsonString(content); if (check.length != 0) { - showInfoDialog('error', "", $.i18n('infoDialog_import_jsonerror_text', f.name, JSON.stringify(check))); + showInfoDialog('error', "", $.i18n('infoDialog_import_jsonerror_text', f.name, JSON.stringify(check.message))); dis_imp_btn(true); } else { content = JSON.parse(content); //check for hyperion json - if (typeof content.leds === 'undefined' || typeof content.general === 'undefined') { - showInfoDialog('error', "", $.i18n('infoDialog_import_hyperror_text', f.name)); + if (typeof content.global === 'undefined' || typeof content.instances === 'undefined') { + showInfoDialog('error', "", $.i18n('infoDialog_import_version_error_text', f.name)); dis_imp_btn(true); } else { @@ -143,10 +138,10 @@ $(document).ready(function () { $('#btn_import_conf').off().on('click', function () { showInfoDialog('import', $.i18n('infoDialog_import_confirm_title'), $.i18n('infoDialog_import_confirm_text', confName)); - $('#id_btn_import').off().on('click', function () { + $('#id_btn_import').off().on('click', function () { requestRestoreConfig(importedConf); - setTimeout(initRestart, 100); }); + }); $('#select_import_conf').off().on('change', function (e) { @@ -157,18 +152,19 @@ $(document).ready(function () { }); //export - $('#btn_export_conf').off().on('click', function () { - var name = window.serverConfig.general.name; + $('#btn_export_conf').off().on('click', async () => + { + const name = window.serverConfig.general.name; - var d = new Date(); - var month = d.getMonth() + 1; - var day = d.getDate(); + const d = new Date(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const timestamp = `${d.getFullYear()}-${month}-${day}`; - var timestamp = d.getFullYear() + '.' + - (month < 10 ? '0' : '') + month + '.' + - (day < 10 ? '0' : '') + day; - - download(JSON.stringify(window.serverConfig, null, "\t"), 'Hyperion-' + window.currentVersion + '-Backup (' + name + ') ' + timestamp + '.json', "application/json"); + const configBackup = await requestConfig(); + if (configBackup.success === true) { + download(JSON.stringify(configBackup.info, null, "\t"), 'HyperionBackup-' + timestamp + '_v' + window.currentVersion + '.json', "application/json"); + } }); //create introduction @@ -180,3 +176,8 @@ $(document).ready(function () { removeOverlay(); }); + +$(window.hyperion).on("cmd-config-restoreconfig", function (event) { + setTimeout(initRestart, 100); +}); + diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index 6609b3bd..81f4ebcc 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -128,7 +128,7 @@ $(document).ready(function () { requestSysInfo(); }); - $(window.hyperion).on("cmd-config-getconfig", function (event) { + $(window.hyperion).on("cmd-config-getconfig-old", function (event) { window.serverConfig = event.response.info; window.showOptHelp = window.serverConfig.general.showOptHelp; @@ -278,7 +278,7 @@ $(document).ready(function () { window.currentHyperionInstance = 0; window.currentHyperionInstanceName = getInstanceNameByIndex(0); - requestServerConfig(); + requestServerConfigOld(); setTimeout(requestServerInfo, 100) setTimeout(requestTokenInfo, 200) } @@ -296,7 +296,7 @@ $(document).ready(function () { }); $(window.hyperion).on("cmd-instance-switchTo", function (event) { - requestServerConfig(); + requestServerConfigOld(); setTimeout(requestServerInfo, 200) setTimeout(requestTokenInfo, 400) }); @@ -338,11 +338,6 @@ $(function () { }); }); -// hotfix body padding when bs modals overlap -$(document.body).on('hide.bs.modal,hidden.bs.modal', function () { - $('body').css('padding-right', '0'); -}); - //Dark Mode $("#btn_darkmode").off().on("click", function (e) { if (getStorage("darkMode") != "on") { diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index 8153e827..6e3dae53 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -37,9 +37,8 @@ const ENDLESS = -1; function initRestart() { $(window.hyperion).off(); - requestServerConfigReload(); window.watchdog = 10; - connectionLostDetection('restart'); + connectionLostDetection('restart'); } function connectionLostDetection(type) @@ -138,9 +137,10 @@ function initWebSocket() if (error == "Service Unavailable") { window.location.reload(); } else { - $(window.hyperion).trigger({type:"error",reason:error}); + $(window.hyperion).trigger({type:"error", reason:error}); } - console.log("[window.websocket::onmessage] ",error) + let errorData = response.hasOwnProperty("errorData")? response.errorData : ""; + console.log("[window.websocket::onmessage] ",error, ", Description:", errorData); } } } @@ -344,6 +344,11 @@ function requestServerConfig() sendToHyperion("config", "getconfig"); } +function requestServerConfigOld() +{ + sendToHyperion("config", "getconfig-old"); +} + function requestServerConfigReload() { sendToHyperion("config", "reload"); @@ -522,3 +527,14 @@ async function requestServiceDiscovery(type, params) { return sendAsyncToHyperion("service", "discover", data); } +async function requestConfig(globalTypes, instances, instanceTypes) { + let globalFilter = { "global": { "types": globalTypes } }; + let instanceFilter = { "instances": { "ids": instances, "types": instanceTypes } }; + + //todo create filter + let filter; + + return sendAsyncToHyperion("config", "getconfig", filter); +} + + diff --git a/assets/webconfig/js/ledsim.js b/assets/webconfig/js/ledsim.js index af633ba7..52a75ced 100644 --- a/assets/webconfig/js/ledsim.js +++ b/assets/webconfig/js/ledsim.js @@ -129,7 +129,7 @@ $(document).ready(function () { } }); // apply new serverinfos - $(window.hyperion).on("cmd-config-getconfig", function (event) { + $(window.hyperion).on("cmd-config-getconfig-old", function (event) { leds = event.response.info.leds; grabberConfig = event.response.info.grabberV4L2; updateLedLayout(); diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 4bfeb85e..c291c5e3 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -239,7 +239,7 @@ function showInfoDialog(type, header, message) { $('#id_body').html(''); if (header == "") $('#id_body').append('

' + $.i18n('infoDialog_general_error_title') + '

'); - $('#id_footer').html(''); + $('#id_footer').html(''); } else if (type == "select") { $('#id_body').html(''); @@ -256,9 +256,9 @@ function showInfoDialog(type, header, message) { $('#id_footer').html('' + $.i18n('InfoDialog_nowrite_foottext') + ''); } else if (type == "import") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); } else if (type == "delInst") { $('#id_body').html(''); @@ -321,7 +321,7 @@ function showInfoDialog(type, header, message) { $(document).on('click', '[data-dismiss-modal]', function () { var target = $(this).data('dismiss-modal'); $($.find(target)).modal('hide'); -}); + }); } function createHintH(type, text, container) { @@ -1222,7 +1222,7 @@ function getSystemInfo() { info += '- Avail Video Cap.: ' + window.serverInfo.grabbers.video.available + '\n'; info += '- Avail Audio Cap.: ' + window.serverInfo.grabbers.audio.available + '\n'; info += '- Avail Services: ' + window.serverInfo.services + '\n'; - info += '- Config path: ' + shy.rootPath + '\n'; + info += '- Config database: ' + shy.configDatabaseFile + '\n'; info += '- Database: ' + (shy.readOnlyMode ? "ready-only" : "read/write") + '\n'; info += '- Mode: ' + (shy.isGuiMode ? "GUI" : "Non-GUI") + '\n'; diff --git a/include/api/API.h b/include/api/API.h index 96e1f9b8..4fc891bb 100644 --- a/include/api/API.h +++ b/include/api/API.h @@ -245,12 +245,6 @@ protected: /// bool saveSettings(const QJsonObject &data); - /// - /// @brief Restore settings object. Requires ADMIN ACCESS - /// @param data The data object - /// - bool restoreSettings(const QJsonObject &data); - /// /// @brief Set the authorizationn state /// @param authorized True, if authorized diff --git a/include/api/JsonAPI.h b/include/api/JsonAPI.h index 35d7e204..60b092f5 100644 --- a/include/api/JsonAPI.h +++ b/include/api/JsonAPI.h @@ -186,7 +186,7 @@ private: /// void handleSourceSelectCommand(const QJsonObject &message, const JsonApiCommand& cmd); - /// Handle an incoming JSON GetConfig message and check subcommand + /// Handle an incoming JSON Config message and check subcommand /// /// @param message the incoming message /// @@ -204,6 +204,12 @@ private: /// void handleConfigSetCommand(const QJsonObject &message, const JsonApiCommand& cmd); + /// Handle an incoming JSON GetConfig message from handleConfigCommand() + /// + /// @param message the incoming message + /// + void handleConfigGetCommand(const QJsonObject &message, const JsonApiCommand& cmd); + /// Handle an incoming JSON RestoreConfig message from handleConfigCommand() /// /// @param message the incoming message diff --git a/include/api/JsonApiCommand.h b/include/api/JsonApiCommand.h index c63d968b..4345b56a 100644 --- a/include/api/JsonApiCommand.h +++ b/include/api/JsonApiCommand.h @@ -84,6 +84,7 @@ public: DeleteToken, Discover, GetConfig, + GetConfigOld, GetInfo, GetPendingTokenRequests, GetProperties, @@ -134,6 +135,7 @@ public: case DeleteToken: return "deleteToken"; case Discover: return "discover"; case GetConfig: return "getconfig"; + case GetConfigOld: return "getconfig-old"; case GetInfo: return "getInfo"; case GetPendingTokenRequests: return "getPendingTokenRequests"; case GetProperties: return "getProperties"; @@ -274,6 +276,7 @@ public: { {"color", ""}, { Command::Color, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, { {"componentstate", ""}, { Command::ComponentState, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, { {"config", "getconfig"}, { Command::Config, SubCommand::GetConfig, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"config", "getconfig-old"}, { Command::Config, SubCommand::GetConfigOld, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, { {"config", "getschema"}, { Command::Config, SubCommand::GetSchema, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, { {"config", "reload"}, { Command::Config, SubCommand::Reload, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, { {"config", "restoreconfig"}, { Command::Config, SubCommand::RestoreConfig, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, diff --git a/include/api/JsonInfo.h b/include/api/JsonInfo.h index 1346f97e..e7d8b8c6 100644 --- a/include/api/JsonInfo.h +++ b/include/api/JsonInfo.h @@ -31,6 +31,8 @@ public: static QJsonObject getSystemInfo(const Hyperion* hyperion); QJsonObject discoverSources (const QString& sourceType, const QJsonObject& params); + static QJsonObject getConfiguration(const QList& instances = {}, bool addGlobalConfig = true, const QStringList& instanceFilteredTypes = {}, const QStringList& globalFilterTypes = {} ); + private: template diff --git a/include/db/AuthTable.h b/include/db/AuthTable.h index 161c3856..f2ecb50e 100644 --- a/include/db/AuthTable.h +++ b/include/db/AuthTable.h @@ -1,12 +1,7 @@ -#pragma once +#ifndef AUTHSTABLE_H +#define AUTHSTABLE_H -// hyperion #include -#include - -// qt -#include -#include namespace hyperion { const char DEFAULT_USER[] = "Hyperion"; @@ -21,69 +16,30 @@ class AuthTable : public DBManager public: /// construct wrapper with auth table - AuthTable(const QString& rootPath = "", QObject* parent = nullptr, bool readonlyMode = false) - : DBManager(parent) - { - setReadonlyMode(readonlyMode); - if(!rootPath.isEmpty()){ - // Init Hyperion database usage - setRootPath(rootPath); - setDatabaseName("hyperion"); - } - // init Auth table - setTable("auth"); - // create table columns - createTable(QStringList()<<"user TEXT"<<"password BLOB"<<"token BLOB"<<"salt BLOB"<<"comment TEXT"<<"id TEXT"<<"created_at TEXT"<<"last_use TEXT"); - }; + explicit AuthTable(QObject* parent = nullptr); /// /// @brief Create a user record, if called on a existing user the auth is recreated /// @param[in] user The username - /// @param[in] pw The password + /// @param[in] password The password /// @return true on success else false /// - inline bool createUser(const QString& user, const QString& pw) - { - // new salt - QByteArray salt = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); - QVariantMap map; - map["user"] = user; - map["salt"] = salt; - map["password"] = hashPasswordWithSalt(pw,salt); - map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("user",user)); - return createRecord(cond, map); - } + bool createUser(const QString& user, const QString& password); /// /// @brief Test if user record exists /// @param[in] user The user id /// @return true on success else false /// - inline bool userExist(const QString& user) - { - VectorPair cond; - cond.append(CPair("user",user)); - return recordExists(cond); - } + bool userExist(const QString& user); /// /// @brief Test if a user is authorized for access with given pw. - /// @param user The user name - /// @param pw The password - /// @return True on success else false + /// @param user The user name + /// @param password The password + /// @return True on success else false /// - inline bool isUserAuthorized(const QString& user, const QString& pw) - { - if(userExist(user) && (calcPasswordHashOfUser(user, pw) == getPasswordHashOfUser(user))) - { - updateUserUsed(user); - return true; - } - return false; - } + bool isUserAuthorized(const QString& user, const QString& password); /// /// @brief Test if a user token is authorized for access. @@ -91,197 +47,92 @@ public: /// @param token The token /// @return True on success else false /// - inline bool isUserTokenAuthorized(const QString& usr, const QString& token) - { - if(getUserToken(usr) == token.toUtf8()) - { - updateUserUsed(usr); - return true; - } - return false; - } + bool isUserTokenAuthorized(const QString& usr, const QString& token); /// /// @brief Update token of a user. It's an alternate login path which is replaced on startup. This token is NOT hashed(!) /// @param user The user name /// @return True on success else false /// - inline bool setUserToken(const QString& user) - { - QVariantMap map; - map["token"] = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); - - VectorPair cond; - cond.append(CPair("user", user)); - return updateRecord(cond, map); - } + bool setUserToken(const QString& user); /// /// @brief Get token of a user. This token is NOT hashed(!) /// @param user The user name /// @return The token /// - inline const QByteArray getUserToken(const QString& user) - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("user", user)); - getRecord(cond, results, QStringList()<<"token"); - - return results["token"].toByteArray(); - } + const QByteArray getUserToken(const QString& user); /// /// @brief update password of given user. The user should be tested (isUserAuthorized) to verify this change - /// @param user The user name - /// @param newPw The new password to set - /// @return True on success else false + /// @param user The user name + /// @param newassword The new password to set + /// @return True on success else false /// - inline bool updateUserPassword(const QString& user, const QString& newPw) - { - QVariantMap map; - map["password"] = calcPasswordHashOfUser(user, newPw); - - VectorPair cond; - cond.append(CPair("user", user)); - return updateRecord(cond, map); - } + bool updateUserPassword(const QString& user, const QString& newPassword); /// /// @brief Reset password of Hyperion user !DANGER! Used in Hyperion main.cpp /// @return True on success else false /// - inline bool resetHyperionUser() - { - QVariantMap map; - map["password"] = calcPasswordHashOfUser(hyperion::DEFAULT_USER, hyperion::DEFAULT_PASSWORD); - - VectorPair cond; - cond.append(CPair("user", hyperion::DEFAULT_USER)); - return updateRecord(cond, map); - } + bool resetHyperionUser(); /// /// @brief Update 'last_use' column entry for the corresponding user /// @param[in] user The user to search for /// - inline void updateUserUsed(const QString& user) - { - QVariantMap map; - map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("user", user)); - updateRecord(cond, map); - } + void updateUserUsed(const QString& user); /// /// @brief Test if token record exists, updates last_use on success /// @param[in] token The token id /// @return true on success else false /// - inline bool tokenExist(const QString& token) - { - QVariantMap map; - map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("token", hashToken(token))); - if(recordExists(cond)) - { - // update it - createRecord(cond,map); - return true; - } - return false; - } + bool tokenExist(const QString& token); /// /// @brief Create a new token record with comment /// @param[in] token The token id as plaintext /// @param[in] comment The comment for the token (eg a human readable identifier) - /// @param[in] id The id for the token + /// @param[in] identifier The identifier for the token /// @return true on success else false /// - inline bool createToken(const QString& token, const QString& comment, const QString& id) - { - QVariantMap map; - map["comment"] = comment; - map["id"] = idExist(id) ? QUuid::createUuid().toString().remove("{").remove("}").left(5) : id; - map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("token", hashToken(token))); - return createRecord(cond, map); - } + bool createToken(const QString& token, const QString& comment, const QString& identifier); /// - /// @brief Delete token record by id - /// @param[in] id The token id + /// @brief Delete token record by identifier + /// @param[in] identifier The token identifier /// @return true on success else false /// - inline bool deleteToken(const QString& id) - { - VectorPair cond; - cond.append(CPair("id", id)); - return deleteRecord(cond); - } + bool deleteToken(const QString& identifier); /// - /// @brief Rename token record by id - /// @param[in] id The token id + /// @brief Rename token record by identifier + /// @param[in] identifier The token identifier /// @param[in] comment The new comment /// @return true on success else false /// - inline bool renameToken(const QString &id, const QString &comment) - { - QVariantMap map; - map["comment"] = comment; - - VectorPair cond; - cond.append(CPair("id", id)); - return updateRecord(cond, map); - } + bool renameToken(const QString &identifier, const QString &comment); /// /// @brief Get all 'comment', 'last_use' and 'id' column entries /// @return A vector of all lists /// - inline const QVector getTokenList() - { - QVector results; - getRecords(results, QStringList() << "comment" << "id" << "last_use"); - - return results; - } + const QVector getTokenList(); /// - /// @brief Test if id exists - /// @param[in] id The id + /// @brief Test if identifier exists + /// @param[in] identifier The identifier /// @return true on success else false /// - inline bool idExist(const QString& id) - { - - VectorPair cond; - cond.append(CPair("id", id)); - return recordExists(cond); - } + bool identifierExist(const QString& identifier); /// /// @brief Get the passwort hash of a user from db /// @param user The user name /// @return password as hash /// - inline const QByteArray getPasswordHashOfUser(const QString& user) - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("user", user)); - getRecord(cond, results, QStringList()<<"password"); - - return results["password"].toByteArray(); - } + const QByteArray getPasswordHashOfUser(const QString& user); /// /// @brief Calc the password hash of a user based on user name and password @@ -289,36 +140,22 @@ public: /// @param pw The password /// @return The calced password hash /// - inline const QByteArray calcPasswordHashOfUser(const QString& user, const QString& pw) - { - // get salt - QVariantMap results; - VectorPair cond; - cond.append(CPair("user", user)); - getRecord(cond, results, QStringList()<<"salt"); - - // calc - return hashPasswordWithSalt(pw,results["salt"].toByteArray()); - } + const QByteArray calcPasswordHashOfUser(const QString& user, const QString& password); /// /// @brief Create a password hash of plaintex password + salt - /// @param pw The plaintext password + /// @param password The plaintext password /// @param salt The salt /// @return The password hash with salt /// - inline const QByteArray hashPasswordWithSalt(const QString& pw, const QByteArray& salt) - { - return QCryptographicHash::hash(pw.toUtf8().append(salt), QCryptographicHash::Sha512).toHex(); - } + const QByteArray hashPasswordWithSalt(const QString& password, const QByteArray& salt); /// /// @brief Create a token hash /// @param token The plaintext token /// @return The token hash /// - inline const QByteArray hashToken(const QString& token) - { - return QCryptographicHash::hash(token.toUtf8(), QCryptographicHash::Sha512).toHex(); - } + const QByteArray hashToken(const QString& token); }; + +#endif // AUTHSTABLE_H diff --git a/include/db/ConfigImportExport.h b/include/db/ConfigImportExport.h new file mode 100644 index 00000000..19353ca7 --- /dev/null +++ b/include/db/ConfigImportExport.h @@ -0,0 +1,22 @@ +#ifndef CONFIGIMPORTEXPORT_H +#define CONFIGIMPORTEXPORT_H + +#include + +#include +#include + +class ConfigImportExport : public DBManager +{ +public: + ConfigImportExport(QObject* parent = nullptr); + + // TODO: Check naming seConfiguration + QPair importJson(const QString& configFile); + bool exportJson(const QString& path = "") const; + + QPair setConfiguration(const QJsonObject& config); + QJsonObject getConfiguration(const QList& instances = {}, bool addGlobalConfig = true, const QStringList& instanceFilteredTypes = {}, const QStringList& globalFilterTypes = {} ) const; +}; + +#endif // CONFIGIMPORTEXPORT_H diff --git a/include/db/DBManager.h b/include/db/DBManager.h index 4d36471d..de401dab 100644 --- a/include/db/DBManager.h +++ b/include/db/DBManager.h @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include class QSqlDatabase; class QSqlQuery; @@ -26,13 +29,22 @@ class DBManager : public QObject Q_OBJECT public: - DBManager(QObject* parent = nullptr); - ~DBManager() override; + explicit DBManager(QObject* parent = nullptr); + + static void initializeDatabase(const QDir& dataDirectory, bool isReadOnly); + + static QDir getDataDirectory() { return _dataDirectory;} + static QDir getDirectory() { return _databaseDirectory;} + static QFileInfo getFileInfo() { return _databaseFile;} + static bool isReadOnly() { return _isReadOnly; } + + /// + /// @brief Sets the database in read-only mode. + /// Updates will not written to the tables + /// @param[in] readOnly True read-only, false - read/write + /// + static void setReadonly(bool isReadOnly) { _isReadOnly = isReadOnly; } - /// set root path - void setRootPath(const QString& rootPath); - /// define the database to work with - void setDatabaseName(const QString& dbn) { _dbn = dbn; }; /// set a table to work with void setTable(const QString& table); @@ -98,6 +110,18 @@ public: /// bool getRecords(QVector& results, const QStringList& tColumns = QStringList(), const QStringList& tOrder = QStringList()) const; + /// + /// @brief Get data of multiple records, you need to specify the columns. This search is without conditions. Good to grab all data from db + /// @param[in] conditions condition to search for (WHERE) + /// @param[out] results results of query + /// @param[in] tColumns target columns to search in (optional) if not provided returns all columns + /// @param[in] tOrder target order columns with order by ASC/DESC (optional) + /// @return True on success else false + /// + bool getRecords(const VectorPair& conditions, QVector& results, const QStringList& tColumns = {}, const QStringList& tOrder = {}) const; + + bool getRecords(const QString& condition, const QVariantList& bindValues, QVector& results, const QStringList& tColumns = {}, const QStringList& tOrder = {}) const; + /// /// @brief Delete a record determined by conditions /// @param[in] conditions conditions of the row to delete it (WHERE) @@ -119,23 +143,26 @@ public: /// bool deleteTable(const QString& table) const; - /// - /// @brief Sets a table in read-only mode. - /// Updates will not written to the table - /// @param[in] readOnly True read-only, false - read/write - /// - void setReadonlyMode(bool readOnly) { _readonlyMode = readOnly; }; + bool executeQuery(QSqlQuery& query) const; + +protected: + Logger* _log; private: + static QDir _dataDirectory; + static QDir _databaseDirectory; + static QFileInfo _databaseFile; + static QThreadStorage _databasePool; + static bool _isReadOnly; - Logger* _log; - /// databse connection & file name, defaults to hyperion - QString _dbn = "hyperion"; - /// table in database - QString _table; + /// databse connection & file name, defaults to hyperion + QString _dbn = "hyperion"; - bool _readonlyMode; + /// table in database + QString _table; - /// addBindValue to query given by QVariantList - void doAddBindValue(QSqlQuery& query, const QVariantList& variants) const; + /// addBindValues to query given by QVariantList + void addBindValues(QSqlQuery& query, const QVariantList& variants) const; + + QString constructExecutedQuery(const QSqlQuery& query) const; }; diff --git a/include/db/InstanceTable.h b/include/db/InstanceTable.h index 912ecbaf..7fbb580e 100644 --- a/include/db/InstanceTable.h +++ b/include/db/InstanceTable.h @@ -1,11 +1,7 @@ -#pragma once +#ifndef INSTANCETABLE_H +#define INSTANCETABLE_H -// db #include -#include - -// qt -#include /// /// @brief Hyperion instance manager specific database interface. prepares also the Hyperion database for all follow up usage (Init QtSqlConnection) along with db name @@ -14,22 +10,7 @@ class InstanceTable : public DBManager { public: - InstanceTable(const QString& rootPath, QObject* parent = nullptr, bool readonlyMode = false) - : DBManager(parent) - { - - setReadonlyMode(readonlyMode); - // Init Hyperion database usage - setRootPath(rootPath); - setDatabaseName("hyperion"); - - // Init instance table - setTable("instances"); - createTable(QStringList()<<"instance INTEGER"<<"friendly_name TEXT"<<"enabled INTEGER DEFAULT 0"<<"last_use TEXT"); - - // start/create the first Hyperion instance index 0 - createInstance(); - }; + explicit InstanceTable(QObject* parent = nullptr); /// /// @brief Create a new Hyperion instance entry, the name needs to be unique @@ -37,53 +18,19 @@ public: /// @param[out] inst The id that has been assigned /// @return True on success else false /// - inline bool createInstance(const QString& name, quint8& inst) - { - VectorPair fcond; - fcond.append(CPair("friendly_name",name)); + bool createInstance(const QString& name, quint8& inst); - // check duplicate - if(!recordExists(fcond)) - { - inst = 0; - VectorPair cond; - cond.append(CPair("instance",inst)); - - // increment to next avail index - while(recordExists(cond)) - { - inst++; - cond.removeFirst(); - cond.append(CPair("instance",inst)); - } - // create - QVariantMap data; - data["friendly_name"] = name; - data["instance"] = inst; - VectorPair lcond; - return createRecord(lcond, data); - } - return false; - } + /// + /// @brief Create first Hyperion instance entry, if index 0 is not found. + /// + void createDefaultInstance(); /// /// @brief Delete a Hyperion instance /// @param inst The id that has been assigned /// @return True on success else false /// - inline bool deleteInstance(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance",inst)); - if(deleteRecord(cond)) - { - // delete settings entries - SettingsTable settingsTable(inst); - settingsTable.deleteInstance(); - return true; - } - return false; - } + bool deleteInstance(quint8 inst); /// /// @brief Assign a new name for the given instance @@ -91,141 +38,59 @@ public: /// @param name The new name of the instance /// @return True on success else false (instance not found) /// - inline bool saveName(quint8 inst, const QString& name) - { - VectorPair fcond; - fcond.append(CPair("friendly_name",name)); - - // check duplicate - if(!recordExists(fcond)) - { - if(instanceExist(inst)) - { - VectorPair cond; - cond.append(CPair("instance",inst)); - QVariantMap data; - data["friendly_name"] = name; - - return updateRecord(cond, data); - } - } - return false; - } - - + bool saveName(quint8 inst, const QString& name); /// /// @brief Get all instances with all columns - /// @param justEnabled return just enabled instances if true + /// @param onlyEnabled return only enabled instances if true /// @return The found instances /// - inline QVector getAllInstances(bool justEnabled = false) - { - QVector results; - getRecords(results, QStringList(), QStringList() << "instance ASC"); - if(justEnabled) - { - for (auto it = results.begin(); it != results.end();) - { - if( ! (*it)["enabled"].toBool()) - { - it = results.erase(it); - continue; - } - ++it; - } - } - return results; - } + QVector getAllInstances(bool onlyEnabled = false); + + /// + /// @brief Get all instance IDs + /// @param onlyEnabled return only enabled instance IDs if true + /// @return The found instances + /// + QList getAllInstanceIDs (bool onlyEnabled = false); /// /// @brief Test if instance record exists /// @param[in] user The user id /// @return true on success else false /// - inline bool instanceExist(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance",inst)); - return recordExists(cond); - } + bool instanceExist(quint8 inst); /// /// @brief Get instance name by instance index /// @param index The index to search for /// @return The name of this index, may return NOT FOUND if not found /// - inline const QString getNamebyIndex(quint8 index) - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("instance", index)); - getRecord(cond, results, QStringList("friendly_name")); - - QString name = results["friendly_name"].toString(); - return name.isEmpty() ? "NOT FOUND" : name; - } + QString getNamebyIndex(quint8 index); /// /// @brief Update 'last_use' timestamp /// @param inst The instance to update + /// @return True on success else false /// - inline void setLastUse(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance", inst)); - QVariantMap map; - map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - updateRecord(cond, map); - } + bool setLastUse(quint8 inst); /// /// @brief Update 'enabled' column by instance index /// @param inst The instance to update /// @param newState True when enabled else false + /// @return True on success else false /// - inline void setEnable(quint8 inst, bool newState) - { - VectorPair cond; - cond.append(CPair("instance", inst)); - QVariantMap map; - map["enabled"] = newState; - updateRecord(cond, map); - } + bool setEnable(quint8 inst, bool newState); /// /// @brief Get state of 'enabled' column by instance index /// @param inst The instance to get /// @return True when enabled else false /// - inline bool isEnabled(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance", inst)); - QVariantMap results; - getRecord(cond, results); - - return results["enabled"].toBool(); - } + bool isEnabled(quint8 inst); private: - /// - /// @brief Create first Hyperion instance entry, if index 0 is not found. - /// - inline void createInstance() - { - if(instanceExist(0)) - setEnable(0, true); - else - { - QVariantMap data; - data["friendly_name"] = "First LED Hardware instance"; - VectorPair cond; - cond.append(CPair("instance", 0)); - if(createRecord(cond, data)) - setEnable(0, true); - else - throw std::runtime_error("Failed to create Hyperion root instance in db! This should never be the case..."); - } - } }; + +#endif // INSTANCETABLE_H diff --git a/include/db/MetaTable.h b/include/db/MetaTable.h index e1861568..3da71904 100644 --- a/include/db/MetaTable.h +++ b/include/db/MetaTable.h @@ -1,14 +1,9 @@ -#pragma once +#ifndef METATABLE_H +#define METATABLE_H // hyperion #include -// qt -#include -#include -#include -#include - /// /// @brief meta table specific database interface /// @@ -17,47 +12,13 @@ class MetaTable : public DBManager public: /// construct wrapper with plugins table and columns - MetaTable(QObject* parent = nullptr, bool readonlyMode = false) - : DBManager(parent) - { - setReadonlyMode(readonlyMode); - - setTable("meta"); - createTable(QStringList()<<"uuid TEXT"<<"created_at TEXT"); - }; + explicit MetaTable(QObject* parent = nullptr); /// /// @brief Get the uuid, if the uuid is not set it will be created /// @return The uuid /// - inline QString getUUID() const - { - QVector results; - getRecords(results, QStringList() << "uuid"); - - for(const auto & entry : results) - { - if(!entry["uuid"].toString().isEmpty()) - return entry["uuid"].toString(); - } - - // create new uuidv5 based on net adapter MAC, save to db and return - QString hash; - foreach(QNetworkInterface interface, QNetworkInterface::allInterfaces()) - { - if (!(interface.flags() & QNetworkInterface::IsLoopBack)) - { - hash = QCryptographicHash::hash(interface.hardwareAddress().toLocal8Bit(),QCryptographicHash::Sha1).toHex(); - break; - } - } - const QString newUuid = QUuid::createUuidV5(QUuid(), hash).toString().mid(1, 36); - VectorPair cond; - cond.append(CPair("uuid",newUuid)); - QVariantMap map; - map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - createRecord(cond, map); - - return newUuid; - } + QString getUUID() const; }; + +#endif // METATABLE_H diff --git a/include/db/SettingsTable.h b/include/db/SettingsTable.h index d1b31c21..4052cf8c 100644 --- a/include/db/SettingsTable.h +++ b/include/db/SettingsTable.h @@ -1,12 +1,12 @@ -#pragma once +#ifndef SETTINGSTABLE_H +#define SETTINGSTABLE_H -// hyperion #include -// qt -#include #include +const int GLOABL_INSTANCE_ID = 0; + /// /// @brief settings table db interface /// @@ -15,14 +15,7 @@ class SettingsTable : public DBManager public: /// construct wrapper with settings table - SettingsTable(quint8 instance, QObject* parent = nullptr) - : DBManager(parent) - , _hyperion_inst(instance) - { - setTable("settings"); - // create table columns - createTable(QStringList()<<"type TEXT"<<"config TEXT"<<"hyperion_inst INTEGER"<<"updated_at TEXT"); - }; + SettingsTable(quint8 instance, QObject* parent = nullptr); /// /// @brief Create or update a settings record @@ -30,19 +23,7 @@ public: /// @param[in] config The configuration data /// @return true on success else false /// - inline bool createSettingsRecord(const QString& type, const QString& config) const - { - QVariantMap map; - map["config"] = config; - map["updated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - return createRecord(cond, map); - } + bool createSettingsRecord(const QString& type, const QString& config) const; /// /// @brief Test if record exist, type can be global setting or local (instance) @@ -50,76 +31,33 @@ public: /// @param[in] hyperion_inst The instance of hyperion assigned (might be empty) /// @return true on success else false /// - inline bool recordExist(const QString& type) const - { - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - return recordExists(cond); - } + bool recordExist(const QString& type) const; /// /// @brief Get 'config' column of settings entry as QJsonDocument /// @param[in] type The settings type /// @return The QJsonDocument /// - inline QJsonDocument getSettingsRecord(const QString& type) const - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - getRecord(cond, results, QStringList("config")); - return QJsonDocument::fromJson(results["config"].toByteArray()); - } + QJsonDocument getSettingsRecord(const QString& type) const; /// /// @brief Get 'config' column of settings entry as QString /// @param[in] type The settings type /// @return The QString /// - inline QString getSettingsRecordString(const QString& type) const - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - getRecord(cond, results, QStringList("config")); - return results["config"].toString(); - } + QString getSettingsRecordString(const QString& type) const; /// /// @brief Delete all settings entries associated with this instance, called from InstanceTable of HyperionIManager /// - inline void deleteInstance() const - { - VectorPair cond; - cond.append(CPair("hyperion_inst",_hyperion_inst)); - deleteRecord(cond); - } + void deleteInstance() const; - inline bool isSettingGlobal(const QString& type) const - { - // list of global settings - QStringList list; - // server port services - list << "jsonServer" << "protoServer" << "flatbufServer" << "forwarder" << "webConfig" << "network" - // capture - << "framegrabber" << "grabberV4L2" << "grabberAudio" - //Events - << "osEvents" << "cecEvents" << "schedEvents" - // other - << "logger" << "general"; + static const QVector& getGlobalSettingTypes(); - return list.contains(type); - } + static bool isSettingGlobal(const QString& type); private: const quint8 _hyperion_inst; }; + +#endif // SETTINGSTABLE_H diff --git a/include/hyperion/AuthManager.h b/include/hyperion/AuthManager.h index 790f70b6..a4b02154 100644 --- a/include/hyperion/AuthManager.h +++ b/include/hyperion/AuthManager.h @@ -23,7 +23,7 @@ class AuthManager : public QObject private: friend class HyperionDaemon; /// constructor is private, can be called from HyperionDaemon - AuthManager(QObject *parent = nullptr, bool readonlyMode = false); + AuthManager(QObject *parent = nullptr); public: struct AuthDefinition diff --git a/include/hyperion/Hyperion.h b/include/hyperion/Hyperion.h index a15eda8e..c42f91c4 100644 --- a/include/hyperion/Hyperion.h +++ b/include/hyperion/Hyperion.h @@ -108,8 +108,6 @@ public: /// QString getActiveDeviceType() const; - bool getReadOnlyMode() const {return _readOnlyMode; } - public slots: /// @@ -340,14 +338,6 @@ public slots: /// bool saveSettings(const QJsonObject& config, bool correct = false); - /// - /// @brief Restore a complete json config - /// @param config The entire config object - /// @param correct If true will correct json against schema before save - /// @return True on success else false - /// - bool restoreSettings(const QJsonObject& config, bool correct = false); - /// ############ /// COMPONENTREGISTER /// @@ -552,7 +542,7 @@ private: /// @brief Constructs the Hyperion instance, just accessible for HyperionIManager /// @param instance The instance index /// - Hyperion(quint8 instance, bool readonlyMode = false); + Hyperion(quint8 instance); /// instance index const quint8 _instIndex; @@ -615,6 +605,4 @@ private: /// Boblight instance BoblightServer* _boblightServer; #endif - - bool _readOnlyMode; }; diff --git a/include/hyperion/HyperionIManager.h b/include/hyperion/HyperionIManager.h index d7610d5b..60a17076 100644 --- a/include/hyperion/HyperionIManager.h +++ b/include/hyperion/HyperionIManager.h @@ -59,12 +59,18 @@ public slots: /// QVector getInstanceData() const; + QString getInstanceName(quint8 inst = 0); /// /// @brief Get all instance indicies of running instances /// QList getRunningInstanceIdx() const; + /// + /// @brief Get all instance indicies configured + /// + QList getInstanceIds() const; + /// /// @brief Start a Hyperion instance /// @param instance Instance index @@ -115,8 +121,6 @@ public slots: /// bool saveName(quint8 inst, const QString& name); - QString getRootPath() const { return _rootPath; } - signals: /// /// @brief Emits whenever the state of a instance changes according to enum instanceState @@ -195,9 +199,8 @@ private: friend class HyperionDaemon; /// /// @brief Construct the Manager - /// @param The root path of all userdata /// - HyperionIManager(const QString& rootPath, QObject* parent = nullptr, bool readonlyMode = false); + HyperionIManager(QObject* parent = nullptr); /// /// @brief Start all instances that are marked as enabled in db. Non blocking @@ -218,12 +221,9 @@ private: private: Logger* _log; InstanceTable* _instanceTable; - const QString _rootPath; QMap _runningInstances; + QList _startQueue; - - bool _readonlyMode; - /// All pending requests QMap _pendingRequests; }; diff --git a/include/hyperion/SettingsManager.h b/include/hyperion/SettingsManager.h index 9d525cf6..2515e1de 100644 --- a/include/hyperion/SettingsManager.h +++ b/include/hyperion/SettingsManager.h @@ -4,12 +4,14 @@ #include #include + +#include using namespace semver; // qt includes #include -const int GLOABL_INSTANCE_ID = 255; +const char DEFAULT_VERSION[] = "2.0.0-alpha.8"; class Hyperion; class SettingsTable; @@ -26,7 +28,7 @@ public: /// @params instance Instance index of HyperionInstanceManager /// @params parent The parent hyperion instance /// - SettingsManager(quint8 instance, QObject* parent = nullptr, bool readonlyMode = false); + SettingsManager(quint8 instance, QObject* parent = nullptr); /// /// @brief Save a complete json configuration @@ -52,10 +54,19 @@ public: QJsonDocument getSetting(settings::type type) const; /// - /// @brief get the full settings object of this instance (with global settings) + /// @brief get a single setting json from configuration + /// @param type The type as string + /// @return The requested json data as QJsonDocument + /// + QJsonDocument getSetting(const QString& type) const; + /// + /// @brief get the selected settings objects of this instance (including global settings) /// @return The requested json /// - QJsonObject getSettings() const; + //QJsonObject getSettings(const QStringList& type = {} ) const; + + QJsonObject getSettings(const QStringList& filteredTypes = {}) const; + QJsonObject getSettings(const QVariant& instance, const QStringList& filteredTypes = {} ) const; signals: /// @@ -73,8 +84,7 @@ private: /// bool handleConfigUpgrade(QJsonObject& config); - - bool resolveConfigVersion(QJsonObject& config); + bool resolveConfigVersion(const QJsonObject& config); /// Logger instance Logger* _log; @@ -97,5 +107,4 @@ private: semver::version _configVersion; semver::version _previousVersion; - bool _readonlyMode; }; diff --git a/include/utils/JsonUtils.h b/include/utils/JsonUtils.h index 10b89d90..62bcce6c 100644 --- a/include/utils/JsonUtils.h +++ b/include/utils/JsonUtils.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -94,4 +95,10 @@ namespace JsonUtils { /// @return true on success else false /// bool resolveRefs(const QJsonObject& schema, QJsonObject& obj, Logger* log); + + + /// + /// @brief Function to convert QJsonValue to QString using QJsonDocument + /// + QString jsonValueToQString(const QJsonValue &value, QJsonDocument::JsonFormat format = QJsonDocument::Compact); } diff --git a/libsrc/api/API.cpp b/libsrc/api/API.cpp index 961b415c..9e90f32c 100644 --- a/libsrc/api/API.cpp +++ b/libsrc/api/API.cpp @@ -405,20 +405,6 @@ bool API::saveSettings(const QJsonObject &data) return isSaved; } -bool API::restoreSettings(const QJsonObject &data) -{ - bool isRestored {true}; - if (!_adminAuthorized) - { - isRestored = false; - } - else - { - QMetaObject::invokeMethod(_hyperion, "restoreSettings", Qt::DirectConnection, Q_RETURN_ARG(bool, isRestored), Q_ARG(QJsonObject, data), Q_ARG(bool, true)); - } - return isRestored; -} - bool API::updateHyperionPassword(const QString &password, const QString &newPassword) { bool isPwUpdated {true}; diff --git a/libsrc/api/JSONRPC_schema/schema-config.json b/libsrc/api/JSONRPC_schema/schema-config.json index 204661cf..e41731ad 100644 --- a/libsrc/api/JSONRPC_schema/schema-config.json +++ b/libsrc/api/JSONRPC_schema/schema-config.json @@ -10,18 +10,38 @@ "subcommand": { "type" : "string", "required" : true, - "enum" : ["getconfig","getschema","setconfig","restoreconfig","reload"] - }, - "instance" : { - "type" : "integer", - "minimum": 0, - "maximum": 255 + "enum" : ["getconfig","getconfig-old","getschema","setconfig","restoreconfig","reload"] }, "tan" : { "type" : "integer" }, + "global" : { + "types": { + "type": "array", + "required": false, + "items" : { + "type" : "string" + } + } + }, + "instances" : { + "ids" : { + "type": "array", + "required": true, + "items" : {}, + "minItems": 1 + }, + "types": { + "type": "array", + "required": false, + "items" :{ + "type" : "string" + } + } + }, "config": { - "type" : "object" + "required": false, + "$ref": "schema-config-exchange" } }, "additionalProperties": false diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index ff4c6841..d2917aa3 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -39,6 +39,7 @@ // auth manager #include +#include #ifdef ENABLE_MDNS // mDNS discover @@ -697,7 +698,11 @@ void JsonAPI::handleConfigCommand(const QJsonObject& message, const JsonApiComma handleSchemaGetCommand(message, cmd); break; - case SubCommand::GetConfig: + case SubCommand::GetConfig: + handleConfigGetCommand(message, cmd); + break; + + case SubCommand::GetConfigOld: sendSuccessDataReply(_hyperion->getQJsonConfig(), cmd); break; @@ -722,45 +727,130 @@ void JsonAPI::handleConfigCommand(const QJsonObject& message, const JsonApiComma void JsonAPI::handleConfigSetCommand(const QJsonObject &message, const JsonApiCommand& cmd) { - if (message.contains("config")) + if (DBManager::isReadOnly()) { - QJsonObject config = message["config"].toObject(); - if (API::isHyperionEnabled()) - { - if ( API::saveSettings(config) ) { - sendSuccessReply(cmd); - } else { - sendErrorReply("Save settings failed", cmd); + sendErrorReply("Database Error", {"Hyperion is running in read-only mode","Configuration updates are not possible"}, cmd); + return; + } + QJsonObject config = message["config"].toObject(); + + QJsonObject schema = QJsonFactory::readSchema(":schema-config-exchange"); + QPair validationResult = JsonUtils::validate("setConfig", config, schema, _log); + if (!validationResult.first) + { + sendErrorReply("Invalid JSON configuration data provided!", validationResult.second, cmd); + return; + } + + // TODO: Implement new setconfig schema + if (config.contains("global") || config.contains("instances")) + { + Warning(_log, "New set config schema is not yet supported!"); + config.remove("global"); + config.remove("instanceIds"); + config.remove("instances"); + } + + if (config.isEmpty()) + { + sendErrorReply("Update configuration failed", {"No configuration data provided!"}, cmd); + return; + } + + if (API::isHyperionEnabled()) + { + if ( API::saveSettings(config) ) { + sendSuccessReply(cmd); + } else { + sendErrorReply("Save settings failed", cmd); + } + } + else + { + sendErrorReply("Updating the configuration while Hyperion is disabled is not possible", cmd); + } +} + +void JsonAPI::handleConfigGetCommand(const QJsonObject &message, const JsonApiCommand& cmd) +{ + bool addGlobalConfig {false}; + QStringList globalFilterTypes; + + if (message.contains("global")) + { + addGlobalConfig = true; + const QJsonObject globalConfig = message["global"].toObject(); + + const QJsonArray globalTypes = globalConfig["types"].toArray(); + for (const QJsonValue &type : globalTypes) { + if (type.isString()) { + globalFilterTypes.append(type.toString()); } } - else - { - sendErrorReply("Saving configuration while Hyperion is disabled isn't possible", cmd); + } + + QList instanceListFilter; + QStringList instanceFilterTypes; + + bool addInstanceConfig {false}; + if (message.contains("instances")) + { + addInstanceConfig = true; + const QJsonObject instances = message["instances"].toObject(); + const QJsonArray instanceIds = instances["ids"].toArray(); + for (const QJsonValue &idx : instanceIds) { + if (idx.isDouble()) { + instanceListFilter.append(static_cast(idx.toInt())); + } } + + const QJsonArray instanceTypes = instances["types"].toArray(); + for (const QJsonValue &type : instanceTypes) { + if (type.isString()) { + instanceFilterTypes.append(type.toString()); + } + } + } + + if (!addGlobalConfig && ! addInstanceConfig) + { + addGlobalConfig = true; + } + + const QJsonObject settings = JsonInfo::getConfiguration(instanceListFilter, addGlobalConfig, instanceFilterTypes, globalFilterTypes); + if (!settings.empty()) + { + sendSuccessDataReply(settings, cmd); + } + else + { + sendErrorReply("Generating full config failed", cmd); } } void JsonAPI::handleConfigRestoreCommand(const QJsonObject &message, const JsonApiCommand& cmd) { - if (message.contains("config")) + QJsonObject config = message["config"].toObject(); + if (API::isHyperionEnabled()) { - QJsonObject config = message["config"].toObject(); - if (API::isHyperionEnabled()) + ConfigImportExport configImport; + QPair result = configImport.setConfiguration(config); + if (result.first) { - if ( API::restoreSettings(config) ) - { - sendSuccessReply(cmd); - } - else - { - sendErrorReply("Restore settings failed", cmd); - } + QString infoMsg {"Restarting after importing configuration successfully."}; + sendSuccessDataReply(infoMsg, cmd); + Info(_log, "%s", QSTRING_CSTR(infoMsg)); + emit signalEvent(Event::Restart); } else { - sendErrorReply("Restoring configuration while Hyperion is disabled is not possible", cmd); + sendErrorReply("Restore configuration failed", result.second, cmd); } } + else + { + sendErrorReply("Restoring configuration while Hyperion is disabled is not possible", cmd); + } } void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const JsonApiCommand& cmd) @@ -1156,7 +1246,7 @@ void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCom { QString replyMsg; - const quint8 &inst = static_cast(message["instance"].toInt()); + const quint8 inst = static_cast(message["instance"].toInt()); const QString &name = message["name"].toString(); switch (cmd.subCommand) { @@ -1191,7 +1281,6 @@ void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCom break; case SubCommand::DeleteInstance: - handleConfigRestoreCommand(message, cmd); if (API::deleteInstance(inst, replyMsg)) { sendSuccessReply(cmd); @@ -1213,7 +1302,7 @@ void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCom if (cmd.subCommand == SubCommand::CreateInstance) { replyMsg = API::createInstance(name); } else { - replyMsg = setInstanceName(inst, name); + replyMsg = API::setInstanceName(inst, name); } if (replyMsg.isEmpty()) { diff --git a/libsrc/api/JsonInfo.cpp b/libsrc/api/JsonInfo.cpp index e2a73ffe..2da7b0db 100644 --- a/libsrc/api/JsonInfo.cpp +++ b/libsrc/api/JsonInfo.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -498,8 +499,8 @@ QJsonObject JsonInfo::getSystemInfo(const Hyperion* hyperion) hyperionInfo["gitremote"] = QString(HYPERION_GIT_REMOTE); hyperionInfo["time"] = QString(__DATE__ " " __TIME__); hyperionInfo["id"] = AuthManager::getInstance()->getID(); - hyperionInfo["rootPath"] = HyperionIManager::getInstance()->getRootPath(); - hyperionInfo["readOnlyMode"] = hyperion->getReadOnlyMode(); + hyperionInfo["configDatabaseFile"] = DBManager::getFileInfo().absoluteFilePath(); + hyperionInfo["readOnlyMode"] = DBManager::isReadOnly(); QCoreApplication* app = QCoreApplication::instance(); hyperionInfo["isGuiMode"] = qobject_cast(app) != nullptr; @@ -618,3 +619,9 @@ QJsonArray JsonInfo::discoverScreenInputs(const QJsonObject& params) const return screenInputs; } + +QJsonObject JsonInfo::getConfiguration(const QList& instancesfilter, bool addGlobalConfig, const QStringList& instanceFilteredTypes, const QStringList& globalFilterTypes ) +{ + ConfigImportExport configExport; + return configExport.getConfiguration(instancesfilter, addGlobalConfig, instanceFilteredTypes, globalFilterTypes ); +} diff --git a/libsrc/db/AuthTable.cpp b/libsrc/db/AuthTable.cpp new file mode 100644 index 00000000..32ce8e55 --- /dev/null +++ b/libsrc/db/AuthTable.cpp @@ -0,0 +1,176 @@ + +// hyperion +#include +#include + +// qt +#include +#include + +/// construct wrapper with auth table +AuthTable::AuthTable(QObject* parent) + : DBManager(parent) +{ + // init Auth table + setTable("auth"); + // create table columns + createTable(QStringList()<<"user TEXT"<<"password BLOB"<<"token BLOB"<<"salt BLOB"<<"comment TEXT"<<"id TEXT"<<"created_at TEXT"<<"last_use TEXT"); +}; + +bool AuthTable::createUser(const QString& user, const QString& password) +{ + // new salt + QByteArray salt = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); + QVariantMap map; + map["user"] = user; + map["salt"] = salt; + map["password"] = hashPasswordWithSalt(password,salt); + map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + return createRecord({{"user",user}}, map); +} + +bool AuthTable::userExist(const QString& user) +{ + return recordExists({{"user",user}}); +} + +bool AuthTable::isUserAuthorized(const QString& user, const QString& password) +{ + if(userExist(user) && (calcPasswordHashOfUser(user, password) == getPasswordHashOfUser(user))) + { + updateUserUsed(user); + return true; + } + return false; +} + +bool AuthTable::isUserTokenAuthorized(const QString& usr, const QString& token) +{ + if(getUserToken(usr) == token.toUtf8()) + { + updateUserUsed(usr); + return true; + } + return false; +} + +bool AuthTable::setUserToken(const QString& user) +{ + QVariantMap map; + map["token"] = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); + + return updateRecord({{"user",user}}, map); +} + +const QByteArray AuthTable::getUserToken(const QString& user) +{ + QVariantMap results; + getRecord({{"user",user}}, results, QStringList()<<"token"); + + return results["token"].toByteArray(); +} + +bool AuthTable::updateUserPassword(const QString& user, const QString& newPassword) +{ + QVariantMap map; + map["password"] = calcPasswordHashOfUser(user, newPassword); + + return updateRecord({{"user",user}}, map); +} + +bool AuthTable::resetHyperionUser() +{ + QVariantMap map; + map["password"] = calcPasswordHashOfUser(hyperion::DEFAULT_USER, hyperion::DEFAULT_PASSWORD); + + return updateRecord({{"user", hyperion::DEFAULT_USER}}, map); +} + +void AuthTable::updateUserUsed(const QString& user) +{ + QVariantMap map; + map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + updateRecord({{"user",user}}, map); +} + +bool AuthTable::tokenExist(const QString& token) +{ + QVariantMap map; + map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + VectorPair cond; + cond.append(CPair("token", hashToken(token))); + if(recordExists(cond)) + { + // update it + createRecord(cond,map); + return true; + } + return false; +} + +bool AuthTable::createToken(const QString& token, const QString& comment, const QString& identifier) +{ + QVariantMap map; + map["comment"] = comment; + map["id"] = identifierExist(identifier) ? QUuid::createUuid().toString().remove("{").remove("}").left(5) : identifier; + map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + return createRecord({{"token", hashToken(token)}}, map); +} + +bool AuthTable::deleteToken(const QString& identifier) +{ + return deleteRecord({{"id", identifier}}); +} + +bool AuthTable::renameToken(const QString &identifier, const QString &comment) +{ + QVariantMap map; + map["comment"] = comment; + + return updateRecord({{"id", identifier}}, map); +} + +const QVector AuthTable::getTokenList() +{ + QVector results; + getRecords(results, QStringList() << "comment" << "id" << "last_use"); + + return results; +} + +bool AuthTable::identifierExist(const QString& identifier) +{ + return recordExists({{"id", identifier}}); +} + +const QByteArray AuthTable::getPasswordHashOfUser(const QString& user) +{ + QVariantMap results; + getRecord({{"user",user}}, results, QStringList()<<"password"); + + return results["password"].toByteArray(); +} + +const QByteArray AuthTable::calcPasswordHashOfUser(const QString& user, const QString& password) +{ + // get salt + QVariantMap results; + getRecord({{"user",user}}, results, QStringList()<<"salt"); + + // calc + return hashPasswordWithSalt(password,results["salt"].toByteArray()); +} + +const QByteArray AuthTable::hashPasswordWithSalt(const QString& password, const QByteArray& salt) +{ + return QCryptographicHash::hash(password.toUtf8().append(salt), QCryptographicHash::Sha512).toHex(); +} + +const QByteArray AuthTable::hashToken(const QString& token) +{ + return QCryptographicHash::hash(token.toUtf8(), QCryptographicHash::Sha512).toHex(); +} diff --git a/libsrc/db/CMakeLists.txt b/libsrc/db/CMakeLists.txt index 1beb3fe5..979c099a 100644 --- a/libsrc/db/CMakeLists.txt +++ b/libsrc/db/CMakeLists.txt @@ -1,10 +1,17 @@ add_library(database ${CMAKE_SOURCE_DIR}/include/db/AuthTable.h ${CMAKE_SOURCE_DIR}/include/db/DBManager.h + ${CMAKE_SOURCE_DIR}/include/db/ConfigImportExport.h ${CMAKE_SOURCE_DIR}/include/db/InstanceTable.h ${CMAKE_SOURCE_DIR}/include/db/MetaTable.h ${CMAKE_SOURCE_DIR}/include/db/SettingsTable.h + ${CMAKE_SOURCE_DIR}/libsrc/db/AuthTable.cpp ${CMAKE_SOURCE_DIR}/libsrc/db/DBManager.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/ConfigImportExport.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/InstanceTable.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/MetaTable.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/SettingsTable.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/DB_schemas.qrc ) target_link_libraries(database diff --git a/libsrc/db/ConfigImportExport.cpp b/libsrc/db/ConfigImportExport.cpp new file mode 100644 index 00000000..a47d2827 --- /dev/null +++ b/libsrc/db/ConfigImportExport.cpp @@ -0,0 +1,261 @@ +#include "db/SettingsTable.h" +#include +#include + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +ConfigImportExport::ConfigImportExport(QObject* parent) + : DBManager(parent) +{ + Q_INIT_RESOURCE(DB_schemas); +} + +QPair ConfigImportExport::importJson(const QString& configFile) +{ + Info(_log,"Import configuration file '%s'", QSTRING_CSTR(configFile)); + + QJsonObject config; + QPair result = JsonUtils::readFile(configFile, config, _log, false); + + if (!result.first) + { + QString errorText = QString("Import configuration file '%s' failed!").arg(configFile); + result.second.prepend(errorText); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + return result; + } + return setConfiguration(config); +} + +bool ConfigImportExport::exportJson(const QString& path) const +{ + bool isExported {false}; + + QDir exportPath{path}; + if (path.isEmpty()) + { + exportPath.setPath(getDataDirectory().absoluteFilePath("archive")); + } + + QString jsonFile; + if (QDir().mkpath(exportPath.absolutePath())) + { + const QJsonObject configurtion = getConfiguration(); + if (!configurtion.isEmpty()) + { + const QJsonObject generalSettings = configurtion.value("global").toObject().value("settings").toObject().value("general").toObject(); + const QString configVersion = generalSettings.value("configVersion").toString(); + + jsonFile = exportPath.absoluteFilePath(QString("HyperionBackup_%1_v%2.json").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_hh:mm:ss:zzz"), configVersion )); + if (FileUtils::writeFile(jsonFile, QJsonDocument(configurtion).toJson(QJsonDocument::Indented), _log)) + { + isExported = true; + } + } + } + + if (isExported) + { + Info(_log, "Successfully exported configuration to '%s'", QSTRING_CSTR(jsonFile)); + } + else + { + Error(_log, "Failed to export configuration to '%s'", QSTRING_CSTR(jsonFile)); + } + + return isExported; +} + +QPair ConfigImportExport::setConfiguration(const QJsonObject& config) +{ + Debug(_log, "Start import JSON configuration"); + + QStringList errorList; + if (config.isEmpty()) + { + QString errorText {"No configuration data provided!"}; + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + return qMakePair (false, errorList ); + } + + // check basic message + QJsonObject schema = QJsonFactory::readSchema(":schema-config-exchange"); + QPair validationResult = JsonUtils::validate("importConfig", config, schema, _log); + if (!validationResult.first) + { + Error(_log, "Invalid JSON configuration data provided!"); + return qMakePair (false, validationResult.second ); + } + + Info(_log, "Create backup of current configuration"); + if (!exportJson()) + { + Warning(_log, "Backup of current configuration failed"); + } + + QSqlDatabase idb = getDB(); + if (!idb.transaction()) + { + QString errorText = QString("Could not create a database transaction. Error: %1").arg(idb.lastError().text()); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + return qMakePair (false, errorList ); + } + + bool errorOccured {false}; + if (!deleteTable("instances") || !deleteTable("settings")) + { + QString errorText = "Failed to clear tables before import"; + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + errorOccured = true; + } + else + { + SettingsTable settingsTableGlobal(GLOABL_INSTANCE_ID); + const QJsonObject globalConfig = config.value("global").toObject(); + const QJsonObject globalSettings = globalConfig.value("settings").toObject(); + + for (QJsonObject::const_iterator it = globalSettings.constBegin(); it != globalSettings.constEnd(); ++it) + { + if (!settingsTableGlobal.createSettingsRecord(it.key(), JsonUtils::jsonValueToQString(it.value()))) + { + errorOccured = true; + } + } + + InstanceTable instanceTable; + const QJsonArray instancesConfig = config.value("instances").toArray(); + quint8 instanceIdx {0}; + + for (auto instanceItem : instancesConfig) + { + QJsonObject instanceConfig = instanceItem.toObject(); + QString instanceName = instanceConfig.value("name").toString(QString("Instance %1").arg(instanceIdx)); + bool isInstanceEnabled = instanceConfig.value("enabled").toBool(true); + + if (instanceIdx == 0) + { + isInstanceEnabled = true; + } + + if (!instanceTable.createInstance(instanceName, instanceIdx) || + !instanceTable.setEnable(instanceIdx, isInstanceEnabled)) + { + errorOccured = true; + } + + SettingsTable settingsTableInstance(instanceIdx); + const QJsonObject instanceSettings = instanceConfig.value("settings").toObject(); + for (QJsonObject::const_iterator it = instanceSettings.constBegin(); it != instanceSettings.constEnd(); ++it) + { + if (!settingsTableInstance.createSettingsRecord(it.key(), JsonUtils::jsonValueToQString(it.value()))) + { + errorOccured = true; + } + } + ++instanceIdx; + } + + if (errorOccured) + { + QString errorText = "Errors occured during instances' and/or settings' configuration"; + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + } + } + + if (errorOccured) + { + if (!idb.rollback()) + { + QString errorText = QString("Could not create a database transaction. Error: %1").arg(idb.lastError().text()); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + } + } + + if (!idb.commit()) + { + QString errorText = QString("Could not finalise the database changes. Error: %1").arg(idb.lastError().text()); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + } + + if(errorList.isEmpty()) + { + Info(_log, "Successfully imported new configuration"); + } + + return qMakePair (errorList.isEmpty(), errorList ); +} + +QJsonObject ConfigImportExport::getConfiguration(const QList& instancesFilter, bool addGlobalConfig, const QStringList& instanceFilteredTypes, const QStringList& globalFilterTypes ) const +{ + QSqlDatabase idb = getDB(); + if (!idb.transaction()) + { + Error(_log, "Could not create a database transaction. Error: %s", QSTRING_CSTR(idb.lastError().text())); + return {}; + } + + InstanceTable instanceTable; + SettingsManager settingsManager(0, nullptr); + + QJsonObject config; + + if (addGlobalConfig) + { + QJsonObject globalConfig; + + MetaTable metaTable; + globalConfig.insert("uuid", metaTable.getUUID()); + globalConfig.insert("settings", settingsManager.getSettings({}, globalFilterTypes)); + config.insert("global", globalConfig); + } + + QList instances {instancesFilter}; + if (instances.isEmpty()) + { + instances = instanceTable.getAllInstanceIDs(); + } + + QList sortedInstances = instances; + std::sort(sortedInstances.begin(), sortedInstances.end()); + + QJsonArray instanceIdList; + QJsonArray configInstanceList; + for (const quint8 instanceIdx : sortedInstances) + { + QJsonObject instanceConfig; + instanceConfig.insert("id",instanceIdx); + instanceConfig.insert("name", instanceTable.getNamebyIndex(instanceIdx)); + instanceConfig.insert("enabled", instanceTable.isEnabled(instanceIdx)); + instanceConfig.insert("settings", settingsManager.getSettings(static_cast(instanceIdx), instanceFilteredTypes)); + configInstanceList.append(instanceConfig); + + instanceIdList.append(instanceIdx); + } + + config.insert("instanceIds", instanceIdList); + config.insert("instances", configInstanceList); + + if (!idb.commit()) + { + Error(_log, "Could not finalise a database transaction. Error: %s", QSTRING_CSTR(idb.lastError().text())); + } + + return config; +} diff --git a/libsrc/db/DBManager.cpp b/libsrc/db/DBManager.cpp index f4494967..4347a5af 100644 --- a/libsrc/db/DBManager.cpp +++ b/libsrc/db/DBManager.cpp @@ -1,38 +1,48 @@ +#include "utils/settings.h" #include #include #include #include #include -#include #include #include #include +#include #ifdef _WIN32 #include #endif -// not in header because of linking -static QString _rootPath; -static QThreadStorage _databasePool; +#define NO_SQLQUERY_LOGGING + +// Constants +namespace { + const char DATABASE_DIRECTORYNAME[] = "db"; + const char DATABASE_FILENAME[] = "hyperion.db"; + +} //End of constants + + +QDir DBManager::_dataDirectory; +QDir DBManager::_databaseDirectory; +QFileInfo DBManager::_databaseFile; +QThreadStorage DBManager::_databasePool; +bool DBManager::_isReadOnly {false}; DBManager::DBManager(QObject* parent) : QObject(parent) , _log(Logger::getInstance("DB")) - , _readonlyMode (false) { } -DBManager::~DBManager() +void DBManager::initializeDatabase(const QDir& dataDirectory, bool isReadOnly) { -} - -void DBManager::setRootPath(const QString& rootPath) -{ - _rootPath = rootPath; - // create directory - QDir().mkpath(_rootPath+"/db"); + _dataDirectory = dataDirectory; + _databaseDirectory.setPath(_dataDirectory.absoluteFilePath(DATABASE_DIRECTORYNAME)); + QDir().mkpath(_databaseDirectory.absolutePath()); + _databaseFile.setFile(_databaseDirectory,DATABASE_FILENAME); + _isReadOnly = isReadOnly; } void DBManager::setTable(const QString& table) @@ -43,38 +53,39 @@ void DBManager::setTable(const QString& table) QSqlDatabase DBManager::getDB() const { if(_databasePool.hasLocalData()) - return _databasePool.localData(); - else { - auto db = QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()); - _databasePool.setLocalData(db); - db.setDatabaseName(_rootPath+"/db/"+_dbn+".db"); - if(!db.open()) + return _databasePool.localData(); + } + auto database = QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()); + + if (isReadOnly()) { - Error(_log, "%s", QSTRING_CSTR(db.lastError().text())); + database.setConnectOptions("QSQLITE_OPEN_READONLY"); + } + Debug(Logger::getInstance("DB"), "Database is opened in %s mode", _isReadOnly ? "read-only" : "read/write"); + + _databasePool.setLocalData(database); + database.setDatabaseName(_databaseFile.absoluteFilePath()); + if(!database.open()) + { + Error(_log, "%s", QSTRING_CSTR(database.lastError().text())); throw std::runtime_error("Failed to open database connection!"); } - return db; - } + + return database; } bool DBManager::createRecord(const VectorPair& conditions, const QVariantMap& columns) const { - if ( _readonlyMode ) - { - return false; - } - if(recordExists(conditions)) { // if there is no column data, return if(columns.isEmpty()) + { return true; + } - if(!updateRecord(conditions, columns)) - return false; - - return true; + return updateRecord(conditions, columns); } QSqlDatabase idb = getDB(); @@ -84,14 +95,15 @@ bool DBManager::createRecord(const VectorPair& conditions, const QVariantMap& co QVariantList cValues; QStringList prep; QStringList placeh; + // prep merge columns & condition - QVariantMap::const_iterator i = columns.constBegin(); - while (i != columns.constEnd()) { - prep.append(i.key()); - cValues += i.value(); + QVariantMap::const_iterator columnIter = columns.constBegin(); + while (columnIter != columns.constEnd()) { + prep.append(columnIter.key()); + cValues += columnIter.value(); placeh.append("?"); - ++i; + ++columnIter; } for(const auto& pair : conditions) { @@ -101,21 +113,19 @@ bool DBManager::createRecord(const VectorPair& conditions, const QVariantMap& co cValues << pair.second; placeh.append("?"); } - query.prepare(QString("INSERT INTO %1 ( %2 ) VALUES ( %3 )").arg(_table,prep.join(", ")).arg(placeh.join(", "))); + query.prepare(QString("INSERT INTO %1 ( %2 ) VALUES ( %3 )").arg(_table,prep.join(", "), placeh.join(", "))); // add column & condition values - doAddBindValue(query, cValues); - if(!query.exec()) - { - Error(_log, "Failed to create record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prep.join(", ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - return true; + addBindValues(query, cValues); + + return executeQuery(query); } bool DBManager::recordExists(const VectorPair& conditions) const { if(conditions.isEmpty()) + { return false; + } QSqlDatabase idb = getDB(); QSqlQuery query(idb); @@ -127,35 +137,28 @@ bool DBManager::recordExists(const VectorPair& conditions) const for(const auto& pair : conditions) { - prepCond << pair.first+"=?"; + prepCond << pair.first+"= ?"; bindVal << pair.second; } query.prepare(QString("SELECT * FROM %1 %2").arg(_table,prepCond.join(" "))); - doAddBindValue(query, bindVal); - if(!query.exec()) + addBindValues(query, bindVal); + + if (!executeQuery(query)) { - Error(_log, "Failed recordExists(): '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); return false; } int entry = 0; - while (query.next()) { + while (query.next()) + { entry++; } - if(entry) - return true; - - return false; + return entry > 0; } bool DBManager::updateRecord(const VectorPair& conditions, const QVariantMap& columns) const { - if ( _readonlyMode ) - { - return false; - } - QSqlDatabase idb = getDB(); QSqlQuery query(idb); query.setForwardOnly(true); @@ -164,88 +167,75 @@ bool DBManager::updateRecord(const VectorPair& conditions, const QVariantMap& co QStringList prep; // prepare columns valus - QVariantMap::const_iterator i = columns.constBegin(); - while (i != columns.constEnd()) { - prep += i.key()+"=?"; - values += i.value(); + QVariantMap::const_iterator columnIter = columns.constBegin(); + while (columnIter != columns.constEnd()) { + prep += columnIter.key()+"= ?"; + values += columnIter.value(); - ++i; + ++columnIter; } // prepare condition values QStringList prepCond; QVariantList prepBindVal; - if(!conditions.isEmpty()) + if(!conditions.isEmpty()) { prepCond << "WHERE"; + } for(const auto& pair : conditions) { - prepCond << pair.first+"=?"; + prepCond << pair.first+"= ?"; prepBindVal << pair.second; } - query.prepare(QString("UPDATE %1 SET %2 %3").arg(_table,prep.join(", ")).arg(prepCond.join(" "))); + query.prepare(QString("UPDATE %1 SET %2 %3").arg(_table,prep.join(", "), prepCond.join(" "))); // add column values - doAddBindValue(query, values); + addBindValues(query, values); // add condition values - doAddBindValue(query, prepBindVal); - if(!query.exec()) - { - Error(_log, "Failed to update record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - return true; + addBindValues(query, prepBindVal); + + return executeQuery(query); } bool DBManager::getRecord(const VectorPair& conditions, QVariantMap& results, const QStringList& tColumns, const QStringList& tOrder) const { - QSqlDatabase idb = getDB(); - QSqlQuery query(idb); - query.setForwardOnly(true); - - QString sColumns("*"); - if(!tColumns.isEmpty()) - sColumns = tColumns.join(", "); - - QString sOrder(""); - if(!tOrder.isEmpty()) - { - sOrder = " ORDER BY "; - sOrder.append(tOrder.join(", ")); + QVector resultVector{}; + bool success = getRecords(conditions, resultVector, tColumns, tOrder); + if (success && !resultVector.isEmpty()) { + results = resultVector.first(); } - // prep conditions - QStringList prepCond; - QVariantList bindVal; - if(!conditions.isEmpty()) - prepCond << " WHERE"; - - for(const auto& pair : conditions) - { - prepCond << pair.first+"=?"; - bindVal << pair.second; - } - query.prepare(QString("SELECT %1 FROM %2%3%4").arg(sColumns,_table).arg(prepCond.join(" ")).arg(sOrder)); - doAddBindValue(query, bindVal); - - if(!query.exec()) - { - Error(_log, "Failed to get record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - - // go to first row - query.next(); - - QSqlRecord rec = query.record(); - for(int i = 0; i& results, const QStringList& tColumns, const QStringList& tOrder) const +{ + return getRecords({}, results, tColumns, tOrder); +} + +bool DBManager::getRecords(const VectorPair& conditions, QVector& results, const QStringList& tColumns, const QStringList& tOrder) const +{ + // prep conditions + QStringList conditionList; + QVariantList bindValues; + + for(const auto& pair : conditions) + { + conditionList << pair.first; + if (pair.second.isNull()) + { + conditionList << "IS NULL"; + } + else + { + conditionList << "= ?"; + bindValues << pair.second; + } + } + + return getRecords(conditionList.join((" ")), bindValues, results, tColumns, tOrder); +} + +bool DBManager::getRecords(const QString& condition, const QVariantList& bindValues, QVector& results, const QStringList& tColumns, const QStringList& tOrder) const { QSqlDatabase idb = getDB(); QSqlQuery query(idb); @@ -253,20 +243,28 @@ bool DBManager::getRecords(QVector& results, const QStringList& tCo QString sColumns("*"); if(!tColumns.isEmpty()) + { sColumns = tColumns.join(", "); + } QString sOrder(""); if(!tOrder.isEmpty()) { - sOrder = " ORDER BY "; + sOrder = "ORDER BY "; sOrder.append(tOrder.join(", ")); } - query.prepare(QString("SELECT %1 FROM %2%3").arg(sColumns,_table,sOrder)); - - if(!query.exec()) + // prep conditions + QString prepCond; + if(!condition.isEmpty()) + { + prepCond = QString("WHERE %1").arg(condition); + } + + query.prepare(QString("SELECT %1 FROM %2 %3 %4").arg(sColumns,_table, prepCond, sOrder)); + addBindValues(query, bindValues); + if (!executeQuery(query)) { - Error(_log, "Failed to get records: '%s' in table: '%s' Error: %s", QSTRING_CSTR(sColumns), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); return false; } @@ -288,11 +286,6 @@ bool DBManager::getRecords(QVector& results, const QStringList& tCo bool DBManager::deleteRecord(const VectorPair& conditions) const { - if ( _readonlyMode ) - { - return false; - } - if(conditions.isEmpty()) { Error(_log, "Oops, a deleteRecord() call wants to delete the entire table (%s)! Denied it", QSTRING_CSTR(_table)); @@ -310,29 +303,20 @@ bool DBManager::deleteRecord(const VectorPair& conditions) const for(const auto& pair : conditions) { - prepCond << pair.first+"=?"; + prepCond << pair.first+"= ?"; bindValues << pair.second; } query.prepare(QString("DELETE FROM %1 %2").arg(_table,prepCond.join(" "))); - doAddBindValue(query, bindValues); - if(!query.exec()) - { - Error(_log, "Failed to delete record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - return true; + addBindValues(query, bindValues); + + return executeQuery(query); } return false; } bool DBManager::createTable(QStringList& columns) const { - if ( _readonlyMode ) - { - return false; - } - if(columns.isEmpty()) { Error(_log,"Empty tables aren't supported!"); @@ -347,9 +331,9 @@ bool DBManager::createTable(QStringList& columns) const // empty tables aren't supported by sqlite, add one column QString tcolumn = columns.takeFirst(); // default CURRENT_TIMESTAMP is not supported by ALTER TABLE - if(!query.exec(QString("CREATE TABLE %1 ( %2 )").arg(_table,tcolumn))) + query.prepare(QString("CREATE TABLE %1 ( %2 )").arg(_table,tcolumn)); + if (!executeQuery(query)) { - Error(_log, "Failed to create table: '%s' Error: %s", QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); return false; } } @@ -358,8 +342,8 @@ bool DBManager::createTable(QStringList& columns) const int err = 0; for(const auto& column : columns) { - QStringList id = column.split(' '); - if(rec.indexOf(id.at(0)) == -1) + QStringList columName = column.split(' '); + if(rec.indexOf(columName.at(0)) == -1) { if(!createColumn(column)) { @@ -367,79 +351,91 @@ bool DBManager::createTable(QStringList& columns) const } } } - if(err) - return false; - - return true; + return err == 0; } bool DBManager::createColumn(const QString& column) const { - if ( _readonlyMode ) - { - return false; - } - QSqlDatabase idb = getDB(); QSqlQuery query(idb); - if(!query.exec(QString("ALTER TABLE %1 ADD COLUMN %2").arg(_table,column))) - { - Error(_log, "Failed to create column: '%s' in table: '%s' Error: %s", QSTRING_CSTR(column), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - return true; + + query.prepare(QString("ALTER TABLE %1 ADD COLUMN %2").arg(_table,column)); + return executeQuery(query); } bool DBManager::tableExists(const QString& table) const { QSqlDatabase idb = getDB(); QStringList tables = idb.tables(); - if(tables.contains(table)) - return true; - return false; + return tables.contains(table); } bool DBManager::deleteTable(const QString& table) const { - if ( _readonlyMode ) - { - return false; - } - if(tableExists(table)) { QSqlDatabase idb = getDB(); QSqlQuery query(idb); - if(!query.exec(QString("DROP TABLE %1").arg(table))) - { - Error(_log, "Failed to delete table: '%s' Error: %s", QSTRING_CSTR(table), QSTRING_CSTR(idb.lastError().text())); - return false; - } + + query.prepare(QString("DROP TABLE %1").arg(table)); + return executeQuery(query); } return true; } -void DBManager::doAddBindValue(QSqlQuery& query, const QVariantList& variants) const +void DBManager::addBindValues(QSqlQuery& query, const QVariantList& bindValues) const { - for(const auto& variant : variants) + if (!bindValues.isEmpty()) { - auto t = variant.userType(); - switch(t) + for(const auto& value : bindValues) { - case QMetaType::UInt: - case QMetaType::Int: - case QMetaType::Bool: - query.addBindValue(variant.toInt()); - break; - case QMetaType::Double: - query.addBindValue(variant.toFloat()); - break; - case QMetaType::QByteArray: - query.addBindValue(variant.toByteArray()); - break; - default: - query.addBindValue(variant.toString()); - break; + query.addBindValue(value); } } } + +QString DBManager::constructExecutedQuery(const QSqlQuery& query) const +{ + QString executedQuery = query.executedQuery(); + + // Check if the query uses positional placeholders + if (executedQuery.contains('?')) { + QVariantList boundValues = query.boundValues(); // Get bound values as a list + // Iterate through the bound values and replace placeholders + for (const QVariant &value : boundValues) { + // Replace the first occurrence of '?' with the actual value + QString valueStr; + if (value.canConvert()) + { + valueStr = value.toString(); + } + else + { + valueStr = "Unkown"; + } + executedQuery.replace(executedQuery.indexOf('?'), 1, valueStr); + } + } + return executedQuery; +} + +bool DBManager::executeQuery(QSqlQuery& query) const +{ + if(!query.exec()) + { + QString finalQuery = constructExecutedQuery(query); + QString errorText = query.lastError().text(); + + Debug(_log, "Database Error: '%s', SqlQuery: '%s'", QSTRING_CSTR(errorText), QSTRING_CSTR(finalQuery)); + Error(_log, "Database Error: '%s'", QSTRING_CSTR(errorText)); + + return false; + } + +#ifdef SQLQUERY_LOGGING + QString finalQuery = constructExecutedQuery(query); + Debug(_log, "SqlQuery executed: '%s'", QSTRING_CSTR(finalQuery)); +#endif + + return true; +} diff --git a/libsrc/db/DB_schema/schema-config-exchange.json b/libsrc/db/DB_schema/schema-config-exchange.json new file mode 100644 index 00000000..d0f757ca --- /dev/null +++ b/libsrc/db/DB_schema/schema-config-exchange.json @@ -0,0 +1,70 @@ +{ + "type": "object", + "required": true, + "properties": { + "global": { + "type": "object", + "properties": { + "settings": { + "type": "object", + "required": true, + "additionalProperties": { + "type": [ + "object", + "array" + ], + "properties": {}, + "additionalProperties": true + } + }, + "uuid": { + "type": "string", + "format": "uuid", + "required": false + } + } + }, + "instanceIds": { + "type": "array", + "required": false, + "items": { + "type": "integer" + }, + "minItems": 1 + }, + "instances": { + "type": "array", + "required": false, + "items": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "name": { + "type": "string", + "minLength": 5 + }, + "settings": { + "type": "object", + "required": true, + "additionalProperties": { + "type": [ + "object", + "array" + ], + "properties": {}, + "additionalProperties": true + } + } + } + } + } + }, + "additionalProperties": true +} diff --git a/libsrc/db/DB_schemas.qrc b/libsrc/db/DB_schemas.qrc new file mode 100644 index 00000000..428bedac --- /dev/null +++ b/libsrc/db/DB_schemas.qrc @@ -0,0 +1,5 @@ + + + DB_schema/schema-config-exchange.json + + diff --git a/libsrc/db/InstanceTable.cpp b/libsrc/db/InstanceTable.cpp new file mode 100644 index 00000000..702e124a --- /dev/null +++ b/libsrc/db/InstanceTable.cpp @@ -0,0 +1,142 @@ + +// db +#include +#include + +// qt +#include + +InstanceTable::InstanceTable(QObject* parent) + : DBManager(parent) +{ + // Init instance table + setTable("instances"); + createTable(QStringList()<<"instance INTEGER"<<"friendly_name TEXT"<<"enabled INTEGER DEFAULT 0"<<"last_use TEXT"); +} + +bool InstanceTable::createInstance(const QString& name, quint8& inst) +{ + // check duplicate + if(!recordExists({{"friendly_name", name}})) + { + QList instanceList = getAllInstanceIDs(false); + + inst = 0; + while (instanceList.contains(inst)) + { + ++inst; + } + + // create + QVariantMap data; + data["friendly_name"] = name; + data["instance"] = inst; + return createRecord({}, data); + } + + return false; +} + +bool InstanceTable::deleteInstance(quint8 inst) +{ + Debug(_log,""); + if(deleteRecord({{"instance",inst}})) + { + // delete settings entries + SettingsTable settingsTable(inst); + settingsTable.deleteInstance(); + return true; + } + return false; +} + +bool InstanceTable::saveName(quint8 inst, const QString& name) +{ + // check duplicate + if(!recordExists({{"friendly_name", name}})) + { + if(instanceExist(inst)) + { + return updateRecord({{"instance",inst}}, {{"friendly_name", name}}); + } + } + return false; +} + +QVector InstanceTable::getAllInstances(bool onlyEnabled) +{ + QVector results; + + VectorPair onlyEnabledCondition {}; + if (onlyEnabled) + { + onlyEnabledCondition = {{"enabled", true}}; + } + getRecords(onlyEnabledCondition, results, {}, {"instance ASC"}); + return results; +} + +QList InstanceTable::getAllInstanceIDs (bool onlyEnabled) +{ + QVector instanceList = getAllInstances(onlyEnabled); + QList instanceIds; + for (const QVariantMap &idx : std::as_const(instanceList)) + { + instanceIds.append(static_cast(idx.value("instance").toInt())); + } + + return instanceIds; +} + +bool InstanceTable::instanceExist(quint8 inst) +{ + return recordExists({{"instance",inst}}); +} + +QString InstanceTable::getNamebyIndex(quint8 index) +{ + QVariantMap results; + getRecord({{"instance", index}}, results, {"friendly_name"}); + + QString name = results["friendly_name"].toString(); + return name.isEmpty() ? "NOT FOUND" : name; +} + +bool InstanceTable::setLastUse(quint8 inst) +{ + return updateRecord({{"instance", inst}}, {{"last_use", QDateTime::currentDateTimeUtc().toString(Qt::ISODate)}}); +} + +bool InstanceTable::setEnable(quint8 inst, bool newState) +{ + return updateRecord({{"instance", inst}}, {{"enabled", newState}}); +} + +bool InstanceTable::isEnabled(quint8 inst) +{ + QVariantMap results; + getRecord({{"instance", inst}}, results); + + return results["enabled"].toBool(); +} + +void InstanceTable::createDefaultInstance() +{ + if(instanceExist(0)) + { + setEnable(0, true); + } + else + { + if(createRecord({{"instance", 0}}, {{"friendly_name", "First LED Hardware instance"}})) + { + setEnable(0, true); + } + else + { + throw std::runtime_error("Failed to create Hyperion root instance in db! This should never be the case..."); + } + } +} + + diff --git a/libsrc/db/MetaTable.cpp b/libsrc/db/MetaTable.cpp new file mode 100644 index 00000000..1ee723e6 --- /dev/null +++ b/libsrc/db/MetaTable.cpp @@ -0,0 +1,47 @@ + +#include + +// qt +#include +#include +#include +#include + +MetaTable::MetaTable(QObject* parent) + : DBManager(parent) +{ + setTable("meta"); + createTable(QStringList()<<"uuid TEXT"<<"created_at TEXT"); +}; + +QString MetaTable::getUUID() const +{ + QVector results; + getRecords(results, QStringList() << "uuid"); + + for(const auto & entry : std::as_const(results)) + { + if(!entry["uuid"].toString().isEmpty()) + { + return entry["uuid"].toString(); + } + } + + // create new uuidv5 based on net adapter MAC, save to db and return + QString hash; + foreach(QNetworkInterface interface, QNetworkInterface::allInterfaces()) + { + if (!(interface.flags() & QNetworkInterface::IsLoopBack)) + { + hash = QCryptographicHash::hash(interface.hardwareAddress().toLocal8Bit(),QCryptographicHash::Sha1).toHex(); + break; + } + } + const QString newUuid = QUuid::createUuidV5(QUuid(), hash).toString().mid(1, 36); + QVariantMap map; + map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + createRecord({{"uuid",newUuid}}, map); + + return newUuid; +} diff --git a/libsrc/db/SettingsTable.cpp b/libsrc/db/SettingsTable.cpp new file mode 100644 index 00000000..f4dc0050 --- /dev/null +++ b/libsrc/db/SettingsTable.cpp @@ -0,0 +1,99 @@ +#include + +#include +#include + +SettingsTable::SettingsTable(quint8 instance, QObject* parent) + : DBManager(parent) + , _hyperion_inst(instance) +{ + setTable("settings"); + // create table columns + createTable(QStringList()<<"type TEXT"<<"config TEXT"<<"hyperion_inst INTEGER"<<"updated_at TEXT"); +} + +bool SettingsTable::createSettingsRecord(const QString& type, const QString& config) const +{ + QVariantMap map; + map["config"] = config; + map["updated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isSettingGlobal(type)) + { + cond.append(CPair("AND hyperion_inst",_hyperion_inst)); + } + return createRecord(cond, map); +} + +bool SettingsTable::recordExist(const QString& type) const +{ + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isSettingGlobal(type)) + { + cond.append(CPair("AND hyperion_inst",_hyperion_inst)); + } + return recordExists(cond); +} + +QJsonDocument SettingsTable::getSettingsRecord(const QString& type) const +{ + QVariantMap results; + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isSettingGlobal(type)) + { + cond.append(CPair("AND hyperion_inst",_hyperion_inst)); + } + getRecord(cond, results, QStringList("config")); + return QJsonDocument::fromJson(results["config"].toByteArray()); +} + +QString SettingsTable::getSettingsRecordString(const QString& type) const +{ + QVariantMap results; + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isSettingGlobal(type)) + { + cond.append(CPair("AND hyperion_inst",_hyperion_inst)); + } + getRecord(cond, results, QStringList("config")); + return results["config"].toString(); +} + +void SettingsTable::deleteInstance() const +{ + deleteRecord({{"hyperion_inst",_hyperion_inst}}); +} + +const QVector& SettingsTable::getGlobalSettingTypes() { + static const QVector globalSettingTypes = { + "jsonServer", + "protoServer", + "flatbufServer", + "forwarder", + "webConfig", + "network", + "framegrabber", + "grabberV4L2", + "grabberAudio", + "osEvents", + "cecEvents", + "schedEvents", + "general", + "logger" + }; + return globalSettingTypes; +} + +bool SettingsTable::isSettingGlobal(const QString& type) +{ + return getGlobalSettingTypes().contains(type); +} diff --git a/libsrc/hyperion/AuthManager.cpp b/libsrc/hyperion/AuthManager.cpp index 4e60807e..ba22cbaf 100644 --- a/libsrc/hyperion/AuthManager.cpp +++ b/libsrc/hyperion/AuthManager.cpp @@ -10,10 +10,10 @@ AuthManager *AuthManager::manager = nullptr; -AuthManager::AuthManager(QObject *parent, bool readonlyMode) +AuthManager::AuthManager(QObject *parent) : QObject(parent) - , _authTable(new AuthTable("", this, readonlyMode)) - , _metaTable(new MetaTable(this, readonlyMode)) + , _authTable(new AuthTable(this)) + , _metaTable(new MetaTable(this)) , _pendingRequests() , _timer(new QTimer(this)) , _authBlockTimer(new QTimer(this)) @@ -209,7 +209,7 @@ QVector AuthManager::getPendingRequests() const bool AuthManager::renameToken(const QString &id, const QString &comment) { - if (_authTable->idExist(id)) + if (_authTable->identifierExist(id)) { if (_authTable->renameToken(id, comment)) { @@ -222,7 +222,7 @@ bool AuthManager::renameToken(const QString &id, const QString &comment) bool AuthManager::deleteToken(const QString &id) { - if (_authTable->idExist(id)) + if (_authTable->identifierExist(id)) { if (_authTable->deleteToken(id)) { diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index de719578..77bedcef 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -47,10 +47,10 @@ #include #endif -Hyperion::Hyperion(quint8 instance, bool readonlyMode) +Hyperion::Hyperion(quint8 instance) : QObject() , _instIndex(instance) - , _settingsManager(new SettingsManager(instance, this, readonlyMode)) + , _settingsManager(new SettingsManager(instance, this)) , _componentRegister(nullptr) , _ledString(LedString::createLedString(getSetting(settings::LEDS).array(), hyperion::createColorOrder(getSetting(settings::DEVICE).object()))) , _imageProcessor(nullptr) @@ -73,7 +73,6 @@ Hyperion::Hyperion(quint8 instance, bool readonlyMode) #if defined(ENABLE_BOBLIGHT_SERVER) , _boblightServer(nullptr) #endif - , _readOnlyMode(readonlyMode) { qRegisterMetaType("ComponentList"); @@ -320,16 +319,22 @@ QJsonDocument Hyperion::getSetting(settings::type type) const return _settingsManager->getSetting(type); } +// TODO: Remove function, if UI is able to handle full configuration +QJsonObject Hyperion::getQJsonConfig() const +{ + const QJsonObject instanceConfig = _settingsManager->getSettings(); + const QJsonObject globalConfig = _settingsManager->getSettings({},QStringList()); + + QVariantMap map = instanceConfig.toVariantMap(); + map.insert(globalConfig.toVariantMap()); + return QJsonObject::fromVariantMap(map); +} + bool Hyperion::saveSettings(const QJsonObject& config, bool correct) { return _settingsManager->saveSettings(config, correct); } -bool Hyperion::restoreSettings(const QJsonObject& config, bool correct) -{ - return _settingsManager->restoreSettings(config, correct); -} - int Hyperion::getLatchTime() const { return _ledDeviceWrapper->getLatchTime(); @@ -597,11 +602,6 @@ int Hyperion::setEffect(const QString &effectName, const QJsonObject &args, int } #endif -QJsonObject Hyperion::getQJsonConfig() const -{ - return _settingsManager->getSettings(); -} - void Hyperion::setLedMappingType(int mappingType) { if(mappingType != _imageProcessor->getUserLedMappingType()) diff --git a/libsrc/hyperion/HyperionIManager.cpp b/libsrc/hyperion/HyperionIManager.cpp index 268cbf75..254d1bf0 100644 --- a/libsrc/hyperion/HyperionIManager.cpp +++ b/libsrc/hyperion/HyperionIManager.cpp @@ -9,15 +9,14 @@ HyperionIManager* HyperionIManager::HIMinstance; -HyperionIManager::HyperionIManager(const QString& rootPath, QObject* parent, bool readonlyMode) +HyperionIManager::HyperionIManager(QObject* parent) : QObject(parent) , _log(Logger::getInstance("HYPERION-INSTMGR")) - , _instanceTable( new InstanceTable(rootPath, this, readonlyMode) ) - , _rootPath( rootPath ) - , _readonlyMode(readonlyMode) + , _instanceTable( new InstanceTable()) { HIMinstance = this; qRegisterMetaType("InstanceState"); + _instanceTable->createDefaultInstance(); } Hyperion* HyperionIManager::getHyperionInstance(quint8 instance) @@ -45,14 +44,32 @@ QVector HyperionIManager::getInstanceData() const return instances; } +QString HyperionIManager::getInstanceName(quint8 inst) +{ + return _instanceTable->getNamebyIndex(inst); +} + QList HyperionIManager::getRunningInstanceIdx() const { return _runningInstances.keys(); } +QList HyperionIManager::getInstanceIds() const +{ + return _instanceTable->getAllInstanceIDs(); +} + + void HyperionIManager::startAll() { - for(const auto & entry : _instanceTable->getAllInstances(true)) + const QVector instances = _instanceTable->getAllInstances(true); + if (instances.isEmpty()) + { + Error(_log, "No enabled instances found to be started"); + return; + } + + for(const auto & entry : instances) { startInstance(entry["instance"].toInt()); } @@ -62,7 +79,7 @@ void HyperionIManager::stopAll() { // copy the instances due to loop corruption, even with .erase() return next iter QMap instCopy = _runningInstances; - for(const auto instance : instCopy) + for(auto *const instance : instCopy) { instance->stop(); } @@ -131,7 +148,7 @@ bool HyperionIManager::startInstance(quint8 inst, bool block, QObject* caller, i { QThread* hyperionThread = new QThread(); hyperionThread->setObjectName("HyperionThread"); - Hyperion* hyperion = new Hyperion(inst, _readonlyMode); + Hyperion* hyperion = new Hyperion(inst); hyperion->moveToThread(hyperionThread); // setup thread management connect(hyperionThread, &QThread::started, hyperion, &Hyperion::start); @@ -156,7 +173,7 @@ bool HyperionIManager::startInstance(quint8 inst, bool block, QObject* caller, i if(block) { - while(!hyperionThread->isRunning()){}; + while(!hyperionThread->isRunning()){} } if (!_pendingRequests.contains(inst) && caller != nullptr) @@ -203,10 +220,10 @@ bool HyperionIManager::stopInstance(quint8 inst) bool HyperionIManager::createInstance(const QString& name, bool start) { - quint8 inst; + quint8 inst = 0; if(_instanceTable->createInstance(name, inst)) { - Info(_log,"New Hyperion instance created with name '%s'",QSTRING_CSTR(name)); + Info(_log,"New Hyperion instance [%d] created with name '%s'", inst, QSTRING_CSTR(name)); emit instanceStateChanged(InstanceState::H_CREATED, inst, name); emit change(); @@ -221,7 +238,9 @@ bool HyperionIManager::deleteInstance(quint8 inst) { // inst 0 can't be deleted if(!isInstAllowed(inst)) + { return false; + } // stop it if required as blocking and wait stopInstance(inst); diff --git a/libsrc/hyperion/SettingsManager.cpp b/libsrc/hyperion/SettingsManager.cpp index cd5e8727..91191c00 100644 --- a/libsrc/hyperion/SettingsManager.cpp +++ b/libsrc/hyperion/SettingsManager.cpp @@ -12,30 +12,20 @@ #include #include -// write config to filesystem -#include - #include using namespace semver; -// Constants -namespace { - const char DEFAULT_VERSION[] = "2.0.0-alpha.8"; -} //End of constants - QJsonObject SettingsManager::schemaJson; -SettingsManager::SettingsManager(quint8 instance, QObject* parent, bool readonlyMode) +SettingsManager::SettingsManager(quint8 instance, QObject* parent) : QObject(parent) , _log(Logger::getInstance("SETTINGSMGR", "I" + QString::number(instance))) , _instance(instance) , _sTable(new SettingsTable(instance, this)) , _configVersion(DEFAULT_VERSION) , _previousVersion(DEFAULT_VERSION) - , _readonlyMode(readonlyMode) { - _sTable->setReadonlyMode(_readonlyMode); // get schema if (schemaJson.isEmpty()) { @@ -188,26 +178,75 @@ SettingsManager::SettingsManager(quint8 instance, QObject* parent, bool readonly QJsonDocument SettingsManager::getSetting(settings::type type) const { - return _sTable->getSettingsRecord(settings::typeToString(type)); + return getSetting(settings::typeToString(type)); } -QJsonObject SettingsManager::getSettings() const +QJsonDocument SettingsManager::getSetting(const QString& type) const { - QJsonObject config; - for (const auto& key : _qconfig.keys()) + return _sTable->getSettingsRecord(type); +} + +QJsonObject SettingsManager::getSettings(const QStringList& filteredTypes ) const +{ + return getSettings(_instance, filteredTypes); +} + +QJsonObject SettingsManager::getSettings(const QVariant& instance, const QStringList& filteredTypes ) const +{ + QJsonObject settingsObject; + QStringList settingsKeys({ "type", "config" }); + QString settingsCondition; + QVariantList conditionValues; + + if (instance.isNull() ) { - //Read all records from database to ensure that global settings are read across instances - QJsonDocument doc = _sTable->getSettingsRecord(key); - if (doc.isArray()) - { - config.insert(key, doc.array()); + settingsCondition = "hyperion_inst IS NULL"; + } + else + { + settingsCondition = "hyperion_inst = ?"; + conditionValues.append(instance); + } + + if (!filteredTypes.isEmpty()) + { + QStringList seletedSettingTypes; + for (const auto &type : filteredTypes) { + seletedSettingTypes << QString("%1=?").arg("type"); + conditionValues.append(type); } - else + settingsCondition += QString (" AND (%1)").arg(seletedSettingTypes.join(" OR ")); + } + + QVector settingsList; + if (_sTable->getRecords(settingsCondition, conditionValues, settingsList, settingsKeys)) + { + for (const QVariantMap &setting : std::as_const(settingsList)) { - config.insert(key, doc.object()); + QString type = setting.value("type").toString(); + QByteArray configObject = setting.value("config").toByteArray(); + QJsonDocument jsonDoc = QJsonDocument::fromJson(configObject); + + if (!jsonDoc.isNull()) + { + QJsonValue config; + + if (jsonDoc.isArray()) + { + config = jsonDoc.array(); + } + else if (jsonDoc.isObject()) + { + config = jsonDoc.object(); + } + settingsObject.insert(type, config); + } else + { + qWarning() << "Failed to parse JSON string:" << configObject; + } } } - return config; + return settingsObject; } bool SettingsManager::restoreSettings(QJsonObject config, bool correct) @@ -299,7 +338,7 @@ inline QString fixVersion(const QString& version) return newVersion; } -bool SettingsManager::resolveConfigVersion(QJsonObject& config) +bool SettingsManager::resolveConfigVersion(const QJsonObject& config) { bool isValid = false; if (config.contains("general")) diff --git a/libsrc/utils/JsonUtils.cpp b/libsrc/utils/JsonUtils.cpp index 1fd44f68..19303cd7 100644 --- a/libsrc/utils/JsonUtils.cpp +++ b/libsrc/utils/JsonUtils.cpp @@ -7,6 +7,8 @@ //qt includes #include #include +#include +#include #include #include @@ -158,4 +160,31 @@ namespace JsonUtils { } return true; } + + QString jsonValueToQString(const QJsonValue &value, QJsonDocument::JsonFormat format) + { + switch (value.type()) { + case QJsonValue::Object: + { + return QJsonDocument(value.toObject()).toJson(format); + } + case QJsonValue::Array: + { + return QJsonDocument(value.toArray()).toJson(format); + } + case QJsonValue::String: + case QJsonValue::Double: + case QJsonValue::Bool: + { + return value.toString(); + } + case QJsonValue::Null: + { + return "Null"; + } + default: + break; + } + return QString(); + } }; diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index 6c71475d..27ce5c49 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -65,14 +65,14 @@ HyperionDaemon* HyperionDaemon::daemon = nullptr; -HyperionDaemon::HyperionDaemon(const QString& rootPath, QObject* parent, bool logLvlOverwrite, bool readonlyMode) +HyperionDaemon::HyperionDaemon(const QString& rootPath, QObject* parent, bool logLvlOverwrite) : QObject(parent), _log(Logger::getInstance("DAEMON")) - , _instanceManager(new HyperionIManager(rootPath, this, readonlyMode)) - , _settingsManager(new SettingsManager(GLOABL_INSTANCE_ID, this, readonlyMode)) // init settings, this settingsManager accesses global settings which are independent from instances + , _instanceManager(new HyperionIManager(this)) + , _settingsManager(new SettingsManager(GLOABL_INSTANCE_ID, this)) // init settings, this settingsManager accesses global settings which are independent from instances #if defined(ENABLE_EFFECTENGINE) , _pyInit(new PythonInit()) #endif - , _authManager(new AuthManager(this, readonlyMode)) + , _authManager(new AuthManager(this)) , _netOrigin(new NetOrigin(this)) , _currVideoMode(VideoMode::VIDEO_2D) { diff --git a/src/hyperiond/hyperiond.h b/src/hyperiond/hyperiond.h index 70c5b74d..d15689c9 100644 --- a/src/hyperiond/hyperiond.h +++ b/src/hyperiond/hyperiond.h @@ -108,7 +108,7 @@ class HyperionDaemon : public QObject friend SysTray; public: - HyperionDaemon(const QString& rootPath, QObject *parent, bool logLvlOverwrite, bool readonlyMode = false); + HyperionDaemon(const QString& rootPath, QObject *parent, bool logLvlOverwrite); ~HyperionDaemon() override; /// diff --git a/src/hyperiond/main.cpp b/src/hyperiond/main.cpp index 6dfd35c4..5136140a 100644 --- a/src/hyperiond/main.cpp +++ b/src/hyperiond/main.cpp @@ -1,3 +1,4 @@ + #include #include #include @@ -35,6 +36,7 @@ #include #include #include +#include #include <../../include/db/AuthTable.h> #include "detectProcess.h" @@ -131,7 +133,10 @@ int main(int argc, char** argv) BooleanOption & versionOption = parser.add (0x0, "version", "Show version information"); Option & userDataOption = parser.add