Refactor Settings DB and Handling (#1786)

* Refactor config API

* Corrections

* Test Qt 6.8

* Revert "Test Qt 6.8"

This reverts commit eceebec49e.

* Corrections 2

* Update Changelog

* Add configFilter element for getconfig call

* Do not create errors for DB updates when in read-only mode

* Have configuration migration and validation before Hyperion starts

* Correct Tests

* Corrections

* Add migration items

* Correct windows build

* Ensure that first instance as default one exists

* Remove dependency between AuthManager and SSDPHandler

* Correct typos

* Address CodeQL findings

* Replace CamkeSettings by Presets and provide debug scenarios
This commit is contained in:
LordGrey
2024-09-30 22:03:13 +02:00
committed by GitHub
parent aed4abc03b
commit ecceb4e7ae
88 changed files with 4407 additions and 2472 deletions

176
libsrc/db/AuthTable.cpp Normal file
View File

@@ -0,0 +1,176 @@
// hyperion
#include <db/AuthTable.h>
#include <QCryptographicHash>
// qt
#include <QDateTime>
#include <QUuid>
/// 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<QVariantMap> AuthTable::getTokenList()
{
QVector<QVariantMap> 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();
}

View File

@@ -1,10 +1,18 @@
add_library(database
${CMAKE_SOURCE_DIR}/include/db/AuthTable.h
${CMAKE_SOURCE_DIR}/include/db/DBManager.h
${CMAKE_SOURCE_DIR}/include/db/DBConfigManager.h
${CMAKE_SOURCE_DIR}/include/db/DBMigrationManager.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/DBConfigManager.cpp
${CMAKE_SOURCE_DIR}/libsrc/db/DBMigrationManager.cpp
${CMAKE_SOURCE_DIR}/libsrc/db/InstanceTable.cpp
${CMAKE_SOURCE_DIR}/libsrc/db/MetaTable.cpp
${CMAKE_SOURCE_DIR}/libsrc/db/SettingsTable.cpp
)
target_link_libraries(database

View File

@@ -0,0 +1,382 @@
#include <db/DBConfigManager.h>
#include <db/DBMigrationManager.h>
#include "db/SettingsTable.h"
#include <db/MetaTable.h>
#include <db/InstanceTable.h>
#include <hyperion/SettingsManager.h>
#include <qsqlrecord.h>
#include <utils/JsonUtils.h>
#include <utils/jsonschema/QJsonFactory.h>
#include <HyperionConfig.h>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QDir>
#include <QDateTime>
namespace {
const char SETTINGS_FULL_SCHEMA_FILE[] = ":/schema-settings-full.json";
}
DBConfigManager::DBConfigManager(QObject* parent)
: DBManager(parent)
{
}
QPair<bool, QStringList> DBConfigManager::importJson(const QString& configFile)
{
Info(_log,"Import configuration file '%s'", QSTRING_CSTR(configFile));
QJsonObject config;
QPair<bool, QStringList> result = JsonUtils::readFile(configFile, config, _log, false);
if (!result.first)
{
QString errorText = QString("Import configuration file '%1' failed!").arg(configFile);
result.second.prepend(errorText);
Error(_log, "%s", QSTRING_CSTR(errorText));
return result;
}
DBMigrationManager migtrationManger;
migtrationManger.migrateSettings(config);
return updateConfiguration(config, true);
}
bool DBConfigManager::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<bool, QStringList> DBConfigManager::validateConfiguration()
{
QJsonObject config = getConfiguration();
return validateConfiguration(config, false);
}
QPair<bool, QStringList> DBConfigManager::validateConfiguration(QJsonObject& config, bool doCorrections)
{
Info(_log, "Validate configuration%s", doCorrections ? " and apply corrections, if required" : "");
QStringList errorList;
if (config.isEmpty())
{
QString errorText {"No configuration data provided!"};
Error(_log, "'%s'", QSTRING_CSTR(errorText));
errorList.append(errorText);
return qMakePair (false, errorList );
}
QJsonObject schema = QJsonFactory::readSchema(SETTINGS_FULL_SCHEMA_FILE);
bool wasCorrected {false};
if (doCorrections)
{
QJsonValue configValue(config);
QPair<bool, QStringList> correctionResult = JsonUtils::correct(__FUNCTION__, configValue, schema, _log);
wasCorrected = correctionResult.first;
if (wasCorrected)
{
config = configValue.toObject();
}
}
else
{
QPair<bool, QStringList> validationResult = JsonUtils::validate(__FUNCTION__, config, schema, _log);
if (!validationResult.first)
{
Error(_log, "Configuration has errors!");
return qMakePair (false, validationResult.second );
}
}
Info(_log, "Configuration is valid%s", wasCorrected ? ", but had to be corrected" : "");
return qMakePair (true, errorList );
}
QPair<bool, QStringList> DBConfigManager::updateConfiguration()
{
QJsonObject config = getConfiguration();
return updateConfiguration(config, true);
}
QPair<bool, QStringList> DBConfigManager::addMissingDefaults()
{
Debug(_log, "Add default settings for missing configuration items");
QStringList errorList;
SettingsTable globalSettingsTable;
QPair<bool, QStringList> result = globalSettingsTable.addMissingDefaults();
errorList.append(result.second);
InstanceTable instanceTable;
//Ensure that first instance as default one exists
instanceTable.createDefaultInstance();
const QList<quint8> instances = instanceTable.getAllInstanceIDs();
for (const auto &instanceIdx : instances)
{
SettingsTable instanceSettingsTable(instanceIdx);
result = instanceSettingsTable.addMissingDefaults();
errorList.append(result.second);
}
if(errorList.isEmpty())
{
Debug(_log, "Successfully defaulted settings for missing configuration items");
}
return qMakePair (errorList.isEmpty(), errorList );
}
QPair<bool, QStringList> DBConfigManager::updateConfiguration(QJsonObject& config, bool doCorrections)
{
Info(_log, "Update configuration database");
QPair<bool, QStringList> validationResult = validateConfiguration(config, doCorrections);
if (!validationResult.first)
{
return validationResult;
}
Info(_log, "Create backup of current configuration");
if (!exportJson())
{
Warning(_log, "Backup of current configuration failed");
}
QStringList errorList;
QSqlDatabase idb = getDB();
if (!startTransaction(idb, errorList))
{
return qMakePair(false, errorList);
}
// Clear existing tables and import the new configuration.
bool errorOccurred = false;
if (!deleteTable("instances") && deleteTable("settings"))
{
errorOccurred = true;
logErrorAndAppend("Failed to clear tables before import", errorList);
}
else
{
errorOccurred = !importGlobalSettings(config, errorList) || !importInstances(config, errorList);
}
// Rollback if any error occurred during the import process.
if (errorOccurred)
{
if (!rollbackTransaction(idb, errorList))
{
return qMakePair(false, errorList);
}
}
commiTransaction(idb, errorList);
if (errorList.isEmpty())
{
Info(_log, "Successfully imported new configuration");
}
return qMakePair(errorList.isEmpty(), errorList);
}
// Function to import global settings
bool DBConfigManager::importGlobalSettings(const QJsonObject& config, QStringList& errorList)
{
SettingsTable settingsTableGlobal;
const QJsonObject globalConfig = config.value("global").toObject();
const QJsonObject globalSettings = globalConfig.value("settings").toObject();
bool errorOccurred = false;
for (QJsonObject::const_iterator it = globalSettings.constBegin(); it != globalSettings.constEnd(); ++it)
{
if (!settingsTableGlobal.createSettingsRecord(it.key(), JsonUtils::jsonValueToQString(it.value())))
{
errorOccurred = true;
logErrorAndAppend("Failed to import global setting", errorList);
}
}
return !errorOccurred;
}
// Function to import instances
bool DBConfigManager::importInstances(const QJsonObject& config, QStringList& errorList)
{
InstanceTable instanceTable;
const QJsonArray instancesConfig = config.value("instances").toArray();
bool errorOccurred = false;
quint8 instanceIdx = 0;
for (const auto& instanceItem : instancesConfig)
{
if (!importInstance(instanceTable, instanceItem.toObject(), instanceIdx, errorList))
{
errorOccurred = true;
}
++instanceIdx;
}
return !errorOccurred;
}
// Function to import a single instance
bool DBConfigManager::importInstance(InstanceTable& instanceTable, const QJsonObject& instanceConfig, quint8 instanceIdx, QStringList& errorList)
{
QString instanceName = instanceConfig.value("name").toString(QString("Instance %1").arg(instanceIdx));
bool isInstanceEnabled = instanceConfig.value("enabled").toBool(true);
if (instanceIdx == 0)
{
isInstanceEnabled = true; // The first instance must be enabled.
}
if (!instanceTable.createInstance(instanceName, instanceIdx) ||
!instanceTable.setEnable(instanceIdx, isInstanceEnabled))
{
logErrorAndAppend("Failed to import instance", errorList);
return false;
}
SettingsTable settingsTableInstance(instanceIdx);
const QJsonObject instanceSettings = instanceConfig.value("settings").toObject();
return importInstanceSettings(settingsTableInstance, instanceSettings, errorList);
}
// Function to import instance settings
bool DBConfigManager::importInstanceSettings(SettingsTable& settingsTable, const QJsonObject& instanceSettings, QStringList& errorList)
{
bool errorOccurred = false;
for (QJsonObject::const_iterator it = instanceSettings.constBegin(); it != instanceSettings.constEnd(); ++it)
{
if (!settingsTable.createSettingsRecord(it.key(), JsonUtils::jsonValueToQString(it.value())))
{
errorOccurred = true;
logErrorAndAppend("Failed to import instance setting", errorList);
}
}
return !errorOccurred;
}
QJsonObject DBConfigManager::getConfiguration(const QList<quint8>& instancesFilter, const QStringList& instanceFilteredTypes, const QStringList& globalFilterTypes ) const
{
QSqlDatabase idb = getDB();
if (!startTransaction(idb))
{
return {};
}
InstanceTable instanceTable;
SettingsTable settingsTable;
QJsonObject config;
QJsonObject globalConfig;
MetaTable metaTable;
globalConfig.insert("uuid", metaTable.getUUID());
globalConfig.insert("settings", settingsTable.getSettings(globalFilterTypes));
config.insert("global", globalConfig);
QList<quint8> instances {instancesFilter};
if (instances.isEmpty())
{
instances = instanceTable.getAllInstanceIDs();
}
QList<quint8> 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", settingsTable.getSettings(static_cast<quint8>(instanceIdx), instanceFilteredTypes));
configInstanceList.append(instanceConfig);
instanceIdList.append(instanceIdx);
}
config.insert("instanceIds", instanceIdList);
config.insert("instances", configInstanceList);
if (!commiTransaction(idb))
{
return {};
}
return config;
}
QPair<bool, QStringList> DBConfigManager::migrateConfiguration()
{
Info(_log, "Check, if configuration database is required to be migrated");
DBMigrationManager migtrationManger;
if (migtrationManger.isMigrationRequired())
{
QJsonObject config = getConfiguration();
if (migtrationManger.migrateSettings(config))
{
return updateConfiguration(config, true);
}
}
Info(_log, "Database migration is not required");
return qMakePair (true, QStringList{} );
}

View File

@@ -1,38 +1,48 @@
#include "utils/settings.h"
#include <db/DBManager.h>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QThreadStorage>
#include <QUuid>
#include <QDir>
#include <QMetaType>
#include <QJsonObject>
#ifdef _WIN32
#include <stdexcept>
#endif
// not in header because of linking
static QString _rootPath;
static QThreadStorage<QSqlDatabase> _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<QSqlDatabase> 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,42 @@ 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");
}
#ifdef SQLQUERY_LOGGING
Debug(Logger::getInstance("DB"), "Database is opened in %s mode", _isReadOnly ? "read-only" : "read/write");
#endif
_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 +98,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 +116,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,33 +140,66 @@ 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 entry > 0;
}
return false;
bool DBManager::recordsNotExisting(const QVariantList& testValues,const QString& column, QStringList& nonExistingRecs, const QString& condition ) const
{
QSqlDatabase idb = getDB();
QSqlQuery query(idb);
query.setForwardOnly(true);
// prep conditions
QString prepCond;
if(!condition.isEmpty())
{
prepCond = QString("WHERE %1").arg(condition);
}
QVector<QString> valueItem(testValues.size(), "(?)");
QString values = QStringList::fromVector(valueItem).join(",");
query.prepare(
QString("SELECT v.[column1] [%1] FROM ( VALUES %2 ) [v] WHERE %1 NOT IN ( SELECT %1 from settings %3 )")
.arg(column,values, prepCond)
);
addBindValues(query, testValues);
if (!executeQuery(query))
{
return false;
}
while (query.next()) {
nonExistingRecs << query.value(0).toString();
}
return true;
}
bool DBManager::updateRecord(const VectorPair& conditions, const QVariantMap& columns) const
{
if ( _readonlyMode )
if (isReadOnly())
{
return false;
return true;
}
QSqlDatabase idb = getDB();
@@ -164,88 +210,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<QVariantMap> 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<rec.count(); i++)
{
results[rec.fieldName(i)] = rec.value(i);
}
return true;
return success;
}
bool DBManager::getRecords(QVector<QVariantMap>& results, const QStringList& tColumns, const QStringList& tOrder) const
{
return getRecords({}, results, tColumns, tOrder);
}
bool DBManager::getRecords(const VectorPair& conditions, QVector<QVariantMap>& 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<QVariantMap>& results, const QStringList& tColumns, const QStringList& tOrder) const
{
QSqlDatabase idb = getDB();
QSqlQuery query(idb);
@@ -253,20 +286,28 @@ bool DBManager::getRecords(QVector<QVariantMap>& 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;
}
@@ -285,12 +326,11 @@ bool DBManager::getRecords(QVector<QVariantMap>& results, const QStringList& tCo
return true;
}
bool DBManager::deleteRecord(const VectorPair& conditions) const
{
if ( _readonlyMode )
if (_isReadOnly)
{
return false;
return true;
}
if(conditions.isEmpty())
@@ -310,29 +350,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 +378,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 +389,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 +398,168 @@ 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('?')) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
QVariantList boundValues = query.boundValues(); // Get bound values as a list
#else
QVariantMap boundValues = query.boundValues(); // Get bound values as a list
#endif
// 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<QString>())
{
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;
}
bool DBManager::startTransaction(QSqlDatabase& idb) const
{
if (!idb.transaction())
{
QString errorText = QString("Could not create a database transaction. Error: %1").arg(idb.lastError().text());
Error(_log, "'%s'", QSTRING_CSTR(errorText));
return false;
}
return true;
}
bool DBManager::startTransaction(QSqlDatabase& idb, QStringList& errorList)
{
if (!idb.transaction())
{
QString errorText = QString("Could not create a database transaction. Error: %1").arg(idb.lastError().text());
logErrorAndAppend(errorText, errorList);
return false;
}
return true;
}
bool DBManager::commiTransaction(QSqlDatabase& idb) const
{
if (!idb.commit())
{
QString errorText = QString("Could not finalize the database changes. Error: %1").arg(idb.lastError().text());
Error(_log, "'%s'", QSTRING_CSTR(errorText));
return false;
}
return true;
}
bool DBManager::commiTransaction(QSqlDatabase& idb, QStringList& errorList)
{
if (!idb.commit())
{
QString errorText = QString("Could not finalize the database changes. Error: %1").arg(idb.lastError().text());
logErrorAndAppend(errorText, errorList);
return false;
}
return true;
}
bool DBManager::rollbackTransaction(QSqlDatabase& idb) const
{
if (!idb.rollback())
{
QString errorText = QString("Could not rollback the database transaction. Error: %1").arg(idb.lastError().text());
Error(_log, "'%s'", QSTRING_CSTR(errorText));
return false;
}
return true;
}
bool DBManager::rollbackTransaction(QSqlDatabase& idb, QStringList& errorList)
{
if (!idb.rollback())
{
QString errorText = QString("Could not rollback the database transaction. Error: %1").arg(idb.lastError().text());
logErrorAndAppend(errorText, errorList);
return false;
}
return true;
}
// Function to log error and append it to the error list
void DBManager::logErrorAndAppend(const QString& errorText, QStringList& errorList)
{
Error(_log, "'%s'", QSTRING_CSTR(errorText));
errorList.append(errorText);
}

View File

@@ -0,0 +1,767 @@
#include <db/DBMigrationManager.h>
#include "db/SettingsTable.h"
#include <utils/Logger.h>
#include <utils/QStringUtils.h>
#include <HyperionConfig.h>
#include <QJsonObject>
DBMigrationManager::DBMigrationManager(QObject *parent)
: DBManager{parent}
{
}
bool DBMigrationManager::isMigrationRequired()
{
bool isNewRelease = false;
SettingsTable settingsTableGlobal;
if (settingsTableGlobal.resolveConfigVersion())
{
semver::version BUILD_VERSION(HYPERION_VERSION);
if (!BUILD_VERSION.isValid())
{
Error(_log, "Current Hyperion version [%s] is invalid. Exiting...", BUILD_VERSION.getVersion().c_str());
exit(1);
}
const semver::version& currentVersion = settingsTableGlobal.getConfigVersion();
if (currentVersion > BUILD_VERSION)
{
Error(_log, "Database version [%s] is greater than current Hyperion version [%s]. Exiting...", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str());
exit(1);
}
if (currentVersion < BUILD_VERSION)
{
isNewRelease = true;
}
}
return isNewRelease;
}
bool DBMigrationManager::migrateSettings(QJsonObject& config)
{
bool migrated = false;
semver::version BUILD_VERSION(HYPERION_VERSION);
SettingsTable settingsTableGlobal;
QJsonObject generalConfig = config.value("global").toObject().value("settings").toObject().value("general").toObject();
if (settingsTableGlobal.resolveConfigVersion(generalConfig))
{
semver::version currentVersion = settingsTableGlobal.getConfigVersion();
if (currentVersion < BUILD_VERSION)
{
Info(_log, "Migration from current version [%s] to new version [%s] started", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str());
// Extract, modify, and reinsert the global settings
QJsonObject globalSettings = config.value("global").toObject().value("settings").toObject();
upgradeGlobalSettings(currentVersion, globalSettings);
QJsonObject globalConfig = config.value("global").toObject();
globalConfig.insert("settings", globalSettings);
config.insert("global", globalConfig);
// Update each instance directly within the config
QJsonArray instancesConfig = config.value("instances").toArray();
for (int i = 0; i < instancesConfig.size(); ++i)
{
QJsonObject instanceConfig = instancesConfig[i].toObject();
QJsonObject instanceSettings = instanceConfig.value("settings").toObject();
upgradeInstanceSettings(currentVersion, static_cast<quint8>(i), instanceSettings);
// Reinsert the modified instance settings back into the instanceConfig
instanceConfig.insert("settings", instanceSettings);
instancesConfig.replace(i, instanceConfig);
}
config.insert("instances", instancesConfig);
Info(_log, "Migration from current version [%s] to new version [%s] finished", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str());
migrated = true;
}
}
return migrated;
}
bool DBMigrationManager::upgradeGlobalSettings(const semver::version& currentVersion, QJsonObject& config)
{
bool migrated = false;
semver::version migratedVersion = currentVersion;
//Migration step for versions < alpha 9
upgradeGlobalSettings_alpha_9(migratedVersion, config);
//Migration step for versions < 2.0.12
upgradeGlobalSettings_2_0_12(migratedVersion, config);
//Migration step for versions < 2.0.16
upgradeGlobalSettings_2_0_16(migratedVersion, config);
//Migration step for versions < 2.0.17
upgradeGlobalSettings_2_1_0(migratedVersion, config);
// Set the daqtabase version to the current build version
QJsonObject generalConfig = config["general"].toObject();
// Update the configVersion if necessary
if (generalConfig["configVersion"].toString() != HYPERION_VERSION) {
generalConfig["configVersion"] = HYPERION_VERSION;
migrated = true;
}
// Re-insert the modified "general" object back into the config
config["general"] = generalConfig;
return migrated;
}
bool DBMigrationManager::upgradeInstanceSettings(const semver::version& currentVersion, quint8 instance, QJsonObject& config)
{
bool migrated = false;
semver::version migratedVersion = currentVersion;
//Migration step for versions < alpha 9
upgradeInstanceSettings_alpha_9(migratedVersion, instance, config);
//Migration step for versions < 2.0.12
upgradeInstanceSettings_2_0_12(migratedVersion, instance, config);
//Migration step for versions < 2.0.13
upgradeInstanceSettings_2_0_13(migratedVersion, instance, config);
//Migration step for versions < 2.0.16
upgradeInstanceSettings_2_0_16(migratedVersion, instance, config);
return migrated;
}
bool DBMigrationManager::upgradeGlobalSettings_alpha_9(semver::version& currentVersion, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.0-alpha.9" };
if (currentVersion < targetVersion)
{
Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
if (config.contains("grabberV4L2"))
{
QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject();
if (newGrabberV4L2Config.contains("encoding_format"))
{
newGrabberV4L2Config.remove("encoding_format");
newGrabberV4L2Config["grabberV4L2"] = newGrabberV4L2Config;
migrated = true;
}
//Add new element enable
if (!newGrabberV4L2Config.contains("enable"))
{
newGrabberV4L2Config["enable"] = false;
migrated = true;
}
config["grabberV4L2"] = newGrabberV4L2Config;
Debug(_log, "GrabberV4L2 records migrated");
}
if (config.contains("grabberAudio"))
{
QJsonObject newGrabberAudioConfig = config["grabberAudio"].toObject();
//Add new element enable
if (!newGrabberAudioConfig.contains("enable"))
{
newGrabberAudioConfig["enable"] = false;
migrated = true;
}
config["grabberAudio"] = newGrabberAudioConfig;
Debug(_log, "GrabberAudio records migrated");
}
if (config.contains("framegrabber"))
{
QJsonObject newFramegrabberConfig = config["framegrabber"].toObject();
//Align element namings with grabberV4L2
//Rename element type -> device
if (newFramegrabberConfig.contains("type"))
{
newFramegrabberConfig["device"] = newFramegrabberConfig["type"].toString();
newFramegrabberConfig.remove("type");
migrated = true;
}
//Rename element frequency_Hz -> fps
if (newFramegrabberConfig.contains("frequency_Hz"))
{
newFramegrabberConfig["fps"] = newFramegrabberConfig["frequency_Hz"].toInt(25);
newFramegrabberConfig.remove("frequency_Hz");
migrated = true;
}
//Rename element display -> input
if (newFramegrabberConfig.contains("display"))
{
newFramegrabberConfig["input"] = newFramegrabberConfig["display"];
newFramegrabberConfig.remove("display");
migrated = true;
}
//Add new element enable
if (!newFramegrabberConfig.contains("enable"))
{
newFramegrabberConfig["enable"] = false;
migrated = true;
}
config["framegrabber"] = newFramegrabberConfig;
Debug(_log, "Framegrabber records migrated");
}
}
return migrated;
}
bool DBMigrationManager::upgradeGlobalSettings_2_0_12(semver::version& currentVersion, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.12" };
if (currentVersion < targetVersion)
{
Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
// Have Hostname/IP-address separate from port for Forwarder
if (config.contains("forwarder"))
{
QJsonObject newForwarderConfig = config["forwarder"].toObject();
QJsonArray json;
if (newForwarderConfig.contains("json"))
{
const QJsonArray oldJson = newForwarderConfig["json"].toArray();
QJsonObject newJsonConfig;
for (const QJsonValue& value : oldJson)
{
if (value.isString())
{
QString oldHost = value.toString();
// Resolve hostname and port
QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts);
QString host = addressparts[0];
if (host != "127.0.0.1")
{
newJsonConfig["host"] = host;
if (addressparts.size() > 1)
{
newJsonConfig["port"] = addressparts[1].toInt();
}
else
{
newJsonConfig["port"] = 19444;
}
newJsonConfig["name"] = host;
json.append(newJsonConfig);
migrated = true;
}
}
}
if (!json.isEmpty())
{
newForwarderConfig["jsonapi"] = json;
}
newForwarderConfig.remove("json");
migrated = true;
}
QJsonArray flatbuffer;
if (newForwarderConfig.contains("flat"))
{
const QJsonArray oldFlatbuffer = newForwarderConfig["flat"].toArray();
QJsonObject newFlattbufferConfig;
for (const QJsonValue& value : oldFlatbuffer)
{
if (value.isString())
{
QString oldHost = value.toString();
// Resolve hostname and port
QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts);
QString host = addressparts[0];
if (host != "127.0.0.1")
{
newFlattbufferConfig["host"] = host;
if (addressparts.size() > 1)
{
newFlattbufferConfig["port"] = addressparts[1].toInt();
}
else
{
newFlattbufferConfig["port"] = 19400;
}
newFlattbufferConfig["name"] = host;
flatbuffer.append(newFlattbufferConfig);
}
}
if (!flatbuffer.isEmpty())
{
newForwarderConfig["flatbuffer"] = flatbuffer;
}
newForwarderConfig.remove("flat");
migrated = true;
}
}
if (json.isEmpty() && flatbuffer.isEmpty())
{
newForwarderConfig["enable"] = false;
}
if (migrated)
{
config["forwarder"] = newForwarderConfig;
Debug(_log, "Forwarder records migrated");
currentVersion = targetVersion;
}
}
}
return migrated;
}
bool DBMigrationManager::upgradeGlobalSettings_2_0_16(semver::version& currentVersion, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.16" };
if (currentVersion < targetVersion)
{
Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
if (config.contains("cecEvents"))
{
bool isCECEnabled {false};
if (config.contains("grabberV4L2"))
{
QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject();
if (newGrabberV4L2Config.contains("cecDetection"))
{
isCECEnabled = newGrabberV4L2Config.value("cecDetection").toBool(false);
newGrabberV4L2Config.remove("cecDetection");
config["grabberV4L2"] = newGrabberV4L2Config;
QJsonObject newGCecEventsConfig = config["cecEvents"].toObject();
newGCecEventsConfig["enable"] = isCECEnabled;
if (!newGCecEventsConfig.contains("actions"))
{
QJsonObject action1
{
{"action", "Suspend"},
{"event", "standby"}
};
QJsonObject action2
{
{"action", "Resume"},
{"event", "set stream path"}
};
QJsonArray actions { action1, action2 };
newGCecEventsConfig.insert("actions",actions);
}
config["cecEvents"] = newGCecEventsConfig;
migrated = true;
Debug(_log, "CEC configuration records migrated");
}
}
}
}
return migrated;
}
bool DBMigrationManager::upgradeGlobalSettings_2_1_0(semver::version& currentVersion, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.17-beta.2" };
if (currentVersion < targetVersion)
{
Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
if (config.contains("general"))
{
QJsonObject newGeneralConfig = config["general"].toObject();
newGeneralConfig.remove("previousVersion");
config.insert("general", newGeneralConfig);
Debug(_log, "General settings migrated");
migrated = true;
}
if (config.contains("network"))
{
QJsonObject newNetworkConfig = config["network"].toObject();
newNetworkConfig.remove("apiAuth");
newNetworkConfig.remove("localAdminAuth");
config.insert("network", newNetworkConfig);
Debug(_log, "Network settings migrated");
migrated = true;
}
}
//Remove wrong instance 255 configuration records, created by the global instance #255
SettingsTable globalSettingsTable(255);
globalSettingsTable.deleteInstance();
migrated = true;
return migrated;
}
bool DBMigrationManager::upgradeInstanceSettings_alpha_9(semver::version& currentVersion, quint8 instance, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.0-alpha.9" };
if (currentVersion < targetVersion)
{
Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
// LED LAYOUT UPGRADE
// from { hscan: { minimum: 0.2, maximum: 0.3 }, vscan: { minimum: 0.2, maximum: 0.3 } }
// from { h: { min: 0.2, max: 0.3 }, v: { min: 0.2, max: 0.3 } }
// to { hmin: 0.2, hmax: 0.3, vmin: 0.2, vmax: 0.3}
if (config.contains("leds"))
{
const QJsonArray ledarr = config["leds"].toArray();
const QJsonObject firstLed = ledarr[0].toObject();
if (firstLed.contains("hscan") || firstLed.contains("h"))
{
const bool whscan = firstLed.contains("hscan");
QJsonArray newLedarr;
for (const auto& entry : ledarr)
{
const QJsonObject led = entry.toObject();
QJsonObject hscan;
QJsonObject vscan;
QJsonValue hmin;
QJsonValue hmax;
QJsonValue vmin;
QJsonValue vmax;
QJsonObject nL;
if (whscan)
{
hscan = led["hscan"].toObject();
vscan = led["vscan"].toObject();
hmin = hscan["minimum"];
hmax = hscan["maximum"];
vmin = vscan["minimum"];
vmax = vscan["maximum"];
}
else
{
hscan = led["h"].toObject();
vscan = led["v"].toObject();
hmin = hscan["min"];
hmax = hscan["max"];
vmin = vscan["min"];
vmax = vscan["max"];
}
// append to led object
nL["hmin"] = hmin;
nL["hmax"] = hmax;
nL["vmin"] = vmin;
nL["vmax"] = vmax;
newLedarr.append(nL);
}
// replace
config["leds"] = newLedarr;
migrated = true;
Info(_log, "Instance [%u]: LED Layout migrated", instance);
}
}
if (config.contains("ledConfig"))
{
QJsonObject oldLedConfig = config["ledConfig"].toObject();
if (!oldLedConfig.contains("classic"))
{
QJsonObject newLedConfig;
newLedConfig.insert("classic", oldLedConfig);
QJsonObject defaultMatrixConfig{ {"ledshoriz", 1}
,{"ledsvert", 1}
,{"cabling","snake"}
,{"start","top-left"}
};
newLedConfig.insert("matrix", defaultMatrixConfig);
config["ledConfig"] = newLedConfig;
migrated = true;
Info(_log, "Instance [%u]: LED-Config migrated", instance);
}
}
// LED Hardware count is leading for versions after alpha 9
// Setting Hardware LED count to number of LEDs configured via layout, if layout number is greater than number of hardware LEDs
if (config.contains("device"))
{
QJsonObject newDeviceConfig = config["device"].toObject();
if (newDeviceConfig.contains("hardwareLedCount"))
{
int hwLedcount = newDeviceConfig["hardwareLedCount"].toInt();
if (config.contains("leds"))
{
const QJsonArray ledarr = config["leds"].toArray();
int layoutLedCount = ledarr.size();
if (hwLedcount < layoutLedCount)
{
Warning(_log, "Instance [%u]: HwLedCount/Layout mismatch! Setting Hardware LED count to number of LEDs configured via layout", instance);
hwLedcount = layoutLedCount;
newDeviceConfig["hardwareLedCount"] = hwLedcount;
migrated = true;
}
}
}
if (newDeviceConfig.contains("type"))
{
QString type = newDeviceConfig["type"].toString();
if (type == "atmoorb" || type == "fadecandy" || type == "philipshue")
{
if (newDeviceConfig.contains("output"))
{
newDeviceConfig["host"] = newDeviceConfig["output"].toString();
newDeviceConfig.remove("output");
migrated = true;
}
}
}
if (migrated)
{
config["device"] = newDeviceConfig;
Debug(_log, "LED-Device records migrated");
}
}
}
return migrated;
}
bool DBMigrationManager::upgradeInstanceSettings_2_0_12(semver::version& currentVersion, quint8 instance, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.12" };
if (currentVersion < targetVersion)
{
Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
// Have Hostname/IP-address separate from port for LED-Devices
if (config.contains("device"))
{
QJsonObject newDeviceConfig = config["device"].toObject();
if (newDeviceConfig.contains("host"))
{
QString oldHost = newDeviceConfig["host"].toString();
// Resolve hostname and port
QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts);
newDeviceConfig["host"] = addressparts[0];
if (addressparts.size() > 1)
{
if (!newDeviceConfig.contains("port"))
{
newDeviceConfig["port"] = addressparts[1].toInt();
}
migrated = true;
}
}
if (newDeviceConfig.contains("type"))
{
QString type = newDeviceConfig["type"].toString();
if (type == "apa102")
{
if (newDeviceConfig.contains("colorOrder"))
{
QString colorOrder = newDeviceConfig["colorOrder"].toString();
if (colorOrder == "bgr")
{
newDeviceConfig["colorOrder"] = "rgb";
migrated = true;
}
}
}
}
if (migrated)
{
config["device"] = newDeviceConfig;
Debug(_log, "LED-Device records migrated");
}
}
}
return migrated;
}
bool DBMigrationManager::upgradeInstanceSettings_2_0_13(semver::version& currentVersion, quint8 instance, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.13" };
if (currentVersion < targetVersion)
{
Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
// Have Hostname/IP-address separate from port for LED-Devices
if (config.contains("device"))
{
QJsonObject newDeviceConfig = config["device"].toObject();
if (newDeviceConfig.contains("type"))
{
QString type = newDeviceConfig["type"].toString();
const QStringList serialDevices{ "adalight", "dmx", "atmo", "sedu", "tpm2", "karate" };
if (serialDevices.contains(type))
{
if (!newDeviceConfig.contains("rateList"))
{
newDeviceConfig["rateList"] = "CUSTOM";
migrated = true;
}
}
if (type == "adalight")
{
if (newDeviceConfig.contains("lightberry_apa102_mode"))
{
bool lightberry_apa102_mode = newDeviceConfig["lightberry_apa102_mode"].toBool();
if (lightberry_apa102_mode)
{
newDeviceConfig["streamProtocol"] = "1";
}
else
{
newDeviceConfig["streamProtocol"] = "0";
}
newDeviceConfig.remove("lightberry_apa102_mode");
migrated = true;
}
}
}
if (migrated)
{
config["device"] = newDeviceConfig;
Debug(_log, "LED-Device records migrated");
}
}
}
return migrated;
}
bool DBMigrationManager::upgradeInstanceSettings_2_0_16(semver::version& currentVersion, quint8 instance, QJsonObject& config)
{
bool migrated = false;
const semver::version targetVersion{ "2.0.16" };
if (currentVersion >= targetVersion) return migrated;
Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
currentVersion = targetVersion;
if (config.contains("device"))
{
QJsonObject newDeviceConfig = config["device"].toObject();
auto convertIntToString = [&](const QString& key) {
if (newDeviceConfig.contains(key) && newDeviceConfig[key].isDouble()) {
int value = newDeviceConfig[key].toInt();
newDeviceConfig[key] = QString::number(value);
migrated = true;
}
};
if (newDeviceConfig.contains("type"))
{
QString type = newDeviceConfig["type"].toString();
if (type == "philipshue")
{
convertIntToString("groupId");
if (newDeviceConfig.contains("lightIds"))
{
QJsonArray lightIds = newDeviceConfig["lightIds"].toArray();
for (int i = 0; i < lightIds.size(); ++i)
{
if (lightIds[i].isDouble())
{
lightIds[i] = QString::number(lightIds[i].toInt());
migrated = true;
}
}
newDeviceConfig["lightIds"] = lightIds;
}
}
else if (type == "nanoleaf")
{
const auto updatePanelOrder = [&](const QString& key, const QString& zeroStr, const QString& oneStr) {
if (newDeviceConfig.contains(key))
{
int order = newDeviceConfig[key].isDouble() ? newDeviceConfig[key].toInt() : newDeviceConfig[key].toString().toInt();
newDeviceConfig[key] = (order == 0) ? zeroStr : oneStr;
migrated = true;
}
};
newDeviceConfig.remove("panelStartPos");
migrated = true;
updatePanelOrder("panelOrderTopDown", "top2down", "bottom2up");
updatePanelOrder("panelOrderLeftRight", "left2right", "right2left");
}
}
if (migrated)
{
config["device"] = newDeviceConfig;
Debug(_log, "LED-Device records migrated");
}
}
return migrated;
}

