diff --git a/discover/qml/Feedback.qml b/discover/qml/Feedback.qml index b20b71de..b8330225 100644 --- a/discover/qml/Feedback.qml +++ b/discover/qml/Feedback.qml @@ -1,70 +1,71 @@ import org.kde.kirigami 2.5 as Kirigami import org.kde.userfeedback 1.0 as UserFeedback +import org.kde.kquickcontrolsaddons 2.0 as KQCA import org.kde.discover.app 1.0 import QtQml 2.0 UserFeedback.Provider { readonly property list actions: [ Kirigami.Action { text: i18n("Submit usage information") tooltip: i18n("Sends anonymized usage information to KDE so we can better understand our users. For more information see https://kde.org/privacypolicy-apps.php.") onTriggered: { provider.submit() showPassiveNotification(i18n("Submitting usage information..."), "short", i18n("Configure"), provider.encouraged) } }, Kirigami.Action { text: i18n("Configure feedback...") onTriggered: { provider.encouraged() } } ] id: provider submissionInterval: 7 surveyInterval: 30 feedbackServer: "https://telemetry.kde.org/" encouragementInterval: 30 applicationStartsUntilEncouragement: 1 applicationUsageTimeUntilEncouragement: 1 telemetryMode: UserFeedbackSettings.feedbackLevel function encouraged() { - KCMShell.open("kcm_feedback"); + KQCA.KCMShell.open("kcm_feedback"); } property var lastSurvey: null function openSurvey() { Qt.openUrlExternally(lastSurvey.url); surveyCompleted(lastSurvey); } onShowEncouragementMessage: { showPassiveNotification(i18n("You can help us improving this application by sharing statistics and participate in surveys."), 5000, i18n("Contribute..."), encouraged) } onSurveyAvailable: { lastSurvey = survey showPassiveNotification(i18n("We are looking for your feedback!"), 5000, i18n("Participate..."), openSurvey) } UserFeedback.ApplicationVersionSource { mode: UserFeedback.Provider.BasicSystemInformation } UserFeedback.PlatformInfoSource { mode: UserFeedback.Provider.BasicSystemInformation } UserFeedback.QtVersionSource { mode: UserFeedback.Provider.BasicSystemInformation } UserFeedback.StartCountSource { mode: UserFeedback.Provider.BasicUsageStatistics } UserFeedback.UsageTimeSource { mode: UserFeedback.Provider.BasicUsageStatistics } UserFeedback.LocaleInfoSource { mode: UserFeedback.Provider.DetailedSystemInformation } UserFeedback.OpenGLInfoSource{ mode: UserFeedback.Provider.DetailedSystemInformation } UserFeedback.ScreenInfoSource { mode: UserFeedback.Provider.DetailedSystemInformation } UserFeedback.PropertySource { mode: UserFeedback.Provider.DetailedUsageStatistics name: "Application Source Name" sourceId: "applicationSourceName" data: { "value": ResourcesModel.applicationSourceName } description: "The source for applications" } } diff --git a/libdiscover/appstream/AppStreamUtils.cpp b/libdiscover/appstream/AppStreamUtils.cpp index 562e0965..890e941e 100644 --- a/libdiscover/appstream/AppStreamUtils.cpp +++ b/libdiscover/appstream/AppStreamUtils.cpp @@ -1,110 +1,117 @@ /*************************************************************************** * Copyright © 2017 Aleix Pol Gonzalez * * * * 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 . * ***************************************************************************/ #include "AppStreamUtils.h" #include #include +#include #include #include "utils.h" #include #include #include #include #if APPSTREAM_HAS_SPDX #include #endif using namespace AppStreamUtils; QUrl AppStreamUtils::imageOfKind(const QList& images, AppStream::Image::Kind kind) { QUrl ret; Q_FOREACH (const AppStream::Image &i, images) { if (i.kind() == kind) { ret = i.url(); break; } } return ret; } QString AppStreamUtils::changelogToHtml(const AppStream::Component& appdata) { if(appdata.releases().isEmpty()) return {}; const auto release = appdata.releases().constFirst(); if (release.description().isEmpty()) return {}; QString changelog = QLatin1String("

") + release.version() + QLatin1String("

") + QStringLiteral("

") + release.description() + QStringLiteral("

