pmaports/temp/discover/0001-Add-support-for-Alpine-Linux-apk-backend.patch

3332 lines
125 KiB
Diff
Raw Normal View History

From 6fbb948082bad66a821c8332e2fb9cb7965065f8 Mon Sep 17 00:00:00 2001
From: Alexey Minnekhanov <alexeymin@postmarketos.org>
Date: Sun, 12 Jan 2020 01:02:39 +0300
Subject: [PATCH] Add support for Alpine Linux apk backend
Alpine Package Keeper (apk) is package manager
for Alpine Linux.
---
discover/FeaturedModel.cpp | 6 +-
.../AlpineApkAuthActionFactory.cpp | 118 ++++
.../AlpineApkAuthActionFactory.h | 41 ++
.../AlpineApkBackend/AlpineApkAuthHelper.cpp | 302 +++++++++++
.../AlpineApkBackend/AlpineApkAuthHelper.h | 66 +++
.../AlpineApkBackend/AlpineApkBackend.cpp | 503 ++++++++++++++++++
.../AlpineApkBackend/AlpineApkBackend.h | 99 ++++
.../AlpineApkBackend/AlpineApkResource.cpp | 341 ++++++++++++
.../AlpineApkBackend/AlpineApkResource.h | 93 ++++
.../AlpineApkReviewsBackend.cpp | 35 ++
.../AlpineApkReviewsBackend.h | 52 ++
.../AlpineApkSourcesBackend.cpp | 195 +++++++
.../AlpineApkSourcesBackend.h | 57 ++
.../AlpineApkBackend/AlpineApkTransaction.cpp | 141 +++++
.../AlpineApkBackend/AlpineApkTransaction.h | 49 ++
.../AlpineApkBackend/AlpineApkUpdater.cpp | 295 ++++++++++
.../AlpineApkBackend/AlpineApkUpdater.h | 197 +++++++
.../AppstreamDataDownloader.cpp | 303 +++++++++++
.../AppstreamDataDownloader.h | 139 +++++
.../backends/AlpineApkBackend/CMakeLists.txt | 85 +++
.../org.kde.discover.alpineapkbackend.actions | 5 +
libdiscover/backends/CMakeLists.txt | 10 +
22 files changed, 3129 insertions(+), 3 deletions(-)
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.cpp
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.cpp
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
create mode 100644 libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
create mode 100644 libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
create mode 100644 libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h
create mode 100644 libdiscover/backends/AlpineApkBackend/CMakeLists.txt
create mode 100644 libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
diff --git a/discover/FeaturedModel.cpp b/discover/FeaturedModel.cpp
index 1393e206..038f22c1 100644
--- a/discover/FeaturedModel.cpp
+++ b/discover/FeaturedModel.cpp
@@ -98,13 +98,13 @@ void FeaturedModel::refresh()
setUris(uris);
}
-void FeaturedModel::setUris(const QVector<QUrl>& uris)
+void FeaturedModel::setUris(const QVector<QUrl> &uris)
{
if (!m_backend)
return;
- QSet<ResultsStream*> streams;
- foreach(const auto &uri, uris) {
+ QSet<ResultsStream *> streams;
+ for (const QUrl &uri: uris) {
AbstractResourcesBackend::Filters filter;
filter.resourceUrl = uri;
streams << m_backend->search(filter);
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.cpp
new file mode 100644
index 00000000..972f8ec5
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.cpp
@@ -0,0 +1,118 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include <KLocalizedString>
+#include <kauth_version.h>
+
+#include "AlpineApkAuthActionFactory.h"
+#include "alpineapk_backend_logging.h"
+
+namespace ActionFactory {
+
+static KAuth::Action createAlpineApkKAuthAction()
+{
+ KAuth::Action action(QStringLiteral("org.kde.discover.alpineapkbackend.pkgmgmt"));
+ action.setHelperId(QStringLiteral("org.kde.discover.alpineapkbackend"));
+ if (!action.isValid()) {
+ qCWarning(LOG_ALPINEAPK) << "Created KAuth action is not valid!";
+ return action;
+ }
+
+ // set action description
+ // setDetails deprecated since KF 5.68, use setDetailsV2() with DetailsMap.
+#if KAUTH_VERSION < QT_VERSION_CHECK(5, 68, 0)
+ action.setDetails(i18n("Package management"));
+#else
+ static const KAuth::Action::DetailsMap details{
+ { KAuth::Action::AuthDetail::DetailMessage, i18n("Package management") }
+ };
+ action.setDetailsV2(details);
+#endif
+
+ // change default timeout to 1 minute, bcause default DBus timeout
+ // of 25 seconds is not enough
+ action.setTimeout(1 * 60 * 1000);
+
+ return action;
+}
+
+KAuth::ExecuteJob *createUpdateAction(const QString &fakeRoot)
+{
+ KAuth::Action action = createAlpineApkKAuthAction();
+ if (!action.isValid()) {
+ return nullptr;
+ }
+ // update-action specific details
+ action.setTimeout(2 * 60 * 1000); // 2 minutes
+ action.addArgument(QLatin1String("pkgAction"), QLatin1String("update"));
+ action.addArgument(QLatin1String("fakeRoot"), fakeRoot);
+ return action.execute();
+}
+
+KAuth::ExecuteJob *createUpgradeAction(bool onlySimulate)
+{
+ KAuth::Action action = createAlpineApkKAuthAction();
+ if (!action.isValid()) {
+ return nullptr;
+ }
+ action.setTimeout(3 * 60 * 60 * 1000); // 3 hours, system upgrade can take really long
+ action.addArgument(QLatin1String("pkgAction"), QLatin1String("upgrade"));
+ action.addArgument(QLatin1String("onlySimulate"), onlySimulate);
+ return action.execute();
+}
+
+KAuth::ExecuteJob *createAddAction(const QString &pkgName)
+{
+ KAuth::Action action = createAlpineApkKAuthAction();
+ if (!action.isValid()) {
+ return nullptr;
+ }
+ action.setTimeout(1 * 60 * 60 * 1000); // 1 hour, in case package is really big?
+ action.addArgument(QLatin1String("pkgAction"), QLatin1String("add"));
+ action.addArgument(QLatin1String("pkgName"), pkgName);
+ return action.execute();
+}
+
+KAuth::ExecuteJob *createDelAction(const QString &pkgName)
+{
+ KAuth::Action action = createAlpineApkKAuthAction();
+ if (!action.isValid()) {
+ return nullptr;
+ }
+ action.setTimeout(1 * 60 * 60 * 1000); // although deletion is almost instant
+ action.addArgument(QLatin1String("pkgAction"), QLatin1String("del"));
+ action.addArgument(QLatin1String("pkgName"), pkgName);
+ return action.execute();
+}
+
+KAuth::ExecuteJob *createRepoconfigAction(const QVariant &repoUrls)
+{
+ KAuth::Action action = createAlpineApkKAuthAction();
+ if (!action.isValid()) {
+ return nullptr;
+ }
+ // should be instant, writes few lines to /etc/apk/repositories
+ action.setTimeout(1 * 60 * 1000); // 1 minute
+ action.addArgument(QLatin1String("pkgAction"), QLatin1String("repoconfig"));
+ action.addArgument(QLatin1String("repoList"), repoUrls);
+ return action.execute();
+}
+
+} // namespace ActionFactory
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h
new file mode 100644
index 00000000..ff5667f8
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthActionFactory.h
@@ -0,0 +1,41 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef AlpineApkAuthActionFactory_H
+#define AlpineApkAuthActionFactory_H
+
+#include <QString>
+#include <QVariant>
+
+#include <KAuthAction>
+#include <KAuthActionReply>
+#include <KAuthExecuteJob>
+
+namespace ActionFactory {
+
+KAuth::ExecuteJob *createUpdateAction(const QString &fakeRoot);
+KAuth::ExecuteJob *createUpgradeAction(bool onlySimulate = false);
+KAuth::ExecuteJob *createAddAction(const QString &pkgName);
+KAuth::ExecuteJob *createDelAction(const QString &pkgName);
+KAuth::ExecuteJob *createRepoconfigAction(const QVariant &repoUrls);
+
+} // namespace ActionFactory
+
+#endif
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
new file mode 100644
index 00000000..97affc01
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.cpp
@@ -0,0 +1,302 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include <QProcess>
+#include <QDebug>
+#include <QLoggingCategory>
+#include <QSocketNotifier>
+#include <QScopedPointer>
+#include <QFile>
+
+#include <KAuthHelperSupport>
+#include <kauth_version.h>
+
+#include "AlpineApkAuthHelper.h"
+
+#ifdef QT_DEBUG
+Q_LOGGING_CATEGORY(LOG_AUTHHELPER, "org.kde.discover.alpineapkbackend.authhelper", QtDebugMsg)
+#else
+Q_LOGGING_CATEGORY(LOG_AUTHHELPER, "org.kde.discover.alpineapkbackend.authhelper", QtWarningMsg)
+#endif
+
+using namespace KAuth;
+
+AlpineApkAuthHelper::AlpineApkAuthHelper() {}
+
+AlpineApkAuthHelper::~AlpineApkAuthHelper()
+{
+ closeDatabase();
+}
+
+bool AlpineApkAuthHelper::openDatabase(const QVariantMap &args, bool readwrite)
+{
+ // is already opened?
+ if (m_apkdb.isOpen()) {
+ return true;
+ }
+
+ // maybe set fakeRoot (needs to be done before Database::open()
+ const QString fakeRoot = args.value(QLatin1String("fakeRoot"), QString()).toString();
+ if (!fakeRoot.isEmpty()) {
+ m_apkdb.setFakeRoot(fakeRoot);
+ }
+
+ // calculate flags to use during open
+ QtApk::DbOpenFlags fl = QtApk::QTAPK_OPENF_ENABLE_PROGRESSFD;
+ if (readwrite) {
+ fl |= QtApk::QTAPK_OPENF_READWRITE;
+ }
+
+ if (!m_apkdb.open(fl)) {
+ return false;
+ }
+ return true;
+}
+
+void AlpineApkAuthHelper::closeDatabase()
+{
+ // close database only if opened
+ if (m_apkdb.isOpen()) {
+ // this also stops bg thread
+ m_apkdb.close();
+ }
+}
+
+void AlpineApkAuthHelper::setupTransactionPostCreate(QtApk::Transaction *trans)
+{
+ m_currentTransaction = trans; // remember current transaction here
+
+ // receive progress notifications
+ QObject::connect(trans, &QtApk::Transaction::progressChanged,
+ this, &AlpineApkAuthHelper::reportProgress);
+
+ // receive error messages
+ QObject::connect(trans, &QtApk::Transaction::errorOccured,
+ this, &AlpineApkAuthHelper::onTransactionError);
+
+ // what to do when transaction is complete
+ QObject::connect(trans, &QtApk::Transaction::finished,
+ this, &AlpineApkAuthHelper::onTransactionFinished);
+
+ if (!m_loop) {
+ m_loop = new QEventLoop(this);
+ }
+}
+
+void AlpineApkAuthHelper::reportProgress(float percent)
+{
+ int p = static_cast<int>(percent);
+ if (p < 0) p = 0;
+ if (p > 100) p = 100;
+ HelperSupport::progressStep(p);
+}
+
+void AlpineApkAuthHelper::onTransactionError(const QString &msg)
+{
+ qCWarning(LOG_AUTHHELPER).nospace() << "ERROR occured in transaction \""
+ << m_currentTransaction->desc()
+ << "\": " << msg;
+ // construct error message to use in helper reply
+ const QString errMsg = m_currentTransaction->desc() + QLatin1String(" failed: ") + msg;
+ m_actionReply.setErrorDescription(errMsg);
+ m_actionReply.setData({
+ { QLatin1String("errorString"), errMsg }
+ });
+ m_trans_ok = false;
+}
+
+void AlpineApkAuthHelper::onTransactionFinished()
+{
+ m_lastChangeset = m_currentTransaction->changeset();
+ m_currentTransaction->deleteLater();
+ m_currentTransaction = nullptr;
+ m_loop->quit();
+}
+
+// single entry point for all package management actions
+ActionReply AlpineApkAuthHelper::pkgmgmt(const QVariantMap &args)
+{
+ m_actionReply = ActionReply::HelperErrorReply();
+ HelperSupport::progressStep(0);
+
+ // actual package management action to perform is passed in "pkgAction" argument
+ if (!args.contains(QLatin1String("pkgAction"))) {
+ m_actionReply.setError(ActionReply::InvalidActionError);
+ m_actionReply.setErrorDescription(QLatin1String("Please pass \'pkgAction\' argument."));
+ HelperSupport::progressStep(100);
+ return m_actionReply;
+ }
+
+ const QString pkgAction = args.value(QLatin1String("pkgAction")).toString();
+
+ if (pkgAction == QStringLiteral("update")) {
+ update(args);
+ } else if (pkgAction == QStringLiteral("add")) {
+ add(args);
+ } else if (pkgAction == QStringLiteral("del")) {
+ del(args);
+ } else if (pkgAction == QStringLiteral("upgrade")) {
+ upgrade(args);
+ } else if (pkgAction == QStringLiteral("repoconfig")) {
+ repoconfig(args);
+ } else {
+ // error: unknown pkgAction
+ m_actionReply.setError(ActionReply::NoSuchActionError);
+ m_actionReply.setErrorDescription(QLatin1String("Please pass a valid \'pkgAction\' argument. "
+ "Action \"%1\" is not recognized.").arg(pkgAction));
+ }
+
+ HelperSupport::progressStep(100);
+ return m_actionReply;
+}
+
+void AlpineApkAuthHelper::update(const QVariantMap &args)
+{
+ if (!openDatabase(args)) {
+ m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
+ return;
+ }
+
+ m_trans_ok = true;
+ QtApk::Transaction *trans = m_apkdb.updatePackageIndex();
+ setupTransactionPostCreate(trans);
+
+ trans->start();
+ m_loop->exec();
+
+ if (m_trans_ok) {
+ int updatesCount = m_apkdb.upgradeablePackagesCount();
+ m_actionReply = ActionReply::SuccessReply();
+ m_actionReply.setData({
+ { QLatin1String("updatesCount"), updatesCount }
+ });
+ }
+}
+
+void AlpineApkAuthHelper::add(const QVariantMap &args)
+{
+ if (!openDatabase(args)) {
+ m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
+ return;
+ }
+
+ const QString pkgName = args.value(QLatin1String("pkgName"), QString()).toString();
+ if (pkgName.isEmpty()) {
+ m_actionReply.setErrorDescription(QStringLiteral("Specify pkgName for adding!"));
+ return;
+ }
+
+ m_trans_ok = true;
+ QtApk::Transaction *trans = m_apkdb.add(pkgName);
+ setupTransactionPostCreate(trans);
+
+ trans->start();
+ m_loop->exec();
+
+ if (m_trans_ok) {
+ m_actionReply = ActionReply::SuccessReply();
+ }
+}
+
+void AlpineApkAuthHelper::del(const QVariantMap &args)
+{
+ if (!openDatabase(args)) {
+ m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
+ return;
+ }
+
+ const QString pkgName = args.value(QLatin1String("pkgName"), QString()).toString();
+ if (pkgName.isEmpty()) {
+ m_actionReply.setErrorDescription(QStringLiteral("Specify pkgName for removing!"));
+ return;
+ }
+
+ const bool delRdepends = args.value(QLatin1String("delRdepends"), false).toBool();
+
+ QtApk::DbDelFlags delFlags = QtApk::QTAPK_DEL_DEFAULT;
+ if (delRdepends) {
+ delFlags = QtApk::QTAPK_DEL_RDEPENDS;
+ }
+
+ m_trans_ok = true;
+ QtApk::Transaction *trans = m_apkdb.del(pkgName, delFlags);
+ setupTransactionPostCreate(trans);
+
+ trans->start();
+ m_loop->exec();
+
+ if (m_trans_ok) {
+ m_actionReply = ActionReply::SuccessReply();
+ }
+}
+
+void AlpineApkAuthHelper::upgrade(const QVariantMap &args)
+{
+ if (!openDatabase(args)) {
+ m_actionReply.setErrorDescription(QStringLiteral("Failed to open database!"));
+ return;
+ }
+
+ bool onlySimulate = args.value(QLatin1String("onlySimulate"), false).toBool();
+ QtApk::DbUpgradeFlags flags = QtApk::QTAPK_UPGRADE_DEFAULT;
+ if (onlySimulate) {
+ flags = QtApk::QTAPK_UPGRADE_SIMULATE;
+ qCDebug(LOG_AUTHHELPER) << "Simulating upgrade run.";
+ }
+
+ m_trans_ok = true;
+
+ QtApk::Transaction *trans = m_apkdb.upgrade(flags);
+ setupTransactionPostCreate(trans);
+
+ trans->start();
+ m_loop->exec();
+
+ if (m_trans_ok) {
+ m_actionReply = ActionReply::SuccessReply();
+ QVariantMap replyData;
+ const QVector<QtApk::ChangesetItem> ch = m_lastChangeset.changes();
+ QVector<QVariant> chVector;
+ QVector<QtApk::Package> pkgVector;
+ for (const QtApk::ChangesetItem &it: ch) {
+ pkgVector << it.newPackage;
+ }
+ replyData.insert(QLatin1String("changes"), QVariant::fromValue(pkgVector));
+ replyData.insert(QLatin1String("onlySimulate"), onlySimulate);
+ m_actionReply.setData(replyData);
+ }
+}
+
+void AlpineApkAuthHelper::repoconfig(const QVariantMap &args)
+{
+ if (args.contains(QLatin1String("repoList"))) {
+ const QVariant v = args.value(QLatin1String("repoList"));
+ const QVector<QtApk::Repository> repoVec = v.value<QVector<QtApk::Repository>>();
+ if (QtApk::Database::saveRepositories(repoVec)) {
+ m_actionReply = ActionReply::SuccessReply(); // OK
+ } else {
+ m_actionReply.setErrorDescription(QStringLiteral("Failed to write repositories config!"));
+ }
+ } else {
+ m_actionReply.setErrorDescription(QStringLiteral("repoList parameter is missing in request!"));
+ }
+}
+
+KAUTH_HELPER_MAIN("org.kde.discover.alpineapkbackend", AlpineApkAuthHelper)
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
new file mode 100644
index 00000000..240e6ed3
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkAuthHelper.h
@@ -0,0 +1,66 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include <QEventLoop>
+#include <QObject>
+#include <QVariant>
+#include <KAuthActionReply>
+
+#include <QtApk>
+
+using namespace KAuth;
+
+class AlpineApkAuthHelper : public QObject
+{
+ Q_OBJECT
+public:
+ AlpineApkAuthHelper();
+ ~AlpineApkAuthHelper() override;
+
+public Q_SLOTS:
+ // single entry point for all package management operations
+ ActionReply pkgmgmt(const QVariantMap &args);
+
+protected:
+ // helpers
+ bool openDatabase(const QVariantMap &args, bool readwrite = true);
+ void closeDatabase();
+ void setupTransactionPostCreate(QtApk::Transaction *trans);
+
+ // individual pakckage management actions
+ void update(const QVariantMap &args);
+ void add(const QVariantMap &args);
+ void del(const QVariantMap &args);
+ void upgrade(const QVariantMap &args);
+ void repoconfig(const QVariantMap &args);
+
+protected Q_SLOTS:
+ void reportProgress(float percent);
+ void onTransactionError(const QString &msg);
+ void onTransactionFinished();
+
+private:
+ QtApk::DatabaseAsync m_apkdb; // runs transactions in bg thread
+ QtApk::Transaction *m_currentTransaction = nullptr;
+ QEventLoop *m_loop = nullptr; // event loop that will run and wait while bg transaction is in progress
+ ActionReply m_actionReply; // return value for main action slots
+ bool m_trans_ok = true; // flag to indicate if bg transaction was successful
+ QtApk::Changeset m_lastChangeset; // changeset from last completed transaction
+};
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
new file mode 100644
index 00000000..4bfe165b
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.cpp
@@ -0,0 +1,503 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include "AlpineApkBackend.h"
+#include "AlpineApkResource.h"
+#include "AlpineApkReviewsBackend.h"
+#include "AlpineApkTransaction.h"
+#include "AlpineApkSourcesBackend.h"
+#include "AlpineApkUpdater.h"
+#include "AppstreamDataDownloader.h"
+#include "alpineapk_backend_logging.h" // generated by ECM
+
+#include "resources/SourcesModel.h"
+#include "Transaction/Transaction.h"
+#include "Category/Category.h"
+
+#include <KLocalizedString>
+
+#include <AppStreamQt/pool.h>
+
+#include <QAction>
+#include <QtConcurrentRun>
+#include <QDebug>
+#include <QFuture>
+#include <QFutureWatcher>
+#include <QLoggingCategory>
+#include <QSet>
+#include <QThread>
+#include <QThreadPool>
+#include <QTimer>
+
+DISCOVER_BACKEND_PLUGIN(AlpineApkBackend)
+
+AlpineApkBackend::AlpineApkBackend(QObject *parent)
+ : AbstractResourcesBackend(parent)
+ , m_updater(new AlpineApkUpdater(this))
+ , m_reviews(new AlpineApkReviewsBackend(this))
+ , m_updatesTimeoutTimer(new QTimer(this))
+{
+#ifndef QT_DEBUG
+ const_cast<QLoggingCategory &>(LOG_ALPINEAPK()).setEnabled(QtDebugMsg, false);
+#endif
+
+ // connections with our updater
+ QObject::connect(m_updater, &AlpineApkUpdater::updatesCountChanged,
+ this, &AlpineApkBackend::updatesCountChanged);
+ QObject::connect(m_updater, &AlpineApkUpdater::checkForUpdatesFinished,
+ this, &AlpineApkBackend::finishCheckForUpdates);
+ QObject::connect(m_updater, &AlpineApkUpdater::fetchingUpdatesProgressChanged,
+ this, &AlpineApkBackend::setFetchingUpdatesProgress);
+
+ // safety measure: make sure update check process can finish in some finite time
+ QObject::connect(m_updatesTimeoutTimer, &QTimer::timeout,
+ this, &AlpineApkBackend::finishCheckForUpdates);
+ m_updatesTimeoutTimer->setTimerType(Qt::CoarseTimer);
+ m_updatesTimeoutTimer->setSingleShot(true);
+ m_updatesTimeoutTimer->setInterval(5 * 60 * 1000); // 5 minutes
+
+ // load packages data in a separate thread; it takes a noticeable amount of time
+ // and this way UI is not blocked here
+ m_fetching = true; // we are busy!
+ QFuture<void> loadResFuture = QtConcurrent::run(QThreadPool::globalInstance(), this,
+ &AlpineApkBackend::loadResources);
+
+ QObject::connect(&m_voidFutureWatcher, &QFutureWatcher<void>::finished,
+ this, &AlpineApkBackend::onLoadResourcesFinished);
+ m_voidFutureWatcher.setFuture(loadResFuture);
+
+ SourcesModel::global()->addSourcesBackend(new AlpineApkSourcesBackend(this));
+}
+
+// this fills in m_appStreamComponents
+void AlpineApkBackend::loadAppStreamComponents()
+{
+ AppStream::Pool *appStreamPool = new AppStream::Pool();
+ appStreamPool->setFlags(AppStream::Pool::FlagReadCollection |
+ AppStream::Pool::FlagReadMetainfo |
+ AppStream::Pool::FlagReadDesktopFiles);
+ appStreamPool->setCacheFlags(AppStream::Pool::CacheFlagUseUser |
+ AppStream::Pool::CacheFlagUseSystem);
+
+ // hey hey! cool stuff
+ appStreamPool->addMetadataLocation(AppstreamDataDownloader::getAppStreamCacheDir());
+
+ if (!appStreamPool->load()) {
+ qCWarning(LOG_ALPINEAPK) << "backend: Failed to load appstream data:"
+ << appStreamPool->lastError();
+ } else {
+ m_appStreamComponents = appStreamPool->components();
+ qCDebug(LOG_ALPINEAPK) << "backend: loaded AppStream metadata OK:"
+ << m_appStreamComponents.size() << "components.";
+ // collect all categories present in appstream metadata
+ // QSet<QString> collectedCategories;
+ // for (const AppStream::Component &component : m_appStreamComponents) {
+ // const QStringList cats = component.categories();
+ // for (const QString &cat : cats) {
+ // collectedCategories.insert(cat);
+ // }
+ // }
+ // for (const QString &cat : collectedCategories) {
+ // qCDebug(LOG_ALPINEAPK) << " collected category: " << cat;
+ // m_collectedCategories << cat;
+ // }
+ }
+ delete appStreamPool;
+}
+
+// this uses m_appStreamComponents and m_availablePackages
+// to fill in m_resourcesAppstreamData
+void AlpineApkBackend::parseAppStreamMetadata()
+{
+ if (m_availablePackages.size() > 0) {
+
+ for (const QtApk::Package &pkg: qAsConst(m_availablePackages)) {
+
+ // try to find appstream data for this package
+ AppStream::Component appstreamComponent;
+ for (const auto& appsC : qAsConst(m_appStreamComponents)) {
+ // find result which package name is exactly the one we want
+ if (appsC.packageNames().contains(pkg.name)) {
+ // workaround for kate (Kate Sessions is found first, but
+ // package name = "kate" too, bugged metadata?)
+ if (pkg.name == QStringLiteral("kate")) {
+ // qCDebug(LOG_ALPINEAPK) << appsC.packageNames() << appsC.id();
+ // ^^ ("kate") "org.kde.plasma.katesessions"
+ if (appsC.id() != QStringLiteral("org.kde.kate")) {
+ continue;
+ }
+ }
+ appstreamComponent = appsC;
+ break; // exit for() loop
+ }
+ }
+
+ const QString key = pkg.name.toLower();
+ m_resourcesAppstreamData.insert(key, appstreamComponent);
+ }
+ }
+}
+
+static AbstractResource::Type toDiscoverResourceType(const AppStream::Component &component)
+{
+ AbstractResource::Type resType = AbstractResource::Type::Technical; // default
+ // determine resource type here
+ switch (component.kind()) {
+ case AppStream::Component::KindDesktopApp:
+ case AppStream::Component::KindConsoleApp:
+ case AppStream::Component::KindWebApp:
+ resType = AbstractResource::Type::Application;
+ break;
+ case AppStream::Component::KindAddon:
+ resType = AbstractResource::Type::Addon;
+ break;
+ default:
+ resType = AbstractResource::Type::Technical;
+ break;
+ }
+ return resType;
+}
+
+void AlpineApkBackend::fillResourcesAndApplyAppStreamData()
+{
+ // now the tricky part - we need to reapply appstream component metadata to each resource
+ if (m_availablePackages.size() > 0) {
+ for (const QtApk::Package &pkg: m_availablePackages) {
+ const QString key = pkg.name.toLower();
+
+ AppStream::Component &appsComponent = m_resourcesAppstreamData[key];
+ const AbstractResource::Type resType = toDiscoverResourceType(appsComponent);
+
+ AlpineApkResource *res = m_resources.value(key, nullptr);
+ if (res == nullptr) {
+ // during first run of this function during initial load
+ // m_resources hash is empty, so we need to insert new items
+ res = new AlpineApkResource(pkg, appsComponent, resType, this);
+ res->setCategoryName(QStringLiteral("alpine_packages"));
+ res->setOriginSource(QStringLiteral("apk"));
+ res->setSection(QStringLiteral("dummy"));
+ m_resources.insert(key, res);
+ QObject::connect(res, &AlpineApkResource::stateChanged,
+ this, &AlpineApkBackend::updatesCountChanged);
+ } else {
+ // this is not an initial run, just update existing resource
+ res->setAppStreamData(appsComponent);
+ }
+ }
+ }
+}
+
+void AlpineApkBackend::reloadAppStreamMetadata()
+{
+ // mark us as "Loading..."
+ m_fetching = true;
+ emit fetchingChanged();
+
+ loadAppStreamComponents();
+ parseAppStreamMetadata();
+ fillResourcesAndApplyAppStreamData();
+
+ // mark us as "done loading"
+ m_fetching = false;
+ emit fetchingChanged();
+}
+
+// this function is executed in the background thread
+void AlpineApkBackend::loadResources()
+{
+ Q_EMIT this->passiveMessage(i18n("Loading, please wait..."));
+
+ qCDebug(LOG_ALPINEAPK) << "backend: loading AppStream metadata...";
+
+ loadAppStreamComponents();
+
+ qCDebug(LOG_ALPINEAPK) << "backend: populating resources...";
+
+ if (m_apkdb.open(QtApk::QTAPK_OPENF_READONLY)) {
+ m_availablePackages = m_apkdb.getAvailablePackages();
+ m_installedPackages = m_apkdb.getInstalledPackages();
+ m_apkdb.close();
+ }
+
+ parseAppStreamMetadata();
+
+ qCDebug(LOG_ALPINEAPK) << " available" << m_availablePackages.size()
+ << "packages";
+ qCDebug(LOG_ALPINEAPK) << " installed" << m_installedPackages.size()
+ << "packages";
+}
+
+void AlpineApkBackend::onLoadResourcesFinished()
+{
+ qCDebug(LOG_ALPINEAPK) << "backend: appstream data loaded and sorted; fill in resources";
+
+ fillResourcesAndApplyAppStreamData();
+
+ // update "installed/not installed" state
+ if (m_installedPackages.size() > 0) {
+ for (const QtApk::Package &pkg: m_installedPackages) {
+ const QString key = pkg.name.toLower();
+ if (m_resources.contains(key)) {
+ m_resources.value(key)->setState(AbstractResource::Installed);
+ }
+ }
+ }
+
+ qCDebug(LOG_ALPINEAPK) << "backend: resources loaded.";
+
+ m_fetching = false;
+ emit fetchingChanged();
+ // ^^ this causes the UI to update "Featured" page and show
+ // to user that we actually have loaded packages data
+
+ // schedule check for updates 1 sec after we've loaded all resources
+ QTimer::singleShot(1000, this, &AlpineApkBackend::checkForUpdates);
+
+ // AppStream appdata downloader can download updated metadata files
+ // in a background thread. When potential download is finished,
+ // appstream data will be reloaded.
+ m_appstreamDownloader = new AppstreamDataDownloader(nullptr);
+ QObject::connect(m_appstreamDownloader, &AppstreamDataDownloader::downloadFinished,
+ this, &AlpineApkBackend::onAppstreamDataDownloaded, Qt::QueuedConnection);
+ m_appstreamDownloader->start();
+}
+
+void AlpineApkBackend::onAppstreamDataDownloaded()
+{
+ if (m_appstreamDownloader) {
+ if (m_appstreamDownloader->cacheWasUpdated()) {
+ // it means we need to reload previously loaded appstream metadata
+ // m_fetching is true if loadResources() is still executing
+ // in a background thread
+ if (!m_fetching) {
+ qCDebug(LOG_ALPINEAPK) << "AppStream metadata was updated; re-applying it to all resources";
+ reloadAppStreamMetadata();
+ } else {
+ qCWarning(LOG_ALPINEAPK) << "AppStream metadata was updated, but cannot apply it: still fetching";
+ // it should not really happen, but if it happens,
+ // then downloaded metadata will be used on the next
+ // discover launch anyway.
+ }
+ }
+ delete m_appstreamDownloader;
+ m_appstreamDownloader = nullptr;
+ }
+}
+
+QVector<Category *> AlpineApkBackend::category() const
+{
+ static QPair<FilterType, QString> s_apkFlt(
+ FilterType::CategoryFilter, QLatin1String("alpine_packages"));
+
+ // Display a single root category
+ // we could add more, but Alpine apk does not have this concept
+ static Category *s_rootCat = new Category(
+ i18nc("Root category name", "Alpine Linux packages"),
+ QStringLiteral("package-x-generic"), // icon
+ { s_apkFlt }, // orFilters - include packages that match filter
+ { displayName() }, // pluginName
+ {}, // subCategories - none
+ QUrl(), // decoration (what is it?)
+ false // isAddons
+ );
+
+ return { s_rootCat };
+
+// static QVector<Category *> s_cats;
+// if (s_cats.isEmpty()) {
+// // fill only once
+// s_cats << s_rootCat;
+// for (const QString &scat : m_collectedCategories) {
+// Category *cat = new Category(
+// scat, // name
+// QStringLiteral("package-x-generic"), // icon
+// {}, // orFilters
+// { displayName() }, // pluginName
+// {}, // subcategories
+// QUrl(), // decoration
+// false // isAddons
+// );
+// s_cats << cat;
+// }
+// }
+// return s_cats;
+ // ^^ causes deep hang in discover in recalculating QML bindings
+}
+
+int AlpineApkBackend::updatesCount() const
+{
+ return m_updater->updatesCount();
+}
+
+ResultsStream *AlpineApkBackend::search(const AbstractResourcesBackend::Filters &filter)
+{
+ QVector<AbstractResource*> ret;
+ if (!filter.resourceUrl.isEmpty()) {
+ return findResourceByPackageName(filter.resourceUrl);
+ } else {
+ for (AbstractResource *resource: qAsConst(m_resources)) {
+ // skip technical package types (not apps/addons)
+ // that are not upgradeable
+ // (does not work because for now all Alpine packages are "technical"
+ // if (resource->type() == AbstractResource::Technical
+ // && filter.state != AbstractResource::Upgradeable) {
+ // continue;
+ // }
+
+ // skip not-requested states
+ if (resource->state() < filter.state) {
+ continue;
+ }
+
+ if(resource->name().contains(filter.search, Qt::CaseInsensitive)
+ || resource->comment().contains(filter.search, Qt::CaseInsensitive)) {
+ ret += resource;
+ }
+ }
+ }
+ return new ResultsStream(QStringLiteral("AlpineApkStream"), ret);
+}
+
+ResultsStream *AlpineApkBackend::findResourceByPackageName(const QUrl &searchUrl)
+{
+// if (search.isLocalFile()) {
+// AlpineApkResource* res = new AlpineApkResource(
+// search.fileName(), AbstractResource::Technical, this);
+// res->setSize(666);
+// res->setState(AbstractResource::None);
+// m_resources.insert(res->packageName(), res);
+// connect(res, &AlpineApkResource::stateChanged, this, &AlpineApkBackend::updatesCountChanged);
+// return new ResultsStream(QStringLiteral("AlpineApkStream-local"), { res });
+// }
+
+ AlpineApkResource *result = nullptr;
+
+ // QUrl("appstream://org.kde.krita.desktop")
+ // smart workaround for appstream URLs - handle "featured" apps
+ if (searchUrl.scheme() == QLatin1String("appstream")) {
+ // remove leading "org.kde."
+ QString pkgName = searchUrl.host();
+ if (pkgName.startsWith(QLatin1String("org.kde."))) {
+ pkgName = pkgName.mid(8);
+ }
+ // remove trailing ".desktop"
+ if (pkgName.endsWith(QLatin1String(".desktop"))) {
+ pkgName = pkgName.left(pkgName.length() - 8);
+ }
+ // now we can search for "krita" package
+ result = m_resources.value(pkgName);
+ }
+
+ // QUrl("apk://krita")
+ // handle packages from Alpine repos
+ if (searchUrl.scheme() == QLatin1String("apk")) {
+ const QString pkgName = searchUrl.host();
+ result = m_resources.value(pkgName);
+ }
+
+ if (!result) {
+ return new ResultsStream(QStringLiteral("AlpineApkStream"), {});
+ }
+ return new ResultsStream(QStringLiteral("AlpineApkStream"), { result });
+}
+
+AbstractBackendUpdater *AlpineApkBackend::backendUpdater() const
+{
+ return m_updater;
+}
+
+AbstractReviewsBackend *AlpineApkBackend::reviewsBackend() const
+{
+ return m_reviews;
+}
+
+Transaction* AlpineApkBackend::installApplication(AbstractResource *app, const AddonList &addons)
+{
+ return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app),
+ addons, Transaction::InstallRole);
+}
+
+Transaction* AlpineApkBackend::installApplication(AbstractResource *app)
+{
+ return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app),
+ Transaction::InstallRole);
+}
+
+Transaction* AlpineApkBackend::removeApplication(AbstractResource *app)
+{
+ return new AlpineApkTransaction(qobject_cast<AlpineApkResource *>(app),
+ Transaction::RemoveRole);
+}
+
+int AlpineApkBackend::fetchingUpdatesProgress() const
+{
+ if (!m_fetching) return 100;
+ return m_fetchProgress;
+}
+
+void AlpineApkBackend::checkForUpdates()
+{
+ if (m_fetching) {
+ qCDebug(LOG_ALPINEAPK) << "backend: checkForUpdates(): already fetching";
+ return;
+ }
+
+ qCDebug(LOG_ALPINEAPK) << "backend: start checkForUpdates()";
+
+ // safety measure - finish updates check in some time
+ m_updatesTimeoutTimer->start();
+
+ // let our updater do the job
+ m_updater->startCheckForUpdates();
+
+ // update UI
+ m_fetching = true;
+ m_fetchProgress = 0;
+ emit fetchingChanged();
+ emit fetchingUpdatesProgressChanged();
+}
+
+void AlpineApkBackend::finishCheckForUpdates()
+{
+ m_updatesTimeoutTimer->stop(); // stop safety timer
+ // update UI
+ m_fetching = false;
+ emit fetchingChanged();
+ emit fetchingUpdatesProgressChanged();
+}
+
+QString AlpineApkBackend::displayName() const
+{
+ return i18nc("Backend plugin display name", "Alpine APK backend");
+}
+
+bool AlpineApkBackend::hasApplications() const
+{
+ return true;
+}
+
+void AlpineApkBackend::setFetchingUpdatesProgress(int percent)
+{
+ m_fetchProgress = percent;
+ emit fetchingUpdatesProgressChanged();
+}
+
+// needed because DISCOVER_BACKEND_PLUGIN(AlpineApkBackend) contains Q_OBJECT
+#include "AlpineApkBackend.moc"
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
new file mode 100644
index 00000000..07b6b7be
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkBackend.h
@@ -0,0 +1,99 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef AlpineApkBackend_H
+#define AlpineApkBackend_H
+
+#include <resources/AbstractResourcesBackend.h>
+
+#include <QFutureWatcher>
+#include <QVariantList>
+
+#include <QtApk>
+
+#include <AppStreamQt/component.h>
+
+class AlpineApkReviewsBackend;
+class AlpineApkUpdater;
+class AlpineApkResource;
+class AppstreamDataDownloader;
+class KJob;
+class QTimer;
+
+class AlpineApkBackend : public AbstractResourcesBackend
+{
+ Q_OBJECT
+
+public:
+ explicit AlpineApkBackend(QObject *parent = nullptr);
+
+ QVector<Category *> category() const override;
+ int updatesCount() const override;
+ AbstractBackendUpdater *backendUpdater() const override;
+ AbstractReviewsBackend *reviewsBackend() const override;
+ ResultsStream *search(const AbstractResourcesBackend::Filters &filter) override;
+ ResultsStream *findResourceByPackageName(const QUrl &search);
+ QHash<QString, AlpineApkResource *> resources() const { return m_resources; }
+ QHash<QString, AlpineApkResource *> *resourcesPtr() { return &m_resources; }
+ bool isValid() const override { return true; } // No external file dependencies that could cause runtime errors
+
+ Transaction *installApplication(AbstractResource *app) override;
+ Transaction *installApplication(AbstractResource *app, const AddonList &addons) override;
+ Transaction *removeApplication(AbstractResource *app) override;
+ bool isFetching() const override { return m_fetching; }
+ int fetchingUpdatesProgress() const override;
+ void checkForUpdates() override;
+ QString displayName() const override;
+ bool hasApplications() const override;
+
+public Q_SLOTS:
+ void setFetchingUpdatesProgress(int percent);
+
+private Q_SLOTS:
+ void finishCheckForUpdates();
+ void loadAppStreamComponents();
+ void parseAppStreamMetadata();
+ void reloadAppStreamMetadata();
+ void fillResourcesAndApplyAppStreamData();
+ void loadResources();
+ void onLoadResourcesFinished();
+ void onAppstreamDataDownloaded();
+
+public:
+ QtApk::Database *apkdb() { return &m_apkdb; }
+
+private:
+ QHash<QString, AlpineApkResource *> m_resources;
+ QHash<QString, AppStream::Component> m_resourcesAppstreamData;
+ AlpineApkUpdater *m_updater;
+ AlpineApkReviewsBackend *m_reviews;
+ QtApk::Database m_apkdb;
+ QVector<QtApk::Package> m_availablePackages;
+ QVector<QtApk::Package> m_installedPackages;
+ bool m_fetching = false;
+ int m_fetchProgress = 0;
+ QTimer *m_updatesTimeoutTimer;
+ QList<AppStream::Component> m_appStreamComponents;
+ // QVector<QString> m_collectedCategories;
+ QFutureWatcher<void> m_voidFutureWatcher;
+ AppstreamDataDownloader *m_appstreamDownloader;
+};
+
+#endif // AlpineApkBackend_H
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
new file mode 100644
index 00000000..8f493a49
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.cpp
@@ -0,0 +1,341 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include "AlpineApkResource.h"
+#include "alpineapk_backend_logging.h" // generated by ECM
+
+#include <AppStreamQt/icon.h>
+#include <AppStreamQt/pool.h>
+#include <AppStreamQt/release.h>
+
+#include <QFileInfo>
+#include <QIcon>
+#include <QProcess>
+
+// libdiscover
+#include "appstream/AppStreamUtils.h"
+#include "config-paths.h"
+#include "Transaction/AddonList.h"
+
+AlpineApkResource::AlpineApkResource(const QtApk::Package &apkPkg,
+ AppStream::Component &component,
+ AbstractResource::Type typ,
+ AbstractResourcesBackend *parent)
+ : AbstractResource(parent)
+ , m_state(AbstractResource::State::None)
+ , m_type(typ)
+ , m_pkg(apkPkg)
+ , m_appsC(component)
+{
+}
+
+QList<PackageState> AlpineApkResource::addonsInformation()
+{
+ return m_addons;
+}
+
+QString AlpineApkResource::availableVersion() const
+{
+ return m_availableVersion;
+}
+
+QStringList AlpineApkResource::categories()
+{
+ if (hasAppStreamData()) {
+ return m_appsC.categories();
+ }
+ return { m_category };
+}
+
+QString AlpineApkResource::comment()
+{
+ if (hasAppStreamData()) {
+ return m_appsC.summary();
+ }
+ return m_pkg.description;
+}
+
+int AlpineApkResource::size()
+{
+ return static_cast<int>(m_pkg.size);
+}
+
+QUrl AlpineApkResource::homepage()
+{
+ if (hasAppStreamData()) {
+ return m_appsC.url(AppStream::Component::UrlKindHomepage);
+ }
+ return QUrl::fromUserInput(m_pkg.url);
+}
+
+QUrl AlpineApkResource::helpURL()
+{
+ if (hasAppStreamData()) {
+ return m_appsC.url(AppStream::Component::UrlKindHelp);
+ }
+ return QUrl();
+}
+
+QUrl AlpineApkResource::bugURL()
+{
+ if (hasAppStreamData()) {
+ return m_appsC.url(AppStream::Component::UrlKindBugtracker);
+ }
+ return QUrl();
+}
+
+QUrl AlpineApkResource::donationURL()
+{
+ if (hasAppStreamData()) {
+ return m_appsC.url(AppStream::Component::UrlKindDonation);
+ }
+ return QUrl();
+}
+
+///xdg-compatible icon name to represent the resource, url or QIcon
+QVariant AlpineApkResource::icon() const
+{
+ if (hasAppStreamData()) {
+ const QList<AppStream::Icon> icns = m_appsC.icons();
+ if (icns.size() == 0) {
+ return QStringLiteral("package-x-generic");
+ }
+ QIcon ico;
+ const AppStream::Icon &appIco = icns.first();
+
+ switch (appIco.kind()) {
+ case AppStream::Icon::KindStock:
+ // we can create icons of this type directly from theme
+ ico = QIcon::fromTheme(appIco.name());
+ break;
+ case AppStream::Icon::KindLocal:
+ case AppStream::Icon::KindCached: {
+ // try from predefined standard Alpine path
+ const QString appstreamIconsPath = QLatin1String("/usr/share/app-info/icons/");
+ const QString path = appstreamIconsPath + appIco.url().path();
+ if (QFileInfo::exists(path)) {
+ ico.addFile(path, appIco.size());
+ } else {
+ const QString altPath = appstreamIconsPath +
+ QStringLiteral("%1x%2/").arg(appIco.size().width()).arg(appIco.size().height()) +
+ appIco.url().path();
+ if (QFileInfo::exists(altPath)) {
+ ico.addFile(altPath, appIco.size());
+ }
+ }
+ } break;
+ default: break;
+ }
+
+ // return icon only if we successfully loaded it
+ if (!ico.isNull()) {
+ return QVariant::fromValue<QIcon>(ico);
+ }
+
+ // try to load from icon theme by package name, this is better
+ // than nothing and works surprisingly well for many packages
+ ico = QIcon::fromTheme(m_pkg.name);
+ if (!ico.isNull()) {
+ return QVariant::fromValue<QIcon>(ico);
+ }
+ }
+ return QStringLiteral("package-x-generic");
+}
+
+QString AlpineApkResource::installedVersion() const
+{
+ return m_pkg.version;
+}
+
+QJsonArray AlpineApkResource::licenses()
+{
+ return {
+ QJsonObject {
+ { QStringLiteral("name"), m_pkg.license },
+ { QStringLiteral("url"), QStringLiteral("https://spdx.org/license-list") },
+ }
+ };
+}
+
+QString AlpineApkResource::longDescription()
+{
+ if (hasAppStreamData()) {
+ return m_appsC.description();
+ }
+ return m_pkg.description;
+}
+
+QString AlpineApkResource::name() const
+{
+ if (hasAppStreamData()) {
+ return m_appsC.name();
+ }
+ return m_pkg.name;
+}
+
+QString AlpineApkResource::origin() const
+{
+ return m_originSoruce;
+}
+
+QString AlpineApkResource::packageName() const
+{
+ return m_pkg.name;
+}
+
+QString AlpineApkResource::section()
+{
+ return m_sectionName;
+}
+
+AbstractResource::State AlpineApkResource::state()
+{
+ return m_state;
+}
+
+void AlpineApkResource::fetchChangelog()
+{
+ if (hasAppStreamData()) {
+ emit changelogFetched(AppStreamUtils::changelogToHtml(m_appsC));
+ }
+}
+
+void AlpineApkResource::fetchScreenshots()
+{
+ if (hasAppStreamData()) {
+ const QPair<QList<QUrl>, QList<QUrl> > sc = AppStreamUtils::fetchScreenshots(m_appsC);
+ Q_EMIT screenshotsFetched(sc.first, sc.second);
+ }
+}
+
+QString AlpineApkResource::appstreamId() const
+{
+ if (hasAppStreamData()) {
+ return m_appsC.id();
+ }
+ return QString();
+}
+
+void AlpineApkResource::setState(AbstractResource::State state)
+{
+ m_state = state;
+ emit stateChanged();
+}
+
+void AlpineApkResource::setCategoryName(const QString &categoryName)
+{
+ m_category = categoryName;
+}
+
+void AlpineApkResource::setOriginSource(const QString &originSource)
+{
+ m_originSoruce = originSource;
+}
+
+void AlpineApkResource::setSection(const QString &sectionName)
+{
+ m_sectionName = sectionName;
+}
+
+void AlpineApkResource::setAddons(const AddonList &addons)
+{
+ const QStringList addonsToInstall = addons.addonsToInstall();
+ for (const QString &toInstall : addonsToInstall) {
+ setAddonInstalled(toInstall, true);
+ }
+ const QStringList addonsToRemove = addons.addonsToRemove();
+ for (const QString &toRemove : addonsToRemove) {
+ setAddonInstalled(toRemove, false);
+ }
+}
+
+void AlpineApkResource::setAddonInstalled(const QString &addon, bool installed)
+{
+ for(PackageState &elem : m_addons) {
+ if(elem.name() == addon) {
+ elem.setInstalled(installed);
+ }
+ }
+}
+
+void AlpineApkResource::setAvailableVersion(const QString &av)
+{
+ m_availableVersion = av;
+}
+
+bool AlpineApkResource::hasAppStreamData() const
+{
+ return !m_appsC.id().isEmpty();
+}
+
+void AlpineApkResource::setAppStreamData(const AppStream::Component &component)
+{
+ m_appsC = component;
+}
+
+bool AlpineApkResource::canExecute() const
+{
+ if (hasAppStreamData()) {
+ return (m_appsC.kind() == AppStream::Component::KindDesktopApp &&
+ (m_state == AbstractResource::Installed || m_state == AbstractResource::Upgradeable));
+ }
+ return false;
+}
+
+void AlpineApkResource::invokeApplication() const
+{
+ const QString desktopFile = QLatin1String("/usr/share/applications/") + appstreamId();
+ if (QFile::exists(desktopFile)) {
+ QProcess::startDetached(QStringLiteral("kstart5"), {QStringLiteral("--service"), desktopFile});
+ }
+}
+
+QUrl AlpineApkResource::url() const
+{
+ if (hasAppStreamData()) {
+ return QUrl(QStringLiteral("appstream://") + appstreamId());
+ }
+ return QUrl(QLatin1String("apk://") + packageName());
+}
+
+QString AlpineApkResource::author() const
+{
+ if (hasAppStreamData()) {
+ return m_appsC.developerName();
+ }
+ return m_pkg.maintainer;
+}
+
+QString AlpineApkResource::sourceIcon() const
+{
+ return QStringLiteral("player-time");
+}
+
+QDate AlpineApkResource::releaseDate() const
+{
+ if (hasAppStreamData()) {
+ if (!m_appsC.releases().isEmpty()) {
+ auto release = m_appsC.releases().constFirst();
+ return release.timestamp().date();
+ }
+ }
+ // just build date is fine, too
+ return m_pkg.buildTime.date();
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
new file mode 100644
index 00000000..5304a877
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkResource.h
@@ -0,0 +1,93 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef ALPINEAPKRESOURCE_H
+#define ALPINEAPKRESOURCE_H
+
+#include <resources/AbstractResource.h>
+#include <QtApkPackage.h>
+#include <AppStreamQt/component.h>
+
+class AddonList;
+
+class AlpineApkResource : public AbstractResource
+{
+ Q_OBJECT
+
+public:
+ explicit AlpineApkResource(const QtApk::Package &apkPkg,
+ AppStream::Component &component,
+ AbstractResource::Type typ,
+ AbstractResourcesBackend *parent);
+
+ QList<PackageState> addonsInformation() override;
+ QString section() override;
+ QString origin() const override;
+ QString longDescription() override;
+ QString availableVersion() const override;
+ QString installedVersion() const override;
+ QJsonArray licenses() override;
+ int size() override;
+ QUrl homepage() override;
+ QUrl helpURL() override;
+ QUrl bugURL() override;
+ QUrl donationURL() override;
+ QStringList categories() override;
+ AbstractResource::State state() override;
+ QVariant icon() const override;
+ QString comment() override;
+ QString name() const override;
+ QString packageName() const override;
+ AbstractResource::Type type() const override { return m_type; }
+ bool canExecute() const override;
+ void invokeApplication() const override;
+ void fetchChangelog() override;
+ void fetchScreenshots() override;
+ QString appstreamId() const override;
+ QUrl url() const override;
+ QString author() const override;
+ QString sourceIcon() const override;
+ QDate releaseDate() const override;
+
+ void setState(State state);
+ void setCategoryName(const QString &categoryName);
+ void setOriginSource(const QString &originSource);
+ void setSection(const QString &sectionName);
+ void setAddons(const AddonList &addons);
+ void setAddonInstalled(const QString &addon, bool installed);
+ void setAvailableVersion(const QString &av);
+ void setAppStreamData(const AppStream::Component &component);
+
+private:
+ bool hasAppStreamData() const;
+
+public:
+ AbstractResource::State m_state;
+ const AbstractResource::Type m_type;
+ QtApk::Package m_pkg;
+ QString m_availableVersion;
+ QString m_category;
+ QString m_originSoruce;
+ QString m_sectionName;
+ QList<PackageState> m_addons;
+ AppStream::Component m_appsC;
+};
+
+#endif // ALPINEAPKRESOURCE_H
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.cpp
new file mode 100644
index 00000000..fd7ad47f
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.cpp
@@ -0,0 +1,35 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include "AlpineApkReviewsBackend.h"
+#include "AlpineApkBackend.h"
+#include "resources/AbstractResource.h"
+
+AlpineApkReviewsBackend::AlpineApkReviewsBackend(AlpineApkBackend *parent)
+ : AbstractReviewsBackend(parent)
+{
+}
+
+void AlpineApkReviewsBackend::fetchReviews(AbstractResource *app, int page)
+{
+ Q_UNUSED(page)
+ static const QVector<ReviewPtr> reviews;
+ Q_EMIT reviewsReady(app, reviews, false);
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h
new file mode 100644
index 00000000..435f845b
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkReviewsBackend.h
@@ -0,0 +1,52 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef ALPINEAPKREVIEWSBACKEND_H
+#define ALPINEAPKREVIEWSBACKEND_H
+
+#include "ReviewsBackend/AbstractReviewsBackend.h"
+
+class AlpineApkBackend;
+
+class AlpineApkReviewsBackend : public AbstractReviewsBackend
+{
+ Q_OBJECT
+
+public:
+ explicit AlpineApkReviewsBackend(AlpineApkBackend *parent = nullptr);
+
+ QString userName() const override { return QStringLiteral("dummy"); }
+ void login() override {}
+ void logout() override {}
+ void registerAndLogin() override {}
+
+ Rating *ratingForApplication(AbstractResource *) const override { return nullptr; }
+ bool hasCredentials() const override { return false; }
+ void deleteReview(Review *) override {}
+ void fetchReviews(AbstractResource *app, int page = 1) override;
+ bool isFetching() const override { return false; }
+ bool isReviewable() const override { return false; }
+ void submitReview(AbstractResource *, const QString &, const QString &, const QString &) override {}
+ void flagReview(Review *, const QString&, const QString&) override {}
+ void submitUsefulness(Review *, bool) override {}
+ bool isResourceSupported(AbstractResource *) const override { return false; }
+};
+
+#endif // ALPINEAPKREVIEWSBACKEND_H
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
new file mode 100644
index 00000000..a126483a
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.cpp
@@ -0,0 +1,195 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include "AlpineApkSourcesBackend.h"
+#include "AlpineApkAuthActionFactory.h"
+#include "alpineapk_backend_logging.h" // generated by ECM
+
+#include <QDebug>
+#include <QAction>
+#include <QVector>
+
+// KF5
+#include <KAuthExecuteJob>
+#include <KLocalizedString>
+
+// libapk-qt
+#include <QtApk>
+
+AlpineApkSourcesBackend::AlpineApkSourcesBackend(AbstractResourcesBackend *parent)
+ : AbstractSourcesBackend(parent)
+ , m_sourcesModel(new QStandardItemModel(this))
+ , m_refreshAction(new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")),
+ QStringLiteral("Reload"), this))
+ , m_saveAction(new QAction(QIcon::fromTheme(QStringLiteral("document-save")),
+ QStringLiteral("Save"), this))
+ // ^^ unfortunately QML side ignores icons for custom actions
+{
+ loadSources();
+ QObject::connect(m_refreshAction, &QAction::triggered,
+ this, &AlpineApkSourcesBackend::loadSources);
+ QObject::connect(m_saveAction, &QAction::triggered,
+ this, &AlpineApkSourcesBackend::saveSources);
+ // track enabling/disabling repo source
+ QObject::connect(m_sourcesModel, &QStandardItemModel::itemChanged,
+ this, &AlpineApkSourcesBackend::onItemChanged);
+}
+
+QAbstractItemModel *AlpineApkSourcesBackend::sources()
+{
+ return m_sourcesModel;
+}
+
+QStandardItem *AlpineApkSourcesBackend::sourceForId(const QString& id) const
+{
+ for (int i = 0; i < m_sourcesModel->rowCount(); ++i) {
+ QStandardItem *item = m_sourcesModel->item(i, 0);
+ if (item->data(AbstractSourcesBackend::IdRole) == id) {
+ return item;
+ }
+ }
+ return nullptr;
+}
+
+bool AlpineApkSourcesBackend::addSource(const QString &id)
+{
+ m_repos.append(QtApk::Repository(id, QString(), true));
+ fillModelFromRepos();
+ return true;
+}
+
+void AlpineApkSourcesBackend::loadSources()
+{
+ m_repos = QtApk::Database::getRepositories();
+ fillModelFromRepos();
+}
+
+void AlpineApkSourcesBackend::fillModelFromRepos()
+{
+ m_sourcesModel->clear();
+ for (const QtApk::Repository &repo: m_repos) {
+ if (repo.url.isEmpty()) {
+ continue;
+ }
+ qCDebug(LOG_ALPINEAPK) << "source backend: Adding source:" << repo.url << repo.enabled;
+ QStandardItem *it = new QStandardItem(repo.url);
+ it->setData(repo.url, AbstractSourcesBackend::IdRole);
+ it->setData(repo.comment, Qt::ToolTipRole);
+ it->setCheckable(true);
+ it->setCheckState(repo.enabled ? Qt::Checked : Qt::Unchecked);
+ m_sourcesModel->appendRow(it);
+ }
+}
+
+void AlpineApkSourcesBackend::saveSources()
+{
+ const QVariant repoUrls = QVariant::fromValue<QVector<QtApk::Repository>>(m_repos);
+
+ // run with elevated privileges
+ KAuth::ExecuteJob *reply = ActionFactory::createRepoconfigAction(repoUrls);
+ if (!reply) return;
+
+ QObject::connect(reply, &KAuth::ExecuteJob::result, this, [this] (KJob *job) {
+ KAuth::ExecuteJob *reply = static_cast<KAuth::ExecuteJob *>(job);
+ if (reply->error() != 0) {
+ const QString errMessage = reply->errorString();
+ qCWarning(LOG_ALPINEAPK) << "KAuth helper returned error:"
+ << reply->error() << errMessage;
+ if (reply->error() == KAuth::ActionReply::Error::AuthorizationDeniedError) {
+ Q_EMIT passiveMessage(i18n("Authorization denied"));
+ } else {
+ Q_EMIT passiveMessage(i18n("Error: ") + errMessage);
+ }
+ }
+ this->loadSources();
+ });
+
+ reply->start();
+}
+
+void AlpineApkSourcesBackend::onItemChanged(QStandardItem *item)
+{
+ // update internal storage vector and reload model from it
+ // otherwise checks state are not updated in UI
+ const Qt::CheckState cs = item->checkState();
+ const QModelIndex idx = m_sourcesModel->indexFromItem(item);
+ m_repos[idx.row()].enabled = (cs == Qt::Checked);
+ fillModelFromRepos();
+}
+
+bool AlpineApkSourcesBackend::removeSource(const QString &id)
+{
+ const QStandardItem *it = sourceForId(id);
+ if (!it) {
+ qCWarning(LOG_ALPINEAPK) << "source backend: couldn't find " << id;
+ return false;
+ }
+ m_repos.remove(it->row());
+ return m_sourcesModel->removeRow(it->row());
+}
+
+QString AlpineApkSourcesBackend::idDescription()
+{
+ return i18nc("Adding repo", "Enter Alpine repository URL, for example: "
+ "http://dl-cdn.alpinelinux.org/alpine/edge/testing/");
+}
+
+QVariantList AlpineApkSourcesBackend::actions() const
+{
+ static const QVariantList s_actions {
+ QVariant::fromValue<QObject *>(m_saveAction),
+ QVariant::fromValue<QObject *>(m_refreshAction),
+ };
+ return s_actions;
+}
+
+bool AlpineApkSourcesBackend::supportsAdding() const
+{
+ return true;
+}
+
+bool AlpineApkSourcesBackend::canMoveSources() const
+{
+ return true;
+}
+
+bool AlpineApkSourcesBackend::moveSource(const QString& sourceId, int delta)
+{
+ int row = sourceForId(sourceId)->row();
+ QList<QStandardItem *> prevRow = m_sourcesModel->takeRow(row);
+ if (prevRow.isEmpty()) {
+ return false;
+ }
+
+ const int destRow = row + delta;
+ m_sourcesModel->insertRow(destRow, prevRow);
+ if (destRow == 0 || row == 0) {
+ Q_EMIT firstSourceIdChanged();
+ }
+ if (destRow == (m_sourcesModel->rowCount() - 1)
+ || row == (m_sourcesModel->rowCount() - 1)) {
+ Q_EMIT lastSourceIdChanged();
+ }
+
+ // swap also items in internal storage vector
+ m_repos.swapItemsAt(row, destRow);
+
+ return true;
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
new file mode 100644
index 00000000..eacda22d
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkSourcesBackend.h
@@ -0,0 +1,57 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef ALPINEAPKSOURCESBACKEND_H
+#define ALPINEAPKSOURCESBACKEND_H
+
+#include <resources/AbstractSourcesBackend.h>
+#include <QStandardItemModel>
+
+#include <QtApkRepository.h>
+
+class AlpineApkSourcesBackend : public AbstractSourcesBackend
+{
+public:
+ explicit AlpineApkSourcesBackend(AbstractResourcesBackend *parent);
+
+ QAbstractItemModel *sources() override;
+ bool addSource(const QString &id) override;
+ bool removeSource(const QString &id) override;
+ QString idDescription() override;
+ QVariantList actions() const override;
+ bool supportsAdding() const override;
+ bool canMoveSources() const override;
+ bool moveSource(const QString &sourceId, int delta) override;
+
+private:
+ QStandardItem *sourceForId(const QString &id) const;
+ bool addSourceFull(const QString &id, const QString &comment, bool enabled);
+ void loadSources();
+ void saveSources();
+ void fillModelFromRepos();
+ void onItemChanged(QStandardItem* item);
+
+ QStandardItemModel *m_sourcesModel = nullptr;
+ QAction *m_refreshAction = nullptr;
+ QAction *m_saveAction = nullptr;
+ QVector<QtApk::Repository> m_repos;
+};
+
+#endif // ALPINEAPKSOURCESBACKEND_H
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
new file mode 100644
index 00000000..26bf1bd0
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.cpp
@@ -0,0 +1,141 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include "AlpineApkTransaction.h"
+#include "AlpineApkBackend.h"
+#include "AlpineApkResource.h"
+#include "AlpineApkAuthActionFactory.h"
+#include "alpineapk_backend_logging.h" // generated by ECM
+
+// Qt
+#include <QDebug>
+#include <QTimer>
+
+// KF5
+#include <KAuthExecuteJob>
+#include <KLocalizedString>
+
+AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *res, Role role)
+ : AlpineApkTransaction(res, {}, role)
+{
+}
+
+AlpineApkTransaction::AlpineApkTransaction(AlpineApkResource *res, const AddonList &addons, Transaction::Role role)
+ : Transaction(res->backend(), res, role, addons)
+ , m_resource(res)
+ , m_backend(static_cast<AlpineApkBackend *>(res->backend()))
+{
+ setCancellable(false);
+ setStatus(QueuedStatus);
+ // seems like Discover's transactions are supposed to start
+ // automatically; no dedicated method to start transaction?
+ startTransaction();
+}
+
+void AlpineApkTransaction::proceed()
+{
+ startTransaction();
+}
+
+void AlpineApkTransaction::cancel()
+{
+ setStatus(CancelledStatus);
+}
+
+void AlpineApkTransaction::startTransaction()
+{
+ KAuth::ExecuteJob *reply = nullptr;
+ switch(role()) {
+ case InstallRole:
+ reply = ActionFactory::createAddAction(m_resource->m_pkg.name);
+ break;
+ case RemoveRole:
+ reply = ActionFactory::createDelAction(m_resource->m_pkg.name);
+ break;
+ case ChangeAddonsRole:
+ qCWarning(LOG_ALPINEAPK) << "Addons are not supported by Alpine APK Backend!";
+ break;
+ }
+
+ if (!reply) {
+ return;
+ }
+
+ // get result of this job
+ QObject::connect(reply, &KAuth::ExecuteJob::result, this, [this](KJob *job) {
+ KAuth::ExecuteJob *reply = static_cast<KAuth::ExecuteJob *>(job);
+ const QVariantMap &replyData = reply->data();
+ if (reply->error() == 0) {
+ finishTransactionOK();
+ } else {
+ QString message = replyData.value(QLatin1String("errorString"),
+ reply->errorString()).toString();
+ if (reply->error() == KAuth::ActionReply::Error::AuthorizationDeniedError) {
+ message = i18n("Error: Authorization denied");
+ }
+ finishTransactionWithError(message);
+ }
+ });
+
+ // get progress reports for this job
+ QObject::connect(reply, QOverload<KJob*, unsigned long>::of(&KAuth::ExecuteJob::percent), this,
+ [this](KJob *job, unsigned long percent) {
+ Q_UNUSED(job)
+ if (percent >= 40 && role() == InstallRole) {
+ setStatus(CommittingStatus);
+ }
+ setProgress(static_cast<int>(percent));
+ });
+
+ setProgress(0);
+ if (role() == InstallRole) {
+ setStatus(DownloadingStatus);
+ } else {
+ setStatus(CommittingStatus);
+ }
+
+ reply->start();
+}
+
+void AlpineApkTransaction::finishTransactionOK()
+{
+ AbstractResource::State newState;
+ switch(role()) {
+ case InstallRole:
+ case ChangeAddonsRole:
+ newState = AbstractResource::Installed;
+ break;
+ case RemoveRole:
+ newState = AbstractResource::None;
+ break;
+ }
+ m_resource->setAddons(addons());
+ m_resource->setState(newState);
+ setStatus(DoneStatus);
+ deleteLater();
+}
+
+void AlpineApkTransaction::finishTransactionWithError(const QString &errMsg)
+{
+ qCWarning(LOG_ALPINEAPK) << "Transaction finished with error:" << errMsg;
+ Q_EMIT passiveMessage(i18n("Error") + QStringLiteral(":\n") + errMsg);
+ setStatus(DoneWithErrorStatus);
+ deleteLater();
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
new file mode 100644
index 00000000..cab1f6b9
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkTransaction.h
@@ -0,0 +1,49 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef ALPINEAPKTRANSACTION_H
+#define ALPINEAPKTRANSACTION_H
+
+#include <Transaction/Transaction.h>
+
+class AlpineApkBackend;
+class AlpineApkResource;
+
+class AlpineApkTransaction : public Transaction
+{
+Q_OBJECT
+public:
+ AlpineApkTransaction(AlpineApkResource *res, Role role);
+ AlpineApkTransaction(AlpineApkResource *res, const AddonList &list, Role role);
+
+ void cancel() override;
+ void proceed() override;
+
+private Q_SLOTS:
+ void startTransaction();
+ void finishTransactionOK();
+ void finishTransactionWithError(const QString &errMsg);
+
+private:
+ AlpineApkResource *m_resource;
+ AlpineApkBackend *m_backend;
+};
+
+#endif // ALPINEAPKTRANSACTION_H
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
new file mode 100644
index 00000000..14df959c
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.cpp
@@ -0,0 +1,295 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include "AlpineApkUpdater.h"
+#include "AlpineApkResource.h"
+#include "AlpineApkBackend.h"
+#include "AlpineApkAuthActionFactory.h"
+#include "alpineapk_backend_logging.h"
+#include "utils.h"
+
+#include <KAuthExecuteJob>
+#include <KLocalizedString>
+
+#include <QtApk>
+
+
+AlpineApkUpdater::AlpineApkUpdater(AbstractResourcesBackend *parent)
+ : AbstractBackendUpdater(parent)
+ , m_backend(static_cast<AlpineApkBackend *>(parent))
+{
+ //
+}
+
+void AlpineApkUpdater::prepare()
+{
+ QtApk::Database *db = m_backend->apkdb();
+
+ if (db->isOpen()) {
+ return;
+ }
+
+ // readonly is fine for a simulation of upgrade
+ if (!db->open(QtApk::QTAPK_OPENF_READONLY)) {
+ emit passiveMessage(i18n("Failed to open APK database!"));
+ return;
+ }
+
+ if (!db->upgrade(QtApk::QTAPK_UPGRADE_SIMULATE, &m_upgradeable)) {
+ emit passiveMessage(i18n("Failed to get a list of packages to upgrade!"));
+ db->close();
+ return;
+ }
+ // close DB ASAP
+ db->close();
+
+ m_updatesCount = m_upgradeable.changes().size();
+ qCDebug(LOG_ALPINEAPK) << "updater: prepare: updates count" << m_updatesCount;
+
+ m_allUpdateable.clear();
+ m_markedToUpdate.clear();
+ QHash<QString, AlpineApkResource *> *resources = m_backend->resourcesPtr();
+ for (const QtApk::ChangesetItem &it : qAsConst(m_upgradeable.changes())) {
+ const QtApk::Package &oldPkg = it.oldPackage;
+ const QString newVersion = it.newPackage.version;
+ AlpineApkResource *res = resources->value(oldPkg.name);
+ if (res) {
+ res->setAvailableVersion(newVersion);
+ m_allUpdateable.insert(res);
+ m_markedToUpdate.insert(res);
+ }
+ }
+
+ // emitting this signal here leads to infinite recursion
+ // emit updatesCountChanged(m_updatesCount);
+}
+
+bool AlpineApkUpdater::hasUpdates() const
+{
+ return (m_updatesCount > 0);
+}
+
+qreal AlpineApkUpdater::progress() const
+{
+ return m_upgradeProgress;
+}
+
+void AlpineApkUpdater::removeResources(const QList<AbstractResource *> &apps)
+{
+ const QSet<AbstractResource *> checkSet = kToSet(apps);
+ m_markedToUpdate -= checkSet;
+}
+
+void AlpineApkUpdater::addResources(const QList<AbstractResource *> &apps)
+{
+ const QSet<AbstractResource *> checkSet = kToSet(apps);
+ m_markedToUpdate += checkSet;
+}
+
+QList<AbstractResource *> AlpineApkUpdater::toUpdate() const
+{
+ return m_allUpdateable.values();
+}
+
+QDateTime AlpineApkUpdater::lastUpdate() const
+{
+ qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+ return QDateTime();
+}
+
+bool AlpineApkUpdater::isCancelable() const
+{
+ qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+ return false;
+}
+
+bool AlpineApkUpdater::isProgressing() const
+{
+ qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO << m_progressing;
+ return m_progressing;
+}
+
+bool AlpineApkUpdater::isMarked(AbstractResource *res) const
+{
+ return m_markedToUpdate.contains(res);
+}
+
+void AlpineApkUpdater::fetchChangelog() const
+{
+ qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+}
+
+double AlpineApkUpdater::updateSize() const
+{
+ double sum = 0.0;
+ for (AbstractResource *res : m_markedToUpdate) {
+ sum += res->size();
+ }
+ return sum;
+}
+
+quint64 AlpineApkUpdater::downloadSpeed() const
+{
+ qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+ return 0;
+}
+
+void AlpineApkUpdater::cancel()
+{
+ qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+}
+
+void AlpineApkUpdater::start()
+{
+ qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+
+ // run upgrade with elevated privileges
+ KAuth::ExecuteJob *reply = ActionFactory::createUpgradeAction();
+ if (!reply) return;
+
+ QObject::connect(reply, &KAuth::ExecuteJob::result,
+ this, &AlpineApkUpdater::handleKAuthUpgradeHelperReply);
+ // qOverload is needed because of conflict with getter named percent()
+ QObject::connect(reply, QOverload<KJob *, unsigned long>::of(&KAuth::ExecuteJob::percent),
+ this, &AlpineApkUpdater::handleKAuthUpgradeHelperProgress);
+
+ m_progressing = true;
+ m_upgradeProgress = 0.0;
+ Q_EMIT progressingChanged(m_progressing);
+
+ reply->start();
+}
+
+void AlpineApkUpdater::proceed()
+{
+ qCDebug(LOG_ALPINEAPK) << Q_FUNC_INFO;
+}
+
+int AlpineApkUpdater::updatesCount()
+{
+ return m_updatesCount;
+}
+
+void AlpineApkUpdater::startCheckForUpdates()
+{
+ QtApk::Database *db = m_backend->apkdb();
+
+ // run updates check with elevated privileges to access
+ // system package manager files
+ KAuth::ExecuteJob *reply = ActionFactory::createUpdateAction(db->fakeRoot());
+ if (!reply) return;
+ QObject::connect(reply, &KAuth::ExecuteJob::result,
+ this, &AlpineApkUpdater::handleKAuthUpdateHelperReply);
+ // qOverload is needed because of conflict with getter named percent()
+ QObject::connect(reply, QOverload<KJob *, unsigned long>::of(&KAuth::ExecuteJob::percent),
+ this, &AlpineApkUpdater::handleKAuthUpdateHelperProgress);
+
+ m_progressing = true;
+ Q_EMIT progressingChanged(m_progressing);
+ Q_EMIT progressChanged(0);
+
+ reply->start();
+}
+
+void AlpineApkUpdater::handleKAuthUpdateHelperReply(KJob *job)
+{
+ KAuth::ExecuteJob *reply = static_cast<KAuth::ExecuteJob *>(job);
+ const QVariantMap &replyData = reply->data();
+ if (reply->error() == 0) {
+ m_updatesCount = replyData.value(QLatin1String("updatesCount")).toInt();
+ qCDebug(LOG_ALPINEAPK) << "KAuth helper update reply received, updatesCount:" << m_updatesCount;
+ Q_EMIT updatesCountChanged(m_updatesCount);
+ } else {
+ handleKAuthHelperError(reply, replyData);
+ }
+
+ m_progressing = false;
+ Q_EMIT progressingChanged(m_progressing);
+
+ // we are not in the state "Fetching updates" now, update UI
+ Q_EMIT checkForUpdatesFinished();
+}
+
+void AlpineApkUpdater::handleKAuthUpdateHelperProgress(KJob *job, unsigned long percent)
+{
+ Q_UNUSED(job)
+ qCDebug(LOG_ALPINEAPK) << " fetch updates progress: " << percent;
+ Q_EMIT fetchingUpdatesProgressChanged(percent);
+ Q_EMIT progressChanged(static_cast<qreal>(percent));
+}
+
+void AlpineApkUpdater::handleKAuthUpgradeHelperProgress(KJob *job, unsigned long percent)
+{
+ Q_UNUSED(job)
+ qCDebug(LOG_ALPINEAPK) << " upgrade progress: " << percent;
+ qreal newProgress = static_cast<qreal>(percent);
+ if (newProgress != m_upgradeProgress) {
+ m_upgradeProgress = newProgress;
+ Q_EMIT progressChanged(m_upgradeProgress);
+ }
+}
+
+void AlpineApkUpdater::handleKAuthUpgradeHelperReply(KJob *job)
+{
+ KAuth::ExecuteJob *reply = static_cast<KAuth::ExecuteJob *>(job);
+ const QVariantMap &replyData = reply->data();
+ if (reply->error() == 0) {
+ QVariant pkgsV = replyData.value(QLatin1String("changes"));
+ bool onlySimulate = replyData.value(QLatin1String("onlySimulate"), false).toBool();
+ if (onlySimulate) {
+ qCDebug(LOG_ALPINEAPK) << "KAuth helper upgrade reply received, simulation mode";
+ QVector<QtApk::Package> pkgVector = pkgsV.value<QVector<QtApk::Package>>();
+ qCDebug(LOG_ALPINEAPK) << " num changes:" << pkgVector.size();
+ for (const QtApk::Package &pkg : pkgVector) {
+ qCDebug(LOG_ALPINEAPK) << " " << pkg.name << pkg.version;
+ }
+ }
+ } else {
+ handleKAuthHelperError(reply, replyData);
+ }
+
+ m_progressing = false;
+ Q_EMIT progressingChanged(m_progressing);
+}
+
+void AlpineApkUpdater::handleKAuthHelperError(
+ KAuth::ExecuteJob *reply,
+ const QVariantMap &replyData)
+{
+ // error message should be received as part of JSON reply from helper
+ QString message = replyData.value(QLatin1String("errorString"),
+ reply->errorString()).toString();
+ if (reply->error() == KAuth::ActionReply::Error::AuthorizationDeniedError) {
+ qCWarning(LOG_ALPINEAPK) << "updater: KAuth helper returned AuthorizationDeniedError";
+ Q_EMIT passiveMessage(i18n("Authorization denied"));
+ } else {
+ // if received error message is empty, try other ways to get error text for user
+ // there are multiple ways to get error messages in kauth/kjob
+ if (message.isEmpty()) {
+ message = reply->errorString();
+ if (message.isEmpty()) {
+ message = reply->errorText();
+ }
+ }
+ qCDebug(LOG_ALPINEAPK) << "updater: KAuth helper returned error:" << message << reply->error();
+ Q_EMIT passiveMessage(i18n("Error") + QStringLiteral(":\n") + message);
+ }
+}
+
diff --git a/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
new file mode 100644
index 00000000..6ca3ce07
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AlpineApkUpdater.h
@@ -0,0 +1,197 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef ALPINEAPKUPDATER_H
+#define ALPINEAPKUPDATER_H
+
+#include "resources/AbstractBackendUpdater.h"
+#include "resources/AbstractResourcesBackend.h"
+
+#include <QSet>
+#include <QDateTime>
+#include <QTimer>
+#include <QVariant>
+#include <QMap>
+#include <QVector>
+
+#include <QtApkChangeset.h>
+
+class AbstractResourcesBackend;
+class AlpineApkBackend;
+class KJob;
+namespace KAuth {
+ class ExecuteJob;
+}
+
+class AlpineApkUpdater : public AbstractBackendUpdater
+{
+ Q_OBJECT
+ Q_PROPERTY(int updatesCount READ updatesCount NOTIFY updatesCountChanged)
+
+public:
+ explicit AlpineApkUpdater(AbstractResourcesBackend *parent = nullptr);
+
+ /**
+ * This method is called, when Muon switches to the updates view.
+ * Here the backend should mark all upgradeable packages as to be upgraded.
+ */
+ void prepare() override;
+
+ /**
+ * @returns true if the backend contains packages which can be updated
+ */
+ bool hasUpdates() const override;
+ /**
+ * @returns the progress of the update in percent
+ */
+ qreal progress() const override;
+
+ /**
+ * This method is used to remove resources from the list of packages
+ * marked to be upgraded. It will potentially be called before \start.
+ */
+ void removeResources(const QList<AbstractResource*> &apps) override;
+
+ /**
+ * This method is used to add resource to the list of packages marked to be upgraded.
+ * It will potentially be called before \start.
+ */
+ void addResources(const QList<AbstractResource*> &apps) override;
+
+ /**
+ * @returns the list of updateable resources in the system
+ */
+ QList<AbstractResource *> toUpdate() const override;
+
+ /**
+ * @returns the QDateTime when the last update happened
+ */
+ QDateTime lastUpdate() const override;
+
+ /**
+ * @returns whether the updater can currently be canceled or not
+ * @see cancelableChanged
+ */
+ bool isCancelable() const override;
+
+ /**
+ * @returns whether the updater is currently running or not
+ * this property decides, if there will be progress reporting in the GUI.
+ * This has to stay true during the whole transaction!
+ * @see progressingChanged
+ */
+ bool isProgressing() const override;
+
+ /**
+ * @returns whether @p res is marked for update
+ */
+ bool isMarked(AbstractResource* res) const override;
+
+ void fetchChangelog() const override;
+
+ /**
+ * @returns the size of all the packages set to update combined
+ */
+ double updateSize() const override;
+
+ /**
+ * @returns the speed at which we are downloading
+ */
+ quint64 downloadSpeed() const override;
+
+public Q_SLOTS:
+ /**
+ * If \isCancelable is true during the transaction, this method has
+ * to be implemented and will potentially be called when the user
+ * wants to cancel the update.
+ */
+ void cancel() override;
+
+ /**
+ * This method starts the update. All packages which are in \toUpdate
+ * are going to be updated.
+ *
+ * From this moment on the AbstractBackendUpdater should continuously update
+ * the other methods to show its progress.
+ *
+ * @see progress
+ * @see progressChanged
+ * @see isProgressing
+ * @see progressingChanged
+ */
+ void start() override;
+
+ /**
+ * Answers a proceed request
+ */
+ void proceed() override;
+
+Q_SIGNALS:
+ void checkForUpdatesFinished();
+ void updatesCountChanged(int updatesCount);
+ void fetchingUpdatesProgressChanged(int progress);
+ //void cancelTransaction();
+
+public Q_SLOTS:
+ int updatesCount();
+ void startCheckForUpdates();
+
+ // KAuth handler slots
+ // update
+ void handleKAuthUpdateHelperReply(KJob *job);
+ void handleKAuthUpdateHelperProgress(KJob *job, unsigned long percent);
+ // upgrade
+ void handleKAuthUpgradeHelperReply(KJob *job);
+ void handleKAuthUpgradeHelperProgress(KJob *job, unsigned long percent);
+
+ //void transactionRemoved(Transaction* t);
+ //void cleanup();
+
+public:
+ QVector<QtApk::ChangesetItem> &changes() { return m_upgradeable.changes(); }
+ const QVector<QtApk::ChangesetItem> &changes() const { return m_upgradeable.changes(); }
+
+protected:
+ void handleKAuthHelperError(KAuth::ExecuteJob *reply, const QVariantMap &replyData);
+
+private:
+ AlpineApkBackend *const m_backend;
+ int m_updatesCount = 0;
+ QtApk::Changeset m_upgradeable;
+ QSet<AbstractResource *> m_allUpdateable;
+ QSet<AbstractResource *> m_markedToUpdate;
+// void resourcesChanged(AbstractResource* res, const QVector<QByteArray>& props);
+// void refreshUpdateable();
+// void transactionAdded(Transaction* newTransaction);
+// void transactionProgressChanged();
+// void refreshProgress();
+// QVector<Transaction*> transactions() const;
+
+// QSet<AbstractResource*> m_upgradeable;
+// QSet<AbstractResource*> m_pendingResources;
+ bool m_progressing = false;
+ qreal m_upgradeProgress = 0.0;
+// QDateTime m_lastUpdate;
+// QTimer m_timer;
+// bool m_canCancel = false;
+};
+
+
+#endif // ALPINEAPKUPDATER_H
diff --git a/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
new file mode 100644
index 00000000..16587994
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.cpp
@@ -0,0 +1,303 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#include <QDateTime>
+#include <QDir>
+#include <QEventLoop>
+#include <QFile>
+#include <QFileInfo>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonParseError>
+#include <QJsonObject>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QStandardPaths>
+#include <QThreadPool>
+#include <QUrl>
+#include <QtConcurrentRun>
+
+#include "AppstreamDataDownloader.h"
+#include "alpineapk_backend_logging.h"
+
+namespace DiscoverVersion {
+// contains static QLatin1String version("5.20.5"); definition
+// autogenerated from top CMakeLists.txt
+#include "../../../DiscoverVersion.h"
+}
+
+#ifdef ALPINE_LINUX_BUILD
+/**
+ * @brief ApkAppstreamDataDownloader::getApkArch
+ * Reads current configured system apk architecture
+ * from "/etc/apk/arch" file.
+ * @return "x86_64" / "armhf" / "armv7" / "aarch64" and so on
+ */
+static QString getApkArch()
+{
+ static QString s_retArch;
+ if (!s_retArch.isEmpty()) {
+ return s_retArch;
+ }
+ QFile archFile(QStringLiteral("/etc/apk/arch"));
+ if (!archFile.open(QIODevice::ReadOnly)) {
+ // TODO: we could try to guess at compile time by checking presence of
+ // defines like __x86_64__ (check with: "gcc -march=native -dM -E - </dev/null")
+ // but that seems like outside of scope of this small function
+ return s_retArch;
+ }
+ s_retArch = QString::fromUtf8(archFile.readAll()).trimmed();
+ archFile.close();
+ return s_retArch;
+}
+
+static inline void replaceCARCH(QString &in, const QString &arch)
+{
+ // URLs in JSON look like this:
+ // https://appstream.alpinelinux.org/data/edge/main/Components-main-@CARCH@.xml.gz
+ // we need to replace @CARCH@ with a real arch value
+ in.replace(QLatin1String("@CARCH@"), arch);
+}
+#endif
+
+QString AppstreamDataDownloader::getAppStreamCacheDir()
+{
+ QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
+ // ^^ "~/.cache/discover"
+ cachePath += QStringLiteral("/appstream_data");
+ QDir cacheDir(cachePath);
+ if (!cacheDir.exists()) {
+ const bool ok = cacheDir.mkpath(QStringLiteral("."));
+ if (ok) {
+ qCDebug(LOG_ALPINEAPK) << "Created appstream data cache dir:" << cachePath;
+ } else {
+ qCWarning(LOG_ALPINEAPK) << "Failed to create appstream data cache dir:" << cachePath;
+ }
+ }
+ return cachePath;
+}
+
+AppstreamDataDownloader::AppstreamDataDownloader(QObject *parent)
+ : QObject(parent)
+{
+}
+
+void AppstreamDataDownloader::setCacheExpirePeriodSecs(qint64 secs)
+{
+ m_cacheExpireSeconds = secs;
+}
+
+void AppstreamDataDownloader::loadUrlsJson(const QString &jsonPath)
+{
+ const QString jsonBaseName = QFileInfo(jsonPath).baseName();
+ QFile jsonFile(jsonPath);
+ if (!jsonFile.open(QIODevice::ReadOnly)) {
+ qCWarning(LOG_ALPINEAPK) << "Failed to open JSON:" << jsonPath << "for reading!";
+ Q_EMIT downloadFinished();
+ return;
+ }
+ const QByteArray jsonBa = jsonFile.readAll();
+ jsonFile.close();
+
+ QJsonParseError jsonError;
+ const QJsonDocument jDoc = QJsonDocument::fromJson(jsonBa, &jsonError);
+ if (jDoc.isNull()) {
+ qCWarning(LOG_ALPINEAPK) << "Failed to parse JSON:" << jsonPath << "!";
+ qCWarning(LOG_ALPINEAPK) << jsonError.errorString();
+ Q_EMIT downloadFinished();
+ return;
+ }
+ // JSON structure:
+ // {
+ // "urls": [
+ // "https://...", "https://...", "https://..."
+ // ]
+ // }
+ const QJsonObject rootObj = jDoc.object();
+ const QJsonArray urls = rootObj.value(QLatin1String("urls")).toArray();
+ for (const QJsonValue &urlValue : urls) {
+ QString url = urlValue.toString();
+#ifdef ALPINE_LINUX_BUILD
+ replaceCARCH(url, getApkArch());
+#endif
+ m_urls.append(url);
+ // prefixes are used to avoid name clashes with similar URL paths
+ // from other JSON files. json file basename is used as prefix
+ m_urlPrefixes.insert(url, jsonBaseName);
+ }
+}
+
+QString AppstreamDataDownloader::getLocalFileSavePath(const QUrl &urlToDownload)
+{
+ // we are adding a prefix here to local file name to avoid possible
+ // file name clashes with files from other JSONs
+ const QString urlPrefix = m_urlPrefixes.value(urlToDownload.toString(), QString());
+ const QFileInfo urlInfo(urlToDownload.path());
+ const QString localCacheFile = AppstreamDataDownloader::getAppStreamCacheDir()
+ + QDir::separator()
+ + urlPrefix + QLatin1Char('_')
+ + urlInfo.fileName();
+ // aka "~/.cache/discover/appstream_data/urlPrefix_fileName.xml.gz"
+ return localCacheFile;
+}
+
+void AppstreamDataDownloader::start()
+{
+ m_urls.clear();
+ // load json files with appdata URLs configuration
+ const QString path = QStandardPaths::locate(
+ QStandardPaths::GenericDataLocation,
+ QLatin1String("libdiscover/external-appstream-urls"),
+ QStandardPaths::LocateDirectory);
+ if (path.isEmpty()) {
+ qCWarning(LOG_ALPINEAPK) << "external-appstream-urls directory does not exist.";
+ return;
+ }
+
+ QDir jsonsDir(path);
+ // search for all JSON files in that directory and load each one
+ QFileInfoList fileList = jsonsDir.entryInfoList({QStringLiteral("*.json")}, QDir::Files);
+ for (const QFileInfo &fi : fileList) {
+ qCDebug(LOG_ALPINEAPK) << " reading URLs JSON: " << fi.absoluteFilePath();
+ loadUrlsJson(fi.absoluteFilePath());
+ }
+
+ qCDebug(LOG_ALPINEAPK) << "appstream_downloader: urls:" << m_urls;
+
+ // check if download is needed at all, maybe all files are already up to date?
+
+ getAppStreamCacheDir(); // can create a cache dir if not exists
+
+ const QDateTime dtNow = QDateTime::currentDateTime();
+ m_urlsToDownload.clear();
+ for (const QString &url : m_urls) {
+ const QUrl urlToDownload(url, QUrl::TolerantMode);
+ const QString localCacheFile = getLocalFileSavePath(urlToDownload);
+ const QFileInfo localFi(localCacheFile);
+ if (localFi.exists()) {
+ int modifiedSecsAgo = localFi.lastModified().secsTo(dtNow);
+ if (modifiedSecsAgo >= m_cacheExpireSeconds) {
+ m_urlsToDownload.append(url);
+ }
+ qCDebug(LOG_ALPINEAPK) << " appstream metadata file: " << localFi.fileName()
+ << " was last modified " << modifiedSecsAgo << " seconds ago";
+ } else {
+ // locally downloaded file does not even exist, we need to download it
+ m_urlsToDownload.append(url);
+ qCDebug(LOG_ALPINEAPK) << " appstream metadata file: " << localFi.fileName()
+ << " does not exist, queued for downloading";
+ }
+ }
+
+ if (m_urlsToDownload.size() > 0) {
+ // some files are outdated; download is needed
+ qCDebug(LOG_ALPINEAPK) << "appstream_downloader: We will need to download "
+ << m_urlsToDownload.size() << " file(s)";
+
+ // start downloader in a background thread
+ QFuture<void> downloaderFuture = QtConcurrent::run(
+ QThreadPool::globalInstance(), this, &AppstreamDataDownloader::download);
+
+ // directly connect signal to signal
+ QObject::connect(&m_voidFutureWatcher, &QFutureWatcher<void>::finished,
+ this, &AppstreamDataDownloader::downloadFinished);
+ m_voidFutureWatcher.setFuture(downloaderFuture);
+ } else {
+ // no need to download anything
+ qCDebug(LOG_ALPINEAPK) << "appstream_downloader: All appstream data files "
+ "are up to date, not downloading anything";
+ Q_EMIT downloadFinished();
+ return;
+ }
+}
+
+// this function runs in background thread
+void AppstreamDataDownloader::download()
+{
+ QNetworkAccessManager nam;
+ QList<QNetworkReply *> replies;
+ QEventLoop loop;
+
+ // start a HTTP GET request for each URL
+ const QStringList urls = m_urlsToDownload;
+ const QString discoverVersion(QStringLiteral("plasma-discover %1").arg(DiscoverVersion::version));
+ for (const QString &url : urls) {
+ const QUrl uurl(url, QUrl::TolerantMode);
+ QNetworkRequest req(uurl);
+ req.setHeader(QNetworkRequest::UserAgentHeader, discoverVersion);
+ replies.push_back(nam.get(req));
+ }
+
+ for (QNetworkReply *rep : replies) {
+ // lambda that stops the loop when all requests have finished
+ // intentionaly use contextless lambda, it is not called otherwise
+ QObject::connect(rep, &QNetworkReply::finished, [&loop, &replies, rep] () {
+ const int numReplies = replies.size();
+ int numFinished = 0;
+ for (QNetworkReply *arep : replies) {
+ if (arep->isFinished()) {
+ numFinished++;
+ }
+ }
+ if (numFinished >= numReplies) {
+ loop.quit();
+ }
+ qCDebug(LOG_ALPINEAPK).nospace()
+ << "appstream_downloader: " << rep->url()
+ << " request finished (" << numFinished << "/" << numReplies << ")";
+ });
+ }
+
+ // wait for all requests to finish
+ loop.exec();
+
+ qCDebug(LOG_ALPINEAPK) << "appstream_downloader: all downloads have finished!";
+
+ int numErrors = 0;
+ for (QNetworkReply *rep : replies) {
+ const QString localCacheFile = getLocalFileSavePath(rep->url());
+
+ if (rep->error() == QNetworkReply::NoError) {
+ // read received reply contents and save it to file
+ const QByteArray data = rep->readAll();
+ QFile fout(localCacheFile);
+ if (fout.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ fout.write(data);
+ fout.close();
+ m_cacheWasUpdated = true;
+ qCDebug(LOG_ALPINEAPK) << "appstream_downloader: saved: " << localCacheFile;
+ } else {
+ qCWarning(LOG_ALPINEAPK) << "appstream_downloader: failed to save:" << localCacheFile;
+ }
+ } else {
+ // download failed for some reason
+ QFileInfo urlinfo(rep->url().path());
+ qCWarning(LOG_ALPINEAPK) << "appstream_downloader: failed to download"
+ << urlinfo.fileName() << rep->errorString();
+ numErrors++;
+ }
+ }
+
+ // cleanup: delete all replies objects
+ for (QNetworkReply *arep : replies) {
+ arep->deleteLater();
+ }
+}
diff --git a/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h
new file mode 100644
index 00000000..05771982
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/AppstreamDataDownloader.h
@@ -0,0 +1,139 @@
+/***************************************************************************
+ * Copyright © 2020 Alexey Min <alexey.min@gmail.com> *
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ ***************************************************************************/
+
+#ifndef AlpineAppstreamDataDownloader_H
+#define AlpineAppstreamDataDownloader_H
+
+#include <QFutureWatcher>
+#include <QHash>
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <QUrl>
+
+/**
+ * @brief The AppstreamDataDownloader class
+ *
+ * @details The job of this class is to download appstream data
+ * gzipped XMLs from the Web server hosted somewhere (for
+ * Alpine Linux - at https://appstream.alpinelinux.org).
+ *
+ * Some distros (for example, Alpine Linux) do not provide
+ * appstream data as installable package, instead they host it
+ * on the internet and you have to download and install them
+ * manually.
+ *
+ * Logic behind this decision is beyond my understanding, but we
+ * have what we have... Those files are not very large (from few
+ * kilobytes to couple of megabytes) but we still need them.
+ *
+ * URLs to download archives from are stored in JSON files in
+ * /usr/share/libdiscover/external-appstream-urls/ directory
+ * (QStandardPaths::GenericDataLocation/libdiscover/external-appstream-urls
+ * from C++/Qt code, ${DATA_INSTALL_DIR}/libdiscover/external-appstream-urls
+ * from cmake).
+ *
+ * JSON file format:
+ * ---------------------
+ * {
+ * "urls": [ "https://url1", "https://url2", ... ]
+ * }
+ * ---------------------
+ *
+ * This class can load any amount of those JSON files,
+ * fetch URLs from them and download all files pointed by
+ * those URLs to discover's cache directory:
+ * "~/.cache/discover/appstream_data" (aka QStandardPaths::CacheLocation).
+ * If files are already present in cache and not outdated,
+ * they are not downloaded again. Default cache expiration
+ * time is 2 days.
+ */
+class AppstreamDataDownloader: public QObject
+{
+ Q_OBJECT
+public:
+ explicit AppstreamDataDownloader(QObject *parent = nullptr);
+
+ /**
+ * @brief getAppstreamCacheDir
+ * @details Use return value of this function to add extra metadata
+ * directories to AppStream loader, in case of AppStreamQt:
+ * AppStream::Pool::addMetadataLocation().
+ * This method creates a cache dir if it does not exist.
+ * @return directory where downloaded files are stored.
+ */
+ static QString getAppStreamCacheDir();
+
+ /**
+ * @brief cacheWasUpdated
+ * @details Call this after receiving downloadFinished() signal to
+ * test if there actually was something new downloaded.
+ *
+ * @return true, if new files were actually downloaded, or
+ * false is files already present in cache are up to date.
+ */
+ bool cacheWasUpdated() const { return m_cacheWasUpdated; }
+
+ /**
+ * @brief getCacheExpirePeriodSecs
+ * @return cache expire timeout in seconds
+ */
+ qint64 getCacheExpirePeriodSecs() const { return m_cacheExpireSeconds; }
+
+ /**
+ * @brief setCacheExpirePeriodSecs
+ * @param secs - new cache expiration timeout, in seconds.
+ */
+ void setCacheExpirePeriodSecs(qint64 secs);
+
+public Q_SLOTS:
+ /**
+ * @brief start
+ * Start the background thread that does all the job.
+ * downloadFinished() signal will be emitted when everything is done.
+ * start() may finish immediately if all cached files are
+ * up to date and no downloads are needed.
+ */
+ void start();
+
+Q_SIGNALS:
+ /**
+ * @brief downloadFinished
+ * This signal is emitted when download job is finished.
+ * To check if there were actual downloads performed, call
+ * cacheWasUpdated().
+ */
+ void downloadFinished();
+
+private:
+ QString getLocalFileSavePath(const QUrl &urlTodownload);
+ void loadUrlsJson(const QString &path);
+ void download();
+
+protected:
+ qint64 m_cacheExpireSeconds = 2 * 24 * 3600; // 2 days
+ QStringList m_urls;
+ QStringList m_urlsToDownload;
+ QHash<QString, QString> m_urlPrefixes;
+ QFutureWatcher<void> m_voidFutureWatcher;
+ bool m_cacheWasUpdated = false;
+};
+
+#endif
diff --git a/libdiscover/backends/AlpineApkBackend/CMakeLists.txt b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
new file mode 100644
index 00000000..8602dce7
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/CMakeLists.txt
@@ -0,0 +1,85 @@
+find_package(KF5Auth CONFIG REQUIRED) # Probably should be moved to top CMakeLists
+
+set(alpineapkbackend_SRCS
+ AlpineApkAuthActionFactory.h
+ AlpineApkAuthActionFactory.cpp
+ AlpineApkBackend.cpp
+ AlpineApkBackend.h
+ AlpineApkResource.cpp
+ AlpineApkResource.h
+ AlpineApkReviewsBackend.cpp
+ AlpineApkReviewsBackend.h
+ AlpineApkSourcesBackend.cpp
+ AlpineApkSourcesBackend.h
+ AlpineApkUpdater.cpp
+ AlpineApkUpdater.h
+ AlpineApkTransaction.cpp
+ AlpineApkTransaction.h
+ AppstreamDataDownloader.h
+ AppstreamDataDownloader.cpp
+)
+
+ecm_qt_declare_logging_category(
+ alpineapkbackend_SRCS # sources_var
+ HEADER alpineapk_backend_logging.h
+ IDENTIFIER LOG_ALPINEAPK
+ CATEGORY_NAME org.kde.plasma.discover.alpineapk
+ DEFAULT_SEVERITY Debug
+)
+
+add_library(
+ alpineapk-backend
+ MODULE
+ ${alpineapkbackend_SRCS}
+)
+
+target_link_libraries(
+ alpineapk-backend
+ PRIVATE
+ Qt5::Core
+ Qt5::Widgets
+ Qt5::Concurrent
+ KF5::CoreAddons
+ KF5::ConfigCore
+ KF5::AuthCore
+ Discover::Common
+ ApkQt::ApkQt
+ AppStreamQt
+)
+
+# KAuth helper exe
+add_executable(alpineapk_kauth_helper
+ AlpineApkAuthHelper.cpp
+ AlpineApkAuthHelper.h
+ org.kde.discover.alpineapkbackend.actions
+)
+set_source_files_properties(
+ org.kde.discover.alpineapkbackend.actions
+ PROPERTIES HEADER_FILE_ONLY ON
+)
+target_link_libraries(alpineapk_kauth_helper
+ Qt5::Core
+ KF5::AuthCore
+ ApkQt::ApkQt
+)
+
+kauth_install_actions(org.kde.discover.alpineapkbackend org.kde.discover.alpineapkbackend.actions)
+kauth_install_helper_files(alpineapk_kauth_helper org.kde.discover.alpineapkbackend root)
+
+install(
+ TARGETS alpineapk-backend
+ DESTINATION ${PLUGIN_INSTALL_DIR}/discover
+)
+
+install(
+ TARGETS alpineapk_kauth_helper
+ DESTINATION ${KAUTH_HELPER_INSTALL_DIR}
+)
+
+# add_library(AlpineApkNotifier MODULE AlpineApkNotifier.cpp)
+
+# target_link_libraries(AlpineApkNotifier Discover::Notifiers)
+
+# set_target_properties(AlpineApkNotifier PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/plasma-discover)
+
+# install(TARGETS AlpineApkNotifier DESTINATION ${PLUGIN_INSTALL_DIR}/discover-notifier)
diff --git a/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
new file mode 100644
index 00000000..c9bb5f9f
--- /dev/null
+++ b/libdiscover/backends/AlpineApkBackend/org.kde.discover.alpineapkbackend.actions
@@ -0,0 +1,5 @@
+[org.kde.discover.alpineapkbackend.pkgmgmt]
+Name=Package management
+Description=Install or remove packages, upgrade system
+Policy=auth_admin
+Persistence=session
diff --git a/libdiscover/backends/CMakeLists.txt b/libdiscover/backends/CMakeLists.txt
index 5f87f639..18947339 100644
--- a/libdiscover/backends/CMakeLists.txt
+++ b/libdiscover/backends/CMakeLists.txt
@@ -45,4 +45,14 @@ if(BUILD_FwupdBackend AND TARGET PkgConfig::Fwupd)
add_subdirectory(FwupdBackend)
endif()
+find_package(ApkQt CONFIG)
+set_package_properties(ApkQt PROPERTIES
+ DESCRIPTION "C++/Qt interface library for Alpine package keeper"
+ URL "https://www.alpinelinux.org"
+ PURPOSE "Required to build the Alpine APK backend"
+ TYPE OPTIONAL)
+option(BUILD_AlpineApkBackend "Build Alpine APK support." "ON")
+if(BUILD_AlpineApkBackend AND ApkQt_FOUND)
+ add_subdirectory(AlpineApkBackend)
+endif()
--
2.30.0