142
libsrc/db/InstanceTable.cpp Normal file
View File

@@ -0,0 +1,142 @@
// db
#include <db/InstanceTable.h>
#include <db/SettingsTable.h>
// qt
#include <QDateTime>
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<quint8> 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<QVariantMap> InstanceTable::getAllInstances(bool onlyEnabled)
{
QVector<QVariantMap> results;
VectorPair onlyEnabledCondition {};
if (onlyEnabled)
{
onlyEnabledCondition = {{"enabled", true}};
}
getRecords(onlyEnabledCondition, results, {}, {"instance ASC"});
return results;
}
QList<quint8> InstanceTable::getAllInstanceIDs (bool onlyEnabled)
{
QVector<QVariantMap> instanceList = getAllInstances(onlyEnabled);
QList<quint8> instanceIds;
for (const QVariantMap &idx : std::as_const(instanceList))
{
instanceIds.append(static_cast<quint8>(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...");
}
}
}

47
libsrc/db/MetaTable.cpp Normal file
View File

@@ -0,0 +1,47 @@
#include <db/MetaTable.h>
// qt
#include <QDateTime>
#include <QUuid>
#include <QNetworkInterface>
#include <QCryptographicHash>
MetaTable::MetaTable(QObject* parent)
: DBManager(parent)
{
setTable("meta");
createTable(QStringList()<<"uuid TEXT"<<"created_at TEXT");
};
QString MetaTable::getUUID() const
{
QVector<QVariantMap> 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;
}

389
libsrc/db/SettingsTable.cpp Normal file
View File

@@ -0,0 +1,389 @@
#include <db/SettingsTable.h>
#include <utils/jsonschema/QJsonFactory.h>
#include <utils/JsonUtils.h>
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSqlDatabase>
#include <QSqlError>
namespace {
const char DEFAULT_INSTANCE_SETTINGS_SCHEMA_FILE[] = ":/schema-settings-instance.json";
const char GLOBAL_SETTINGS_SCHEMA_FILE[] = ":/schema-settings-global.json";
const char DEFAULT_SETTINGS_SCHEMA_FILE[] = ":/schema-settings-default.json";
const char DEFAULT_SETTINGS_CONFIGURATION_FILE[] = ":/hyperion_default.settings";
}
QVector<QString> SettingsTable::globalSettingTypes;
bool SettingsTable::areGlobalSettingTypesInitialised = false;
QVector<QString> SettingsTable::instanceSettingTypes;
bool SettingsTable::areInstanceSettingTypesInitialised = false;
QJsonObject SettingsTable::defaultSettings;
bool SettingsTable::areDefaultSettingsInitialised = false;
SettingsTable::SettingsTable(quint8 instance, QObject* parent)
: DBManager(parent)
, _instance(instance)
, _configVersion(DEFAULT_CONFIG_VERSION)
{
setTable("settings");
// create table columns
createTable(QStringList()<<"type TEXT"<<"config TEXT"<<"hyperion_inst INTEGER"<<"updated_at TEXT");
}
const QVector<QString>& SettingsTable::getGlobalSettingTypes() const
{
if (!areGlobalSettingTypesInitialised) {
globalSettingTypes = initializeGlobalSettingTypes();
areGlobalSettingTypesInitialised = true;
}
return globalSettingTypes;
}
QVector<QString> SettingsTable::initializeGlobalSettingTypes() const
{
QJsonObject schemaJson;
try
{
schemaJson = QJsonFactory::readSchema(GLOBAL_SETTINGS_SCHEMA_FILE);
}
catch (const std::runtime_error& error)
{
throw std::runtime_error(error.what());
}
const QVector<QString> types = schemaJson.value("properties").toObject().keys().toVector();
return types;
}
bool SettingsTable::isGlobalSettingType(const QString& type) const {
return getGlobalSettingTypes().contains(type);
}
bool SettingsTable::isInstanceSettingType(const QString& type) const {
return getInstanceSettingTypes().contains(type);
}
const QVector<QString>& SettingsTable::getInstanceSettingTypes() const
{
if (!areInstanceSettingTypesInitialised) {
instanceSettingTypes = initializeInstanceSettingTypes();
areInstanceSettingTypesInitialised = true;
}
return instanceSettingTypes;
}
QVector<QString> SettingsTable::initializeInstanceSettingTypes() const
{
QJsonObject schemaJson;
try
{
schemaJson = QJsonFactory::readSchema(DEFAULT_INSTANCE_SETTINGS_SCHEMA_FILE);
}
catch (const std::runtime_error& error)
{
throw std::runtime_error(error.what());
}
const QVector<QString> types = schemaJson.value("properties").toObject().keys().toVector();
return types;
}
const QJsonObject& SettingsTable::getDefaultSettings() const
{
if (!areDefaultSettingsInitialised) {
defaultSettings = initializeDefaultSettings();
areDefaultSettingsInitialised = true;
}
return defaultSettings;
}
QJsonObject SettingsTable::initializeDefaultSettings() const
{
QJsonObject defaultConfig;
if ( QJsonFactory::load(DEFAULT_SETTINGS_SCHEMA_FILE, DEFAULT_SETTINGS_CONFIGURATION_FILE, defaultConfig) < 0)
{
Error(_log,"Failed to read default config");
}
return defaultConfig;
}
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(!isGlobalSettingType(type))
{
cond.append(CPair("AND hyperion_inst",_instance));
}
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(!isGlobalSettingType(type))
{
cond.append(CPair("AND hyperion_inst",_instance));
}
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(!isGlobalSettingType(type))
{
cond.append(CPair("AND hyperion_inst",_instance));
}
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(!isGlobalSettingType(type))
{
cond.append(CPair("AND hyperion_inst",_instance));
}
getRecord(cond, results, QStringList("config"));
return results["config"].toString();
}
QJsonObject SettingsTable::getSettings(const QStringList& filteredTypes ) const
{
return getSettings(_instance, filteredTypes);
}
QJsonObject SettingsTable::getSettings(const QVariant& instance, const QStringList& filteredTypes ) const
{
QJsonObject settingsObject;
QStringList settingsKeys({ "type", "config" });
QString settingsCondition;
QVariantList conditionValues;
if (instance.isNull() || instance == GLOABL_INSTANCE_ID )
{
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);
}
settingsCondition += QString (" AND (%1)").arg(seletedSettingTypes.join(" OR "));
}
QVector<QVariantMap> settingsList;
if (getRecords(settingsCondition, conditionValues, settingsList, settingsKeys))
{
for (const QVariantMap &setting : std::as_const(settingsList))
{
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);
}
}
}
return settingsObject;
}
QStringList SettingsTable::nonExtingTypes() const
{
QStringList testTypes;
QString condition {"hyperion_inst"};
if(_instance == GLOABL_INSTANCE_ID)
{
condition += " IS NULL";
testTypes = getGlobalSettingTypes().toList();
}
else
{
condition += QString(" = %1").arg(_instance);
testTypes = getInstanceSettingTypes().toList();
}
QVariantList testTypesList;
testTypesList.reserve(testTypes.size());
for (const QString &str : std::as_const(testTypes)) {
testTypesList.append(QVariant(str));
}
QStringList nonExistingRecs;
recordsNotExisting(testTypesList, "type", nonExistingRecs, condition );
return nonExistingRecs;
}
QPair<bool, QStringList> SettingsTable::addMissingDefaults()
{
QStringList errorList;
QJsonObject defaultSettings;
if (_instance == GLOABL_INSTANCE_ID)
{
defaultSettings = getDefaultSettings().value("global").toObject();
}
else
{
defaultSettings = getDefaultSettings().value("instance").toObject();
}
const QStringList missingTypes = nonExtingTypes();
if (missingTypes.empty())
{
Debug(_log, "Instance [%u]: No missing configuration items identified", _instance);
return qMakePair (true, errorList );
}
QSqlDatabase idb = getDB();
if (!startTransaction(idb, errorList))
{
return qMakePair(false, errorList);
}
bool errorOccured {false};
Info(_log, "Instance [%u]: Add default settings for %d missing configuration items",_instance, missingTypes.size());
for (const auto &missingType: missingTypes)
{
if (!createSettingsRecord(missingType, JsonUtils::jsonValueToQString(defaultSettings.value(missingType))))
{
errorOccured = true;
}
}
if (errorOccured)
{
QString errorText = "Errors occured while adding missing settings to instance configuration items";
Error(_log, "'%s'", QSTRING_CSTR(errorText));
errorList.append(errorText);
if (!idb.rollback())
{
errorText = QString("Could not create a database transaction. Error: %1").arg(idb.lastError().text());
Error(_log, "'%s'", QSTRING_CSTR(errorText));
errorList.append(errorText);
}
}
commiTransaction(idb, errorList);
if(errorList.isEmpty())
{
Debug(_log, "Instance [%u]: Successfully defaulted settings for %d missing configuration items", _instance, missingTypes.size());
}
return qMakePair (errorList.isEmpty(), errorList );
}
void SettingsTable::deleteInstance() const
{
deleteRecord({{"hyperion_inst",_instance}});
}
QString SettingsTable::fixVersion(const QString& version)
{
QString newVersion;
// Use a static QRegularExpression to avoid re-creating it every time
static const QRegularExpression regEx(
"(\\d+\\.\\d+\\.\\d+-?[a-zA-Z-\\d]*\\.?[\\d]*)",
QRegularExpression::CaseInsensitiveOption | QRegularExpression::MultilineOption
);
// Try fixing version number, remove dot-separated pre-release identifiers not supported
QRegularExpressionMatch match = regEx.match(version);
if (match.hasMatch())
{
newVersion = match.captured(1);
}
return newVersion;
}
bool SettingsTable::resolveConfigVersion()
{
QJsonObject generalConfig = getSettingsRecord({"general"}).object();
return resolveConfigVersion(generalConfig);
}
bool SettingsTable::resolveConfigVersion(QJsonObject generalConfig)
{
bool isValid = false;
QString configVersion = generalConfig["configVersion"].toString();
if (!configVersion.isEmpty())
{
isValid = _configVersion.setVersion(configVersion.toStdString());
if (!isValid)
{
isValid = _configVersion.setVersion(fixVersion(configVersion).toStdString());
if (isValid)
{
Info(_log, "Invalid config version [%s] fixed. Updated to [%s]", QSTRING_CSTR(configVersion), _configVersion.getVersion().c_str());
}
}
}
else
{
isValid = true;
}
return isValid;
}
QString SettingsTable::getConfigVersionString()
{
return _configVersion.getVersion().data();
}
semver::version SettingsTable::getConfigVersion()
{
return _configVersion;
}