"); return changelog; } QPair, QList > AppStreamUtils::fetchScreenshots(const AppStream::Component& appdata) { QList screenshots, thumbnails; Q_FOREACH (const AppStream::Screenshot &s, appdata.screenshots()) { const auto images = s.images(); const QUrl thumbnail = AppStreamUtils::imageOfKind(images, AppStream::Image::KindThumbnail); const QUrl plain = AppStreamUtils::imageOfKind(images, AppStream::Image::KindSource); if (plain.isEmpty()) qWarning() << "invalid screenshot for" << appdata.name(); screenshots << plain; thumbnails << (thumbnail.isEmpty() ? plain : thumbnail); } return {screenshots, thumbnails}; } QJsonArray AppStreamUtils::licenses(const AppStream::Component& appdata) { #if APPSTREAM_HAS_SPDX QJsonArray ret; const auto licenses = AppStream::SPDX::tokenizeLicense(appdata.projectLicense()); #if !APPSTREAM_HAS_SPDX_LICENSEURL static const QLatin1String prop ("@LicenseRef-proprietary="); #endif for (const auto &token : licenses) { QString license = token; license.remove(0, 1); //tokenize prefixes with an @ for some reason if (!AppStream::SPDX::isLicenseId(license)) continue; #if APPSTREAM_HAS_SPDX_LICENSEURL ret.append(QJsonObject{ {QStringLiteral("name"), license}, {QStringLiteral("url"), { AppStream::SPDX::licenseUrl(license) } }}); #else if (license.startsWith(prop)) ret.append(QJsonObject{ {QStringLiteral("name"), i18n("Proprietary")}, {QStringLiteral("url"), license.mid(prop.size())} }); else ret.append(QJsonObject{ {QStringLiteral("name"), license}, {QStringLiteral("url"), { QLatin1String("https://spdx.org/licenses/") + AppStream::SPDX::asSpdxId(license) + QLatin1String(".html#licenseText") } }}); #endif } return ret; #else return { QJsonObject { {QStringLiteral("name"), appdata.projectLicense() } } }; #endif } -QString AppStreamUtils::appstreamId(const QUrl &appstreamUrl) +QStringList AppStreamUtils::appstreamIds(const QUrl &appstreamUrl) { - return appstreamUrl.host().isEmpty() ? appstreamUrl.path() : appstreamUrl.host(); + QStringList ret; + ret += appstreamUrl.host().isEmpty() ? appstreamUrl.path() : appstreamUrl.host(); + if (appstreamUrl.hasQuery()) { + QUrlQuery query(appstreamUrl); + ret << query.queryItemValue(QStringLiteral("alt")).split(QLatin1Char(','), Qt::SkipEmptyParts); + } + return ret; } diff --git a/libdiscover/appstream/AppStreamUtils.h b/libdiscover/appstream/AppStreamUtils.h index dec86a52..3e4fc150 100644 --- a/libdiscover/appstream/AppStreamUtils.h +++ b/libdiscover/appstream/AppStreamUtils.h @@ -1,43 +1,43 @@ /*************************************************************************** * Copyright © 2017 Aleix Pol Gonzalez * * * * 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 . * ***************************************************************************/ #ifndef APPSTREAMUTILS_H #define APPSTREAMUTILS_H #include #include #include #include namespace AppStreamUtils { Q_DECL_EXPORT QUrl imageOfKind(const QList &images, AppStream::Image::Kind kind); Q_DECL_EXPORT QString changelogToHtml(const AppStream::Component &appdata); Q_DECL_EXPORT QPair, QList> fetchScreenshots(const AppStream::Component &appdata); Q_DECL_EXPORT QJsonArray licenses(const AppStream::Component &appdata); -Q_DECL_EXPORT QString appstreamId(const QUrl &appstreamUrl); +Q_DECL_EXPORT QStringList appstreamIds(const QUrl &appstreamUrl); } #endif diff --git a/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp index e331046d..2bd0014c 100644 --- a/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp +++ b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp @@ -1,1324 +1,1324 @@ /*************************************************************************** * Copyright © 2013 Aleix Pol Gonzalez * * Copyright © 2017 Jan Grulich * * * * 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 . * ***************************************************************************/ #include "FlatpakBackend.h" #include "FlatpakFetchDataJob.h" #include "FlatpakResource.h" #include "FlatpakSourcesBackend.h" #include "FlatpakJobTransaction.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include DISCOVER_BACKEND_PLUGIN(FlatpakBackend) QDebug operator<<(QDebug debug, const FlatpakResource::Id& id) { QDebugStateSaver saver(debug); debug.nospace() << "FlatpakResource::Id("; debug.nospace() << "name:" << id.id << ','; debug.nospace() << "branch:" << id.branch << ','; debug.nospace() << "origin:" << id.origin << ','; debug.nospace() << "type:" << id.type; debug.nospace() << ')'; return debug; } static FlatpakResource::Id idForInstalledRef(FlatpakInstallation *installation, FlatpakInstalledRef *ref, const QString &postfix) { const FlatpakResource::ResourceType appType = flatpak_ref_get_kind(FLATPAK_REF(ref)) == FLATPAK_REF_KIND_APP ? FlatpakResource::DesktopApp : FlatpakResource::Runtime; const QString appId = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))) + postfix; const QString arch = QString::fromUtf8(flatpak_ref_get_arch(FLATPAK_REF(ref))); const QString branch = QString::fromUtf8(flatpak_ref_get_branch(FLATPAK_REF(ref))); return { installation, QString::fromUtf8(flatpak_installed_ref_get_origin(ref)), appType, appId, branch, arch }; } FlatpakBackend::FlatpakBackend(QObject* parent) : AbstractResourcesBackend(parent) , m_updater(new StandardBackendUpdater(this)) , m_reviews(AppStreamIntegration::global()->reviews()) , m_refreshAppstreamMetadataJobs(0) , m_cancellable(g_cancellable_new()) , m_threadPool(new QThreadPool(this)) { g_autoptr(GError) error = nullptr; connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &FlatpakBackend::updatesCountChanged); // Load flatpak installation if (!setupFlatpakInstallations(&error)) { qWarning() << "Failed to setup flatpak installations:" << error->message; } else { loadAppsFromAppstreamData(); m_sources = new FlatpakSourcesBackend(m_installations, this); SourcesModel::global()->addSourcesBackend(m_sources); } connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, [this] { m_reviews->emitRatingFetched(this, kTransform>(m_resources.values(), [] (AbstractResource* r) { return r; })); }); /* Override the umask to 022 to make it possible to share files between * the plasma-discover process and flatpak system helper process. * * See https://github.com/flatpak/flatpak/pull/2856/ */ umask(022); } FlatpakBackend::~FlatpakBackend() { g_cancellable_cancel(m_cancellable); m_threadPool.waitForDone(200); m_threadPool.clear(); for(auto inst : qAsConst(m_installations)) g_object_unref(inst); g_object_unref(m_cancellable); } bool FlatpakBackend::isValid() const { return m_sources && !m_installations.isEmpty(); } class FlatpakFetchRemoteResourceJob : public QNetworkAccessManager { Q_OBJECT public: FlatpakFetchRemoteResourceJob(const QUrl &url, FlatpakBackend *backend) : QNetworkAccessManager(backend) , m_backend(backend) , m_url(url) { } void start() { QNetworkRequest req(m_url); req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); auto replyGet = get(req); connect(replyGet, &QNetworkReply::finished, this, [this, replyGet] { QScopedPointer replyPtr(replyGet); const QUrl originalUrl = replyGet->request().url(); if (replyGet->error() != QNetworkReply::NoError) { qWarning() << "couldn't download" << originalUrl << replyGet->errorString(); Q_EMIT jobFinished(false, nullptr); return; } const QUrl fileUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1Char('/') + originalUrl.fileName()); auto replyPut = put(QNetworkRequest(fileUrl), replyGet->readAll()); connect(replyPut, &QNetworkReply::finished, this, [this, originalUrl, fileUrl, replyPut]() { QScopedPointer replyPtr(replyPut); if (replyPut->error() != QNetworkReply::NoError) { qWarning() << "couldn't save" << originalUrl << replyPut->errorString(); Q_EMIT jobFinished(false, nullptr); return; } if (!fileUrl.isLocalFile()) { Q_EMIT jobFinished(false, nullptr); return; } FlatpakResource *resource = nullptr; if (fileUrl.path().endsWith(QLatin1String(".flatpak"))) { resource = m_backend->addAppFromFlatpakBundle(fileUrl); } else if (fileUrl.path().endsWith(QLatin1String(".flatpakref"))) { resource = m_backend->addAppFromFlatpakRef(fileUrl); } else if (fileUrl.path().endsWith(QLatin1String(".flatpakrepo"))) { resource = m_backend->addSourceFromFlatpakRepo(fileUrl); } if (resource) { resource->setResourceFile(originalUrl); Q_EMIT jobFinished(true, resource); } else { qWarning() << "couldn't create resource from" << fileUrl.toLocalFile(); Q_EMIT jobFinished(false, nullptr); } } ); }); } Q_SIGNALS: void jobFinished(bool success, FlatpakResource *resource); private: FlatpakBackend *const m_backend; const QUrl m_url; }; FlatpakRemote * FlatpakBackend::getFlatpakRemoteByUrl(const QString &url, FlatpakInstallation *installation) const { auto remotes = flatpak_installation_list_remotes(installation, m_cancellable, nullptr); if (!remotes) { return nullptr; } const QByteArray comparableUrl = url.toUtf8(); for (uint i = 0; i < remotes->len; i++) { FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i)); if (comparableUrl == flatpak_remote_get_url(remote)) { return remote; } } return nullptr; } FlatpakInstalledRef * FlatpakBackend::getInstalledRefForApp(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) const { FlatpakInstalledRef *ref = nullptr; g_autoptr(GError) localError = nullptr; if (!flatpakInstallation) { return ref; } const auto type = resource->resourceType() == FlatpakResource::DesktopApp ? FLATPAK_REF_KIND_APP : FLATPAK_REF_KIND_RUNTIME; return flatpak_installation_get_installed_ref(flatpakInstallation, type, resource->flatpakName().toUtf8().constData(), resource->arch().toUtf8().constData(), resource->branch().toUtf8().constData(), m_cancellable, &localError); } FlatpakResource * FlatpakBackend::getAppForInstalledRef(FlatpakInstallation *flatpakInstallation, FlatpakInstalledRef *ref) const { auto r = m_resources.value(idForInstalledRef(flatpakInstallation, ref, {})); if (!r) r = m_resources.value(idForInstalledRef(flatpakInstallation, ref, QStringLiteral(".desktop"))); // if (!r) { // qDebug() << "no" << flatpak_ref_get_name(FLATPAK_REF(ref)); // } return r; } FlatpakResource * FlatpakBackend::getRuntimeForApp(FlatpakResource *resource) const { FlatpakResource *runtime = nullptr; const QString runtimeName = resource->runtime(); const auto runtimeInfo = runtimeName.splitRef(QLatin1Char('/')); if (runtimeInfo.count() != 3) { return runtime; } for(auto it = m_resources.constBegin(), itEnd = m_resources.constEnd(); it!=itEnd; ++it) { const auto& id = it.key(); if (id.type == FlatpakResource::Runtime && id.id == runtimeInfo.at(0) && id.branch == runtimeInfo.at(2)) { runtime = *it; break; } } // TODO if runtime wasn't found, create a new one from available info if (!runtime) { qWarning() << "could not find runtime" << runtimeInfo << resource; } return runtime; } FlatpakResource * FlatpakBackend::addAppFromFlatpakBundle(const QUrl &url) { g_autoptr(GBytes) appstreamGz = nullptr; g_autoptr(GError) localError = nullptr; g_autoptr(GFile) file = nullptr; g_autoptr(FlatpakBundleRef) bundleRef = nullptr; AppStream::Component asComponent; file = g_file_new_for_path(url.toLocalFile().toUtf8().constData()); bundleRef = flatpak_bundle_ref_new(file, &localError); if (!bundleRef) { qWarning() << "Failed to load bundle:" << localError->message; return nullptr; } g_autoptr(GBytes) metadata = flatpak_bundle_ref_get_metadata(bundleRef); appstreamGz = flatpak_bundle_ref_get_appstream(bundleRef); if (appstreamGz) { g_autoptr(GZlibDecompressor) decompressor = nullptr; g_autoptr(GInputStream) streamGz = nullptr; g_autoptr(GInputStream) streamData = nullptr; g_autoptr(GBytes) appstream = nullptr; /* decompress data */ decompressor = g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP); streamGz = g_memory_input_stream_new_from_bytes (appstreamGz); if (!streamGz) { return nullptr; } streamData = g_converter_input_stream_new (streamGz, G_CONVERTER (decompressor)); appstream = g_input_stream_read_bytes (streamData, 0x100000, m_cancellable, &localError); if (!appstream) { qWarning() << "Failed to extract appstream metadata from bundle:" << localError->message; return nullptr; } gsize len = 0; gconstpointer data = g_bytes_get_data(appstream, &len); AppStream::Metadata metadata; metadata.setFormatStyle(AppStream::Metadata::FormatStyleCollection); AppStream::Metadata::MetadataError error = metadata.parse(QString::fromUtf8((char*)data, len), AppStream::Metadata::FormatKindXml); if (error != AppStream::Metadata::MetadataErrorNoError) { qWarning() << "Failed to parse appstream metadata: " << error; return nullptr; } const QList components = metadata.components(); if (components.size()) { asComponent = AppStream::Component(components.first()); } else { qWarning() << "Failed to parse appstream metadata"; return nullptr; } } else { qWarning() << "No appstream metadata in bundle"; QTemporaryFile tempFile; tempFile.setAutoRemove(false); if (!tempFile.open()) { qWarning() << "Failed to get metadata file"; return nullptr; } gsize len = 0; QByteArray metadataContent = QByteArray((char *)g_bytes_get_data(metadata, &len)); tempFile.write(metadataContent); tempFile.close(); // Parse the temporary file QSettings setting(tempFile.fileName(), QSettings::NativeFormat); setting.beginGroup(QLatin1String("Application")); asComponent.setName(setting.value(QLatin1String("name")).toString()); tempFile.remove(); } FlatpakResource *resource = new FlatpakResource(asComponent, preferredInstallation(), this); gsize len = 0; QByteArray metadataContent = QByteArray((char *)g_bytes_get_data(metadata, &len)); if (!updateAppMetadata(resource, metadataContent)) { delete resource; qWarning() << "Failed to update metadata from app bundle"; return nullptr; } g_autoptr(GBytes) iconData = flatpak_bundle_ref_get_icon(bundleRef, 128); if (!iconData) { iconData = flatpak_bundle_ref_get_icon(bundleRef, 64); } if (iconData) { gsize len = 0; char * data = (char *)g_bytes_get_data(iconData, &len); QPixmap pixmap; pixmap.loadFromData(QByteArray(data, len), "PNG"); resource->setBundledIcon(pixmap); } const QString origin = QString::fromUtf8(flatpak_bundle_ref_get_origin(bundleRef)); resource->setDownloadSize(0); resource->setInstalledSize(flatpak_bundle_ref_get_installed_size(bundleRef)); resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::AlreadyKnown); resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::AlreadyKnown); resource->setFlatpakFileType(QStringLiteral("flatpak")); resource->setOrigin(origin.isEmpty() ? i18n("Local bundle") : origin); resource->setResourceFile(url); resource->setState(FlatpakResource::None); resource->setType(FlatpakResource::DesktopApp); addResource(resource); return resource; } FlatpakResource * FlatpakBackend::addAppFromFlatpakRef(const QUrl &url) { QSettings settings(url.toLocalFile(), QSettings::NativeFormat); const QString refurl = settings.value(QStringLiteral("Flatpak Ref/Url")).toString(); const QString name = settings.value(QStringLiteral("Flatpak Ref/Name")).toString(); auto item = m_sources->sourceByUrl(refurl); if (item) { const auto resources = resourcesByAppstreamName(name); for (auto resource : resources) { if (resource->origin() == item->data(AbstractSourcesBackend::IdRole)) { return static_cast(resource); } } } g_autoptr(GError) error = nullptr; g_autoptr(FlatpakRemoteRef) remoteRef = nullptr; { QFile f(url.toLocalFile()); if (!f.open(QFile::ReadOnly | QFile::Text)) { return nullptr; } QByteArray contents = f.readAll(); g_autoptr(GBytes) bytes = g_bytes_new (contents.data(), contents.size()); remoteRef = flatpak_installation_install_ref_file (preferredInstallation(), bytes, m_cancellable, &error); if (!remoteRef) { qWarning() << "Failed to create install ref file:" << error->message; const auto resources = resourcesByAppstreamName(name); if (!resources.isEmpty()) { return qobject_cast(resources.constFirst()); } return nullptr; } } const auto remoteName = flatpak_remote_ref_get_remote_name(remoteRef); auto ref = FLATPAK_REF(remoteRef); AppStream::Component asComponent; asComponent.addUrl(AppStream::Component::UrlKindHomepage, settings.value(QStringLiteral("Flatpak Ref/Homepage")).toString()); asComponent.setDescription(settings.value(QStringLiteral("Flatpak Ref/Description")).toString()); asComponent.setName(settings.value(QStringLiteral("Flatpak Ref/Title")).toString()); asComponent.setSummary(settings.value(QStringLiteral("Flatpak Ref/Comment")).toString()); asComponent.setId(name); const QString iconUrl = settings.value(QStringLiteral("Flatpak Ref/Icon")).toString(); if (!iconUrl.isEmpty()) { AppStream::Icon icon; icon.setKind(AppStream::Icon::KindRemote); icon.setUrl(QUrl(iconUrl)); asComponent.addIcon(icon); } auto resource = new FlatpakResource(asComponent, preferredInstallation(), this); resource->setFlatpakFileType(QStringLiteral("flatpakref")); resource->setOrigin(QString::fromUtf8(remoteName)); resource->updateFromRef(ref); QUrl runtimeUrl = QUrl(settings.value(QStringLiteral("Flatpak Ref/RuntimeRepo")).toString()); if (!runtimeUrl.isEmpty()) { auto installation = preferredInstallation(); // We need to fetch metadata to find information about required runtime auto fw = new QFutureWatcher(this); connect(fw, &QFutureWatcher::finished, this, [this, installation, resource, fw, runtimeUrl]() { const auto metadata = fw->result(); // Even when we failed to fetch information about runtime we still want to show the application if (metadata.isEmpty()) { onFetchMetadataFinished(installation, resource, metadata); } else { updateAppMetadata(resource, metadata); auto runtime = getRuntimeForApp(resource); if (!runtime || (runtime && !runtime->isInstalled())) { FlatpakFetchRemoteResourceJob *fetchRemoteResource = new FlatpakFetchRemoteResourceJob(runtimeUrl, this); connect(fetchRemoteResource, &FlatpakFetchRemoteResourceJob::jobFinished, this, [this, resource] (bool success, FlatpakResource *repoResource) { if (success) { installApplication(repoResource); } addResource(resource); }); fetchRemoteResource->start(); return; } else { addResource(resource); } } fw->deleteLater(); }); fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, installation, resource)); } else { addResource(resource); } return resource; } FlatpakResource * FlatpakBackend::addSourceFromFlatpakRepo(const QUrl &url) { Q_ASSERT(url.isLocalFile()); QSettings settings(url.toLocalFile(), QSettings::NativeFormat); const QString gpgKey = settings.value(QStringLiteral("Flatpak Repo/GPGKey")).toString(); const QString title = settings.value(QStringLiteral("Flatpak Repo/Title")).toString(); const QString repoUrl = settings.value(QStringLiteral("Flatpak Repo/Url")).toString(); if (gpgKey.isEmpty() || title.isEmpty() || repoUrl.isEmpty()) { return nullptr; } if (gpgKey.startsWith(QLatin1String("http://")) || gpgKey.startsWith(QLatin1String("https://"))) { return nullptr; } AppStream::Component asComponent; asComponent.addUrl(AppStream::Component::UrlKindHomepage, settings.value(QStringLiteral("Flatpak Repo/Homepage")).toString()); asComponent.setSummary(settings.value(QStringLiteral("Flatpak Repo/Comment")).toString()); asComponent.setDescription(settings.value(QStringLiteral("Flatpak Repo/Description")).toString()); asComponent.setName(title); asComponent.setId(settings.value(QStringLiteral("Flatpak Ref/Name")).toString()); const QString iconUrl = settings.value(QStringLiteral("Flatpak Repo/Icon")).toString(); if (!iconUrl.isEmpty()) { AppStream::Icon icon; icon.setKind(AppStream::Icon::KindRemote); icon.setUrl(QUrl(iconUrl)); asComponent.addIcon(icon); } auto resource = new FlatpakResource(asComponent, preferredInstallation(), this); // Use metadata only for stuff which are not common for all resources resource->addMetadata(QStringLiteral("gpg-key"), gpgKey); resource->addMetadata(QStringLiteral("repo-url"), repoUrl); resource->setBranch(settings.value(QStringLiteral("Flatpak Repo/DefaultBranch")).toString()); resource->setFlatpakName(url.fileName().remove(QStringLiteral(".flatpakrepo"))); resource->setType(FlatpakResource::Source); auto repo = flatpak_installation_get_remote_by_name(preferredInstallation(), resource->flatpakName().toUtf8().constData(), m_cancellable, nullptr); if (!repo) { resource->setState(AbstractResource::State::None); } else { resource->setState(AbstractResource::State::Installed); } return resource; } void FlatpakBackend::addResource(FlatpakResource *resource) { // Update app with all possible information we have if (!parseMetadataFromAppBundle(resource)) { qWarning() << "Failed to parse metadata from app bundle for" << resource->name(); } auto installation = resource->installation(); updateAppState(installation, resource); // This will update also metadata (required runtime) updateAppSize(installation, resource); m_resources.insert(resource->uniqueId(), resource); if (!resource->extends().isEmpty()) { m_extends.append(resource->extends()); m_extends.removeDuplicates(); } } class FlatpakSource { public: FlatpakSource(FlatpakRemote* remote) : m_remote(remote) {} bool isEnabled() const { return !flatpak_remote_get_disabled(m_remote); } QString appstreamDir() const { g_autoptr(GFile) appstreamDir = flatpak_remote_get_appstream_dir(m_remote, nullptr); if (!appstreamDir) { qWarning() << "No appstream dir for" << flatpak_remote_get_name(m_remote); return {}; } return QString::fromUtf8(g_file_get_path(appstreamDir)); } QString name() const { return QString::fromUtf8(flatpak_remote_get_name(m_remote)); } private: FlatpakRemote* m_remote; }; void FlatpakBackend::loadAppsFromAppstreamData() { for (auto installation : qAsConst(m_installations)) { // Load applications from appstream metadata if (!loadAppsFromAppstreamData(installation)) { qWarning() << "Failed to load packages from appstream data from installation" << installation; } } } bool FlatpakBackend::loadAppsFromAppstreamData(FlatpakInstallation *flatpakInstallation) { Q_ASSERT(flatpakInstallation); GPtrArray *remotes = flatpak_installation_list_remotes(flatpakInstallation, m_cancellable, nullptr); if (!remotes) { return false; } m_refreshAppstreamMetadataJobs += remotes->len; for (uint i = 0; i < remotes->len; i++) { FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i)); g_autoptr(GFile) fileTimestamp = flatpak_remote_get_appstream_timestamp(remote, nullptr); QFileInfo fileInfo = QFileInfo(QString::fromUtf8(g_file_get_path(fileTimestamp))); // Refresh appstream metadata in case they have never been refreshed or the cache is older than 6 hours if (!fileInfo.exists() || fileInfo.lastModified().toUTC().secsTo(QDateTime::currentDateTimeUtc()) > 21600) { refreshAppstreamMetadata(flatpakInstallation, remote); } else { integrateRemote(flatpakInstallation, remote); } } return true; } void FlatpakBackend::metadataRefreshed() { m_refreshAppstreamMetadataJobs--; if (m_refreshAppstreamMetadataJobs == 0) { loadInstalledApps(); checkForUpdates(); } } void FlatpakBackend::integrateRemote(FlatpakInstallation *flatpakInstallation, FlatpakRemote *remote) { Q_ASSERT(m_refreshAppstreamMetadataJobs != 0); FlatpakSource source(remote); if (!source.isEnabled() || flatpak_remote_get_noenumerate(remote)) { metadataRefreshed(); return; } const QString appstreamDirPath = source.appstreamDir(); const QString appstreamIconsPath = source.appstreamDir() + QLatin1String("/icons/"); const QString appDirFileName = appstreamDirPath + QLatin1String("/appstream.xml.gz"); if (!QFile::exists(appDirFileName)) { qWarning() << "No" << appDirFileName << "appstream metadata found for" << source.name(); metadataRefreshed(); return; } auto fw = new QFutureWatcher>(this); const auto sourceName = source.name(); connect(fw, &QFutureWatcher>::finished, this, [this, fw, flatpakInstallation, appstreamIconsPath, sourceName]() { const auto components = fw->result(); foreach (const AppStream::Component& appstreamComponent, components) { FlatpakResource *resource = new FlatpakResource(appstreamComponent, flatpakInstallation, this); resource->setIconPath(appstreamIconsPath); resource->setOrigin(sourceName); addResource(resource); } metadataRefreshed(); acquireFetching(false); fw->deleteLater(); }); acquireFetching(true); fw->setFuture(QtConcurrent::run(&m_threadPool, [appDirFileName]() -> QList { AppStream::Metadata metadata; metadata.setFormatStyle(AppStream::Metadata::FormatStyleCollection); AppStream::Metadata::MetadataError error = metadata.parseFile(appDirFileName, AppStream::Metadata::FormatKindXml); if (error != AppStream::Metadata::MetadataErrorNoError) { qWarning() << "Failed to parse appstream metadata: " << error; return {}; } return metadata.components(); })); } void FlatpakBackend::loadInstalledApps() { for (auto installation : qAsConst(m_installations)) { // Load installed applications and update existing resources with info from installed application if (!loadInstalledApps(installation)) { qWarning() << "Failed to load installed packages from installation" << installation; } } } bool FlatpakBackend::loadInstalledApps(FlatpakInstallation *flatpakInstallation) { Q_ASSERT(flatpakInstallation); g_autoptr(GError) localError = nullptr; g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(flatpakInstallation, m_cancellable, &localError); if (!refs) { qWarning() << "Failed to get list of installed refs for listing updates:" << localError->message; return false; } const QString pathExports = FlatpakResource::installationPath(flatpakInstallation) + QLatin1String("/exports/"); const QString pathApps = pathExports + QLatin1String("share/applications/"); for (uint i = 0; i < refs->len; i++) { FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); const auto name = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))); if (name.endsWith(QLatin1String(".Debug")) || name.endsWith(QLatin1String(".Locale")) || name.endsWith(QLatin1String(".BaseApp")) || name.endsWith(QLatin1String(".Docs"))) continue; const auto res = getAppForInstalledRef(flatpakInstallation, ref); if (res) { res->setState(AbstractResource::Installed); continue; } AppStream::Component cid; AppStream::Metadata metadata; const QString fnDesktop = pathApps + name + QLatin1String(".desktop"); AppStream::Metadata::MetadataError error = metadata.parseFile(fnDesktop, AppStream::Metadata::FormatKindDesktopEntry); if (error != AppStream::Metadata::MetadataErrorNoError) { if (QFile::exists(fnDesktop)) qDebug() << "Failed to parse appstream metadata:" << error << fnDesktop; cid.setId(QString::fromLatin1(flatpak_ref_get_name(FLATPAK_REF(ref)))); #if FLATPAK_CHECK_VERSION(1,1,2) cid.setName(QString::fromLatin1(flatpak_installed_ref_get_appdata_name(ref))); #endif } else cid = metadata.component(); FlatpakResource *resource = new FlatpakResource(cid, flatpakInstallation, this); resource->setIconPath(pathExports); resource->setState(AbstractResource::Installed); resource->setOrigin(QString::fromUtf8(flatpak_installed_ref_get_origin(ref))); resource->updateFromRef(FLATPAK_REF(ref)); addResource(resource); } return true; } void FlatpakBackend::loadLocalUpdates(FlatpakInstallation *flatpakInstallation) { g_autoptr(GError) localError = nullptr; g_autoptr(GPtrArray) refs = nullptr; refs = flatpak_installation_list_installed_refs(flatpakInstallation, m_cancellable, &localError); if (!refs) { qWarning() << "Failed to get list of installed refs for listing updates:" << localError->message; return; } for (uint i = 0; i < refs->len; i++) { FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); const gchar *latestCommit = flatpak_installed_ref_get_latest_commit(ref); if (!latestCommit) { qWarning() << "Couldn't get latest commit for" << flatpak_ref_get_name(FLATPAK_REF(ref)); continue; } const gchar *commit = flatpak_ref_get_commit(FLATPAK_REF(ref)); if (g_strcmp0(commit, latestCommit) == 0) { continue; } FlatpakResource *resource = getAppForInstalledRef(flatpakInstallation, ref); if (resource) { resource->setState(AbstractResource::Upgradeable); updateAppSize(flatpakInstallation, resource); } } } void FlatpakBackend::loadRemoteUpdates(FlatpakInstallation* installation) { auto fw = new QFutureWatcher(this); connect(fw, &QFutureWatcher::finished, this, [this, installation, fw](){ auto refs = fw->result(); onFetchUpdatesFinished(installation, refs); fw->deleteLater(); acquireFetching(false); }); acquireFetching(true); fw->setFuture(QtConcurrent::run(&m_threadPool, [installation, this]() -> GPtrArray * { g_autoptr(GError) localError = nullptr; GPtrArray *refs = flatpak_installation_list_installed_refs_for_update(installation, m_cancellable, &localError); if (!refs) { qWarning() << "Failed to get list of installed refs for listing updates: " << localError->message; } return refs; })); } void FlatpakBackend::onFetchUpdatesFinished(FlatpakInstallation *flatpakInstallation, GPtrArray *updates) { if (!updates) { qWarning() << "could not get updates for" << flatpakInstallation; return; } g_autoptr(GPtrArray) fetchedUpdates = updates; for (uint i = 0; i < fetchedUpdates->len; i++) { FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(fetchedUpdates, i)); FlatpakResource *resource = getAppForInstalledRef(flatpakInstallation, ref); if (resource) { resource->setState(AbstractResource::Upgradeable); updateAppSize(flatpakInstallation, resource); } else qWarning() << "could not find updated resource" << flatpak_ref_get_name(FLATPAK_REF(ref)) << m_resources.size(); } } bool FlatpakBackend::parseMetadataFromAppBundle(FlatpakResource *resource) { g_autoptr(FlatpakRef) ref = nullptr; g_autoptr(GError) localError = nullptr; AppStream::Bundle bundle = resource->appstreamComponent().bundle(AppStream::Bundle::KindFlatpak); // Get arch/branch/commit/name from FlatpakRef if (!bundle.isEmpty()) { ref = flatpak_ref_parse(bundle.id().toUtf8().constData(), &localError); if (!ref) { qWarning() << "Failed to parse" << bundle.id() << localError->message; return false; } else { resource->updateFromRef(ref); } } return true; } class FlatpakRefreshAppstreamMetadataJob : public QThread { Q_OBJECT public: FlatpakRefreshAppstreamMetadataJob(FlatpakInstallation *installation, FlatpakRemote *remote) : QThread() , m_cancellable(g_cancellable_new()) , m_installation(installation) , m_remote(remote) { connect(this, &FlatpakRefreshAppstreamMetadataJob::finished, this, &QObject::deleteLater); } ~FlatpakRefreshAppstreamMetadataJob() { g_object_unref(m_cancellable); } void cancel() { g_cancellable_cancel(m_cancellable); } void run() override { g_autoptr(GError) localError = nullptr; #if FLATPAK_CHECK_VERSION(0,9,4) // With Flatpak 0.9.4 we can use flatpak_installation_update_appstream_full_sync() providing progress reporting which we don't use at this moment, but still // better to use newer function in case the previous one gets deprecated if (!flatpak_installation_update_appstream_full_sync(m_installation, flatpak_remote_get_name(m_remote), nullptr, nullptr, nullptr, nullptr, m_cancellable, &localError)) { #else if (!flatpak_installation_update_appstream_sync(m_installation, flatpak_remote_get_name(m_remote), nullptr, nullptr, m_cancellable, &localError)) { #endif const QString error = localError ? QString::fromUtf8(localError->message) : QStringLiteral(""); qWarning() << "Failed to refresh appstream metadata for " << flatpak_remote_get_name(m_remote) << ": " << error; Q_EMIT jobRefreshAppstreamMetadataFailed(error); } else { Q_EMIT jobRefreshAppstreamMetadataFinished(m_installation, m_remote); } } Q_SIGNALS: void jobRefreshAppstreamMetadataFailed(const QString &errorMessage); void jobRefreshAppstreamMetadataFinished(FlatpakInstallation *installation, FlatpakRemote *remote); private: GCancellable *m_cancellable; FlatpakInstallation *m_installation; FlatpakRemote *m_remote; }; void FlatpakBackend::refreshAppstreamMetadata(FlatpakInstallation *installation, FlatpakRemote *remote) { FlatpakRefreshAppstreamMetadataJob *job = new FlatpakRefreshAppstreamMetadataJob(installation, remote); connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFailed, this, &FlatpakBackend::metadataRefreshed); connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFailed, this, [this] (const QString &errorMessage) { Q_EMIT passiveMessage(errorMessage); }); connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFinished, this, &FlatpakBackend::integrateRemote); connect(job, &FlatpakRefreshAppstreamMetadataJob::finished, this, [this] { acquireFetching(false); }); acquireFetching(true); job->start(); } bool FlatpakBackend::setupFlatpakInstallations(GError **error) { if (qEnvironmentVariableIsSet("FLATPAK_TEST_MODE")) { const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/discover-flatpak-test"); qDebug() << "running flatpak backend on test mode" << path; g_autoptr(GFile) file = g_file_new_for_path(QFile::encodeName(path).constData()); m_installations << flatpak_installation_new_for_path(file, true, m_cancellable, error); return true; } GPtrArray *installations = flatpak_get_system_installations(m_cancellable, error); if (*error) { qWarning() << "Failed to call flatpak_get_system_installations:" << (*error)->message; } for (uint i = 0; installations && i < installations->len; i++) { m_installations << FLATPAK_INSTALLATION(g_ptr_array_index(installations, i)); } auto user = flatpak_installation_new_user(m_cancellable, error); if (user) { m_installations << user; } return !m_installations.isEmpty(); } void FlatpakBackend::updateAppInstalledMetadata(FlatpakInstalledRef *installedRef, FlatpakResource *resource) { // Update the rest resource->updateFromRef(FLATPAK_REF(installedRef)); resource->setInstalledSize(flatpak_installed_ref_get_installed_size(installedRef)); resource->setOrigin(QString::fromUtf8(flatpak_installed_ref_get_origin(installedRef))); if (resource->state() < AbstractResource::Installed) resource->setState(AbstractResource::Installed); } bool FlatpakBackend::updateAppMetadata(FlatpakInstallation* flatpakInstallation, FlatpakResource *resource) { if (resource->resourceType() != FlatpakResource::DesktopApp) { return true; } const QString path = resource->installPath() + QStringLiteral("/metadata"); if (QFile::exists(path)) { return updateAppMetadata(resource, path); } else { auto fw = new QFutureWatcher(this); connect(fw, &QFutureWatcher::finished, this, [this, flatpakInstallation, resource, fw]() { const auto metadata = fw->result(); if (!metadata.isEmpty()) onFetchMetadataFinished(flatpakInstallation, resource, metadata); fw->deleteLater(); }); fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, flatpakInstallation, resource)); // Return false to indicate we cannot continue (right now used only in updateAppSize()) return false; } } void FlatpakBackend::onFetchMetadataFinished(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource, const QByteArray &metadata) { updateAppMetadata(resource, metadata); // Right now we attempt to update metadata for calculating the size so call updateSizeFromRemote() // as it's what we want. In future if there are other reason to update metadata we will need to somehow // distinguish between these calls updateAppSizeFromRemote(flatpakInstallation, resource); } bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource, const QString &path) { // Parse the temporary file QSettings setting(path, QSettings::NativeFormat); setting.beginGroup(QLatin1String("Application")); // Set the runtime in form of name/arch/version which can be later easily parsed resource->setRuntime(setting.value(QLatin1String("runtime")).toString()); // TODO get more information? return true; } bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource, const QByteArray &data) { //We just find the runtime with a regex, QSettings only can read from disk (and so does KConfig) const QRegularExpression rx(QStringLiteral("runtime=(.*)")); const auto match = rx.match(QString::fromUtf8(data)); if (!match.isValid()) { return false; } resource->setRuntime(match.captured(1)); return true; } bool FlatpakBackend::updateAppSize(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) { // Check if the size is already set, we should also distinguish between download and installed size, // right now it doesn't matter whether we get size for installed or not installed app, but if we // start making difference then for not installed app check download and install size separately if (resource->state() == AbstractResource::Installed) { // The size appears to be already set (from updateAppInstalledMetadata() apparently) if (resource->installedSize() > 0) { return true; } } else { if (resource->installedSize() > 0 && resource->downloadSize() > 0) { return true; } } // Check if we know the needed runtime which is needed for calculating the size if (resource->runtime().isEmpty()) { if (!updateAppMetadata(flatpakInstallation, resource)) { return false; } } return updateAppSizeFromRemote(flatpakInstallation, resource); } bool FlatpakBackend::updateAppSizeFromRemote(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) { // Calculate the runtime size if (resource->state() == AbstractResource::None && resource->resourceType() == FlatpakResource::DesktopApp) { auto runtime = getRuntimeForApp(resource); if (runtime) { // Re-check runtime state if case a new one was created updateAppState(flatpakInstallation, runtime); if (!runtime->isInstalled()) { if (!updateAppSize(flatpakInstallation, runtime)) { qWarning() << "Failed to get runtime size needed for total size of" << resource->name(); return false; } // Set required download size to include runtime size even now, in case we fail to // get the app size (e.g. when installing bundles where download size is 0) resource->setDownloadSize(runtime->downloadSize()); } } } if (resource->state() == AbstractResource::Installed) { g_autoptr(FlatpakInstalledRef) ref = nullptr; ref = getInstalledRefForApp(flatpakInstallation, resource); if (!ref) { qWarning() << "Failed to get installed size of" << resource->name(); return false; } resource->setInstalledSize(flatpak_installed_ref_get_installed_size(ref)); } else { if (resource->origin().isEmpty()) { qWarning() << "Failed to get size of" << resource->name() << " because of missing origin"; return false; } auto futureWatcher = new QFutureWatcher(this); connect(futureWatcher, &QFutureWatcher::finished, this, [this, resource, futureWatcher]() { auto value = futureWatcher->result(); if (value.valid) { onFetchSizeFinished(resource, value.downloadSize, value.installedSize); } else { resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::UnknownOrFailed); resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::UnknownOrFailed); } futureWatcher->deleteLater(); }); futureWatcher->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchFlatpakSize, flatpakInstallation, resource)); } return true; } void FlatpakBackend::onFetchSizeFinished(FlatpakResource *resource, guint64 downloadSize, guint64 installedSize) { FlatpakResource *runtime = nullptr; if (resource->state() == AbstractResource::None && resource->resourceType() == FlatpakResource::DesktopApp) { runtime = getRuntimeForApp(resource); } if (runtime && !runtime->isInstalled()) { resource->setDownloadSize(runtime->downloadSize() + downloadSize); } else { resource->setDownloadSize(downloadSize); } resource->setInstalledSize(installedSize); } void FlatpakBackend::updateAppState(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) { FlatpakInstalledRef *ref = getInstalledRefForApp(flatpakInstallation, resource); if (ref) { // If the app is installed, we can set information about commit, arch etc. updateAppInstalledMetadata(ref, resource); } else { // TODO check if the app is actually still available resource->setState(AbstractResource::None); } } void FlatpakBackend::acquireFetching(bool f) { if (f) m_isFetching++; else m_isFetching--; if ((!f && m_isFetching==0) || (f && m_isFetching==1)) { emit fetchingChanged(); } if (m_isFetching==0) Q_EMIT initialized(); } int FlatpakBackend::updatesCount() const { return m_updater->updatesCount(); } bool FlatpakBackend::flatpakResourceLessThan(AbstractResource* l, AbstractResource* r) const { return (l->isInstalled() != r->isInstalled()) ? l->isInstalled() : (l->origin() != r->origin()) ? m_sources->originIndex(l->origin()) < m_sources->originIndex(r->origin()) : l < r; } ResultsStream * FlatpakBackend::search(const AbstractResourcesBackend::Filters &filter) { if (filter.resourceUrl.fileName().endsWith(QLatin1String(".flatpakrepo")) || filter.resourceUrl.fileName().endsWith(QLatin1String(".flatpakref")) || filter.resourceUrl.fileName().endsWith(QLatin1String(".flatpak"))) { auto stream = new ResultsStream(QLatin1String("FlatpakStream-http-")+filter.resourceUrl.fileName()); FlatpakFetchRemoteResourceJob *fetchResourceJob = new FlatpakFetchRemoteResourceJob(filter.resourceUrl, this); connect(fetchResourceJob, &FlatpakFetchRemoteResourceJob::jobFinished, this, [fetchResourceJob, stream] (bool success, FlatpakResource *resource) { if (success) { Q_EMIT stream->resourcesFound({resource}); } stream->finish(); fetchResourceJob->deleteLater(); }); fetchResourceJob->start(); return stream; } else if(filter.resourceUrl.scheme() == QLatin1String("appstream")) { return findResourceByPackageName(filter.resourceUrl); } else if (!filter.resourceUrl.isEmpty() || (!filter.extends.isEmpty() && !m_extends.contains(filter.extends))) return new ResultsStream(QStringLiteral("FlatpakStream-void"), {}); auto stream = new ResultsStream(QStringLiteral("FlatpakStream")); auto f = [this, stream, filter] () { QVector ret; - foreach(auto r, m_resources) { - const bool matchById = filter.search.compare(r->appstreamId(), Qt::CaseInsensitive) == 0; + for (auto r : qAsConst(m_resources)) { + const bool matchById = r->appstreamId().compare(filter.search, Qt::CaseInsensitive) == 0; if (r->type() == AbstractResource::Technical && filter.state != AbstractResource::Upgradeable && !matchById) { continue; } if (r->state() < filter.state) continue; if (!filter.extends.isEmpty() && !r->extends().contains(filter.extends)) continue; if (filter.search.isEmpty() || r->name().contains(filter.search, Qt::CaseInsensitive) || r->comment().contains(filter.search, Qt::CaseInsensitive) || matchById) { ret += r; } } auto f = [this](AbstractResource* l, AbstractResource* r) { return flatpakResourceLessThan(l,r); }; std::sort(ret.begin(), ret.end(), f); if (!ret.isEmpty()) Q_EMIT stream->resourcesFound(ret); stream->finish(); }; if (isFetching()) { connect(this, &FlatpakBackend::initialized, stream, f); } else { QTimer::singleShot(0, this, f); } return stream; } QVector FlatpakBackend::resourcesByAppstreamName(const QString& name) const { QVector resources; foreach(FlatpakResource* res, m_resources) { if (QString::compare(res->appstreamId(), name, Qt::CaseInsensitive)==0 || QString::compare(res->appstreamId(), name + QLatin1String(".desktop"), Qt::CaseInsensitive)==0) resources << res; } auto f = [this](AbstractResource* l, AbstractResource* r) { return flatpakResourceLessThan(l, r); }; std::sort(resources.begin(), resources.end(), f); return resources; } ResultsStream * FlatpakBackend::findResourceByPackageName(const QUrl &url) { if (url.scheme() == QLatin1String("appstream")) { - const auto appstreamId = AppStreamUtils::appstreamId(url); - if (appstreamId.isEmpty()) + const auto appstreamIds = AppStreamUtils::appstreamIds(url); + if (appstreamIds.isEmpty()) Q_EMIT passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); else { auto stream = new ResultsStream(QStringLiteral("FlatpakStream")); - auto f = [this, stream, appstreamId] () { - const auto resources = resourcesByAppstreamName(appstreamId); + auto f = [this, stream, appstreamIds] () { + const auto resources = kAppend>(appstreamIds, [this] (const QString appstreamId) { return resourcesByAppstreamName(appstreamId); }); if (!resources.isEmpty()) Q_EMIT stream->resourcesFound(resources); stream->finish(); }; if (isFetching()) { connect(this, &FlatpakBackend::initialized, stream, f); } else { QTimer::singleShot(0, this, f); } return stream; } } return new ResultsStream(QStringLiteral("FlatpakStream-packageName-void"), {}); } AbstractBackendUpdater * FlatpakBackend::backendUpdater() const { return m_updater; } AbstractReviewsBackend * FlatpakBackend::reviewsBackend() const { return m_reviews.data(); } Transaction* FlatpakBackend::installApplication(AbstractResource *app, const AddonList &addons) { Q_UNUSED(addons); FlatpakResource *resource = qobject_cast(app); if (resource->resourceType() == FlatpakResource::Source) { // Let source backend handle this FlatpakRemote *remote = m_sources->installSource(resource); if (remote) { resource->setState(AbstractResource::Installed); m_refreshAppstreamMetadataJobs++; // Make sure we update appstream metadata first // FIXME we have to let flatpak to return the remote as the one created by FlatpakSourcesBackend will not have appstream directory refreshAppstreamMetadata(preferredInstallation(), flatpak_installation_get_remote_by_name(preferredInstallation(), flatpak_remote_get_name(remote), nullptr, nullptr)); } return nullptr; } FlatpakJobTransaction *transaction = new FlatpakJobTransaction(resource, Transaction::InstallRole); connect(transaction, &FlatpakJobTransaction::statusChanged, this, [this, resource] (Transaction::Status status) { if (status == Transaction::Status::DoneStatus) { FlatpakInstallation *installation = resource->installation(); updateAppState(installation, resource); } }); return transaction; } Transaction* FlatpakBackend::installApplication(AbstractResource *app) { return installApplication(app, {}); } Transaction* FlatpakBackend::removeApplication(AbstractResource *app) { FlatpakResource *resource = qobject_cast(app); if (resource->resourceType() == FlatpakResource::Source) { // Let source backend handle this if (m_sources->removeSource(resource->flatpakName())) { resource->setState(AbstractResource::None); } return nullptr; } FlatpakInstallation *installation = resource->installation(); FlatpakJobTransaction *transaction = new FlatpakJobTransaction(resource, Transaction::RemoveRole); connect(transaction, &FlatpakJobTransaction::statusChanged, this, [this, installation, resource] (Transaction::Status status) { if (status == Transaction::Status::DoneStatus) { updateAppSize(installation, resource); } }); return transaction; } void FlatpakBackend::checkForUpdates() { for (auto installation : qAsConst(m_installations)) { // Load local updates, comparing current and latest commit loadLocalUpdates(installation); // Load updates from remote repositories loadRemoteUpdates(installation); } } QString FlatpakBackend::displayName() const { return QStringLiteral("Flatpak"); } #include "FlatpakBackend.moc" diff --git a/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp b/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp index ccf52f59..fe72161a 100644 --- a/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp +++ b/libdiscover/backends/PackageKitBackend/PackageKitBackend.cpp @@ -1,743 +1,746 @@ /*************************************************************************** * Copyright © 2012 Aleix Pol Gonzalez * * Copyright © 2013 Lukas Appelhans * * * * 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 . * ***************************************************************************/ #include "PackageKitBackend.h" #include "PackageKitSourcesBackend.h" #include "PackageKitResource.h" #include "PackageKitUpdater.h" #include "AppPackageKitResource.h" #include "PKTransaction.h" #include "LocalFilePKResource.h" #include "TransactionSet.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils.h" #include "config-paths.h" #include "libdiscover_backend_debug.h" DISCOVER_BACKEND_PLUGIN(PackageKitBackend) template static void setWhenAvailable(const QDBusPendingReply& pending, W func, QObject* parent) { QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(pending, parent); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, parent, [func](QDBusPendingCallWatcher* watcher) { watcher->deleteLater(); QDBusPendingReply reply = *watcher; func(reply.value()); }); } QString PackageKitBackend::locateService(const QString &filename) { return QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("applications/")+filename); } PackageKitBackend::PackageKitBackend(QObject* parent) : AbstractResourcesBackend(parent) , m_appdata(new AppStream::Pool) , m_updater(new PackageKitUpdater(this)) , m_refresher(nullptr) , m_isFetching(0) , m_reviews(AppStreamIntegration::global()->reviews()) { QTimer* t = new QTimer(this); connect(t, &QTimer::timeout, this, &PackageKitBackend::checkForUpdates); t->setInterval(60 * 60 * 1000); t->setSingleShot(false); t->start(); m_delayedDetailsFetch.setSingleShot(true); m_delayedDetailsFetch.setInterval(100); connect(&m_delayedDetailsFetch, &QTimer::timeout, this, &PackageKitBackend::performDetailsFetch); connect(PackageKit::Daemon::global(), &PackageKit::Daemon::restartScheduled, m_updater, &PackageKitUpdater::enableNeedsReboot); connect(PackageKit::Daemon::global(), &PackageKit::Daemon::isRunningChanged, this, &PackageKitBackend::checkDaemonRunning); connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, [this] { m_reviews->emitRatingFetched(this, kTransform>(m_packages.packages.values(), [] (AbstractResource* r) { return r; })); }); auto proxyWatch = new QFileSystemWatcher(this); proxyWatch->addPath(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1String("/kioslaverc")); connect(proxyWatch, &QFileSystemWatcher::fileChanged, this, [this](){ KProtocolManager::reparseConfiguration(); updateProxy(); }); SourcesModel::global()->addSourcesBackend(new PackageKitSourcesBackend(this)); reloadPackageList(); acquireFetching(true); setWhenAvailable(PackageKit::Daemon::getTimeSinceAction(PackageKit::Transaction::RoleRefreshCache), [this](uint timeSince) { if (timeSince > 3600) checkForUpdates(); else fetchUpdates(); acquireFetching(false); }, this); } PackageKitBackend::~PackageKitBackend() { m_threadPool.waitForDone(200); m_threadPool.clear(); } void PackageKitBackend::updateProxy() { if (PackageKit::Daemon::isRunning()) { static bool everHad = KProtocolManager::useProxy(); if (!everHad && !KProtocolManager::useProxy()) return; everHad = KProtocolManager::useProxy(); PackageKit::Daemon::global()->setProxy(KProtocolManager::proxyFor(QStringLiteral("http")), KProtocolManager::proxyFor(QStringLiteral("https")), KProtocolManager::proxyFor(QStringLiteral("ftp")), KProtocolManager::proxyFor(QStringLiteral("socks")), {}, {}); } } bool PackageKitBackend::isFetching() const { return m_isFetching; } void PackageKitBackend::acquireFetching(bool f) { if (f) m_isFetching++; else m_isFetching--; if ((!f && m_isFetching==0) || (f && m_isFetching==1)) { emit fetchingChanged(); } Q_ASSERT(m_isFetching>=0); } struct DelayedAppStreamLoad { QVector components; QHash missingComponents; bool correct = true; }; static DelayedAppStreamLoad loadAppStream(AppStream::Pool* appdata) { DelayedAppStreamLoad ret; QString error; ret.correct = appdata->load(&error); if (!ret.correct) { qWarning() << "Could not open the AppStream metadata pool" << error; } const auto components = appdata->components(); ret.components.reserve(components.size()); foreach(const AppStream::Component& component, components) { if (component.kind() == AppStream::Component::KindFirmware) continue; const auto pkgNames = component.packageNames(); if (pkgNames.isEmpty()) { const auto entries = component.launchable(AppStream::Launchable::KindDesktopId).entries(); if (component.kind() == AppStream::Component::KindDesktopApp && !entries.isEmpty()) { const QString file = PackageKitBackend::locateService(entries.first()); if (!file.isEmpty()) { ret.missingComponents[file] = component; } } } else { ret.components << component; } } return ret; } void PackageKitBackend::reloadPackageList() { acquireFetching(true); if (m_refresher) { disconnect(m_refresher.data(), &PackageKit::Transaction::finished, this, &PackageKitBackend::reloadPackageList); } m_appdata.reset(new AppStream::Pool); auto fw = new QFutureWatcher(this); connect(fw, &QFutureWatcher::finished, this, [this, fw]() { const auto data = fw->result(); fw->deleteLater(); if (!data.correct && m_packages.packages.isEmpty()) { QTimer::singleShot(0, this, [this]() { Q_EMIT passiveMessage(i18n("Please make sure that Appstream is properly set up on your system")); }); } QStringList neededPackages; neededPackages.reserve(data.components.size()); for (const auto &component: data.components) { const auto pkgNames = component.packageNames(); addComponent(component, pkgNames); neededPackages << pkgNames; } for (auto it = data.missingComponents.constBegin(), itEnd = data.missingComponents.constEnd(); it != itEnd; ++it) { acquireFetching(true); const auto file = it.key(); const auto component = it.value(); auto trans = PackageKit::Daemon::searchFiles(file); connect(trans, &PackageKit::Transaction::package, this, [trans](PackageKit::Transaction::Info info, const QString &packageID){ if (info == PackageKit::Transaction::InfoInstalled) trans->setProperty("installedPackage", packageID); }); connect(trans, &PackageKit::Transaction::finished, this, [this, trans, component](PackageKit::Transaction::Exit status) { const auto pkgidVal = trans->property("installedPackage"); if (status == PackageKit::Transaction::ExitSuccess && !pkgidVal.isNull()) { const auto pkgid = pkgidVal.toString(); auto res = addComponent(component, {PackageKit::Daemon::packageName(pkgid)}); res->clearPackageIds(); res->addPackageId(PackageKit::Transaction::InfoInstalled, pkgid, true); } acquireFetching(false); }); } if (!neededPackages.isEmpty()) { neededPackages.removeDuplicates(); resolvePackages(neededPackages); } else { qCDebug(LIBDISCOVER_BACKEND_LOG) << "empty appstream db"; if (PackageKit::Daemon::backendName() == QLatin1String("aptcc") || PackageKit::Daemon::backendName().isEmpty()) { checkForUpdates(); } } acquireFetching(false); if (!m_appstreamInitialized) { m_appstreamInitialized = true; Q_EMIT loadedAppStream(); } }); fw->setFuture(QtConcurrent::run(&m_threadPool, &loadAppStream, m_appdata.get())); } AppPackageKitResource* PackageKitBackend::addComponent(const AppStream::Component& component, const QStringList& pkgNames) { Q_ASSERT(isFetching()); Q_ASSERT(!pkgNames.isEmpty()); AppPackageKitResource* res = qobject_cast(m_packages.packages[component.id()]); if (!res) { res = new AppPackageKitResource(component, pkgNames.at(0), this); m_packages.packages[component.id()] = res; } else { res->clearPackageIds(); } foreach (const QString& pkg, pkgNames) { m_packages.packageToApp[pkg] += component.id(); } foreach (const QString& pkg, component.extends()) { m_packages.extendedBy[pkg] += res; } return res; } void PackageKitBackend::resolvePackages(const QStringList &packageNames) { PackageKit::Transaction * tArch = PackageKit::Daemon::resolve(packageNames, PackageKit::Transaction::FilterArch); connect(tArch, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageArch); connect(tArch, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); PackageKit::Transaction * tNotArch = PackageKit::Daemon::resolve(packageNames, PackageKit::Transaction::FilterNotArch); connect(tNotArch, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageNotArch); connect(tNotArch, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); TransactionSet* merge = new TransactionSet({tArch, tNotArch}); connect(merge, &TransactionSet::allFinished, this, &PackageKitBackend::getPackagesFinished); } void PackageKitBackend::fetchUpdates() { if (m_updater->isProgressing()) return; m_getUpdatesTransaction = PackageKit::Daemon::getUpdates(); connect(m_getUpdatesTransaction, &PackageKit::Transaction::finished, this, &PackageKitBackend::getUpdatesFinished); connect(m_getUpdatesTransaction, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageToUpdate); connect(m_getUpdatesTransaction, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); connect(m_getUpdatesTransaction, &PackageKit::Transaction::percentageChanged, this, &PackageKitBackend::fetchingUpdatesProgressChanged); m_updatesPackageId.clear(); m_hasSecurityUpdates = false; m_updater->setProgressing(true); fetchingUpdatesProgressChanged(); } void PackageKitBackend::addPackageArch(PackageKit::Transaction::Info info, const QString& packageId, const QString& summary) { addPackage(info, packageId, summary, true); } void PackageKitBackend::addPackageNotArch(PackageKit::Transaction::Info info, const QString& packageId, const QString& summary) { addPackage(info, packageId, summary, false); } void PackageKitBackend::addPackage(PackageKit::Transaction::Info info, const QString &packageId, const QString &summary, bool arch) { if(PackageKit::Daemon::packageArch(packageId) == QLatin1String("source")) { // We do not add source packages, they make little sense here. If source is needed, // we are going to have to consider that in some other way, some other time // If we do not ignore them here, e.g. openSuse entirely fails at installing applications return; } const QString packageName = PackageKit::Daemon::packageName(packageId); QSet r = resourcesByPackageName(packageName); if (r.isEmpty()) { auto pk = new PackageKitResource(packageName, summary, this); r = { pk }; m_packagesToAdd.insert(pk); } foreach(auto res, r) static_cast(res)->addPackageId(info, packageId, arch); } void PackageKitBackend::getPackagesFinished() { for(auto it = m_packages.packages.cbegin(); it != m_packages.packages.cend(); ++it) { auto pkr = qobject_cast(it.value()); if (pkr->packages().isEmpty()) { // qWarning() << "Failed to find package for" << it.key(); m_packagesToDelete += pkr; } } includePackagesToAdd(); } void PackageKitBackend::includePackagesToAdd() { if (m_packagesToAdd.isEmpty() && m_packagesToDelete.isEmpty()) return; acquireFetching(true); foreach(PackageKitResource* res, m_packagesToAdd) { m_packages.packages[res->packageName()] = res; } foreach(PackageKitResource* res, m_packagesToDelete) { const auto pkgs = m_packages.packageToApp.value(res->packageName(), {res->packageName()}); foreach(const auto &pkg, pkgs) { auto res = m_packages.packages.take(pkg); if (res) { if (AppPackageKitResource* ares = qobject_cast(res)) { const auto extends = res->extends(); for(const auto &ext: extends) m_packages.extendedBy[ext].removeAll(ares); } emit resourceRemoved(res); res->deleteLater(); } } } m_packagesToAdd.clear(); m_packagesToDelete.clear(); acquireFetching(false); } void PackageKitBackend::transactionError(PackageKit::Transaction::Error, const QString& message) { qWarning() << "Transaction error: " << message << sender(); Q_EMIT passiveMessage(message); } void PackageKitBackend::packageDetails(const PackageKit::Details& details) { const QSet resources = resourcesByPackageName(PackageKit::Daemon::packageName(details.packageId())); if (resources.isEmpty()) qWarning() << "couldn't find package for" << details.packageId(); foreach(AbstractResource* res, resources) { qobject_cast(res)->setDetails(details); } } QSet PackageKitBackend::resourcesByPackageName(const QString& name) const { return resourcesByPackageNames>({name}); } template T PackageKitBackend::resourcesByPackageNames(const QStringList &pkgnames) const { T ret; ret.reserve(pkgnames.size()); for(const QString &name : pkgnames) { const QStringList names = m_packages.packageToApp.value(name, QStringList(name)); foreach(const QString& name, names) { AbstractResource* res = m_packages.packages.value(name); if (res) ret += res; } } return ret; } void PackageKitBackend::checkForUpdates() { if (PackageKit::Daemon::global()->offline()->updateTriggered()) { qCDebug(LIBDISCOVER_BACKEND_LOG) << "Won't be checking for updates again, the system needs a reboot to apply the fetched offline updates."; return; } if (!m_refresher) { acquireFetching(true); m_refresher = PackageKit::Daemon::refreshCache(false); connect(m_refresher.data(), &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); connect(m_refresher.data(), &PackageKit::Transaction::finished, this, [this]() { m_refresher = nullptr; fetchUpdates(); acquireFetching(false); }); } else { qWarning() << "already resetting"; } } QList PackageKitBackend::componentsById(const QString& id) const { Q_ASSERT(m_appstreamInitialized); return m_appdata->componentsById(id); } ResultsStream* PackageKitBackend::search(const AbstractResourcesBackend::Filters& filter) { if (!filter.resourceUrl.isEmpty()) { return findResourceByPackageName(filter.resourceUrl); } else if (!filter.extends.isEmpty()) { const auto ext = kTransform>(m_packages.extendedBy.value(filter.extends), [](AppPackageKitResource* a){ return a; }); return new ResultsStream(QStringLiteral("PackageKitStream-extends"), ext); } else if (filter.search.isEmpty()) { return new ResultsStream(QStringLiteral("PackageKitStream-all"), kFilter>(m_packages.packages, [](AbstractResource* res) { return res->type() != AbstractResource::Technical && !qobject_cast(res)->extendsItself(); })); } else { auto stream = new ResultsStream(QStringLiteral("PackageKitStream-search")); const auto f = [this, stream, filter] () { const QList components = m_appdata->search(filter.search); const QStringList ids = kTransform(components, [](const AppStream::Component& comp) { return comp.id(); }); if (!ids.isEmpty()) { const auto resources = kFilter>(resourcesByPackageNames>(ids), [](AbstractResource* res){ return !qobject_cast(res)->extendsItself(); }); Q_EMIT stream->resourcesFound(resources); } PackageKit::Transaction * tArch = PackageKit::Daemon::resolve(filter.search, PackageKit::Transaction::FilterArch); connect(tArch, &PackageKit::Transaction::package, this, &PackageKitBackend::addPackageArch); connect(tArch, &PackageKit::Transaction::package, stream, [stream](PackageKit::Transaction::Info /*info*/, const QString &packageId){ stream->setProperty("packageId", packageId); }); connect(tArch, &PackageKit::Transaction::finished, stream, [stream, ids, this](PackageKit::Transaction::Exit status) { getPackagesFinished(); if (status == PackageKit::Transaction::Exit::ExitSuccess) { const auto packageId = stream->property("packageId"); if (!packageId.isNull()) { const auto res = resourcesByPackageNames>({PackageKit::Daemon::packageName(packageId.toString())}); Q_EMIT stream->resourcesFound(kFilter>(res, [ids](AbstractResource* res){ return !ids.contains(res->appstreamId()); })); } } stream->finish(); }, Qt::QueuedConnection); }; if (!m_appstreamInitialized) { connect(this, &PackageKitBackend::loadedAppStream, stream, f); } else { QTimer::singleShot(0, this, f); } return stream; } } ResultsStream * PackageKitBackend::findResourceByPackageName(const QUrl& url) { if (url.isLocalFile()) { QMimeDatabase db; const auto mime = db.mimeTypeForUrl(url); if ( mime.inherits(QStringLiteral("application/vnd.debian.binary-package")) || mime.inherits(QStringLiteral("application/x-rpm")) || mime.inherits(QStringLiteral("application/x-tar")) || mime.inherits(QStringLiteral("application/x-xz-compressed-tar")) ) { return new ResultsStream(QStringLiteral("PackageKitStream-localpkg"), QVector{ new LocalFilePKResource(url, this)}); } } else if (url.scheme() == QLatin1String("appstream")) { static const QMap deprecatedAppstreamIds = { { QStringLiteral("org.kde.krita.desktop"), QStringLiteral("krita.desktop") }, { QStringLiteral("org.kde.digikam.desktop"), QStringLiteral("digikam.desktop") }, { QStringLiteral("org.kde.ktorrent.desktop"), QStringLiteral("ktorrent.desktop") }, { QStringLiteral("org.kde.gcompris.desktop"), QStringLiteral("gcompris.desktop") }, { QStringLiteral("org.kde.kmymoney.desktop"), QStringLiteral("kmymoney.desktop") }, { QStringLiteral("org.kde.kolourpaint.desktop"), QStringLiteral("kolourpaint.desktop") }, { QStringLiteral("org.blender.blender.desktop"), QStringLiteral("blender.desktop") }, }; - const auto appstreamId = AppStreamUtils::appstreamId(url); - if (appstreamId.isEmpty()) + const auto appstreamIds = AppStreamUtils::appstreamIds(url); + if (appstreamIds.isEmpty()) Q_EMIT passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); else { auto stream = new ResultsStream(QStringLiteral("PackageKitStream-appstream-url")); - const auto f = [this, appstreamId, stream] () { + const auto f = [this, appstreamIds, stream] () { AbstractResource* pkg = nullptr; - const auto deprecatedHost = deprecatedAppstreamIds.value(appstreamId); //try this as fallback + + const QStringList allAppStreamIds = appstreamIds + deprecatedAppstreamIds.values(appstreamIds.first()); for (auto it = m_packages.packages.constBegin(), itEnd = m_packages.packages.constEnd(); it != itEnd; ++it) { - if (it.key().compare(appstreamId, Qt::CaseInsensitive) == 0 - || it.key().compare(deprecatedHost, Qt::CaseInsensitive) == 0 - || (appstreamId.endsWith(QLatin1String(".desktop")) && appstreamId.compare(it.key()+QLatin1String(".desktop"), Qt::CaseInsensitive) == 0)) { + const bool matches = kContains(allAppStreamIds, [&it] (const QString& id) { + return it.key().compare(id, Qt::CaseInsensitive) == 0 || + (id.endsWith(QLatin1String(".desktop")) && id.compare(it.key()+QLatin1String(".desktop"), Qt::CaseInsensitive) == 0); + }); + if (matches) { pkg = it.value(); break; } } if (pkg) Q_EMIT stream->resourcesFound({pkg}); stream->finish(); // if (!pkg) // qCDebug(LIBDISCOVER_BACKEND_LOG) << "could not find" << host << deprecatedHost; }; if (!m_appstreamInitialized) { connect(this, &PackageKitBackend::loadedAppStream, stream, f); } else { QTimer::singleShot(0, this, f); } return stream; } } return new ResultsStream(QStringLiteral("PackageKitStream-unknown-url"), {}); } bool PackageKitBackend::hasSecurityUpdates() const { return m_hasSecurityUpdates; } int PackageKitBackend::updatesCount() const { if (PackageKit::Daemon::global()->offline()->updateTriggered()) return 0; int ret = 0; QSet packages; const auto toUpgrade = upgradeablePackages(); for(auto res: toUpgrade) { const auto packageName = res->packageName(); if (packages.contains(packageName)) { continue; } packages.insert(packageName); ret += 1; } return ret; } Transaction* PackageKitBackend::installApplication(AbstractResource* app, const AddonList& addons) { Transaction* t = nullptr; if(!addons.addonsToInstall().isEmpty()) { QVector appsToInstall = resourcesByPackageNames>(addons.addonsToInstall()); if(!app->isInstalled()) appsToInstall << app; t = new PKTransaction(appsToInstall, Transaction::ChangeAddonsRole); } else if (!app->isInstalled()) t = installApplication(app); if (!addons.addonsToRemove().isEmpty()) { const auto appsToRemove = resourcesByPackageNames>(addons.addonsToRemove()); t = new PKTransaction(appsToRemove, Transaction::RemoveRole); } return t; } Transaction* PackageKitBackend::installApplication(AbstractResource* app) { return new PKTransaction({app}, Transaction::InstallRole); } Transaction* PackageKitBackend::removeApplication(AbstractResource* app) { Q_ASSERT(!isFetching()); return new PKTransaction({app}, Transaction::RemoveRole); } QSet PackageKitBackend::upgradeablePackages() const { if (isFetching() || !m_packagesToAdd.isEmpty()) { return {}; } QSet ret; ret.reserve(m_updatesPackageId.size()); Q_FOREACH (const QString& pkgid, m_updatesPackageId) { const QString pkgname = PackageKit::Daemon::packageName(pkgid); const auto pkgs = resourcesByPackageName(pkgname); if (pkgs.isEmpty()) { qWarning() << "couldn't find resource for" << pkgid; } ret.unite(pkgs); } return kFilter>(ret, [] (AbstractResource* res) { return !static_cast(res)->extendsItself(); }); } void PackageKitBackend::addPackageToUpdate(PackageKit::Transaction::Info info, const QString& packageId, const QString& summary) { if (info == PackageKit::Transaction::InfoBlocked) { return; } if (info == PackageKit::Transaction::InfoRemoving || info == PackageKit::Transaction::InfoObsoleting) { // Don't try updating packages which need to be removed return; } if (info == PackageKit::Transaction::InfoSecurity) m_hasSecurityUpdates = true; m_updatesPackageId += packageId; addPackage(info, packageId, summary, true); } void PackageKitBackend::getUpdatesFinished(PackageKit::Transaction::Exit, uint) { if (!m_updatesPackageId.isEmpty()) { resolvePackages(kTransform(m_updatesPackageId, [](const QString &pkgid) { return PackageKit::Daemon::packageName(pkgid); })); fetchDetails(m_updatesPackageId); } m_updater->setProgressing(false); includePackagesToAdd(); emit updatesCountChanged(); } bool PackageKitBackend::isPackageNameUpgradeable(const PackageKitResource* res) const { return !upgradeablePackageId(res).isEmpty(); } QString PackageKitBackend::upgradeablePackageId(const PackageKitResource* res) const { QString name = res->packageName(); foreach (const QString& pkgid, m_updatesPackageId) { if (PackageKit::Daemon::packageName(pkgid) == name) return pkgid; } return QString(); } void PackageKitBackend::fetchDetails(const QSet& pkgid) { if (!m_delayedDetailsFetch.isActive()) { m_delayedDetailsFetch.start(); } m_packageNamesToFetchDetails += pkgid; } void PackageKitBackend::performDetailsFetch() { Q_ASSERT(!m_packageNamesToFetchDetails.isEmpty()); const auto ids = m_packageNamesToFetchDetails.values(); PackageKit::Transaction* transaction = PackageKit::Daemon::getDetails(ids); connect(transaction, &PackageKit::Transaction::details, this, &PackageKitBackend::packageDetails); connect(transaction, &PackageKit::Transaction::errorCode, this, &PackageKitBackend::transactionError); m_packageNamesToFetchDetails.clear(); } void PackageKitBackend::checkDaemonRunning() { if (!PackageKit::Daemon::isRunning()) { qWarning() << "PackageKit stopped running!"; } else updateProxy(); } AbstractBackendUpdater* PackageKitBackend::backendUpdater() const { return m_updater; } QVector PackageKitBackend::extendedBy(const QString& id) const { return m_packages.extendedBy[id]; } AbstractReviewsBackend* PackageKitBackend::reviewsBackend() const { return m_reviews.data(); } QString PackageKitBackend::displayName() const { return AppStreamIntegration::global()->osRelease()->prettyName(); } int PackageKitBackend::fetchingUpdatesProgress() const { if (!m_getUpdatesTransaction) return 0; if (m_getUpdatesTransaction->status() == PackageKit::Transaction::StatusWait || m_getUpdatesTransaction->status() == PackageKit::Transaction::StatusUnknown) { return m_getUpdatesTransaction->property("lastPercentage").toInt(); } int percentage = percentageWithStatus(m_getUpdatesTransaction->status(), m_getUpdatesTransaction->percentage()); m_getUpdatesTransaction->setProperty("lastPercentage", percentage); return percentage; } #include "PackageKitBackend.moc" diff --git a/libdiscover/backends/SnapBackend/SnapBackend.cpp b/libdiscover/backends/SnapBackend/SnapBackend.cpp index b0bcbf5c..3826d9ce 100644 --- a/libdiscover/backends/SnapBackend/SnapBackend.cpp +++ b/libdiscover/backends/SnapBackend/SnapBackend.cpp @@ -1,228 +1,249 @@ /*************************************************************************** * Copyright © 2013 Aleix Pol Gonzalez * * * * 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 . * ***************************************************************************/ #include "SnapBackend.h" #include "SnapTransaction.h" #include "SnapResource.h" #include "appstream/AppStreamIntegration.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils.h" DISCOVER_BACKEND_PLUGIN(SnapBackend) class SnapSourcesBackend : public AbstractSourcesBackend { public: explicit SnapSourcesBackend(AbstractResourcesBackend * parent) : AbstractSourcesBackend(parent), m_model(new QStandardItemModel(this)) { auto it = new QStandardItem(i18n("Snap")); it->setData(QStringLiteral("Snap"), IdRole); m_model->appendRow(it); } QAbstractItemModel* sources() override { return m_model; } bool addSource(const QString& /*id*/) override { return false; } bool removeSource(const QString& /*id*/) override { return false;} QString idDescription() override { return QStringLiteral("Snap"); } QVariantList actions() const override { return {}; } bool supportsAdding() const override { return false; } bool canMoveSources() const override { return false; } private: QStandardItemModel* const m_model; }; SnapBackend::SnapBackend(QObject* parent) : AbstractResourcesBackend(parent) , m_updater(new StandardBackendUpdater(this)) , m_reviews(AppStreamIntegration::global()->reviews()) { connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, [this] { m_reviews->emitRatingFetched(this, kTransform>(m_resources.values(), [] (AbstractResource* r) { return r; })); }); //make sure we populate the installed resources first refreshStates(); SourcesModel::global()->addSourcesBackend(new SnapSourcesBackend(this)); } SnapBackend::~SnapBackend() = default; int SnapBackend::updatesCount() const { return m_updater->updatesCount(); } static ResultsStream* voidStream() { return new ResultsStream(QStringLiteral("Snap-void"), {}); } ResultsStream * SnapBackend::search(const AbstractResourcesBackend::Filters& filters) { if (!filters.extends.isEmpty()) { return voidStream(); } else if (!filters.resourceUrl.isEmpty()) { return findResourceByPackageName(filters.resourceUrl); } else if (filters.category && filters.category->isAddons()) { return voidStream(); } else if (filters.state >= AbstractResource::Installed || filters.origin == QLatin1String("Snap")) { std::function&)> f = [filters](const QSharedPointer& s) { return filters.search.isEmpty() || s->name().contains(filters.search, Qt::CaseInsensitive) || s->description().contains(filters.search, Qt::CaseInsensitive); }; return populateWithFilter(m_client.getSnaps(), f); } else if (!filters.search.isEmpty()) { return populate(m_client.find(QSnapdClient::FindFlag::None, filters.search)); } return voidStream(); } ResultsStream * SnapBackend::findResourceByPackageName(const QUrl& search) { - Q_ASSERT(!search.host().isEmpty() || !AppStreamUtils::appstreamId(search).isEmpty()); + Q_ASSERT(!search.host().isEmpty() || !AppStreamUtils::appstreamIds(search).isEmpty()); return search.scheme() == QLatin1String("snap") ? populate(m_client.find(QSnapdClient::MatchName, search.host())) : #ifdef SNAP_FIND_COMMON_ID - search.scheme() == QLatin1String("appstream") ? populate(m_client.find(QSnapdClient::MatchCommonId, AppStreamUtils::appstreamId(search))) : + search.scheme() == QLatin1String("appstream") ? populate(kTransform>(AppStreamUtils::appstreamIds(search), + [this] (const QString &id) {return m_client.find(QSnapdClient::MatchCommonId, id); })) : #endif voidStream(); } template -ResultsStream* SnapBackend::populate(T* snaps) +ResultsStream* SnapBackend::populate(T* job) +{ + return populate(QVector{job}); +} + +template +ResultsStream* SnapBackend::populate(const QVector& jobs) { std::function&)> acceptAll = [](const QSharedPointer&){ return true; }; - return populateWithFilter(snaps, acceptAll); + return populateJobsWithFilter(jobs, acceptAll); } template ResultsStream* SnapBackend::populateWithFilter(T* job, std::function& s)>& filter) { - auto stream = new ResultsStream(QStringLiteral("Snap-populate")); + return populateJobsWithFilter({job}, filter); +} - connect(job, &T::complete, stream, [stream, this, job, filter]() { - if (job->error()) { - qDebug() << "error:" << job->error() << job->errorString(); - stream->finish(); - return; - } +template +ResultsStream* SnapBackend::populateJobsWithFilter(const QVector& jobs, std::function& s)>& filter) +{ + auto stream = new ResultsStream(QStringLiteral("Snap-populate")); + stream->setProperty("remaining", jobs.count()); + for(auto job : jobs) { + connect(job, &T::complete, stream, [stream, this, job, filter]() { + const int remaining = stream->property("remaining").toInt() - 1; + stream->setProperty("remaining", remaining); + + if (job->error()) { + qDebug() << "error:" << job->error() << job->errorString(); + if (remaining == 0) + stream->finish(); + return; + } - QVector ret; - QVector resources; - ret.reserve(job->snapCount()); - resources.reserve(job->snapCount()); - for (int i=0, c=job->snapCount(); i snap(job->snap(i)); - - if (!filter(snap)) - continue; - - const auto snapname = snap->name(); - SnapResource* res = m_resources.value(snapname); - if (!res) { - res = new SnapResource(snap, AbstractResource::None, this); - Q_ASSERT(res->packageName() == snapname); - resources += res; - } else { - res->setSnap(snap); + QVector ret; + QVector resources; + ret.reserve(job->snapCount()); + resources.reserve(job->snapCount()); + for (int i=0, c=job->snapCount(); i snap(job->snap(i)); + + if (!filter(snap)) + continue; + + const auto snapname = snap->name(); + SnapResource* res = m_resources.value(snapname); + if (!res) { + res = new SnapResource(snap, AbstractResource::None, this); + Q_ASSERT(res->packageName() == snapname); + resources += res; + } else { + res->setSnap(snap); + } + ret += res; } - ret += res; - } - foreach(SnapResource* res, resources) - m_resources[res->packageName()] = res; + foreach(SnapResource* res, resources) + m_resources[res->packageName()] = res; - if (!ret.isEmpty()) - Q_EMIT stream->resourcesFound(ret); - stream->finish(); - }); - job->runAsync(); + if (!ret.isEmpty()) + Q_EMIT stream->resourcesFound(ret); + + if (remaining == 0) + stream->finish(); + }); + job->runAsync(); + } return stream; } void SnapBackend::setFetching(bool fetching) { if (m_fetching != fetching) { m_fetching = fetching; Q_EMIT fetchingChanged(); } else { qWarning() << "fetching already on state" << fetching; } } AbstractBackendUpdater* SnapBackend::backendUpdater() const { return m_updater; } AbstractReviewsBackend* SnapBackend::reviewsBackend() const { return m_reviews.data(); } Transaction* SnapBackend::installApplication(AbstractResource* app, const AddonList& addons) { Q_ASSERT(addons.isEmpty()); return installApplication(app); } Transaction* SnapBackend::installApplication(AbstractResource* _app) { auto app = qobject_cast(_app); return new SnapTransaction(&m_client, app, Transaction::InstallRole, AbstractResource::Installed); } Transaction* SnapBackend::removeApplication(AbstractResource* _app) { auto app = qobject_cast(_app); return new SnapTransaction(&m_client, app, Transaction::RemoveRole, AbstractResource::None); } QString SnapBackend::displayName() const { return QStringLiteral("Snap"); } void SnapBackend::refreshStates() { auto ret = new StoredResultsStream({populate(m_client.getSnaps())}); connect(ret, &StoredResultsStream::finishedResources, this, [this] (const QVector& resources){ for (auto res: qAsConst(m_resources)) { if (resources.contains(res)) res->setState(AbstractResource::Installed); else res->setState(AbstractResource::None); } }); } #include "SnapBackend.moc" diff --git a/libdiscover/backends/SnapBackend/SnapBackend.h b/libdiscover/backends/SnapBackend/SnapBackend.h index a163e4a3..c4ffd3d9 100644 --- a/libdiscover/backends/SnapBackend/SnapBackend.h +++ b/libdiscover/backends/SnapBackend/SnapBackend.h @@ -1,76 +1,83 @@ /*************************************************************************** * Copyright © 2013 Aleix Pol Gonzalez * * * * 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 . * ***************************************************************************/ #ifndef SNAPBACKEND_H #define SNAPBACKEND_H #include #include #include +#include #include #include class OdrsReviewsBackend; class StandardBackendUpdater; class SnapResource; class SnapBackend : public AbstractResourcesBackend { Q_OBJECT public: explicit SnapBackend(QObject* parent = nullptr); ~SnapBackend() override; ResultsStream * search(const AbstractResourcesBackend::Filters & search) override; ResultsStream * findResourceByPackageName(const QUrl& search); QString displayName() const override; int updatesCount() const override; AbstractBackendUpdater* backendUpdater() const override; AbstractReviewsBackend* reviewsBackend() const override; bool isValid() const override { return m_valid; } 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; } void checkForUpdates() override {} bool hasApplications() const override { return true; } QSnapdClient* client() { return &m_client; } void refreshStates(); private: void setFetching(bool fetching); template ResultsStream* populateWithFilter(T* snaps, std::function&)>& filter); + template + ResultsStream* populateJobsWithFilter(const QVector& snaps, std::function&)>& filter); + template ResultsStream* populate(T* snaps); + template + ResultsStream* populate(const QVector& snaps); + QHash m_resources; StandardBackendUpdater* m_updater; QSharedPointer m_reviews; bool m_valid = true; bool m_fetching = false; QSnapdClient m_client; }; #endif // SNAPBACKEND_H diff --git a/libdiscover/utils.h b/libdiscover/utils.h index 58fd6843..6066a2a4 100644 --- a/libdiscover/utils.h +++ b/libdiscover/utils.h @@ -1,134 +1,145 @@ /*************************************************************************** * Copyright ?? 2012 Aleix Pol Gonzalez * * * * 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 . * ***************************************************************************/ #ifndef UTILS_H #define UTILS_H #include #include #include class OneTimeAction : public QObject { public: OneTimeAction(const std::function &func, QObject* parent) : QObject(parent), m_function(func) {} void trigger() { m_function(); deleteLater(); } private: std::function m_function; }; template static T kTransform(const Q &input, _UnaryOperation op) { T ret; ret.reserve(input.size()); for(const auto& v : input) { ret += op(v); } return ret; } +template +static T kAppend(const Q &input, _UnaryOperation op) +{ + T ret; + ret.reserve(input.size()); + for(const auto& v : input) { + ret.append(op(v)); + } + return ret; +} + template static T kFilter(const Q &input, _UnaryOperation op) { T ret; for(const auto& v : input) { if (op(v)) ret += v; } return ret; } template static int kIndexOf(const Q& list, W func) { int i = 0; for (auto it = list.constBegin(), itEnd = list.constEnd(); it!=itEnd; ++it) { if (func(*it)) return i; ++i; } return -1; } template static bool kContains(const Q& list, W func) -{ return kIndexOf(list, func) != -1; } +{ return std::any_of(list.begin(), list.end(), func); } template static QVector kSetToVector(const QSet & set) { QVector ret; ret.reserve(set.size()); for(auto &x: set) ret.append(x); return ret; } template static QSet kToSet(const QVector & set) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) return QSet(set.begin(), set.end()); #else QSet ret; ret.reserve(set.size()); for(auto &x: set) ret.insert(x); return ret; #endif } template static QSet kToSet(const QList & set) { #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) return QSet(set.begin(), set.end()); #else return set.toSet(); #endif } class ElapsedDebug : private QElapsedTimer { public: ElapsedDebug(const QString &name = QStringLiteral("")) : m_name(name) { start(); } ~ElapsedDebug() { qDebug("elapsed %s: %lld!", m_name.toUtf8().constData(), elapsed()); } void step(const QString &step) { qDebug("step %s(%s): %lld!", m_name.toUtf8().constData(), qPrintable(step), elapsed()); } QString m_name; }; class CallOnDestroy : public QObject { public: CallOnDestroy(std::function f) : m_func(std::move(f)) {} ~CallOnDestroy() { m_func(); } private: std::function m_func; }; #endif