Merge pull request #9 from logos-co/chore/better-config

feat: provide a better configuration management
This commit is contained in:
Arnaud 2026-02-18 15:25:28 +04:00 committed by GitHub
commit 2855adeb86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 124 additions and 98 deletions

View File

@ -72,11 +72,9 @@ After onboarding, settings are saved to a file whose location depends on the OS:
| macOS | `~/Library/Preferences/Logos.LogosStorage.plist` |
| Windows | `HKCU\Software\Logos\LogosStorage` (Registry) |
## Configuration Management
The settings are saved to the preferences file to preserve the onboarding defaults, but the active configuration is stored in `${HOME}/.logos_storage/config.json`. You can tweak the values there directly. Note that running the onboarding again will override any onboarding-related values.
To restart the onboarding process, simply delete the configuration file and relaunch the application.
You can override the configuration by placing a `config.json` file in the app's startup folder. This file takes precedence over any existing configuration.
To restart the onboarding process, simply delete the prefences file and relaunch the application.
The application also provides a JSON editor in the debug panel for runtime configuration tweaks. To apply changes, restart the Storage Module.

View File

@ -30,8 +30,10 @@ int main(int argc, char* argv[]) {
QApplication app(argc, argv);
// Set application properties for Qt Settings
// QCoreApplication::setOrganizationName("Logos");
// QCoreApplication::setOrganizationDomain("logos.co");
// QCoreApplication::setApplicationName("LogosStorage");
QCoreApplication::setOrganizationName("Logos");
QCoreApplication::setOrganizationDomain("logos.co");
QCoreApplication::setApplicationName("LogosStorage");
// Set the plugins directory

View File

@ -201,6 +201,8 @@ LogosResult StorageBackend::init(const QString& configJson = "{}") {
debug("new config is: " + m_configJson);
}
emit initCompleted();
return {true, ""};
}
@ -827,9 +829,20 @@ void StorageBackend::reloadIfChanged(const QString& configJson) {
emit configJsonChanged();
}
bool StorageBackend::validateDataDir(const QString& path) {
QFileInfo info(path);
return info.exists() && info.isDir() && info.isReadable() && info.isWritable();
void StorageBackend::saveUserConfig(const QString& configJson) {
qDebug() << "StorageBackend::saveUserConfig";
QString configPath = getUserConfigPath();
QString folderPath = QFileInfo(configPath).absolutePath();
QDir().mkpath(folderPath);
QFile file(configPath);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
file.write(configJson.toUtf8());
file.close();
debug("Config saved to " + configPath);
} else {
debug("Failed to save config to " + configPath);
}
}
QString StorageBackend::buildConfig(const QString& dataDir, int discPort, int tcpPort) {
@ -871,6 +884,17 @@ QString StorageBackend::buildConfigFromFile(const QString& path) {
void StorageBackend::status(StorageStatus status) { m_status = status; }
QString StorageBackend::getUserConfigPath() { return QDir::homePath() + "/.logos_storage/config.json"; }
QString StorageBackend::getUserConfig() {
QFile file(getUserConfigPath());
if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return QString::fromUtf8(file.readAll());
}
return "{}";
}
QString StorageBackend::defaultDataDir() {
QString home = QDir::homePath();
#ifdef Q_OS_WIN

View File

@ -54,6 +54,8 @@ class StorageBackend : public QObject {
qint64 quotaReservedBytes() const;
Q_INVOKABLE static QString defaultDataDir();
static QString getUserConfig();
static QString getUserConfigPath();
explicit StorageBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
~StorageBackend();
@ -78,13 +80,13 @@ class StorageBackend : public QObject {
void downloadManifest(const QString& cid);
void downloadManifests();
void space();
bool validateDataDir(const QString& path);
LogosResult init(const QString& configJson);
void updateLogLevel(const QString& logLevel);
void reloadIfChanged(const QString& configJson);
void status(StorageStatus status);
QString buildConfig(const QString& dataDir, int discPort, int tcpPort);
QString buildConfigFromFile(const QString& path);
void saveUserConfig(const QString& configJson);
signals:
void startCompleted();
@ -98,6 +100,7 @@ class StorageBackend : public QObject {
void uploadStatusChanged();
void manifestsChanged();
void quotaChanged();
void initCompleted();
private slots:

View File

@ -14,7 +14,6 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) {
qDebug() << "StorageUIPlugin::createWidget called";
QCoreApplication::setOrganizationName("Logos");
QCoreApplication::setOrganizationDomain("logos.co");
QCoreApplication::setApplicationName("LogosStorage");
QQuickWidget* quickWidget = new QQuickWidget();
@ -27,29 +26,23 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) {
qDebug() << "StorageUIPlugin: Loading settings...";
QSettings settings("Logos", "LogosStorage");
int discoveryPort = settings.value("Storage/discoveryPort", 0).toInt();
int tcpPort = settings.value("Storage/tcpPort", 0).toInt();
QString dataDir = settings.value("Storage/dataDir", "").toString();
bool onboardingCompleted = settings.value("Storage/onboardingCompleted", false).toBool();
// Default constructor uses QCoreApplication org/domain/app — same path as QML QtCore.Settings
// QSettings settings;
// int discoveryPort = settings.value("Storage/discoveryPort", 8090).toInt();
// int tcpPort = settings.value("Storage/tcpPort", 0).toInt();
// QString dataDir = settings.value("Storage/dataDir", "").toString();
// bool onboardingCompleted = settings.value("Storage/onboardingCompleted", false).toBool();
qDebug() << "StorageUIPlugin: Settings Loaded onboardingCompleted=" << onboardingCompleted;
qDebug() << "StorageUIPlugin: Settings Loaded dataDir=" << dataDir;
qDebug() << "StorageUIPlugin: Settings Loaded discoveryPort=" << discoveryPort;
qDebug() << "StorageUIPlugin: Settings Loaded tcpPort=" << tcpPort;
// qDebug() << "StorageUIPlugin: Settings file:" << settings.fileName();
// qDebug() << "StorageUIPlugin: onboardingCompleted=" << onboardingCompleted;
// qDebug() << "StorageUIPlugin: dataDir=" << dataDir;
// qDebug() << "StorageUIPlugin: discoveryPort=" << discoveryPort;
// qDebug() << "StorageUIPlugin: tcpPort=" << tcpPort;
QString qmlPath = "qrc:/Main.qml";
// Create backend instance
// Always load Main.qml — QML handles navigation (onboarding vs startNode)
StorageBackend* backend = new StorageBackend(logosAPI, quickWidget);
if (onboardingCompleted) {
qmlPath = "qrc:/StorageView.qml";
}
qDebug() << "StorageUIPlugin: qmlPath=" << qmlPath;
quickWidget->setSource(QUrl(qmlPath));
quickWidget->setSource(QUrl("qrc:/Main.qml"));
if (quickWidget->status() == QQuickWidget::Error) {
qWarning() << "StorageUIPlugin: Failed to load QML:" << quickWidget->errors();
@ -61,33 +54,27 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) {
root->setProperty("backend", QVariant::fromValue(static_cast<QObject*>(backend)));
QString configJson = "{}";
// Build config from settings if onboarding was done, otherwise use empty config
QString configJson = StorageBackend::getUserConfig();
qDebug() << "UserConfig" << StorageBackend::getUserConfigPath();
qDebug() << "configJson" << configJson;
// if (onboardingCompleted && !dataDir.isEmpty()) {
// configJson = backend->buildConfig(dataDir, discoveryPort, tcpPort);
// }
if (onboardingCompleted) {
configJson = backend->buildConfig(dataDir, discoveryPort, tcpPort);
}
// config.json overrides everything (dev/debug use)
// QFileInfo info("config.json");
// if (info.exists() && info.isFile()) {
// qWarning() << "StorageUIPlugin: config.json found — overriding settings config";
// configJson = backend->buildConfigFromFile("config.json");
// }
QFileInfo info("config.json");
if (info.exists() && info.isFile()) {
qWarning()
<< "StorageUIPlugin: config.json is found ! It will override the configuration loaded by the onboarding !";
configJson = backend->buildConfigFromFile("config.json");
}
qDebug() << "StorageUIPlugin: configuration loaded configLoaded=" << configJson;
// qDebug() << "StorageUIPlugin: configJson=" << configJson;
LogosResult result = backend->init(configJson);
if (!result.success) {
QString error = result.getError();
qWarning() << "StorageUIPlugin: Failed to init backend, will use mock version:" << error;
} else if (onboardingCompleted) {
LogosResult result = backend->start();
if (!result.success) {
qWarning() << "StorageUIPlugin: Failed to start the Storage Module.";
}
qWarning() << "StorageUIPlugin: Failed to init backend:" << result.getError();
}
return quickWidget;

View File

@ -18,8 +18,6 @@ Button {
color: {
if (!control.enabled)
return Theme.palette.backgroundElevated
if (control.hovered)
return Theme.palette.backgroundTertiary
return Theme.palette.backgroundSecondary
}
border.width: 1

View File

@ -12,20 +12,19 @@ Item {
property var backend: mockBackend
Timer {
readonly property int running: 2
id: timer
interval: 2000
repeat: false
onTriggered: {
console.log("timer triggered")
root.backend.status = running
root.backend.startCompleted()
console.info(root.backend.status)
}
}
// Timer {
// readonly property int running: 2
// id: timer
// interval: 2000
// repeat: false
// onTriggered: {
// console.log("timer triggered")
// // root.backend.status = running
// // root.backend.startCompleted()
// // console.info(root.backend.status)
// }
// }
QtObject {
id: mockBackend
@ -40,7 +39,8 @@ Item {
}
function start() {
timer.start()
// timer.start()
console.log("mock start callde")
}
function stop() {
@ -62,18 +62,10 @@ Item {
id: settings
category: "Storage"
property int discoveryPort: 0
property int discoveryPort: 8090
property int tcpPort: 0
property string dataDir: ""
property bool onboardingCompleted: false
Component.onCompleted: {
if (onboardingCompleted) {
// stackView.replace(storageView)
// root.backend.start();
}
}
}
StackView {
@ -87,6 +79,10 @@ Item {
OnBoarding {
id: onboardingInstance
backend: root.backend
discoveryPort: settings.discoveryPort
tcpPort: settings.tcpPort
dataDir: settings.dataDir.length > 0 ? settings.dataDir : root.backend.defaultDataDir()
onCompleted: {
settings.discoveryPort = discoveryPort
@ -96,6 +92,7 @@ Item {
let config = root.backend.buildConfig(dataDir,
discoveryPort, tcpPort)
root.backend.saveUserConfig(config)
root.backend.reloadIfChanged(config)
root.backend.start()
@ -107,7 +104,7 @@ Item {
Component {
id: storageView
StorageView {
backend: root.backend // @disable-check M228
backend: root.backend
}
}
@ -132,5 +129,12 @@ Item {
function onStopCompleted() {
stackView.pop()
}
function onInitCompleted() {
if (settings.onboardingCompleted) {
root.backend.start()
stackView.replace(storageView, StackView.Immediate)
}
}
}
}

View File

@ -22,10 +22,6 @@ Rectangle {
QtObject {
id: mockBackend
function validateDataDir(path) {
return path != "error"
}
function defaultDataDir() {
return ".cache/storage"
}
@ -111,17 +107,12 @@ Rectangle {
spacing: Theme.spacing.tiny
LogosTextField {
isValid: text.trim().length > 0
id: dataDirTextField
placeholderText: "Enter the data dir"
text: root.dataDir
Layout.fillWidth: true
onTextChanged: {
if (text.length > 0) {
isValid = root.backend.validateDataDir(text)
} else {
isValid = false
}
root.dataDir = text
}
}

View File

@ -128,16 +128,20 @@ Rectangle {
function updateLogLevel(logLevel) {}
property var manifests: []
property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 // 20 GB default
property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 // 20 GB default
property var quotaUsedBytes: 0
property var quotaReservedBytes: 0
}
function formatBytes(bytes) {
if (bytes <= 0) return "0 B"
if (bytes < 1024) return bytes + " B"
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB"
if (bytes <= 0)
return "0 B"
if (bytes < 1024)
return bytes + " B"
if (bytes < 1024 * 1024)
return (bytes / 1024).toFixed(1) + " KB"
if (bytes < 1024 * 1024 * 1024)
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
}
@ -417,8 +421,8 @@ Rectangle {
width: parent.width - 40
height: root.backend.quotaMaxBytes > 0 ? 36 : 20
readonly property real total: root.backend.quotaMaxBytes
readonly property real used: root.backend.quotaUsedBytes
readonly property real total: root.backend.quotaMaxBytes
readonly property real used: root.backend.quotaUsedBytes
readonly property real reserved: root.backend.quotaReservedBytes
// No quota configured
@ -446,7 +450,9 @@ Rectangle {
// Used (green)
Rectangle {
width: Math.min(parent.width * (spaceBarSection.used / spaceBarSection.total), parent.width)
width: Math.min(
parent.width * (spaceBarSection.used / spaceBarSection.total),
parent.width)
height: parent.height
radius: parent.radius
color: "#4CAF50"
@ -455,8 +461,9 @@ Rectangle {
// Reserved (orange), stacked after used
Rectangle {
x: parent.width * (spaceBarSection.used / spaceBarSection.total)
width: Math.min(parent.width * (spaceBarSection.reserved / spaceBarSection.total),
parent.width - x)
width: Math.min(
parent.width * (spaceBarSection.reserved / spaceBarSection.total),
parent.width - x)
height: parent.height
color: "#FF9800"
}
@ -481,7 +488,8 @@ Rectangle {
font.pixelSize: 10
}
Text {
text: "Free: " + root.formatBytes(spaceBarSection.total - spaceBarSection.used - spaceBarSection.reserved)
text: "Free: " + root.formatBytes(
spaceBarSection.total - spaceBarSection.used - spaceBarSection.reserved)
color: "#888888"
font.pixelSize: 10
}
@ -618,7 +626,7 @@ Rectangle {
font.family: "monospace"
elide: Text.ElideMiddle
anchors.verticalCenter: parent.verticalCenter
ToolTip.visible: hovered
// ToolTip.visible: hovered
ToolTip.text: modelData["cid"] ?? ""
HoverHandler {}
}
@ -899,8 +907,19 @@ Rectangle {
}
onTextChanged: {
// try {
// const jsonData = JSON.parse(text)
// isValid = true
// } catch (e) {
// isValid = false
// }
}
onEditingFinished: {
try {
const jsonData = JSON.parse(text)
root.backend.saveUserConfig(text)
isValid = true
} catch (e) {
isValid = false