diff --git a/discover/qml/ApplicationPage.qml b/discover/qml/ApplicationPage.qml index e87608cc..a688bd58 100644 --- a/discover/qml/ApplicationPage.qml +++ b/discover/qml/ApplicationPage.qml @@ -1,445 +1,445 @@ /* * Copyright (C) 2012 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library/Lesser General Public License * version 2, or (at your option) any later version, as published by the * Free Software Foundation * * 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 Library/Lesser General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.5 import QtQuick.Controls 2.3 import QtQuick.Window 2.1 import QtQuick.Layouts 1.1 import org.kde.discover 2.0 import org.kde.discover.app 1.0 import org.kde.kirigami 2.1 as Kirigami import "navigation.js" as Navigation DiscoverPage { id: appInfo property QtObject application: null readonly property int visibleReviews: 3 clip: true background: Rectangle { color: Kirigami.Theme.viewBackgroundColor } ReviewsPage { id: reviewsSheet model: ReviewsModel { id: reviewsModel resource: appInfo.application } } contextualActions: [originsMenuAction] ActionGroup { id: sourcesGroup exclusive: true } Kirigami.Action { id: originsMenuAction text: i18n("Sources") visible: children.length>1 readonly property var r0: Instantiator { model: ResourcesProxyModel { id: alternativeResourcesModel allBackends: true resourcesUrl: appInfo.application.url } delegate: Action { ActionGroup.group: sourcesGroup text: displayOrigin icon.name: sourceIcon checked: appInfo.application == model.application onTriggered: if(index>=0) { var res = model.application console.assert(res) window.stack.pop() Navigation.openApplication(res) } } onObjectAdded: originsMenuAction.children.push(object) } } actions { main: appbutton.action right: Kirigami.Action { visible: application.isInstalled && application.canExecute && !appbutton.isActive text: application.executeLabel icon.name: "media-playback-start" onTriggered: application.invokeApplication() } } InstallApplicationButton { id: appbutton Layout.rightMargin: Kirigami.Units.smallSpacing application: appInfo.application visible: false } leftPadding: Kirigami.Units.largeSpacing * (applicationWindow().wideScreen ? 2 : 1) rightPadding: Kirigami.Units.largeSpacing * (applicationWindow().wideScreen ? 2 : 1) // Icon, name, caption, screenshots, description and reviews ColumnLayout { spacing: 0 RowLayout { Kirigami.Icon { Layout.preferredHeight: 80 Layout.preferredWidth: 80 source: appInfo.application.icon Layout.rightMargin: Kirigami.Units.smallSpacing * 2 } ColumnLayout { spacing: 0 Kirigami.Heading { level: 1 text: appInfo.application.name lineHeight: 1.0 maximumLineCount: 1 elide: Text.ElideRight Layout.fillWidth: true Layout.alignment: Text.AlignBottom } RowLayout { spacing: Kirigami.Units.largeSpacing Rating { rating: appInfo.application.rating ? appInfo.application.rating.sortableRating : 0 starSize: summary.font.pointSize } Label { text: appInfo.application.rating ? i18np("%1 rating", "%1 ratings", appInfo.application.rating.ratingCount) : i18n("No ratings yet") opacity: 0.5 } } Kirigami.Heading { id: summary level: 4 text: appInfo.application.comment maximumLineCount: 2 lineHeight: lineCount > 1 ? 0.75 : 1.2 elide: Text.ElideRight Layout.fillWidth: true Layout.alignment: Qt.AlignTop } } Layout.bottomMargin: Kirigami.Units.largeSpacing } ApplicationScreenshots { Layout.fillWidth: true visible: count > 0 resource: appInfo.application ScrollBar.horizontal: screenshotsScrollbar } ScrollBar { id: screenshotsScrollbar Layout.fillWidth: true } Label { Layout.topMargin: Kirigami.Units.largeSpacing Layout.fillWidth: true wrapMode: Text.WordWrap text: appInfo.application.longDescription } Kirigami.Heading { Layout.topMargin: Kirigami.Units.largeSpacing text: i18n("What's New") level: 2 visible: changelogLabel.text.length > 0 } Rectangle { color: Kirigami.Theme.linkColor Layout.fillWidth: true height: 1 visible: changelogLabel.text.length > 0 } Label { id: changelogLabel Layout.topMargin: Kirigami.Units.largeSpacing Layout.fillWidth: true wrapMode: Text.WordWrap Component.onCompleted: appInfo.application.fetchChangelog() Connections { target: appInfo.application onChangelogFetched: { changelogLabel.text = changelog } } } LinkButton { id: addonsButton text: i18n("Addons") visible: addonsView.containsAddons onClicked: addonsView.sheetOpen = true } RowLayout { Layout.topMargin: Kirigami.Units.largeSpacing Layout.fillWidth: true Kirigami.Heading { Layout.fillWidth: true text: i18n("Reviews") Layout.alignment: Qt.AlignLeft | Qt.AlignBottom level: 2 visible: rep.count > 0 } LinkButton { visible: reviewsModel.count > visibleReviews text: i18np("Show %1 review...", "Show all %1 reviews...", reviewsModel.count) Layout.alignment: Qt.AlignRight | Qt.AlignBottom onClicked: { reviewsSheet.open() } } } Rectangle { color: Kirigami.Theme.linkColor Layout.fillWidth: true height: 1 visible: rep.count > 0 } Repeater { id: rep model: PaginateModel { sourceModel: reviewsSheet.model pageSize: visibleReviews } delegate: ReviewDelegate { Layout.topMargin: Kirigami.Units.largeSpacing separator: false compact: true Layout.bottomMargin: Kirigami.Units.largeSpacing } } LinkButton { function writeReviewText() { if (appInfo.application.isInstalled) { if (reviewsModel.count > 0) { return i18n("Write a review!") } else { return i18n("Be the first to write a review!") } // App not installed } else { if (reviewsModel.count > 0) { return i18n("Install this app to write a review!") } else { return i18n("Install this app and be the first to write a review!") } } } text: writeReviewText() Layout.alignment: Qt.AlignCenter onClicked: reviewsSheet.openReviewDialog() enabled: appInfo.application.isInstalled visible: reviewsModel.backend && reviewsModel.backend.isResourceSupported(appInfo.application) Layout.topMargin: Kirigami.Units.largeSpacing Layout.bottomMargin: Kirigami.Units.largeSpacing } Repeater { model: application.objects delegate: Loader { property QtObject resource: appInfo.application source: modelData } } Item { height: addonsButton.height width: 1 } // Details/metadata Rectangle { color: Kirigami.Theme.linkColor Layout.fillWidth: true height: 1 Layout.bottomMargin: Kirigami.Units.largeSpacing } GridLayout { rowSpacing: 0 columns: 2 // Category row Label { visible: categoryLabel.visible Layout.alignment: Qt.AlignRight text: i18n("Category:") } Label { id: categoryLabel visible: text.length > 0 Layout.fillWidth: true elide: Text.ElideRight text: appInfo.application.categoryDisplay } // Version row Label { visible: versionLabel.visible Layout.alignment: Qt.AlignRight text: i18n("Version:") } Label { readonly property string version: appInfo.application.isInstalled ? appInfo.application.installedVersion : appInfo.application.availableVersion - readonly property string releaseDate: appInfo.application.releaseDate.toDateString() + readonly property string releaseDate: appInfo.application.releaseDate.toLocaleString() function versionString() { if (version.length == 0) { return "" } else { if (releaseDate.length > 0) { return i18n("%1, released on %2", version, releaseDate) } else { return version } } } id: versionLabel visible: text.length > 0 Layout.fillWidth: true elide: Text.ElideRight text: versionString() } // Author row Label { Layout.alignment: Qt.AlignRight text: i18n("Author:") visible: authorLabel.visible } Label { id: authorLabel Layout.fillWidth: true elide: Text.ElideRight visible: text.length>0 text: appInfo.application.author } // Size row Label { Layout.alignment: Qt.AlignRight text: i18n("Size:") } Label { Layout.fillWidth: true elide: Text.ElideRight text: appInfo.application.sizeDescription } // Source row Label { Layout.alignment: Qt.AlignRight text: i18n("Source:") } Label { Layout.fillWidth: true horizontalAlignment: Text.AlignLeft text: appInfo.application.displayOrigin elide: Text.ElideRight } // License row Label { Layout.alignment: Qt.AlignRight text: i18n("License:") visible: appInfo.application.license.length>0 } UrlButton { Layout.fillWidth: true horizontalAlignment: Text.AlignLeft // tooltip: i18n("See full license terms") text: appInfo.application.license url: "https://spdx.org/licenses/" + appInfo.application.license + ".html#licenseText" } // Homepage row Label { visible: homepageLink.visible Layout.alignment: Qt.AlignRight text: i18n("Homepage:") } UrlButton { id: homepageLink url: application.homepage Layout.fillWidth: true horizontalAlignment: Text.AlignLeft } // "User Guide" row Label { visible: docsLink.visible Layout.alignment: Qt.AlignRight text: i18n("User Guide:") } UrlButton { id: docsLink url: application.helpURL Layout.fillWidth: true horizontalAlignment: Text.AlignLeft } // Donate row Label { visible: donationLink.visible Layout.alignment: Qt.AlignRight text: i18n("Donate:") } UrlButton { id: donationLink url: application.donationURL Layout.fillWidth: true horizontalAlignment: Text.AlignLeft } // "Report a Problem" row Label { visible: bugLink.visible Layout.alignment: Qt.AlignRight text: i18n("Report a Problem:") } UrlButton { id: bugLink url: application.bugURL Layout.fillWidth: true horizontalAlignment: Text.AlignLeft } } } readonly property var addons: AddonsView { id: addonsView application: appInfo.application parent: overlay } } diff --git a/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp index 593e22f3..f2d246f5 100644 --- a/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp +++ b/libdiscover/backends/FlatpakBackend/FlatpakBackend.cpp @@ -1,1280 +1,1279 @@ /*************************************************************************** * 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 DISCOVER_BACKEND_PLUGIN(FlatpakBackend) static FlatpakResource::Id idForInstalledRef(FlatpakInstallation *installation, FlatpakInstalledRef *ref) { const FlatpakResource::ResourceType appType = flatpak_ref_get_kind(FLATPAK_REF(ref)) == FLATPAK_REF_KIND_APP ? FlatpakResource::DesktopApp : FlatpakResource::Runtime; const QString name = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))); const QString appId = appType == FlatpakResource::DesktopApp ? QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))) + QStringLiteral(".desktop") : name; 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, &FlatpakBackend::announceRatingsReady); } FlatpakBackend::~FlatpakBackend() { g_cancellable_cancel(m_cancellable); m_threadPool.waitForDone(200); m_threadPool.clear(); for(auto inst : m_installations) g_object_unref(inst); g_object_unref(m_cancellable); } bool FlatpakBackend::isValid() const { return m_sources && !m_installations.isEmpty(); } void FlatpakBackend::announceRatingsReady() { emitRatingsReady(); const auto ids = m_reviews->appstreamIds().toSet(); foreach(AbstractResource* res, m_resources) { if (ids.contains(res->appstreamId())) { res->ratingFetched(); } } } class FlatpakFetchRemoteResourceJob : public QNetworkAccessManager { Q_OBJECT public: FlatpakFetchRemoteResourceJob(const QUrl &url, FlatpakBackend *backend) : QNetworkAccessManager(backend) , m_backend(backend) , m_url(url) { } void start() { auto replyGet = get(QNetworkRequest(m_url)); connect(replyGet, &QNetworkReply::finished, this, [this, 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]() { if (replyPut->error() == QNetworkReply::NoError) { auto res = m_backend->resourceForFile(fileUrl); if (res) { FlatpakResource *resource = qobject_cast(res); resource->setResourceFile(originalUrl); Q_EMIT jobFinished(true, resource); } else { qWarning() << "couldn't create resource from" << fileUrl.toLocalFile(); Q_EMIT jobFinished(false, nullptr); } } else { qWarning() << "couldn't save" << originalUrl << replyPut->errorString(); Q_EMIT jobFinished(false, nullptr); } }); }); } Q_SIGNALS: void jobFinished(bool success, FlatpakResource *resource); private: FlatpakBackend *m_backend; 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 { return m_resources.value(idForInstalledRef(flatpakInstallation, ref)); } FlatpakResource * FlatpakBackend::getRuntimeForApp(FlatpakResource *resource) const { FlatpakResource *runtime = nullptr; const auto runtimeInfo = resource->runtime().split(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(); 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(settings.value(QStringLiteral("Flatpak Ref/Name")).toString()); 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(settings.value(QStringLiteral("Flatpak Ref/Name")).toString()); 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); fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, installation, resource)); 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(); }); } 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(QStringLiteral("http://")) || gpgKey.startsWith(QStringLiteral("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::integrateRemote(FlatpakInstallation *flatpakInstallation, FlatpakRemote *remote) { Q_ASSERT(m_refreshAppstreamMetadataJobs != 0); m_refreshAppstreamMetadataJobs--; FlatpakSource source(remote); if (!source.isEnabled() || flatpak_remote_get_noenumerate(remote)) { 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(); return; } auto fw = new QFutureWatcher>(this); 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(); })); const auto sourceName = source.name(); acquireFetching(true); 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); } if (!m_refreshAppstreamMetadataJobs) { loadInstalledApps(); checkForUpdates(); } acquireFetching(false); fw->deleteLater(); }); } 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)); if (flatpak_ref_get_kind(FLATPAK_REF(ref)) == FLATPAK_REF_KIND_RUNTIME) { continue; } const auto res = getAppForInstalledRef(flatpakInstallation, ref); if (res) { res->setState(AbstractResource::Installed); continue; } const auto name = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))); 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) { qWarning() << "Failed to parse appstream metadata: " << error << fnDesktop; continue; } auto component = metadata.component(); component.setId(name + QLatin1String(".desktop")); FlatpakResource *resource = new FlatpakResource(component, 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); 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; })); connect(fw, &QFutureWatcher::finished, this, [this, installation, fw](){ auto refs = fw->result(); onFetchUpdatesFinished(installation, refs); fw->deleteLater(); }); } 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); } } } 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 qWarning() << "Failed to refresh appstream metadata for " << flatpak_remote_get_name(m_remote) << ": " << (localError ? localError->message : ""); Q_EMIT jobRefreshAppstreamMetadataFailed(); } else { Q_EMIT jobRefreshAppstreamMetadataFinished(m_installation, m_remote); } } Q_SIGNALS: void jobRefreshAppstreamMetadataFailed(); 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, [this] () { m_refreshAppstreamMetadataJobs--; }); connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFinished, this, &FlatpakBackend::integrateRemote); 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) { g_autoptr(GFile) installationPath = nullptr; if (resource->resourceType() != FlatpakResource::DesktopApp) { return true; } installationPath = flatpak_installation_get_path(flatpakInstallation); const QString path = QString::fromUtf8(g_file_get_path(installationPath)) + QStringLiteral("/app/%1/%2/%3/active/metadata").arg(resource->flatpakName()).arg(resource->arch()).arg(resource->branch()); if (QFile::exists(path)) { return updateAppMetadata(resource, path); } else { auto fw = new QFutureWatcher(this); fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, flatpakInstallation, resource)); connect(fw, &QFutureWatcher::finished, this, [this, flatpakInstallation, resource, fw]() { const auto metadata = fw->result(); if (!metadata.isEmpty()) onFetchMetadataFinished(flatpakInstallation, resource, metadata); fw->deleteLater(); }); // 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 betwen 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 distiguish 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); futureWatcher->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchFlatpakSize, flatpakInstallation, resource)); 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(); }); } 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); - resource->setInstalledSize(installedSize); } else { resource->setDownloadSize(downloadSize); - resource->setInstalledSize(installedSize); } + 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"))) { auto stream = new ResultsStream(QStringLiteral("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) { 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) { if (r->type() == AbstractResource::Technical && filter.state != AbstractResource::Upgradeable) { 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)) { 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")) { if (url.host().isEmpty()) passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); else { auto stream = new ResultsStream(QStringLiteral("FlatpakStream")); auto f = [this, stream, url] () { const auto resources = resourcesByAppstreamName(url.host()); 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, 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, 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); } } AbstractResource * FlatpakBackend::resourceForFile(const QUrl &url) { if (!url.isLocalFile()) { return nullptr; } FlatpakResource *resource = nullptr; if (url.path().endsWith(QLatin1String(".flatpak"))) { resource = addAppFromFlatpakBundle(url); } else if (url.path().endsWith(QLatin1String(".flatpakref"))) { resource = addAppFromFlatpakRef(url); } else if (url.path().endsWith(QLatin1String(".flatpakrepo"))) { resource = addSourceFromFlatpakRepo(url); } return resource; } QString FlatpakBackend::displayName() const { return QStringLiteral("Flatpak"); } #include "FlatpakBackend.moc" diff --git a/libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp b/libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp index 823ab21e..ba11f893 100644 --- a/libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp +++ b/libdiscover/backends/FlatpakBackend/FlatpakSourcesBackend.cpp @@ -1,308 +1,308 @@ /*************************************************************************** * Copyright © 2014 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 "FlatpakSourcesBackend.h" #include "FlatpakResource.h" #include "FlatpakBackend.h" #include #include #include #include #include #include #include #include #include #include #include class FlatpakSourceItem : public QStandardItem { public: FlatpakSourceItem(const QString &text) : QStandardItem(text) { } void setFlatpakInstallation(FlatpakInstallation *installation) { m_installation = installation; } FlatpakInstallation *flatpakInstallation() const { return m_installation; } private: FlatpakInstallation *m_installation; }; FlatpakSourcesBackend::FlatpakSourcesBackend(const QVector &installations, AbstractResourcesBackend * parent) : AbstractSourcesBackend(parent) , m_preferredInstallation(installations.constFirst()) , m_sources(new QStandardItemModel(this)) , m_flathubAction(new QAction(i18n("Add Flathub"), this)) , m_noSourcesItem(new QStandardItem(QStringLiteral("-"))) { m_flathubAction->setToolTip(QStringLiteral("flathub")); connect(m_flathubAction, &QAction::triggered, this, [this](){ addSource(QStringLiteral("https://flathub.org/repo/flathub.flatpakrepo")); }); for (auto installation : installations) { if (!listRepositories(installation)) { qWarning() << "Failed to list repositories from installation" << installation; } } m_noSourcesItem->setEnabled(false); if (m_sources->rowCount() == 0) { m_sources->appendRow(m_noSourcesItem); } } FlatpakSourcesBackend::~FlatpakSourcesBackend() { QStringList ids; for (int i = 0, c = m_sources->rowCount(); iitem(i); ids << it->data(IdRole).toString(); } auto conf = KSharedConfig::openConfig(); KConfigGroup group = conf->group("FlatpakSources"); group.writeEntry("Sources", ids); } QAbstractItemModel* FlatpakSourcesBackend::sources() { return m_sources; } bool FlatpakSourcesBackend::addSource(const QString &id) { FlatpakBackend* backend = qobject_cast(parent()); const QUrl flatpakrepoUrl(id); if (id.isEmpty() || !flatpakrepoUrl.isValid()) return false; if (flatpakrepoUrl.isLocalFile()) { auto res = backend->addSourceFromFlatpakRepo(flatpakrepoUrl); if (res) backend->installApplication(res); else backend->passiveMessage(i18n("Could not add the source %1", flatpakrepoUrl.toDisplayString())); } else { AbstractResourcesBackend::Filters filter; filter.resourceUrl = flatpakrepoUrl; auto stream = new StoredResultsStream ({backend->search(filter)}); connect(stream, &StoredResultsStream::finished, this, [backend, stream, flatpakrepoUrl]() { const auto res = stream->resources(); if (!res.isEmpty()) { Q_ASSERT(res.count() == 1); backend->installApplication(res.first()); } else { backend->passiveMessage(i18n("Could not add the source %1", flatpakrepoUrl.toDisplayString())); } }); } return true; } QStandardItem * FlatpakSourcesBackend::sourceById(const QString& id) const { QStandardItem* sourceIt = nullptr; for (int i = 0, c = m_sources->rowCount(); iitem(i); if (it->data(IdRole) == id) { sourceIt = it; break; } } return sourceIt; } bool FlatpakSourcesBackend::removeSource(const QString &id) { auto sourceIt = sourceById(id); if (sourceIt) { FlatpakSourceItem *sourceItem = static_cast(sourceIt); g_autoptr(GCancellable) cancellable = g_cancellable_new(); g_autoptr(GError) error = nullptr; if (flatpak_installation_remove_remote(sourceItem->flatpakInstallation(), id.toUtf8().constData(), cancellable, &error)) { m_sources->removeRow(sourceItem->row()); if (m_sources->rowCount() == 0) { m_sources->appendRow(m_noSourcesItem); } return true; } else { qWarning() << "Failed to remove " << id << " remote repository:" << error->message; return false; } } else { qWarning() << "couldn't find " << id; return false; } return false; } QList FlatpakSourcesBackend::actions() const { return { m_flathubAction }; } bool FlatpakSourcesBackend::listRepositories(FlatpakInstallation* installation) { Q_ASSERT(installation); g_autoptr(GCancellable) cancellable = g_cancellable_new(); g_autoptr(GPtrArray) remotes = flatpak_installation_list_remotes(installation, cancellable, nullptr); if (!remotes) { return false; } for (uint i = 0; i < remotes->len; i++) { FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i)); if (flatpak_remote_get_noenumerate(remote)) { continue; } addRemote(remote, installation); } return true; } FlatpakRemote * FlatpakSourcesBackend::installSource(FlatpakResource *resource) { g_autoptr(GCancellable) cancellable = g_cancellable_new(); auto remote = flatpak_installation_get_remote_by_name(m_preferredInstallation, resource->flatpakName().toUtf8().constData(), cancellable, nullptr); if (remote) { qWarning() << "Source " << resource->flatpakName() << " already exists"; return nullptr; } remote = flatpak_remote_new(resource->flatpakName().toUtf8().constData()); flatpak_remote_set_url(remote, resource->getMetadata(QStringLiteral("repo-url")).toString().toUtf8().constData()); flatpak_remote_set_noenumerate(remote, false); flatpak_remote_set_title(remote, resource->comment().toUtf8().constData()); const QString gpgKey = resource->getMetadata(QStringLiteral("gpg-key")).toString(); if (!gpgKey.isEmpty()) { gsize dataLen = 0; g_autofree guchar *data = nullptr; g_autoptr(GBytes) bytes = nullptr; data = g_base64_decode(gpgKey.toUtf8().constData(), &dataLen); bytes = g_bytes_new(data, dataLen); flatpak_remote_set_gpg_verify(remote, true); flatpak_remote_set_gpg_key(remote, bytes); } else { flatpak_remote_set_gpg_verify(remote, false); } if (!resource->branch().isEmpty()) { flatpak_remote_set_default_branch(remote, resource->branch().toUtf8().constData()); } if (!flatpak_installation_modify_remote(m_preferredInstallation, remote, cancellable, nullptr)) { qWarning() << "Failed to add source " << resource->flatpakName(); return nullptr; } addRemote(remote, m_preferredInstallation); return remote; } void FlatpakSourcesBackend::addRemote(FlatpakRemote *remote, FlatpakInstallation *installation) { const QString id = QString::fromUtf8(flatpak_remote_get_name(remote)); const QString title = QString::fromUtf8(flatpak_remote_get_title(remote)); const QUrl remoteUrl(QString::fromUtf8(flatpak_remote_get_url(remote))); for(QAction *action: actions()) { if (action->toolTip() == id) { action->setEnabled(false); action->setVisible(false); } } FlatpakSourceItem *it = new FlatpakSourceItem(!title.isEmpty() ? title : id); it->setCheckState(flatpak_remote_get_disabled(remote) ? Qt::Unchecked : Qt::Checked); - it->setData(remoteUrl.host(), Qt::ToolTipRole); + it->setData(remoteUrl.isLocalFile() ? remoteUrl.toLocalFile() : remoteUrl.host(), Qt::ToolTipRole); it->setData(id, IdRole); it->setFlatpakInstallation(installation); int idx = -1; { const auto conf = KSharedConfig::openConfig(); const KConfigGroup group = conf->group("FlatpakSources"); const auto ids = group.readEntry("Sources", QStringList()); const auto ourIdx = ids.indexOf(id); if (ourIdx<0) { //If not present, we put it on top idx = 0; } else { idx=0; for(int c=m_sources->rowCount(); idxitem(idx); const int compIdx = ids.indexOf(compIt->data(IdRole).toString()); if (compIdx >= ourIdx) { break; } } } } m_sources->insertRow(idx, it); if (m_sources->rowCount() == 1) Q_EMIT firstSourceIdChanged(); Q_EMIT lastSourceIdChanged(); if (m_sources->rowCount() > 0) { m_sources->takeRow(m_noSourcesItem->row()); } } QString FlatpakSourcesBackend::idDescription() { return i18n("Flatpak repository URI (*.flatpakrepo)"); } bool FlatpakSourcesBackend::moveSource(const QString& sourceId, int delta) { auto item = sourceById(sourceId); if (!item) return false; const auto row = item->row(); auto prevRow = m_sources->takeRow(row); Q_ASSERT(!prevRow.isEmpty()); const auto destRow = row + (delta>0? delta : delta); m_sources->insertRow(destRow, prevRow); if (destRow == 0 || row == 0) Q_EMIT firstSourceIdChanged(); if (destRow == m_sources->rowCount() - 1 || row == m_sources->rowCount() - 1) Q_EMIT lastSourceIdChanged(); return true; } int FlatpakSourcesBackend::originIndex(const QString& sourceId) const { auto item = sourceById(sourceId); return item ? item->row() : INT_MAX; } diff --git a/libdiscover/backends/FwupdBackend/FwupdBackend.cpp b/libdiscover/backends/FwupdBackend/FwupdBackend.cpp index b4067817..594746ce 100644 --- a/libdiscover/backends/FwupdBackend/FwupdBackend.cpp +++ b/libdiscover/backends/FwupdBackend/FwupdBackend.cpp @@ -1,562 +1,562 @@ /*************************************************************************** * Copyright © 2013 Aleix Pol Gonzalez * * Copyright © 2018 Abhijeet Sharma * * * * 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 "FwupdBackend.h" #include "FwupdResource.h" #include "FwupdTransaction.h" #include "FwupdSourcesBackend.h" #include #include #include #include #include #include #include #include #include DISCOVER_BACKEND_PLUGIN(FwupdBackend) FwupdBackend::FwupdBackend(QObject* parent) : AbstractResourcesBackend(parent) , client(fwupd_client_new()) , m_updater(new StandardBackendUpdater(this)) , m_cancellable(g_cancellable_new()) { connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &FwupdBackend::updatesCountChanged); SourcesModel::global()->addSourcesBackend(new FwupdSourcesBackend(this)); QTimer::singleShot(0, this, &FwupdBackend::checkForUpdates); } QMap FwupdBackend::gchecksumToQChryptographicHash() { static QMap map; if (map.isEmpty()) { map.insert(G_CHECKSUM_SHA1,QCryptographicHash::Sha1); map.insert(G_CHECKSUM_SHA256,QCryptographicHash::Sha256); map.insert(G_CHECKSUM_SHA512,QCryptographicHash::Sha512); map.insert(G_CHECKSUM_MD5,QCryptographicHash::Md5); } return map; } FwupdBackend::~FwupdBackend() { g_cancellable_cancel(m_cancellable); m_threadPool.waitForDone(200); m_threadPool.clear(); g_object_unref(m_cancellable); g_object_unref(client); } void FwupdBackend::addResourceToList(FwupdResource* res) { res->setParent(this); auto &r = m_resources[res->packageName()]; if (r) { Q_EMIT resourceRemoved(r); delete r; } r = res; Q_ASSERT(m_resources.value(res->packageName()) == res); } FwupdResource * FwupdBackend::createDevice(FwupdDevice *device) { const QString name = QString::fromUtf8(fwupd_device_get_name(device)); FwupdResource* res = new FwupdResource(name, nullptr); const QString deviceID = QString::fromUtf8(fwupd_device_get_id(device)); res->setId(QStringLiteral("org.fwupd.%1.device").arg(QString(deviceID).replace(QLatin1Char('/'),QLatin1Char('_')))); res->setDeviceId(deviceID); res->setDeviceDetails(device); return res; } FwupdResource * FwupdBackend::createRelease(FwupdDevice *device) { FwupdResource* res = createDevice(device); FwupdRelease *release = fwupd_device_get_release_default(device); res->setId(QString::fromUtf8(fwupd_release_get_appstream_id(release))); res->setReleaseDetails(release); /* the same as we have already */ if (qstrcmp(fwupd_device_get_version(device), fwupd_release_get_version(release)) == 0) { qWarning() << "Fwupd Error: same firmware version as installed"; } return res; } void FwupdBackend::addUpdates() { g_autoptr(GError) error = nullptr; g_autoptr(GPtrArray) devices = fwupd_client_get_devices(client, nullptr, &error); if (!devices) { if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) qDebug() << "Fwupd Info: No Devices Found"; else handleError(error); return; } for(uint i = 0; i < devices->len; i++) { FwupdDevice *device = (FwupdDevice *)g_ptr_array_index(devices, i); if (!fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED)) continue; if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_LOCKED)) continue; if (!fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)) continue; g_autoptr(GError) error2 = nullptr; g_autoptr(GPtrArray) rels = fwupd_client_get_upgrades(client, fwupd_device_get_id(device), nullptr, &error2); if (rels) { fwupd_device_add_release(device, (FwupdRelease *)g_ptr_array_index(rels, 0)); auto res = createApp(device); if (!res) { qWarning() << "Fwupd Error: Cannot Create App From Device" << fwupd_device_get_name(device); } else { QString longdescription; for(uint j = 0; j < rels->len; j++) { FwupdRelease *release = (FwupdRelease *)g_ptr_array_index(rels, j); if (!fwupd_release_get_description(release)) continue; longdescription += QStringLiteral("Version %1\n").arg(QString::fromUtf8(fwupd_release_get_version(release))); longdescription += QString::fromUtf8(fwupd_release_get_description(release)) + QLatin1Char('\n'); } res->setDescription(longdescription); addResourceToList(res); } } else { if (!g_error_matches(error2, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { handleError(error2); } } } } QByteArray FwupdBackend::getChecksum(const QString &filename, QCryptographicHash::Algorithm hashAlgorithm) { QFile f(filename); if (!f.open(QFile::ReadOnly)) return {}; QCryptographicHash hash(hashAlgorithm); if (!hash.addData(&f)) return {}; return hash.result().toHex(); } FwupdResource* FwupdBackend::createApp(FwupdDevice *device) { FwupdRelease *release = fwupd_device_get_release_default(device); FwupdResource* app = createRelease(device); if (!app->isLiveUpdatable()) { qWarning() << "Fwupd Error: " << app->name() << "[" << app->id() << "]" << "cannot be updated "; return nullptr; } if (app->id().isNull()) { qWarning() << "Fwupd Error: No id for firmware"; return nullptr; } if (app->availableVersion().isNull()) { qWarning() << "Fwupd Error: No version! for " << app->id(); return nullptr; } GPtrArray *checksums = fwupd_release_get_checksums(release); if (checksums->len == 0) { qWarning() << "Fwupd Error: " << app->name() << "[" << app->id() << "] has no checksums, ignoring as unsafe"; return nullptr; } const QUrl update_uri(QString::fromUtf8(fwupd_release_get_uri(release))); if (!update_uri.isValid()) { qWarning() << "Fwupd Error: No Update URI available for" << app->name() << "[" << app->id() << "]"; return nullptr; } /* Checking for firmware in the cache? */ const QString filename_cache = cacheFile(QStringLiteral("fwupd"), QFileInfo(update_uri.path()).fileName()); Q_ASSERT(!filename_cache.isEmpty()); /* Currently LVFS supports SHA1 only*/ const QByteArray checksum_tmp(fwupd_checksum_get_by_kind(checksums, G_CHECKSUM_SHA1)); const QByteArray checksum = getChecksum(filename_cache, QCryptographicHash::Sha1); if (checksum_tmp != checksum) { qWarning() << "Fwupd Error: " << filename_cache << " does not match checksum, expected" << checksum_tmp << "got" << checksum; QFile::remove(filename_cache); return nullptr; } /* link file to application and return its reference */ app->setFile(filename_cache); if (!app->needsReboot()) app->setState(AbstractResource::Upgradeable); return app; } bool FwupdBackend::downloadFile(const QUrl &uri, const QString &filename) { Q_ASSERT(QThread::currentThread() != QCoreApplication::instance()->thread()); QScopedPointer manager(new QNetworkAccessManager); QEventLoop loop; QTimer getTimer; connect(&getTimer, &QTimer::timeout, &loop, &QEventLoop::quit); connect(manager.data(), &QNetworkAccessManager::finished, &loop, &QEventLoop::quit); QScopedPointer reply(manager->get(QNetworkRequest(uri))); getTimer.start(600000); // 60 Seconds TimeOout Period loop.exec(); if (!reply) { return false; } else if (QNetworkReply::NoError != reply->error() ) { qWarning() << "error fetching" << uri; return false; } else if (reply->error() == QNetworkReply::NoError) { QFile file(filename); if (file.open(QIODevice::WriteOnly)) { file.write(reply->readAll()); } else { qWarning() << "Fwupd Error: Cannot Open File to write Data" << filename; } } return true; } void FwupdBackend::refreshRemote(FwupdBackend* backend, FwupdRemote* remote, quint64 cacheAge, GCancellable *cancellable) { if (!fwupd_remote_get_filename_cache_sig(remote)) { qWarning() << "Fwupd Error: " << "Remote " << fwupd_remote_get_id(remote) << "has no cache signature!"; return; } /* check cache age */ if (cacheAge > 0) { const quint64 age = fwupd_remote_get_age(remote); if (age < cacheAge) { // qDebug() << "Fwupd Info:" << fwupd_remote_get_id(remote) << "is only" << age << "seconds old, so ignoring refresh! "; return; } } const QString cacheId = QStringLiteral("fwupd/remotes.d/%1").arg(QString::fromUtf8(fwupd_remote_get_id(remote))); const auto basenameSig = QString::fromUtf8(g_path_get_basename(fwupd_remote_get_filename_cache_sig(remote))); const QString filenameSig = cacheFile(cacheId, basenameSig); if (filenameSig.isEmpty()) return; /* download the signature first*/ const QUrl urlSig(QString::fromUtf8(fwupd_remote_get_metadata_uri_sig(remote))); const QString filenameSigTmp(filenameSig + QStringLiteral(".tmp")); if (!downloadFile(urlSig, filenameSigTmp)) { qWarning() << "failed to download" << urlSig; return; } Q_ASSERT(QFile::exists(filenameSigTmp)); const auto checksum = fwupd_remote_get_checksum(remote); const QCryptographicHash::Algorithm hashAlgorithm = gchecksumToQChryptographicHash()[fwupd_checksum_guess_kind(checksum)]; const QByteArray hash = getChecksum(filenameSigTmp, hashAlgorithm); const QByteArray oldHash = getChecksum(filenameSig, hashAlgorithm); if (oldHash == hash) { qDebug() << "remote hasn't changed:" << fwupd_remote_get_id(remote); QFile::remove(filenameSigTmp); return; } QFile::remove(filenameSig); if (!QFile::rename(filenameSigTmp, filenameSig)) { QFile::remove(filenameSigTmp); qWarning() << "Fwupd Error: cannot save remote signature" << filenameSigTmp << "to" << filenameSig; return; } QFile::remove(filenameSigTmp); const auto basename = QString::fromUtf8(g_path_get_basename(fwupd_remote_get_filename_cache(remote))); const QString filename = cacheFile(cacheId, basename); if (filename.isEmpty()) return; qDebug() << "Fwupd Info: saving new firmware metadata to:" << filename; const QUrl url(QString::fromUtf8(fwupd_remote_get_metadata_uri(remote))); if (!downloadFile(url, filename)) { qWarning() << "Fwupd Error: cannot download file:" << filename; return; } g_autoptr(GError) error = nullptr; if (!fwupd_client_update_metadata(backend->client, fwupd_remote_get_id(remote), filename.toUtf8().constData(), filenameSig.toUtf8().constData(), cancellable, &error)) { backend->handleError(error); } } void FwupdBackend::handleError(GError *perror) { //TODO: localise the error message if (!g_error_matches(perror, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE) && !g_error_matches(perror, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { const QString msg = QString::fromUtf8(perror->message); QTimer::singleShot(0, this, [this, msg](){ Q_EMIT passiveMessage(msg); }); qWarning() << "Fwupd Error" << perror->code << perror->message; } // else // qDebug() << "Fwupd skipped" << perror->code << perror->message; } QString FwupdBackend::cacheFile(const QString &kind, const QString &basename) { const QDir cacheDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); const QString cacheDirFile = cacheDir.filePath(kind); if (!QFileInfo::exists(cacheDirFile) && !cacheDir.mkpath(kind)) { qWarning() << "Fwupd Error: cannot make cache directory!"; return {}; } return cacheDir.filePath(kind + QLatin1Char('/') + basename); } void FwupdBackend::checkForUpdates() { if (m_fetching) return; auto fw = new QFutureWatcher(this); - fw->setFuture(QtConcurrent::run(&m_threadPool, [this] () -> GPtrArray* - { - const uint cacheAge = (24*60*60); // Check for updates every day - g_autoptr(GError) error = nullptr; - - /* get devices */ - GPtrArray* devices = fwupd_client_get_devices(client, m_cancellable, nullptr); - - - g_autoptr(GPtrArray) remotes = fwupd_client_get_remotes(client, m_cancellable, &error); - for(uint i = 0; remotes && i < remotes->len; i++) - { - FwupdRemote *remote = (FwupdRemote *)g_ptr_array_index(remotes, i); - if (!fwupd_remote_get_enabled(remote)) - continue; - - if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_LOCAL) - continue; - - refreshRemote(this, remote, cacheAge, m_cancellable); - } - return devices; - } - )); connect(fw, &QFutureWatcher::finished, this, [this, fw]() { m_fetching = true; emit fetchingChanged(); auto devices = fw->result(); for(uint i = 0; devices && i < devices->len; i++) { FwupdDevice *device = (FwupdDevice *) g_ptr_array_index(devices, i); g_autoptr(GError) error = nullptr; g_autoptr(GPtrArray) releases = fwupd_client_get_releases(client, fwupd_device_get_id(device), nullptr, &error); if (error) { if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE) || g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) continue; handleError(error); } auto res = createDevice(device); for (uint i=0; releases && ilen; ++i) { FwupdRelease *release = (FwupdRelease *)g_ptr_array_index(releases, i); if (res->installedVersion().toUtf8() == fwupd_release_get_version(release)) { res->setReleaseDetails(release); break; } } addResourceToList(res); } g_ptr_array_unref(devices); addUpdates(); m_fetching = false; emit fetchingChanged(); emit initialized(); fw->deleteLater(); }); + fw->setFuture(QtConcurrent::run(&m_threadPool, [this] () -> GPtrArray* + { + const uint cacheAge = (24*60*60); // Check for updates every day + g_autoptr(GError) error = nullptr; + + /* get devices */ + GPtrArray* devices = fwupd_client_get_devices(client, m_cancellable, nullptr); + + + g_autoptr(GPtrArray) remotes = fwupd_client_get_remotes(client, m_cancellable, &error); + for(uint i = 0; remotes && i < remotes->len; i++) + { + FwupdRemote *remote = (FwupdRemote *)g_ptr_array_index(remotes, i); + if (!fwupd_remote_get_enabled(remote)) + continue; + + if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_LOCAL) + continue; + + refreshRemote(this, remote, cacheAge, m_cancellable); + } + return devices; + } + )); } int FwupdBackend::updatesCount() const { return m_updater->updatesCount(); } ResultsStream* FwupdBackend::search(const AbstractResourcesBackend::Filters& filter) { if (filter.resourceUrl.scheme() == QLatin1String("fwupd")) { return findResourceByPackageName(filter.resourceUrl); } else if (!filter.resourceUrl.isEmpty()) { return new ResultsStream(QStringLiteral("FwupdStream-void"), {}); } auto stream = new ResultsStream(QStringLiteral("FwupdStream")); auto f = [this, stream, filter] () { QVector ret; foreach(AbstractResource* r, m_resources) { if (r->state() < filter.state) continue; if (filter.search.isEmpty() || r->name().contains(filter.search, Qt::CaseInsensitive) || r->comment().contains(filter.search, Qt::CaseInsensitive)) { ret += r; } } if (!ret.isEmpty()) Q_EMIT stream->resourcesFound(ret); stream->finish(); }; if (isFetching()) { connect(this, &FwupdBackend::initialized, stream, f); } else { QTimer::singleShot(0, this, f); } return stream; } ResultsStream * FwupdBackend::findResourceByPackageName(const QUrl& search) { auto res = search.scheme() == QLatin1String("fwupd") ? m_resources.value(search.host().replace(QLatin1Char('.'), QLatin1Char(' '))) : nullptr; if (!res) { return new ResultsStream(QStringLiteral("FwupdStream"), {}); } else return new ResultsStream(QStringLiteral("FwupdStream"), { res }); } AbstractBackendUpdater* FwupdBackend::backendUpdater() const { return m_updater; } AbstractReviewsBackend* FwupdBackend::reviewsBackend() const { return nullptr; } Transaction* FwupdBackend::installApplication(AbstractResource* app, const AddonList& addons) { Q_ASSERT(addons.isEmpty()); return installApplication(app); } Transaction* FwupdBackend::installApplication(AbstractResource* app) { return new FwupdTransaction(qobject_cast(app), this); } Transaction* FwupdBackend::removeApplication(AbstractResource* /*app*/) { qWarning() << "should not have reached here, it's not possible to uninstall a firmware"; return nullptr; } AbstractResource * FwupdBackend::resourceForFile(const QUrl& path) { g_autoptr(GError) error = nullptr; QMimeDatabase db; QMimeType type = db.mimeTypeForFile(path.fileName()); FwupdResource* app = nullptr; if (type.isValid() && type.inherits(QStringLiteral("application/vnd.ms-cab-compressed"))) { g_autofree gchar *filename = path.fileName().toUtf8().data(); g_autoptr(GPtrArray) devices = fwupd_client_get_details(client, filename, nullptr, &error); if (devices) { FwupdDevice *device = (FwupdDevice *)g_ptr_array_index(devices, 0); app = createRelease(device); app->setState(AbstractResource::None); for(uint i = 1; i < devices->len; i++) { FwupdDevice *device = (FwupdDevice *)g_ptr_array_index(devices, i); FwupdResource* app_ = createRelease(device); app_->setState(AbstractResource::None); } addResourceToList(app); connect(app, &FwupdResource::stateChanged, this, &FwupdBackend::updatesCountChanged); } else { handleError(error); } } return app; } QString FwupdBackend::displayName() const { return QStringLiteral("Firmware Updates"); } bool FwupdBackend::hasApplications() const { return false; } #include "FwupdBackend.moc" diff --git a/libdiscover/backends/PackageKitBackend/AppPackageKitResource.cpp b/libdiscover/backends/PackageKitBackend/AppPackageKitResource.cpp index 9c477345..45da61f8 100644 --- a/libdiscover/backends/PackageKitBackend/AppPackageKitResource.cpp +++ b/libdiscover/backends/PackageKitBackend/AppPackageKitResource.cpp @@ -1,251 +1,247 @@ /*************************************************************************** * 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 "AppPackageKitResource.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "config-paths.h" #include "utils.h" AppPackageKitResource::AppPackageKitResource(const AppStream::Component& data, const QString &packageName, PackageKitBackend* parent) : PackageKitResource(packageName, QString(), parent) , m_appdata(data) { Q_ASSERT(data.isValid()); } QString AppPackageKitResource::name() const { QString ret; if (!m_appdata.extends().isEmpty()) { auto components = backend()->componentsById(m_appdata.extends().constFirst()); if (components.isEmpty()) qWarning() << "couldn't find" << m_appdata.extends() << "which is supposedly extended by" << m_appdata.id(); else ret = components.constFirst().name() + QStringLiteral(" - ") + m_appdata.name(); } if (ret.isEmpty()) ret = m_appdata.name(); return ret; } QString AppPackageKitResource::longDescription() { const auto desc = m_appdata.description(); if (!desc.isEmpty()) return desc; return PackageKitResource::longDescription(); } static QIcon componentIcon(const AppStream::Component &comp) { QIcon ret; foreach(const AppStream::Icon &icon, comp.icons()) { QStringList stock; switch(icon.kind()) { case AppStream::Icon::KindLocal: ret.addFile(icon.url().toLocalFile(), icon.size()); break; case AppStream::Icon::KindCached: ret.addFile(icon.url().toLocalFile(), icon.size()); break; case AppStream::Icon::KindStock: { const auto ret = QIcon::fromTheme(icon.name()); if (!ret.isNull()) return ret; break; } default: break; } } if (ret.isNull()) { ret = QIcon::fromTheme(QStringLiteral("package-x-generic")); } return ret; } QVariant AppPackageKitResource::icon() const { return componentIcon(m_appdata); } QString AppPackageKitResource::license() { const auto license = m_appdata.projectLicense(); return license.isEmpty() ? PackageKitResource::license() : license; } QStringList AppPackageKitResource::mimetypes() const { return m_appdata.provided(AppStream::Provided::KindMimetype).items(); } QStringList AppPackageKitResource::categories() { auto cats = m_appdata.categories(); if (m_appdata.kind() != AppStream::Component::KindAddon) cats.append(QStringLiteral("Application")); return cats; } QString AppPackageKitResource::comment() { const auto summary = m_appdata.summary(); if (!summary.isEmpty()) return summary; return PackageKitResource::comment(); } QString AppPackageKitResource::appstreamId() const { return m_appdata.id(); } QUrl AppPackageKitResource::homepage() { return m_appdata.url(AppStream::Component::UrlKindHomepage); } QUrl AppPackageKitResource::helpURL() { return m_appdata.url(AppStream::Component::UrlKindHelp); } QUrl AppPackageKitResource::bugURL() { return m_appdata.url(AppStream::Component::UrlKindBugtracker); } QUrl AppPackageKitResource::donationURL() { return m_appdata.url(AppStream::Component::UrlKindDonation); } AbstractResource::Type AppPackageKitResource::type() const { static QString desktop = QString::fromUtf8(qgetenv("XDG_CURRENT_DESKTOP")); const auto desktops = m_appdata.compulsoryForDesktops(); return m_appdata.kind() == AppStream::Component::KindAddon ? Addon : (desktops.isEmpty() || !desktops.contains(desktop)) ? Application : Technical; } void AppPackageKitResource::fetchScreenshots() { const auto sc = AppStreamUtils::fetchScreenshots(m_appdata); Q_EMIT screenshotsFetched(sc.first, sc.second); } QStringList AppPackageKitResource::allPackageNames() const { auto ret = m_appdata.packageNames(); if (ret.isEmpty()) { ret = QStringList{ PackageKit::Daemon::packageName(availablePackageId()) }; } return ret; } QList AppPackageKitResource::addonsInformation() { - const PackageKitBackend* p = static_cast(parent()); - const QVector res = p->extendedBy(m_appdata.id()); - - QList ret; - Q_FOREACH (AppPackageKitResource* r, res) { - ret += PackageState(r->appstreamId(), r->name(), r->comment(), r->isInstalled()); - } - return ret; + return kTransform>( + backend()->extendedBy(m_appdata.id()), + [](AppPackageKitResource* r) { return PackageState(r->appstreamId(), r->name(), r->comment(), r->isInstalled()); } + ); } QStringList AppPackageKitResource::extends() const { return m_appdata.extends(); } QString AppPackageKitResource::changelog() const { return AppStreamUtils::changelogToHtml(m_appdata); } void AppPackageKitResource::invokeApplication() const { auto trans = PackageKit::Daemon::getFiles({installedPackageId()}); connect(trans, &PackageKit::Transaction::files, this, [this](const QString &/*packageID*/, const QStringList &filenames) { const auto allServices = QStandardPaths::locateAll(QStandardPaths::ApplicationsLocation, m_appdata.id()); if (!allServices.isEmpty()) { const auto packageServices = kFilter(allServices, [filenames](const QString &file) { return filenames.contains(file); }); QProcess::startDetached(QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/discover/runservice"), {packageServices}); } else { const QStringList exes = m_appdata.provided(AppStream::Provided::KindBinary).items(); const auto packageExecutables = kFilter(allServices, [filenames](const QString &exe) { return filenames.contains(QLatin1Char('/') + exe); }); if (!packageExecutables.isEmpty()) { QProcess::startDetached(exes.constFirst()); } else { const auto locations = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation); const auto desktopFiles = kFilter(filenames, [locations](const QString &exe) { for (const auto &location: locations) { if (exe.startsWith(location)) return exe.contains(QLatin1String(".desktop")); } return false; }); if (!desktopFiles.isEmpty()) { QProcess::startDetached(QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/discover/runservice"), { desktopFiles }); } } qWarning() << "Could not find any executables" << exes << filenames; } }); } QDate AppPackageKitResource::releaseDate() const { if (!m_appdata.releases().isEmpty()) { auto release = m_appdata.releases().constFirst(); return release.timestamp().date(); } return {}; } QString AppPackageKitResource::author() const { return m_appdata.developerName(); } void AppPackageKitResource::fetchChangelog() { emit changelogFetched(changelog()); } diff --git a/libdiscover/backends/SnapBackend/SnapTransaction.cpp b/libdiscover/backends/SnapBackend/SnapTransaction.cpp index e00d1285..453cddaa 100644 --- a/libdiscover/backends/SnapBackend/SnapTransaction.cpp +++ b/libdiscover/backends/SnapBackend/SnapTransaction.cpp @@ -1,123 +1,130 @@ /*************************************************************************** * 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 "SnapTransaction.h" #include "SnapBackend.h" #include "SnapResource.h" #include #include #include #include #include #include #include "libsnapclient/config-paths.h" #include "utils.h" SnapTransaction::SnapTransaction(QSnapdClient* client, SnapResource* app, Role role, AbstractResource::State newState) : Transaction(app, app, role) , m_client(client) , m_app(app) , m_newState(newState) { if (role == RemoveRole) setRequest(m_client->remove(app->packageName())); else setRequest(m_client->install(app->packageName())); } void SnapTransaction::cancel() { m_request->cancel(); } void SnapTransaction::finishTransaction() { switch(m_request->error()) { case QSnapdRequest::NoError: static_cast(m_app->backend())->refreshStates(); setStatus(DoneStatus); m_app->setState(m_newState); break; case QSnapdRequest::NeedsClassic: setStatus(SetupStatus); if (role() == Transaction::InstallRole) { Q_EMIT proceedRequest(m_app->name(), i18n("This snap application needs security confinement measures disabled.")); return; } break; case QSnapdRequest::AuthDataRequired: { setStatus(SetupStatus); QProcess* p = new QProcess; p->setProgram(QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR "/discover/SnapMacaroonDialog")); p->start(); connect(p, static_cast(&QProcess::finished), this, [this, p] (int code) { p->deleteLater(); if (code != 0) { qWarning() << "login failed... code:" << code << p->readAll(); Q_EMIT passiveMessage(m_request->errorString()); setStatus(DoneWithErrorStatus); return; } const auto doc = QJsonDocument::fromJson(p->readAllStandardOutput()); const auto result = doc.object(); const auto macaroon = result[QStringLiteral("macaroon")].toString(); const auto discharges = kTransform(result[QStringLiteral("discharges")].toArray(), [](const QJsonValue& val) { return val.toString(); }); static_cast(m_app->backend())->client()->setAuthData(new QSnapdAuthData(macaroon, discharges)); m_request->runAsync(); }); } return; default: setStatus(DoneWithErrorStatus); qDebug() << "snap error" << m_request << m_request->error() << m_request->errorString(); Q_EMIT passiveMessage(m_request->errorString()); break; } } void SnapTransaction::proceed() { setRequest(m_client->install(QSnapdClient::Classic, m_app->packageName())); } void SnapTransaction::setRequest(QSnapdRequest* req) { m_request.reset(req); setCancellable(false); connect(m_request.data(), &QSnapdRequest::progress, this, &SnapTransaction::progressed); connect(m_request.data(), &QSnapdRequest::complete, this, &SnapTransaction::finishTransaction); - setStatus(SetupStatus); - setStatus(DownloadingStatus); + setStatus(SetupStatus); m_request->runAsync(); } void SnapTransaction::progressed() { const auto change = m_request->change(); int percentage = 0, count = 0; + + auto status = SetupStatus; for(int i = 0, c = change->taskCount(); itask(i)->progressDone()) / change->task(i)->progressTotal(); + auto task = change->task(i); + if (task->kind() == QLatin1String("download-snap")) { + status = task->status() == QLatin1String("doing") || task->status() == QLatin1String("do") ? DownloadingStatus : CommittingStatus; + } else if (task->kind() == QLatin1String("clear-snap")) { + status = CommittingStatus; + } + percentage += (100 * task->progressDone()) / task->progressTotal(); } setProgress(percentage / qMax(count, 1)); }