File indexing completed on 2024-11-17 04:55:38
0001 /* 0002 * SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez <aleixpol@blue-systems.com> 0003 * SPDX-FileCopyrightText: 2017 Jan Grulich <jgrulich@redhat.com> 0004 * 0005 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0006 */ 0007 0008 #include "FlatpakBackend.h" 0009 #include "FlatpakFetchDataJob.h" 0010 #include "FlatpakJobTransaction.h" 0011 #include "FlatpakRefreshAppstreamMetadataJob.h" 0012 #include "FlatpakSourcesBackend.h" 0013 0014 #include <ReviewsBackend/Rating.h> 0015 #include <Transaction/Transaction.h> 0016 #include <appstream/AppStreamUtils.h> 0017 #include <appstream/OdrsReviewsBackend.h> 0018 #include <resources/SourcesModel.h> 0019 #include <resources/StandardBackendUpdater.h> 0020 #include <utils.h> 0021 0022 #include <AppStreamQt/bundle.h> 0023 #include <AppStreamQt/icon.h> 0024 #include <AppStreamQt/metadata.h> 0025 #include <AppStreamQt/pool.h> 0026 #include <AppStreamQt/release.h> 0027 #include <AppStreamQt/version.h> 0028 0029 #include <KAboutData> 0030 #include <KConfigGroup> 0031 #include <KLocalizedString> 0032 #include <KPluginFactory> 0033 #include <KSharedConfig> 0034 0035 #include <QDebug> 0036 #include <QDir> 0037 #include <QFile> 0038 #include <QFileInfo> 0039 #include <QFutureWatcher> 0040 #include <QNetworkAccessManager> 0041 #include <QSettings> 0042 #include <QTemporaryFile> 0043 #include <QTextStream> 0044 #include <QThread> 0045 #include <QTimer> 0046 #include <QtConcurrentRun> 0047 0048 #include <QRegularExpression> 0049 #include <glib.h> 0050 0051 #include <Category/Category.h> 0052 #include <optional> 0053 #include <set> 0054 #include <sys/stat.h> 0055 0056 DISCOVER_BACKEND_PLUGIN(FlatpakBackend) 0057 0058 using namespace Qt::Literals::StringLiterals; 0059 0060 class FlatpakSource 0061 { 0062 public: 0063 FlatpakSource(FlatpakBackend *backend, FlatpakInstallation *installation) 0064 : m_remote(nullptr) 0065 , m_installation(installation) 0066 , m_backend(backend) 0067 { 0068 g_object_ref(m_installation); 0069 } 0070 0071 FlatpakSource(FlatpakBackend *backend, FlatpakInstallation *installation, FlatpakRemote *remote) 0072 : m_remote(remote) 0073 , m_installation(installation) 0074 , m_backend(backend) 0075 , m_appstreamIconsDir(appstreamDir() + QLatin1String("/icons")) 0076 { 0077 g_object_ref(m_remote); 0078 g_object_ref(m_installation); 0079 } 0080 0081 ~FlatpakSource() 0082 { 0083 if (m_remote) { 0084 g_object_unref(m_remote); 0085 } 0086 g_object_unref(m_installation); 0087 } 0088 0089 QString url() const 0090 { 0091 return m_remote ? QString::fromUtf8(flatpak_remote_get_url(m_remote)) : QString(); 0092 } 0093 0094 bool isEnabled() const 0095 { 0096 return m_remote && !flatpak_remote_get_disabled(m_remote); 0097 } 0098 0099 QString appstreamIconsDir() const 0100 { 0101 return m_appstreamIconsDir; 0102 } 0103 QString appstreamDir() const 0104 { 0105 Q_ASSERT(m_remote); 0106 g_autoptr(GFile) appstreamDir = flatpak_remote_get_appstream_dir(m_remote, nullptr); 0107 if (!appstreamDir) { 0108 qWarning() << "No appstream dir for" << flatpak_remote_get_name(m_remote); 0109 return {}; 0110 } 0111 g_autofree char *path_str = g_file_get_path(appstreamDir); 0112 return QString::fromUtf8(path_str); 0113 } 0114 0115 QString name() const 0116 { 0117 return m_remote ? QString::fromUtf8(flatpak_remote_get_name(m_remote)) : QString(); 0118 } 0119 0120 QString title() const 0121 { 0122 auto ret = m_remote ? QString::fromUtf8(flatpak_remote_get_title(m_remote)) : QString(); 0123 if (flatpak_installation_get_is_user(m_installation)) { 0124 ret = i18nc("user denotes this as user-scoped flatpak repo", "%1 (user)", ret); 0125 } 0126 return ret; 0127 } 0128 0129 FlatpakInstallation *installation() const 0130 { 0131 return m_installation; 0132 } 0133 0134 void addResource(FlatpakResource *resource) 0135 { 0136 // Update app with all possible information we have 0137 if (!m_backend->parseMetadataFromAppBundle(resource)) { 0138 qWarning() << "Failed to parse metadata from app bundle for" << resource->name(); 0139 } 0140 0141 m_backend->updateAppState(resource); 0142 0143 Q_ASSERT(!m_resources.contains(resource->uniqueId()) || m_resources.value(resource->uniqueId()) == resource); 0144 m_resources.insert(resource->uniqueId(), resource); 0145 0146 QObject::connect(resource, &FlatpakResource::sizeChanged, m_backend, [this, resource] { 0147 if (!m_backend->isFetching()) 0148 Q_EMIT m_backend->resourcesChanged(resource, {"size", "sizeDescription"}); 0149 }); 0150 } 0151 0152 FlatpakRemote *remote() const 0153 { 0154 return m_remote; 0155 } 0156 0157 AppStream::ComponentBox componentsByName(const QString &name) 0158 { 0159 auto comps = m_pool->componentsById(name); 0160 if (!comps.isEmpty()) { 0161 return comps; 0162 } 0163 0164 comps = m_pool->componentsByProvided(AppStream::Provided::KindId, name); 0165 return comps; 0166 } 0167 0168 AppStream::ComponentBox componentsByFlatpakId(const QString &ref) 0169 { 0170 AppStream::ComponentBox comps = m_pool->componentsByBundleId(AppStream::Bundle::KindFlatpak, ref, false); 0171 if (!comps.isEmpty()) 0172 return comps; 0173 0174 comps = m_pool->componentsByProvided(AppStream::Provided::KindId, ref.section('/'_L1, 1, 1)); 0175 return comps; 0176 } 0177 0178 AppStream::Pool *m_pool = nullptr; 0179 QHash<FlatpakResource::Id, FlatpakResource *> m_resources; 0180 0181 private: 0182 FlatpakRemote *const m_remote; 0183 FlatpakInstallation *const m_installation; 0184 FlatpakBackend *const m_backend; 0185 const QString m_appstreamIconsDir; 0186 }; 0187 0188 static void populateRemote(FlatpakRemote *remote, const QString &name, const QString &url, const QString &gpgKey) 0189 { 0190 flatpak_remote_set_url(remote, url.toUtf8().constData()); 0191 flatpak_remote_set_noenumerate(remote, false); 0192 flatpak_remote_set_title(remote, name.toUtf8().constData()); 0193 0194 if (!gpgKey.isEmpty()) { 0195 gsize dataLen = 0; 0196 g_autofree guchar *data = nullptr; 0197 g_autoptr(GBytes) bytes = nullptr; 0198 data = g_base64_decode(gpgKey.toUtf8().constData(), &dataLen); 0199 bytes = g_bytes_new(data, dataLen); 0200 flatpak_remote_set_gpg_verify(remote, true); 0201 flatpak_remote_set_gpg_key(remote, bytes); 0202 } else { 0203 flatpak_remote_set_gpg_verify(remote, false); 0204 } 0205 } 0206 0207 QDebug operator<<(QDebug debug, const FlatpakResource::Id &id) 0208 { 0209 QDebugStateSaver saver(debug); 0210 debug.nospace() << "FlatpakResource::Id("; 0211 debug.nospace() << "name:" << id.id << ','; 0212 debug.nospace() << "branch:" << id.branch; 0213 debug.nospace() << ')'; 0214 return debug; 0215 } 0216 0217 static FlatpakResource::Id idForComponent(const AppStream::Component &component) 0218 { 0219 // app/app.getspace.Space/x86_64/stable 0220 const QString bundleId = component.bundle(AppStream::Bundle::KindFlatpak).id(); 0221 auto parts = QStringView(bundleId).split('/'_L1); 0222 Q_ASSERT(parts.size() == 4); 0223 0224 return { 0225 component.id(), 0226 parts[3].toString(), 0227 parts[2].toString(), 0228 }; 0229 } 0230 0231 static FlatpakResource::Id idForInstalledRef(FlatpakInstalledRef *ref, const QString &postfix = QString()) 0232 { 0233 const QString appId = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))) + postfix; 0234 const QString arch = QString::fromUtf8(flatpak_ref_get_arch(FLATPAK_REF(ref))); 0235 const QString branch = QString::fromUtf8(flatpak_ref_get_branch(FLATPAK_REF(ref))); 0236 0237 return {appId, branch, arch}; 0238 } 0239 0240 static std::optional<AppStream::Metadata> metadataFromBytes(GBytes *appstreamGz, GCancellable *cancellable) 0241 { 0242 g_autoptr(GError) localError = nullptr; 0243 g_autoptr(GZlibDecompressor) decompressor = nullptr; 0244 g_autoptr(GInputStream) streamGz = nullptr; 0245 g_autoptr(GInputStream) streamData = nullptr; 0246 g_autoptr(GBytes) appstream = nullptr; 0247 0248 /* decompress data */ 0249 decompressor = g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_GZIP); 0250 streamGz = g_memory_input_stream_new_from_bytes(appstreamGz); 0251 if (!streamGz) { 0252 return {}; 0253 } 0254 0255 streamData = g_converter_input_stream_new(streamGz, G_CONVERTER(decompressor)); 0256 0257 appstream = g_input_stream_read_bytes(streamData, 0x100000, cancellable, &localError); 0258 if (!appstream) { 0259 qWarning() << "Failed to extract appstream metadata from bundle:" << localError->message; 0260 return {}; 0261 } 0262 0263 gsize len = 0; 0264 gconstpointer data = g_bytes_get_data(appstream, &len); 0265 0266 AppStream::Metadata metadata; 0267 #if ASQ_CHECK_VERSION(0, 16, 0) 0268 metadata.setFormatStyle(AppStream::Metadata::FormatStyleCatalog); 0269 #else 0270 metadata.setFormatStyle(AppStream::Metadata::FormatStyleCollection); 0271 #endif 0272 AppStream::Metadata::MetadataError error = metadata.parse(QString::fromUtf8((char *)data, len), AppStream::Metadata::FormatKindXml); 0273 if (error != AppStream::Metadata::MetadataErrorNoError) { 0274 qWarning() << "Failed to parse appstream metadata: " << error; 0275 return {}; 0276 } 0277 return metadata; 0278 } 0279 0280 FlatpakBackend::FlatpakBackend(QObject *parent) 0281 : AbstractResourcesBackend(parent) 0282 , m_updater(new StandardBackendUpdater(this)) 0283 , m_reviews(OdrsReviewsBackend::global()) 0284 , m_cancellable(g_cancellable_new()) 0285 , m_checkForUpdatesTimer(new QTimer(this)) 0286 { 0287 g_autoptr(GError) error = nullptr; 0288 0289 connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &FlatpakBackend::updatesCountChanged); 0290 0291 // Load flatpak installation 0292 if (!setupFlatpakInstallations(&error)) { 0293 qWarning() << "Failed to setup flatpak installations:" << error->message; 0294 } else { 0295 m_sources = new FlatpakSourcesBackend(m_installations, this); 0296 loadAppsFromAppstreamData(); 0297 0298 SourcesModel::global()->addSourcesBackend(m_sources); 0299 } 0300 0301 connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, [this] { 0302 m_reviews->emitRatingFetched(this, kAppend<QList<AbstractResource *>>(m_flatpakSources, [](const auto &source) { 0303 return kTransform<QList<AbstractResource *>>(source->m_resources.values()); 0304 })); 0305 }); 0306 0307 m_checkForUpdatesTimer->setInterval(1000); 0308 m_checkForUpdatesTimer->setSingleShot(true); 0309 connect(m_checkForUpdatesTimer, &QTimer::timeout, this, &FlatpakBackend::checkForUpdates); 0310 0311 /* Override the umask to 022 to make it possible to share files between 0312 * the plasma-discover process and flatpak system helper process. 0313 * 0314 * See https://github.com/flatpak/flatpak/pull/2856/ 0315 */ 0316 umask(022); 0317 } 0318 0319 FlatpakBackend::~FlatpakBackend() 0320 { 0321 g_cancellable_cancel(m_cancellable); 0322 if (!m_threadPool.waitForDone(200)) { 0323 qDebug() << "could not kill them all" << m_threadPool.activeThreadCount(); 0324 } 0325 m_threadPool.clear(); 0326 0327 for (auto inst : std::as_const(m_installations)) 0328 g_object_unref(inst); 0329 m_installations.clear(); 0330 g_object_unref(m_cancellable); 0331 } 0332 0333 bool FlatpakBackend::isValid() const 0334 { 0335 return m_sources && !m_installations.isEmpty(); 0336 } 0337 0338 class FlatpakFetchRemoteResourceJob : public QNetworkAccessManager 0339 { 0340 Q_OBJECT 0341 public: 0342 FlatpakFetchRemoteResourceJob(const QUrl &url, ResultsStream *stream, FlatpakBackend *backend) 0343 : QNetworkAccessManager(backend) 0344 , m_backend(backend) 0345 , m_stream(stream) 0346 , m_url(url) 0347 { 0348 connect(stream, &ResultsStream::destroyed, this, &QObject::deleteLater); 0349 } 0350 0351 void start() 0352 { 0353 if (m_url.isLocalFile()) { 0354 QTimer::singleShot(0, m_stream, [this] { 0355 processFile(m_url); 0356 }); 0357 return; 0358 } 0359 0360 QNetworkRequest req(m_url); 0361 req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); 0362 auto replyGet = get(req); 0363 connect(replyGet, &QNetworkReply::finished, this, [this, replyGet] { 0364 QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> replyPtr(replyGet); 0365 if (replyGet->error() != QNetworkReply::NoError) { 0366 qWarning() << "couldn't download" << m_url << replyGet->errorString(); 0367 m_stream->finish(); 0368 return; 0369 } 0370 const QUrl fileUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) // 0371 + QLatin1Char('/') + m_url.fileName()); 0372 auto replyPut = put(QNetworkRequest(fileUrl), replyGet->readAll()); 0373 connect(replyPut, &QNetworkReply::finished, this, [this, fileUrl, replyPut]() { 0374 QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> replyPtr(replyPut); 0375 if (replyPut->error() != QNetworkReply::NoError) { 0376 qWarning() << "couldn't save" << m_url << replyPut->errorString(); 0377 m_stream->finish(); 0378 return; 0379 } 0380 if (!fileUrl.isLocalFile()) { 0381 m_stream->finish(); 0382 return; 0383 } 0384 0385 processFile(fileUrl); 0386 }); 0387 }); 0388 } 0389 0390 void processFile(const QUrl &fileUrl) 0391 { 0392 const auto path = fileUrl.toLocalFile(); 0393 if (path.endsWith(QLatin1String(".flatpak"))) { 0394 m_backend->addAppFromFlatpakBundle(fileUrl, m_stream); 0395 } else if (path.endsWith(QLatin1String(".flatpakref"))) { 0396 m_backend->addAppFromFlatpakRef(fileUrl, m_stream); 0397 } else if (path.endsWith(QLatin1String(".flatpakrepo"))) { 0398 m_backend->addSourceFromFlatpakRepo(fileUrl, m_stream); 0399 } else { 0400 qWarning() << "unrecognized format" << fileUrl; 0401 } 0402 } 0403 0404 private: 0405 FlatpakBackend *const m_backend; 0406 ResultsStream *const m_stream; 0407 const QUrl m_url; 0408 }; 0409 0410 FlatpakRemote *FlatpakBackend::getFlatpakRemoteByUrl(const QString &url, FlatpakInstallation *installation) const 0411 { 0412 auto remotes = flatpak_installation_list_remotes(installation, m_cancellable, nullptr); 0413 if (!remotes) { 0414 return nullptr; 0415 } 0416 0417 const QByteArray comparableUrl = url.toUtf8(); 0418 for (uint i = 0; i < remotes->len; i++) { 0419 FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i)); 0420 0421 if (comparableUrl == flatpak_remote_get_url(remote)) { 0422 return remote; 0423 } 0424 } 0425 return nullptr; 0426 } 0427 0428 FlatpakInstalledRef *FlatpakBackend::getInstalledRefForApp(const FlatpakResource *resource) const 0429 { 0430 Q_ASSERT(resource->resourceType() != FlatpakResource::Source); 0431 g_autoptr(GError) localError = nullptr; 0432 0433 const auto type = resource->resourceType() == FlatpakResource::DesktopApp ? FLATPAK_REF_KIND_APP : FLATPAK_REF_KIND_RUNTIME; 0434 0435 FlatpakInstalledRef *ref = flatpak_installation_get_installed_ref(resource->installation(), 0436 type, 0437 resource->flatpakName().toUtf8().constData(), 0438 resource->arch().toUtf8().constData(), 0439 resource->branch().toUtf8().constData(), 0440 m_cancellable, 0441 &localError); 0442 return ref; 0443 } 0444 0445 static QString refToBundleId(FlatpakRef *ref) 0446 { 0447 const QString typeAsString = flatpak_ref_get_kind(ref) == FLATPAK_REF_KIND_APP ? QStringLiteral("app") : QStringLiteral("runtime"); 0448 const QString flatpakName = QString::fromUtf8(flatpak_ref_get_name(ref)); 0449 const QString arch = QString::fromUtf8(flatpak_ref_get_arch(ref)); 0450 const QString branch = QString::fromUtf8(flatpak_ref_get_branch(ref)); 0451 return QStringLiteral("%1/%2/%3/%4").arg(typeAsString, flatpakName, arch, branch); 0452 } 0453 0454 FlatpakResource *FlatpakBackend::getAppForInstalledRef(FlatpakInstallation *installation, FlatpakInstalledRef *ref, bool *freshResource) const 0455 { 0456 if (freshResource) 0457 *freshResource = false; 0458 const QString origin = QString::fromUtf8(flatpak_installed_ref_get_origin(ref)); 0459 auto source = findSource(installation, origin); 0460 if (source) { 0461 auto ret = source->m_resources.value(idForInstalledRef(ref, {})); 0462 if (ret) { 0463 return ret; 0464 } 0465 } 0466 0467 const QLatin1String name(flatpak_ref_get_name(FLATPAK_REF(ref))); 0468 const QLatin1String branch(flatpak_ref_get_branch(FLATPAK_REF(ref))); 0469 const QString pathExports = FlatpakResource::installationPath(installation) + QLatin1String("/exports/"); 0470 const QString pathApps = pathExports + QLatin1String("share/applications/"); 0471 const QString refId = refToBundleId(FLATPAK_REF(ref)); 0472 AppStream::Component cid; 0473 if (source && source->m_pool) { 0474 auto comps = source->componentsByFlatpakId(refId); 0475 if (comps.isEmpty()) { 0476 g_autoptr(GBytes) metadata = flatpak_installed_ref_load_appdata(ref, m_cancellable, nullptr); 0477 if (metadata) { 0478 auto meta = metadataFromBytes(metadata, m_cancellable); 0479 comps = meta->components(); 0480 } 0481 } 0482 0483 if (comps.size() >= 1) { 0484 Q_ASSERT(comps.size() == 1); 0485 cid = *comps.indexSafe(0); 0486 } 0487 } 0488 0489 if (!cid.isValid()) { 0490 AppStream::Metadata metadata; 0491 const QString fnDesktop = pathApps + name + QLatin1String(".desktop"); 0492 AppStream::Metadata::MetadataError error = metadata.parseFile(fnDesktop, AppStream::Metadata::FormatKindDesktopEntry); 0493 if (error != AppStream::Metadata::MetadataErrorNoError) { 0494 if (QFile::exists(fnDesktop)) 0495 qDebug() << "Failed to parse appstream metadata:" << error << fnDesktop; 0496 0497 cid.setId(name); 0498 #if FLATPAK_CHECK_VERSION(1, 1, 2) 0499 cid.setName(QString::fromUtf8(flatpak_installed_ref_get_appdata_name(ref))); 0500 #endif 0501 } else 0502 cid = metadata.component(); 0503 } 0504 0505 if (cid.bundle(AppStream::Bundle::KindFlatpak).isEmpty()) { 0506 AppStream::Bundle b; 0507 b.setKind(AppStream::Bundle::KindFlatpak); 0508 b.setId(refId); 0509 cid.addBundle(b); 0510 } 0511 0512 if (source && cid.isValid()) { 0513 auto ret = source->m_resources.value(idForComponent(cid)); 0514 if (ret) { 0515 return ret; 0516 } 0517 } 0518 0519 if (!source) { 0520 return nullptr; 0521 } 0522 0523 FlatpakResource *resource = new FlatpakResource(cid, source->installation(), const_cast<FlatpakBackend *>(this)); 0524 resource->setOrigin(source->name()); 0525 resource->setDisplayOrigin(source->title()); 0526 resource->setIconPath(pathExports); 0527 resource->updateFromRef(FLATPAK_REF(ref)); 0528 resource->setState(AbstractResource::Installed); 0529 source->addResource(resource); 0530 0531 if (freshResource) 0532 *freshResource = true; 0533 0534 Q_ASSERT(resource->uniqueId() == idForInstalledRef(ref) || resource->uniqueId() == idForInstalledRef(ref, QStringLiteral(".desktop"))); 0535 return resource; 0536 } 0537 0538 QSharedPointer<FlatpakSource> FlatpakBackend::findSource(FlatpakInstallation *installation, const QString &origin) const 0539 { 0540 for (const auto &source : m_flatpakSources) { 0541 if (source->installation() == installation && source->name() == origin) { 0542 return source; 0543 } 0544 } 0545 for (const auto &source : m_flatpakLoadingSources) { 0546 if (source->installation() == installation && source->name() == origin) { 0547 return source; 0548 } 0549 } 0550 0551 qWarning() << "Could not find source:" << installation << origin; 0552 return {}; 0553 } 0554 0555 FlatpakResource *FlatpakBackend::getRuntimeForApp(FlatpakResource *resource) const 0556 { 0557 FlatpakResource *runtime = nullptr; 0558 const QString runtimeName = resource->runtime(); 0559 const auto runtimeInfo = QStringView(runtimeName).split(QLatin1Char('/')); 0560 0561 if (runtimeInfo.count() != 3) { 0562 return runtime; 0563 } 0564 0565 for (const auto &source : m_flatpakSources) { 0566 for (auto it = source->m_resources.constBegin(), itEnd = source->m_resources.constEnd(); it != itEnd; ++it) { 0567 const auto &id = it.key(); 0568 if ((*it)->resourceType() == FlatpakResource::Runtime && id.id == runtimeInfo.at(0) && id.branch == runtimeInfo.at(2)) { 0569 runtime = *it; 0570 break; 0571 } 0572 } 0573 } 0574 0575 for (auto installation : m_installations) { 0576 auto instref = flatpak_installation_get_installed_ref(installation, 0577 FLATPAK_REF_KIND_RUNTIME, 0578 runtimeInfo.at(0).toUtf8().constData(), 0579 runtimeInfo.at(1).toUtf8().constData(), 0580 runtimeInfo.at(2).toUtf8().constData(), 0581 m_cancellable, 0582 nullptr); 0583 if (instref) { 0584 return getAppForInstalledRef(installation, instref); 0585 } 0586 } 0587 0588 // TODO if runtime wasn't found, create a new one from available info 0589 if (!runtime) { 0590 qWarning() << "could not find runtime" << runtimeName << resource; 0591 } 0592 0593 return runtime; 0594 } 0595 0596 void FlatpakBackend::addAppFromFlatpakBundle(const QUrl &url, ResultsStream *stream) 0597 { 0598 auto x = qScopeGuard([stream] { 0599 stream->finish(); 0600 }); 0601 g_autoptr(GBytes) appstreamGz = nullptr; 0602 g_autoptr(GError) localError = nullptr; 0603 g_autoptr(GFile) file = nullptr; 0604 g_autoptr(FlatpakBundleRef) bundleRef = nullptr; 0605 AppStream::Component asComponent; 0606 0607 file = g_file_new_for_path(url.toLocalFile().toUtf8().constData()); 0608 bundleRef = flatpak_bundle_ref_new(file, &localError); 0609 0610 if (!bundleRef) { 0611 qWarning() << "Failed to load bundle:" << localError->message; 0612 return; 0613 } 0614 0615 gsize len = 0; 0616 g_autoptr(GBytes) metadata = flatpak_bundle_ref_get_metadata(bundleRef); 0617 const QByteArray metadataContent((char *)g_bytes_get_data(metadata, &len)); 0618 0619 appstreamGz = flatpak_bundle_ref_get_appstream(bundleRef); 0620 if (appstreamGz) { 0621 const auto metadata = metadataFromBytes(appstreamGz, m_cancellable); 0622 if (!metadata.has_value()) { 0623 return; 0624 } 0625 0626 if (std::optional<AppStream::Component> firstComponent = metadata->components().indexSafe(0); firstComponent.has_value()) { 0627 asComponent = *firstComponent; 0628 } else { 0629 qWarning() << "Failed to parse appstream metadata"; 0630 return; 0631 } 0632 } else { 0633 qWarning() << "No appstream metadata in bundle"; 0634 0635 QTemporaryFile tempFile; 0636 tempFile.setAutoRemove(false); 0637 if (!tempFile.open()) { 0638 qWarning() << "Failed to get metadata file"; 0639 return; 0640 } 0641 0642 tempFile.write(metadataContent); 0643 tempFile.close(); 0644 0645 // Parse the temporary file 0646 QSettings setting(tempFile.fileName(), QSettings::NativeFormat); 0647 setting.beginGroup(QLatin1String("Application")); 0648 0649 asComponent.setName(setting.value(QLatin1String("name")).toString()); 0650 0651 tempFile.remove(); 0652 } 0653 0654 g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(preferredInstallation(), m_cancellable, &localError); 0655 if (!refs) { 0656 qWarning() << "Failed to get list of installed refs for listing local updates:" << localError->message; 0657 return; 0658 } 0659 0660 for (uint i = 0; i < refs->len; i++) { 0661 FlatpakRef *ref = FLATPAK_REF(g_ptr_array_index(refs, i)); 0662 FlatpakInstalledRef *iref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); 0663 if (qstrcmp(flatpak_ref_get_commit(ref), flatpak_ref_get_commit(FLATPAK_REF(bundleRef))) == 0) { 0664 auto res = getAppForInstalledRef(preferredInstallation(), iref, nullptr); 0665 if (res) { 0666 Q_EMIT stream->resourcesFound({res}); 0667 } 0668 return; 0669 } 0670 } 0671 0672 FlatpakResource *resource = new FlatpakResource(asComponent, preferredInstallation(), this); 0673 if (!updateAppMetadata(resource, metadataContent)) { 0674 delete resource; 0675 qWarning() << "Failed to update metadata from app bundle"; 0676 return; 0677 } 0678 0679 g_autoptr(GBytes) iconData = flatpak_bundle_ref_get_icon(bundleRef, 128); 0680 if (!iconData) { 0681 iconData = flatpak_bundle_ref_get_icon(bundleRef, 64); 0682 } 0683 0684 if (iconData) { 0685 gsize len = 0; 0686 char *data = (char *)g_bytes_get_data(iconData, &len); 0687 0688 QPixmap pixmap; 0689 pixmap.loadFromData(QByteArray(data, len), "PNG"); 0690 resource->setBundledIcon(pixmap); 0691 } 0692 0693 const QString origin = QString::fromUtf8(flatpak_bundle_ref_get_origin(bundleRef)); 0694 resource->updateFromRef(FLATPAK_REF(bundleRef)); 0695 resource->setDownloadSize(0); 0696 resource->setInstalledSize(flatpak_bundle_ref_get_installed_size(bundleRef)); 0697 resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::AlreadyKnown); 0698 resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::AlreadyKnown); 0699 resource->setFlatpakFileType(FlatpakResource::FileFlatpak); 0700 resource->setOrigin(origin.isEmpty() ? i18n("Local bundle") : origin); 0701 resource->setResourceFile(url); 0702 resource->setState(FlatpakResource::None); 0703 0704 if (!m_localSource) { 0705 m_localSource.reset(new FlatpakSource(this, preferredInstallation())); 0706 m_flatpakSources += m_localSource; 0707 } 0708 m_localSource->addResource(resource); 0709 Q_EMIT stream->resourcesFound({resource}); 0710 } 0711 0712 QString composeRef(bool isRuntime, const QString &name, const QString &branch) 0713 { 0714 return (isRuntime ? "runtime/"_L1 : "app/"_L1) + name + '/'_L1 + QString::fromUtf8(flatpak_get_default_arch()) + '/'_L1 + branch; 0715 } 0716 0717 AppStream::Component fetchComponentFromRemote(const QSettings &settings, GCancellable *cancellable) 0718 { 0719 const QString name = settings.value(QStringLiteral("Flatpak Ref/Name")).toString(); 0720 const QString branch = settings.value(QStringLiteral("Flatpak Ref/Branch")).toString(); 0721 const QString remoteName = settings.value(QStringLiteral("Flatpak Ref/SuggestRemoteName")).toString(); 0722 const bool isRuntime = settings.value(QStringLiteral("Flatpak Ref/IsRuntime")).toBool(); 0723 0724 AppStream::Component asComponent; 0725 asComponent.addUrl(AppStream::Component::UrlKindHomepage, settings.value(QStringLiteral("Flatpak Ref/Homepage")).toString()); 0726 asComponent.setDescription(settings.value(QStringLiteral("Flatpak Ref/Description")).toString()); 0727 asComponent.setName(settings.value(QStringLiteral("Flatpak Ref/Title")).toString()); 0728 asComponent.setSummary(settings.value(QStringLiteral("Flatpak Ref/Comment")).toString()); 0729 asComponent.setId(name); 0730 0731 AppStream::Bundle b; 0732 b.setKind(AppStream::Bundle::KindFlatpak); 0733 b.setId(composeRef(isRuntime, asComponent.name(), branch)); 0734 asComponent.addBundle(b); 0735 0736 // We are going to create a temporary installation and add the remote to it. 0737 // There we will fetch the appstream metadata and then delete that temporary installation. 0738 0739 g_autoptr(GError) localError = nullptr; 0740 const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/discover-flatpak-temporary-") + remoteName; 0741 qDebug() << "Creating temporary installation" << path; 0742 g_autoptr(GFile) file = g_file_new_for_path(QFile::encodeName(path).constData()); 0743 g_autoptr(FlatpakInstallation) tempInstallation = flatpak_installation_new_for_path(file, true, cancellable, &localError); 0744 if (!tempInstallation) { 0745 return asComponent; 0746 } 0747 auto x = qScopeGuard([path] { 0748 QDir(path).removeRecursively(); 0749 }); 0750 0751 g_autoptr(FlatpakRemote) tempRemote = flatpak_remote_new(remoteName.toUtf8().constData()); 0752 populateRemote(tempRemote, 0753 remoteName, 0754 settings.value(QStringLiteral("Flatpak Ref/Url")).toString(), 0755 settings.value(QStringLiteral("Flatpak Ref/GPGKey")).toString()); 0756 if (!flatpak_installation_modify_remote(tempInstallation, tempRemote, cancellable, &localError)) { 0757 qDebug() << "error adding temporary remote" << localError->message; 0758 return asComponent; 0759 } 0760 0761 auto cb = [](const char *status, guint progress, gboolean /*estimating*/, gpointer /*user_data*/) { 0762 qDebug() << "Progress..." << status << progress; 0763 }; 0764 0765 gboolean changed; 0766 if (!flatpak_installation_update_appstream_full_sync(tempInstallation, 0767 remoteName.toUtf8().constData(), 0768 nullptr, 0769 cb, 0770 nullptr, 0771 &changed, 0772 cancellable, 0773 &localError)) { 0774 qDebug() << "error fetching appstream" << localError->message; 0775 return asComponent; 0776 } 0777 Q_ASSERT(changed); 0778 const QString appstreamLocation = path + "/appstream/"_L1 + remoteName + '/'_L1 + QString::fromUtf8(flatpak_get_default_arch()) + "/active"_L1; 0779 0780 AppStream::Pool pool; 0781 #ifdef APPSTREAM_NEW_POOL_API 0782 pool.setLoadStdDataLocations(false); 0783 #if ASQ_CHECK_VERSION(0, 16, 0) 0784 pool.addExtraDataLocation(appstreamLocation, AppStream::Metadata::FormatStyleCatalog); 0785 #else 0786 pool.addExtraDataLocation(appstreamLocation, AppStream::Metadata::FormatStyleCollection); 0787 #endif 0788 #else 0789 pool.clearMetadataLocations(); 0790 pool.addMetadataLocation(appstreamLocation); 0791 pool.setFlags(AppStream::Pool::FlagReadCollection); 0792 pool.setCacheFlags(AppStream::Pool::CacheFlagUseUser); 0793 0794 const QString subdir = flatpak_installation_get_id(tempInstallation) + QLatin1Char('/') + remoteName; 0795 pool.setCacheLocation(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/flatpak-appstream-temp/" + subdir); 0796 QDir().mkpath(pool.cacheLocation()); 0797 #endif 0798 0799 if (!pool.load()) { 0800 qDebug() << "error loading pool" << pool.lastError(); 0801 return asComponent; 0802 } 0803 0804 // TODO optimise, this lookup should happen in libappstream 0805 auto comps = pool.components(); 0806 kFilterInPlace<AppStream::ComponentBox>(comps, [name, branch](const AppStream::Component &component) { 0807 const QString id = component.bundle(AppStream::Bundle::KindFlatpak).id(); 0808 // app/app.getspace.Space/x86_64/stable 0809 return id.section(QLatin1Char('/'), 1, 1) == name && (branch.isEmpty() || id.section(QLatin1Char('/'), 3, 3) == branch); 0810 }); 0811 if (comps.isEmpty()) { 0812 qDebug() << "could not find" << name << "in" << remoteName; 0813 return asComponent; 0814 } 0815 return *comps.indexSafe(0); 0816 } 0817 0818 void FlatpakBackend::addAppFromFlatpakRef(const QUrl &url, ResultsStream *stream) 0819 { 0820 Q_ASSERT(url.isLocalFile()); 0821 QSettings settings(url.toLocalFile(), QSettings::NativeFormat); 0822 const QString refurl = settings.value(QStringLiteral("Flatpak Ref/Url")).toString(); 0823 const QString name = settings.value(QStringLiteral("Flatpak Ref/Name")).toString(); 0824 const QString remoteName = settings.value(QStringLiteral("Flatpak Ref/SuggestRemoteName")).toString(); 0825 const QString branch = settings.value(QStringLiteral("Flatpak Ref/Branch")).toString(); 0826 const bool isRuntime = settings.value(QStringLiteral("Flatpak Ref/IsRuntime")).toBool(); 0827 g_autoptr(GError) error = nullptr; 0828 0829 // If we already added the remote, just go with it 0830 g_autoptr(FlatpakRemote) remote = flatpak_installation_get_remote_by_name(preferredInstallation(), remoteName.toUtf8().constData(), m_cancellable, &error); 0831 g_autofree char *remoteUrl = flatpak_remote_get_url(remote); 0832 if (remote && QString::fromUtf8(remoteUrl) != refurl) { 0833 remote = nullptr; 0834 } 0835 if (remote) { 0836 Q_ASSERT(!m_refreshAppstreamMetadataJobs.contains(remote)); 0837 m_refreshAppstreamMetadataJobs.insert(remote); 0838 auto source = integrateRemote(preferredInstallation(), remote); 0839 if (source) { 0840 const QString ref = composeRef(isRuntime, name, branch); 0841 auto searchComponent = [this, stream, source, ref] { 0842 auto comps = source->componentsByFlatpakId(ref); 0843 auto resources = kTransform<QVector<StreamResult>>(comps, [this, source](const auto &comp) { 0844 return resourceForComponent(comp, source); 0845 }); 0846 Q_EMIT stream->resourcesFound(resources); 0847 stream->finish(); 0848 }; 0849 if (source->m_pool) { 0850 QTimer::singleShot(0, this, searchComponent); 0851 } else { 0852 connect(this, &FlatpakBackend::initialized, stream, searchComponent); 0853 } 0854 return; 0855 } 0856 } 0857 0858 AppStream::Component asComponent = fetchComponentFromRemote(settings, m_cancellable); 0859 const QString iconUrl = settings.value(QStringLiteral("Flatpak Ref/Icon")).toString(); 0860 if (!iconUrl.isEmpty()) { 0861 AppStream::Icon icon; 0862 icon.setKind(AppStream::Icon::KindRemote); 0863 icon.setUrl(QUrl(iconUrl)); 0864 asComponent.addIcon(icon); 0865 } 0866 0867 auto resource = new FlatpakResource(asComponent, preferredInstallation(), this); 0868 resource->setFlatpakFileType(FlatpakResource::FileFlatpakRef); 0869 resource->setResourceFile(url); 0870 resource->setResourceLocation(QUrl(refurl)); 0871 resource->setOrigin(remoteName); 0872 resource->setDisplayOrigin(remote ? QString::fromUtf8(flatpak_remote_get_title(remote)) : QString()); 0873 resource->setFlatpakName(name); 0874 resource->setArch(QString::fromUtf8(flatpak_get_default_arch())); 0875 resource->setBranch(branch); 0876 resource->setType(isRuntime ? FlatpakResource::Runtime : FlatpakResource::DesktopApp); 0877 0878 QUrl runtimeUrl = QUrl(settings.value(QStringLiteral("Flatpak Ref/RuntimeRepo")).toString()); 0879 auto refSource = QSharedPointer<FlatpakSource>::create(this, preferredInstallation()); 0880 resource->setTemporarySource(refSource); 0881 m_flatpakSources += refSource; 0882 if (!runtimeUrl.isEmpty()) { 0883 // We need to fetch metadata to find information about required runtime 0884 auto fw = new QFutureWatcher<QByteArray>(this); 0885 connect(fw, &QFutureWatcher<QByteArray>::finished, this, [this, resource, fw, runtimeUrl, stream, refSource]() { 0886 fw->deleteLater(); 0887 const auto metadata = fw->result(); 0888 // Even when we failed to fetch information about runtime we still want to show the application 0889 if (metadata.isEmpty()) { 0890 onFetchMetadataFinished(resource, metadata); 0891 } else { 0892 updateAppMetadata(resource, metadata); 0893 0894 auto runtime = getRuntimeForApp(resource); 0895 if (!runtime || (runtime && !runtime->isInstalled())) { 0896 auto repoStream = new ResultsStream(QLatin1String("FlatpakStream-searchrepo-") + runtimeUrl.toString()); 0897 connect(repoStream, &ResultsStream::resourcesFound, this, [this, resource, stream, refSource](const QVector<StreamResult> &resources) { 0898 for (auto res : resources) { 0899 installApplication(res.resource); 0900 } 0901 refSource->addResource(resource); 0902 Q_EMIT stream->resourcesFound({resource}); 0903 stream->finish(); 0904 }); 0905 0906 auto fetchRemoteResource = new FlatpakFetchRemoteResourceJob(runtimeUrl, repoStream, this); 0907 fetchRemoteResource->start(); 0908 return; 0909 } else { 0910 refSource->addResource(resource); 0911 } 0912 } 0913 Q_EMIT stream->resourcesFound({resource}); 0914 stream->finish(); 0915 }); 0916 fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, resource, m_cancellable)); 0917 } else { 0918 refSource->addResource(resource); 0919 Q_EMIT stream->resourcesFound({resource}); 0920 stream->finish(); 0921 } 0922 } 0923 0924 void FlatpakBackend::addSourceFromFlatpakRepo(const QUrl &url, ResultsStream *stream) 0925 { 0926 auto x = qScopeGuard([stream] { 0927 stream->finish(); 0928 }); 0929 Q_ASSERT(url.isLocalFile()); 0930 QSettings settings(url.toLocalFile(), QSettings::NativeFormat); 0931 0932 const QString gpgKey = settings.value(QStringLiteral("Flatpak Repo/GPGKey")).toString(); 0933 const QString title = settings.value(QStringLiteral("Flatpak Repo/Title")).toString(); 0934 const QString repoUrl = settings.value(QStringLiteral("Flatpak Repo/Url")).toString(); 0935 0936 if (gpgKey.isEmpty() || title.isEmpty() || repoUrl.isEmpty()) { 0937 return; 0938 } 0939 0940 if (gpgKey.startsWith(QLatin1String("http://")) || gpgKey.startsWith(QLatin1String("https://"))) { 0941 return; 0942 } 0943 0944 AppStream::Component asComponent; 0945 asComponent.addUrl(AppStream::Component::UrlKindHomepage, settings.value(QStringLiteral("Flatpak Repo/Homepage")).toString()); 0946 asComponent.setSummary(settings.value(QStringLiteral("Flatpak Repo/Comment")).toString()); 0947 asComponent.setDescription(settings.value(QStringLiteral("Flatpak Repo/Description")).toString()); 0948 asComponent.setName(title); 0949 asComponent.setId(settings.value(QStringLiteral("Flatpak Repo/Title")).toString()); 0950 0951 const QString iconUrl = settings.value(QStringLiteral("Flatpak Repo/Icon")).toString(); 0952 if (!iconUrl.isEmpty()) { 0953 AppStream::Icon icon; 0954 icon.setKind(AppStream::Icon::KindRemote); 0955 icon.setUrl(QUrl(iconUrl)); 0956 asComponent.addIcon(icon); 0957 } 0958 0959 auto resource = new FlatpakResource(asComponent, preferredInstallation(), this); 0960 // Use metadata only for stuff which are not common for all resources 0961 resource->addMetadata(QStringLiteral("gpg-key"), gpgKey); 0962 resource->addMetadata(QStringLiteral("repo-url"), repoUrl); 0963 resource->setBranch(settings.value(QStringLiteral("Flatpak Repo/DefaultBranch")).toString()); 0964 resource->setFlatpakName(url.fileName().remove(QStringLiteral(".flatpakrepo"))); 0965 resource->setType(FlatpakResource::Source); 0966 0967 g_autoptr(FlatpakRemote) repo = 0968 flatpak_installation_get_remote_by_name(preferredInstallation(), resource->flatpakName().toUtf8().constData(), m_cancellable, nullptr); 0969 if (!repo) { 0970 resource->setState(AbstractResource::State::None); 0971 } else { 0972 resource->setState(AbstractResource::State::Installed); 0973 } 0974 0975 Q_EMIT stream->resourcesFound({resource}); 0976 } 0977 0978 void FlatpakBackend::loadAppsFromAppstreamData() 0979 { 0980 for (auto installation : std::as_const(m_installations)) { 0981 // Load applications from appstream metadata 0982 if (g_cancellable_is_cancelled(m_cancellable)) 0983 break; 0984 0985 if (!loadAppsFromAppstreamData(installation)) { 0986 qWarning() << "Failed to load packages from appstream data from installation" << installation; 0987 } 0988 } 0989 } 0990 0991 bool FlatpakBackend::loadAppsFromAppstreamData(FlatpakInstallation *flatpakInstallation) 0992 { 0993 Q_ASSERT(flatpakInstallation); 0994 0995 GError *error = nullptr; 0996 g_autoptr(GPtrArray) remotes = flatpak_installation_list_remotes(flatpakInstallation, m_cancellable, &error); 0997 if (!remotes) { 0998 qWarning() << "failed to list remotes" << error->message; 0999 return false; 1000 } 1001 1002 for (uint i = 0; i < remotes->len; i++) { 1003 FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i)); 1004 loadRemote(flatpakInstallation, remote); 1005 } 1006 return true; 1007 } 1008 1009 void FlatpakBackend::loadRemote(FlatpakInstallation *installation, FlatpakRemote *remote) 1010 { 1011 g_autoptr(GFile) fileTimestamp = flatpak_remote_get_appstream_timestamp(remote, flatpak_get_default_arch()); 1012 1013 Q_ASSERT(!m_refreshAppstreamMetadataJobs.contains(remote)); 1014 m_refreshAppstreamMetadataJobs.insert(remote); 1015 1016 g_autofree char *path_str = g_file_get_path(fileTimestamp); 1017 QFileInfo fileInfo(QFile::decodeName(path_str)); 1018 if (!fileInfo.exists() || fileInfo.lastModified().toUTC().secsTo(QDateTime::currentDateTimeUtc()) > 21600) { 1019 // Refresh appstream metadata in case they have never been refreshed or the cache is older than 6 hours 1020 checkForRemoteUpdates(installation, remote); 1021 } else { 1022 auto source = integrateRemote(installation, remote); 1023 Q_ASSERT(findSource(installation, QString::fromUtf8(flatpak_remote_get_name(remote))) == source); 1024 } 1025 } 1026 1027 void FlatpakBackend::unloadRemote(FlatpakInstallation *installation, FlatpakRemote *remote) 1028 { 1029 acquireFetching(true); 1030 for (auto it = m_flatpakSources.begin(); it != m_flatpakSources.end();) { 1031 if ((*it)->url() == QString::fromUtf8(flatpak_remote_get_url(remote)) && (*it)->installation() == installation) { 1032 qDebug() << "unloading remote" << (*it) << remote; 1033 it = m_flatpakSources.erase(it); 1034 } else { 1035 ++it; 1036 } 1037 } 1038 acquireFetching(false); 1039 } 1040 1041 void FlatpakBackend::metadataRefreshed(FlatpakRemote *remote) 1042 { 1043 const bool removed = m_refreshAppstreamMetadataJobs.remove(remote); 1044 Q_ASSERT(removed); 1045 if (m_refreshAppstreamMetadataJobs.isEmpty()) { 1046 for (auto installation : std::as_const(m_installations)) { 1047 // Load local updates, comparing current and latest commit 1048 loadLocalUpdates(installation); 1049 1050 if (g_cancellable_is_cancelled(m_cancellable)) 1051 break; 1052 } 1053 } 1054 } 1055 1056 void FlatpakBackend::createPool(QSharedPointer<FlatpakSource> source) 1057 { 1058 if (source->m_pool) { 1059 metadataRefreshed(source->remote()); 1060 return; 1061 } 1062 1063 const QString appstreamDirPath = source->appstreamDir(); 1064 if (!QFile::exists(appstreamDirPath)) { 1065 qWarning() << "No" << appstreamDirPath << "appstream metadata found for" << source->name(); 1066 metadataRefreshed(source->remote()); 1067 return; 1068 } 1069 1070 AppStream::Pool *pool = new AppStream::Pool; 1071 auto fw = new QFutureWatcher<bool>(this); 1072 connect(fw, &QFutureWatcher<bool>::finished, this, [this, fw, pool, source]() { 1073 source->m_pool = pool; 1074 m_flatpakLoadingSources.removeAll(source); 1075 if (fw->result()) { 1076 m_flatpakSources += source; 1077 } else { 1078 qWarning() << "Could not open the AppStream metadata pool" << pool->lastError(); 1079 } 1080 metadataRefreshed(source->remote()); 1081 acquireFetching(false); 1082 fw->deleteLater(); 1083 }); 1084 acquireFetching(true); 1085 1086 #ifdef APPSTREAM_NEW_POOL_API 1087 pool->setLoadStdDataLocations(false); 1088 #if ASQ_CHECK_VERSION(0, 16, 0) 1089 pool->addExtraDataLocation(appstreamDirPath, AppStream::Metadata::FormatStyleCatalog); 1090 #else 1091 pool->addExtraDataLocation(appstreamDirPath, AppStream::Metadata::FormatStyleCollection); 1092 #endif 1093 #else 1094 pool->clearMetadataLocations(); 1095 pool->addMetadataLocation(appstreamDirPath); 1096 pool->setFlags(AppStream::Pool::FlagReadCollection); 1097 pool->setCacheFlags(AppStream::Pool::CacheFlagUseUser); 1098 1099 const QString subdir = flatpak_installation_get_id(source->installation()) + QLatin1Char('/') + source->name(); 1100 pool->setCacheLocation(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/flatpak-appstream/" + subdir); 1101 QDir().mkpath(pool->cacheLocation()); 1102 #endif 1103 1104 fw->setFuture(QtConcurrent::run(&m_threadPool, [pool] { 1105 return pool->load(); 1106 })); 1107 } 1108 1109 QSharedPointer<FlatpakSource> FlatpakBackend::integrateRemote(FlatpakInstallation *flatpakInstallation, FlatpakRemote *remote) 1110 { 1111 Q_ASSERT(m_refreshAppstreamMetadataJobs.contains(remote)); 1112 m_sources->addRemote(remote, flatpakInstallation); 1113 for (auto source : std::as_const(m_flatpakSources)) { 1114 if (source->url() == QString::fromUtf8(flatpak_remote_get_url(remote)) && source->installation() == flatpakInstallation 1115 && source->name() == QString::fromUtf8(flatpak_remote_get_name(remote))) { 1116 createPool(source); 1117 return source; 1118 } 1119 } 1120 for (auto source : std::as_const(m_flatpakLoadingSources)) { 1121 if (source->url() == QString::fromUtf8(flatpak_remote_get_url(remote)) && source->installation() == flatpakInstallation 1122 && source->name() == QString::fromUtf8(flatpak_remote_get_name(remote))) { 1123 createPool(source); 1124 return source; 1125 } 1126 } 1127 1128 auto source = QSharedPointer<FlatpakSource>::create(this, flatpakInstallation, remote); 1129 if (!source->isEnabled() || flatpak_remote_get_noenumerate(remote)) { 1130 m_flatpakSources += source; 1131 metadataRefreshed(remote); 1132 return source; 1133 } 1134 1135 createPool(source); 1136 m_flatpakLoadingSources << source; 1137 return source; 1138 } 1139 1140 void FlatpakBackend::loadLocalUpdates(FlatpakInstallation *flatpakInstallation) 1141 { 1142 g_autoptr(GError) localError = nullptr; 1143 g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(flatpakInstallation, m_cancellable, &localError); 1144 if (!refs) { 1145 qWarning() << "Failed to get list of installed refs for listing local updates:" << localError->message; 1146 return; 1147 } 1148 1149 for (uint i = 0; i < refs->len; i++) { 1150 FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); 1151 1152 const gchar *latestCommit = flatpak_installed_ref_get_latest_commit(ref); 1153 1154 if (!latestCommit) { 1155 qWarning() << "Couldn't get latest commit for" << flatpak_ref_get_name(FLATPAK_REF(ref)); 1156 continue; 1157 } 1158 1159 const gchar *commit = flatpak_ref_get_commit(FLATPAK_REF(ref)); 1160 if (g_strcmp0(commit, latestCommit) == 0) { 1161 continue; 1162 } 1163 1164 FlatpakResource *resource = getAppForInstalledRef(flatpakInstallation, ref); 1165 if (resource) { 1166 resource->setState(AbstractResource::Upgradeable); 1167 updateAppSize(resource); 1168 } 1169 Q_ASSERT(!resource->temporarySource()); 1170 } 1171 } 1172 1173 bool FlatpakBackend::parseMetadataFromAppBundle(FlatpakResource *resource) 1174 { 1175 g_autoptr(GError) localError = nullptr; 1176 g_autoptr(FlatpakRef) ref = flatpak_ref_parse(resource->ref().toUtf8().constData(), &localError); 1177 if (!ref) { 1178 qWarning() << "Failed to parse" << resource->ref() << localError->message; 1179 return false; 1180 } else { 1181 resource->updateFromRef(ref); 1182 } 1183 1184 return true; 1185 } 1186 1187 bool FlatpakBackend::setupFlatpakInstallations(GError **error) 1188 { 1189 if (qEnvironmentVariableIsSet("FLATPAK_TEST_MODE")) { 1190 const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/discover-flatpak-test"); 1191 qDebug() << "running flatpak backend on test mode" << path; 1192 g_autoptr(GFile) file = g_file_new_for_path(QFile::encodeName(path).constData()); 1193 m_installations << flatpak_installation_new_for_path(file, true, m_cancellable, error); 1194 return m_installations.constLast() != nullptr; 1195 } 1196 1197 g_autoptr(GPtrArray) installations = flatpak_get_system_installations(m_cancellable, error); 1198 if (*error) { 1199 qWarning() << "Failed to call flatpak_get_system_installations:" << (*error)->message; 1200 } 1201 for (uint i = 0; installations && i < installations->len; i++) { 1202 auto installation = FLATPAK_INSTALLATION(g_ptr_array_index(installations, i)); 1203 g_object_ref(installation); 1204 m_installations << installation; 1205 } 1206 1207 auto user = flatpak_installation_new_user(m_cancellable, error); 1208 if (user) { 1209 m_installations << user; 1210 } 1211 1212 return !m_installations.isEmpty(); 1213 } 1214 1215 void FlatpakBackend::updateAppInstalledMetadata(FlatpakInstalledRef *installedRef, FlatpakResource *resource) 1216 { 1217 // Update the rest 1218 resource->updateFromRef(FLATPAK_REF(installedRef)); 1219 resource->setInstalledSize(flatpak_installed_ref_get_installed_size(installedRef)); 1220 resource->setOrigin(QString::fromUtf8(flatpak_installed_ref_get_origin(installedRef))); 1221 if (resource->state() < AbstractResource::Installed) 1222 resource->setState(AbstractResource::Installed); 1223 } 1224 1225 bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource) 1226 { 1227 if (resource->resourceType() != FlatpakResource::DesktopApp) { 1228 return true; 1229 } 1230 1231 const QString path = resource->installPath() + QStringLiteral("/metadata"); 1232 1233 if (QFile::exists(path)) { 1234 return updateAppMetadata(resource, path); 1235 } else { 1236 auto fw = new QFutureWatcher<QByteArray>(this); 1237 connect(fw, &QFutureWatcher<QByteArray>::finished, this, [this, resource, fw]() { 1238 const auto metadata = fw->result(); 1239 if (!metadata.isEmpty()) 1240 onFetchMetadataFinished(resource, metadata); 1241 fw->deleteLater(); 1242 }); 1243 fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, resource, m_cancellable)); 1244 1245 // Return false to indicate we cannot continue (right now used only in updateAppSize()) 1246 return false; 1247 } 1248 } 1249 1250 void FlatpakBackend::onFetchMetadataFinished(FlatpakResource *resource, const QByteArray &metadata) 1251 { 1252 updateAppMetadata(resource, metadata); 1253 1254 // Right now we attempt to update metadata for calculating the size so call updateSizeFromRemote() 1255 // as it's what we want. In future if there are other reason to update metadata we will need to somehow 1256 // distinguish between these calls 1257 updateAppSizeFromRemote(resource); 1258 } 1259 1260 bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource, const QString &path) 1261 { 1262 // Parse the temporary file 1263 QSettings setting(path, QSettings::NativeFormat); 1264 setting.beginGroup(QLatin1String("Application")); 1265 // Set the runtime in form of name/arch/version which can be later easily parsed 1266 resource->setRuntime(setting.value(QLatin1String("runtime")).toString()); 1267 // TODO get more information? 1268 return true; 1269 } 1270 1271 bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource, const QByteArray &data) 1272 { 1273 // We just find the runtime with a regex, QSettings only can read from disk (and so does KConfig) 1274 const QRegularExpression rx(QStringLiteral("runtime=(.*)")); 1275 const auto match = rx.match(QString::fromUtf8(data)); 1276 if (!match.isValid()) { 1277 return false; 1278 } 1279 1280 resource->setRuntime(match.captured(1)); 1281 return true; 1282 } 1283 1284 bool FlatpakBackend::updateAppSize(FlatpakResource *resource) 1285 { 1286 // Check if the size is already set, we should also distinguish between download and installed size, 1287 // right now it doesn't matter whether we get size for installed or not installed app, but if we 1288 // start making difference then for not installed app check download and install size separately 1289 1290 if (resource->state() == AbstractResource::Installed) { 1291 // The size appears to be already set (from updateAppInstalledMetadata() apparently) 1292 if (resource->installedSize() > 0) { 1293 return true; 1294 } 1295 } else { 1296 if (resource->installedSize() > 0 && resource->downloadSize() > 0) { 1297 return true; 1298 } 1299 } 1300 1301 // Check if we know the needed runtime which is needed for calculating the size 1302 if (resource->runtime().isEmpty()) { 1303 if (!updateAppMetadata(resource)) { 1304 return false; 1305 } 1306 } 1307 1308 return updateAppSizeFromRemote(resource); 1309 } 1310 1311 bool FlatpakBackend::updateAppSizeFromRemote(FlatpakResource *resource) 1312 { 1313 // Calculate the runtime size 1314 if (resource->state() == AbstractResource::None && resource->resourceType() == FlatpakResource::DesktopApp) { 1315 auto runtime = getRuntimeForApp(resource); 1316 if (runtime) { 1317 // Re-check runtime state if case a new one was created 1318 updateAppState(runtime); 1319 1320 if (!runtime->isInstalled()) { 1321 if (!updateAppSize(runtime)) { 1322 qWarning() << "Failed to get runtime size needed for total size of" << resource->name(); 1323 return false; 1324 } 1325 // Set required download size to include runtime size even now, in case we fail to 1326 // get the app size (e.g. when installing bundles where download size is 0) 1327 resource->setDownloadSize(runtime->downloadSize()); 1328 } 1329 } 1330 } 1331 1332 if (resource->state() == AbstractResource::Installed) { 1333 g_autoptr(FlatpakInstalledRef) ref = nullptr; 1334 ref = getInstalledRefForApp(resource); 1335 if (!ref) { 1336 qWarning() << "Failed to get installed size of" << resource->name(); 1337 return false; 1338 } 1339 resource->setInstalledSize(flatpak_installed_ref_get_installed_size(ref)); 1340 } else if (resource->resourceType() != FlatpakResource::Source) { 1341 if (resource->origin().isEmpty()) { 1342 qWarning() << "Failed to get size of" << resource->name() << " because of missing origin"; 1343 return false; 1344 } 1345 1346 if (resource->propertyState(FlatpakResource::DownloadSize) == FlatpakResource::Fetching) { 1347 return true; 1348 } 1349 1350 auto futureWatcher = new QFutureWatcher<FlatpakRemoteRef *>(this); 1351 connect(futureWatcher, &QFutureWatcher<FlatpakRemoteRef *>::finished, this, [this, resource, futureWatcher]() { 1352 g_autoptr(FlatpakRemoteRef) remoteRef = futureWatcher->result(); 1353 if (remoteRef) { 1354 onFetchSizeFinished(resource, flatpak_remote_ref_get_download_size(remoteRef), flatpak_remote_ref_get_installed_size(remoteRef)); 1355 } else { 1356 resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::UnknownOrFailed); 1357 resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::UnknownOrFailed); 1358 } 1359 futureWatcher->deleteLater(); 1360 }); 1361 resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::Fetching); 1362 resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::Fetching); 1363 1364 futureWatcher->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::findRemoteRef, resource, m_cancellable)); 1365 } 1366 1367 return true; 1368 } 1369 1370 void FlatpakBackend::onFetchSizeFinished(FlatpakResource *resource, guint64 downloadSize, guint64 installedSize) 1371 { 1372 FlatpakResource *runtime = nullptr; 1373 if (resource->state() == AbstractResource::None && resource->resourceType() == FlatpakResource::DesktopApp) { 1374 runtime = getRuntimeForApp(resource); 1375 } 1376 1377 if (runtime && !runtime->isInstalled()) { 1378 resource->setDownloadSize(runtime->downloadSize() + downloadSize); 1379 } else { 1380 resource->setDownloadSize(downloadSize); 1381 } 1382 resource->setInstalledSize(installedSize); 1383 } 1384 1385 void FlatpakBackend::updateAppState(FlatpakResource *resource) 1386 { 1387 g_autoptr(FlatpakInstalledRef) ref = getInstalledRefForApp(resource); 1388 if (ref) { 1389 // If the app is installed, we can set information about commit, arch etc. 1390 updateAppInstalledMetadata(ref, resource); 1391 } else { 1392 // TODO check if the app is actually still available 1393 resource->setState(AbstractResource::None); 1394 } 1395 } 1396 1397 void FlatpakBackend::acquireFetching(bool f) 1398 { 1399 if (f) 1400 m_isFetching++; 1401 else 1402 m_isFetching--; 1403 1404 if ((!f && m_isFetching == 0) || (f && m_isFetching == 1)) { 1405 Q_EMIT fetchingChanged(); 1406 } 1407 1408 if (m_isFetching == 0) 1409 Q_EMIT initialized(); 1410 } 1411 1412 int FlatpakBackend::updatesCount() const 1413 { 1414 return m_updater->updatesCount(); 1415 } 1416 1417 bool FlatpakBackend::flatpakResourceLessThan(const StreamResult &l, const StreamResult &r) const 1418 { 1419 if (l.sortScore == r.sortScore) { 1420 return flatpakResourceLessThan(l.resource, r.resource); 1421 } 1422 return l.sortScore < r.sortScore; 1423 } 1424 1425 bool FlatpakBackend::flatpakResourceLessThan(AbstractResource *l, AbstractResource *r) const 1426 { 1427 // clang-format off 1428 return (l->isInstalled() != r->isInstalled()) ? l->isInstalled() 1429 : (l->origin() != r->origin()) ? m_sources->originIndex(l->origin()) < m_sources->originIndex(r->origin()) 1430 : (l->rating() && r->rating() && l->rating()->ratingPoints() != r->rating()->ratingPoints()) ? l->rating()->ratingPoints() > r->rating()->ratingPoints() 1431 : l < r; 1432 // clang-format on 1433 } 1434 1435 ResultsStream *FlatpakBackend::deferredResultStream(const QString &streamName, std::function<void(ResultsStream *)> callback) 1436 { 1437 ResultsStream *stream = new ResultsStream(streamName); 1438 1439 auto f = [stream, callback = std::move(callback)] { 1440 callback(stream); 1441 }; 1442 1443 if (isFetching()) { 1444 connect(this, &FlatpakBackend::initialized, stream, f); 1445 } else { 1446 QTimer::singleShot(0, this, f); 1447 } 1448 1449 return stream; 1450 } 1451 1452 ResultsStream *FlatpakBackend::search(const AbstractResourcesBackend::Filters &filter) 1453 { 1454 const auto fileName = filter.resourceUrl.fileName(); 1455 if (fileName.endsWith(QLatin1String(".flatpakrepo")) || fileName.endsWith(QLatin1String(".flatpakref")) || fileName.endsWith(QLatin1String(".flatpak"))) { 1456 auto stream = new ResultsStream(QLatin1String("FlatpakStream-http-") + fileName); 1457 FlatpakFetchRemoteResourceJob *fetchResourceJob = new FlatpakFetchRemoteResourceJob(filter.resourceUrl, stream, this); 1458 fetchResourceJob->start(); 1459 return stream; 1460 } else if (filter.resourceUrl.scheme() == QLatin1String("appstream")) { 1461 return findResourceByPackageName(filter.resourceUrl); 1462 } else if (!filter.resourceUrl.isEmpty()) { 1463 return new ResultsStream(QStringLiteral("FlatpakStream-void"), {}); 1464 } else if (filter.state == AbstractResource::Upgradeable) { 1465 return deferredResultStream(u"FlatpakStream-upgradeable"_s, [this](ResultsStream *stream) { 1466 auto fw = new QFutureWatcher<QHash<FlatpakInstallation *, QVector<FlatpakInstalledRef *>>>(this); 1467 connect(fw, &QFutureWatcher<QHash<FlatpakInstallation *, QVector<FlatpakInstalledRef *>>>::finished, this, [this, fw, stream]() { 1468 if (g_cancellable_is_cancelled(m_cancellable)) { 1469 stream->finish(); 1470 fw->deleteLater(); 1471 return; 1472 } 1473 1474 const auto refs = fw->result(); 1475 QVector<StreamResult> resources; 1476 for (auto it = refs.constBegin(), itEnd = refs.constEnd(); it != itEnd; ++it) { 1477 resources.reserve(resources.size() + it->size()); 1478 for (auto ref : std::as_const(it.value())) { 1479 bool fresh; 1480 auto resource = getAppForInstalledRef(it.key(), ref, &fresh); 1481 g_object_unref(ref); 1482 if (resource) { 1483 resource->setState(AbstractResource::Upgradeable, !fresh); 1484 updateAppSize(resource); 1485 if (resource->resourceType() == FlatpakResource::Runtime) { 1486 resources.prepend(resource); 1487 } else { 1488 resources.append(resource); 1489 } 1490 } 1491 } 1492 } 1493 1494 if (!resources.isEmpty()) 1495 Q_EMIT stream->resourcesFound(resources); 1496 stream->finish(); 1497 fw->deleteLater(); 1498 }); 1499 1500 QVector<FlatpakInstallation *> installations = m_installations; 1501 auto cancellable = m_cancellable; 1502 fw->setFuture(QtConcurrent::run(&m_threadPool, [installations, cancellable] { 1503 QHash<FlatpakInstallation *, QVector<FlatpakInstalledRef *>> ret; 1504 if (g_cancellable_is_cancelled(cancellable)) { 1505 qWarning() << "Job cancelled"; 1506 return ret; 1507 } 1508 1509 for (auto installation : std::as_const(installations)) { 1510 g_autoptr(GError) localError = nullptr; 1511 g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs_for_update(installation, cancellable, &localError); 1512 if (!refs) { 1513 qWarning() << "Failed to get list of installed refs for listing updates:" << localError->message; 1514 continue; 1515 } 1516 if (g_cancellable_is_cancelled(cancellable)) { 1517 qWarning() << "Job cancelled"; 1518 ret.clear(); 1519 break; 1520 } 1521 1522 if (refs->len == 0) { 1523 continue; 1524 } 1525 1526 auto ¤t = ret[installation]; 1527 current.reserve(refs->len); 1528 for (uint i = 0; i < refs->len; i++) { 1529 FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); 1530 g_object_ref(ref); 1531 current.append(ref); 1532 } 1533 } 1534 return ret; 1535 })); 1536 }); 1537 } else if (filter.state == AbstractResource::Installed) { 1538 return deferredResultStream(u"FlatpakStream-installed"_s, [this, filter](ResultsStream *stream) { 1539 QVector<StreamResult> resources; 1540 for (auto installation : std::as_const(m_installations)) { 1541 g_autoptr(GError) localError = nullptr; 1542 g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(installation, m_cancellable, &localError); 1543 if (!refs) { 1544 qWarning() << "Failed to get list of installed refs for listing installed:" << localError->message; 1545 continue; 1546 } 1547 1548 resources.reserve(resources.size() + refs->len); 1549 for (uint i = 0; i < refs->len; i++) { 1550 FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i)); 1551 QString name = QString::fromUtf8(flatpak_installed_ref_get_appdata_name(ref)); 1552 if (name.endsWith(QLatin1String(".Debug")) || name.endsWith(QLatin1String(".Locale")) || name.endsWith(QLatin1String(".BaseApp")) 1553 || name.endsWith(QLatin1String(".Docs"))) 1554 continue; 1555 1556 auto resource = getAppForInstalledRef(installation, ref); 1557 if (!resource) { 1558 continue; 1559 } 1560 if (!filter.search.isEmpty() && !resource->name().contains(filter.search, Qt::CaseInsensitive) 1561 && !resource->appstreamId().contains(filter.search, Qt::CaseInsensitive)) 1562 continue; 1563 if (resource->resourceType() == FlatpakResource::Runtime) { 1564 resources.prepend(resource); 1565 } else { 1566 resources.append(resource); 1567 } 1568 } 1569 } 1570 if (!resources.isEmpty()) 1571 Q_EMIT stream->resourcesFound(resources); 1572 stream->finish(); 1573 }); 1574 } else { 1575 return deferredResultStream(u"FlatpakStream"_s, [this, filter](ResultsStream *stream) { 1576 QVector<StreamResult> prioritary, rest; 1577 for (const auto &source : std::as_const(m_flatpakSources)) { 1578 QList<FlatpakResource *> resources; 1579 if (source->m_pool) { 1580 const auto a = !filter.search.isEmpty() ? source->m_pool->search(filter.search) 1581 #if ASQ_CHECK_VERSION(0, 15, 6) 1582 : filter.category ? AppStreamUtils::componentsByCategories(source->m_pool, filter.category, AppStream::Bundle::KindFlatpak) 1583 #endif 1584 : source->m_pool->components(); 1585 resources = kTransform<QList<FlatpakResource *>>(a, [this, &source](const auto &comp) { 1586 return resourceForComponent(comp, source); 1587 }); 1588 } else { 1589 resources = source->m_resources.values(); 1590 } 1591 1592 for (auto r : std::as_const(resources)) { 1593 const bool matchById = r->appstreamId().compare(filter.search, Qt::CaseInsensitive) == 0; 1594 if (r->type() == AbstractResource::Technical && filter.state != AbstractResource::Upgradeable && !matchById) { 1595 continue; 1596 } 1597 if (r->state() < filter.state) 1598 continue; 1599 1600 if (!filter.extends.isEmpty() && !r->extends().contains(filter.extends)) 1601 continue; 1602 1603 if (!filter.mimetype.isEmpty() && !r->mimetypes().contains(filter.mimetype)) 1604 continue; 1605 1606 if (filter.search.isEmpty() || matchById) { 1607 rest += r; 1608 } else if (r->name().contains(filter.search, Qt::CaseInsensitive)) { 1609 prioritary += r; 1610 } else if (r->comment().contains(filter.search, Qt::CaseInsensitive)) { 1611 rest += r; 1612 // trust The search terms provided by appstream are relevant, this makes possible finding "gimp" 1613 // since the name() is "GNU Image Manipulation Program" 1614 } else if (r->appstreamId().contains(filter.search, Qt::CaseInsensitive)) { 1615 rest += r; 1616 } 1617 } 1618 } 1619 auto f = [this](auto l, auto r) { 1620 return flatpakResourceLessThan(l, r); 1621 }; 1622 std::sort(rest.begin(), rest.end(), f); 1623 std::sort(prioritary.begin(), prioritary.end(), f); 1624 rest = prioritary + rest; 1625 if (!rest.isEmpty()) 1626 Q_EMIT stream->resourcesFound(rest); 1627 stream->finish(); 1628 }); 1629 } 1630 } 1631 1632 bool FlatpakBackend::isTracked(FlatpakResource *resource) const 1633 { 1634 const auto uid = resource->uniqueId(); 1635 return std::any_of(m_flatpakSources.constBegin(), m_flatpakSources.constEnd(), [uid](const auto &source) { 1636 return source->m_resources.contains(uid); 1637 }); 1638 } 1639 1640 QVector<StreamResult> FlatpakBackend::resultsByAppstreamName(const QString &name) const 1641 { 1642 QVector<StreamResult> resources; 1643 for (const auto &source : m_flatpakSources) { 1644 if (source->m_pool) { 1645 auto comps = source->componentsByName(name); 1646 resources << kTransform<QVector<StreamResult>>(comps, [this, source](const AppStream::Component &comp) -> StreamResult { 1647 return {resourceForComponent(comp, source), comp.sortScore()}; 1648 }); 1649 } 1650 } 1651 auto f = [this](auto l, auto r) { 1652 return flatpakResourceLessThan(l, r); 1653 }; 1654 std::sort(resources.begin(), resources.end(), f); 1655 return resources; 1656 } 1657 1658 ResultsStream *FlatpakBackend::findResourceByPackageName(const QUrl &url) 1659 { 1660 if (url.scheme() == QLatin1String("appstream")) { 1661 const auto appstreamIds = AppStreamUtils::appstreamIds(url); 1662 if (appstreamIds.isEmpty()) 1663 Q_EMIT passiveMessage(i18n("Malformed appstream url '%1'", url.toDisplayString())); 1664 else { 1665 auto stream = new ResultsStream(QStringLiteral("FlatpakStream-AppStreamUrl")); 1666 auto f = [this, stream, appstreamIds] { 1667 std::set<AbstractResource *> resources; 1668 QVector<StreamResult> resourcesVector; 1669 for (const auto &appstreamId : appstreamIds) { 1670 const auto resourcesFound = resultsByAppstreamName(appstreamId); 1671 for (auto res : resourcesFound) { 1672 auto [x, inserted] = resources.insert(res.resource); 1673 if (inserted) { 1674 resourcesVector.append(res); 1675 } 1676 } 1677 } 1678 if (!resourcesVector.isEmpty()) 1679 Q_EMIT stream->resourcesFound(resourcesVector); 1680 stream->finish(); 1681 }; 1682 1683 if (isFetching()) { 1684 connect(this, &FlatpakBackend::initialized, stream, f); 1685 } else { 1686 QTimer::singleShot(0, this, f); 1687 } 1688 return stream; 1689 } 1690 } 1691 return new ResultsStream(QStringLiteral("FlatpakStream-packageName-void"), {}); 1692 } 1693 1694 FlatpakResource *FlatpakBackend::resourceForComponent(const AppStream::Component &component, const QSharedPointer<FlatpakSource> &source) const 1695 { 1696 const auto ref = idForComponent(component); 1697 auto resource = source->m_resources.value(ref); 1698 if (resource) { 1699 return resource; 1700 } 1701 1702 FlatpakResource *res = new FlatpakResource(component, source->installation(), const_cast<FlatpakBackend *>(this)); 1703 res->setOrigin(source->name()); 1704 res->setDisplayOrigin(source->title()); 1705 res->setIconPath(source->appstreamIconsDir()); 1706 res->updateFromAppStream(); 1707 source->addResource(res); 1708 Q_ASSERT(ref == res->uniqueId()); 1709 return res; 1710 } 1711 1712 AbstractBackendUpdater *FlatpakBackend::backendUpdater() const 1713 { 1714 return m_updater; 1715 } 1716 1717 AbstractReviewsBackend *FlatpakBackend::reviewsBackend() const 1718 { 1719 return m_reviews.data(); 1720 } 1721 1722 void FlatpakBackend::checkRepositories(const QMap<QString, QStringList> &names) 1723 { 1724 auto flatpakInstallationByPath = [this](const QString &path) -> FlatpakInstallation * { 1725 for (auto inst : std::as_const(m_installations)) { 1726 if (FlatpakResource::installationPath(inst) == path) 1727 return inst; 1728 } 1729 return nullptr; 1730 }; 1731 1732 g_autoptr(GError) localError = nullptr; 1733 for (auto it = names.begin(), itEnd = names.end(); it != itEnd; ++it) { 1734 FlatpakInstallation *installation = flatpakInstallationByPath(it.key()); 1735 for (const QString &name : std::as_const(*it)) { 1736 auto remote = flatpak_installation_get_remote_by_name(installation, name.toUtf8().constData(), m_cancellable, &localError); 1737 if (!remote) { 1738 qWarning() << "Could not find remote" << name << "in" << it.key(); 1739 continue; 1740 } 1741 loadRemote(installation, remote); 1742 } 1743 } 1744 } 1745 1746 FlatpakRemote *FlatpakBackend::installSource(FlatpakResource *resource) 1747 { 1748 g_autoptr(GCancellable) cancellable = g_cancellable_new(); 1749 1750 auto remote = flatpak_installation_get_remote_by_name(preferredInstallation(), resource->flatpakName().toUtf8().constData(), cancellable, nullptr); 1751 if (remote) { 1752 qWarning() << "Source " << resource->flatpakName() << " already exists in" << flatpak_installation_get_path(preferredInstallation()); 1753 return nullptr; 1754 } 1755 1756 remote = flatpak_remote_new(resource->flatpakName().toUtf8().constData()); 1757 populateRemote(remote, 1758 resource->comment(), 1759 resource->getMetadata(QStringLiteral("repo-url")).toString(), 1760 resource->getMetadata(QStringLiteral("gpg-key")).toString()); 1761 if (!resource->branch().isEmpty()) { 1762 flatpak_remote_set_default_branch(remote, resource->branch().toUtf8().constData()); 1763 } 1764 1765 g_autoptr(GError) error = nullptr; 1766 if (!flatpak_installation_add_remote(preferredInstallation(), remote, false, cancellable, &error)) { 1767 Q_EMIT passiveMessage(i18n("Failed to add source '%1': %2", resource->flatpakName(), QString::fromUtf8(error->message))); 1768 qWarning() << "Failed to add source " << resource->flatpakName() << error->message; 1769 return nullptr; 1770 } 1771 return remote; 1772 } 1773 1774 Transaction *FlatpakBackend::installApplication(AbstractResource *app, const AddonList &addons) 1775 { 1776 Q_UNUSED(addons); 1777 1778 FlatpakResource *resource = qobject_cast<FlatpakResource *>(app); 1779 1780 if (resource->resourceType() == FlatpakResource::Source) { 1781 // Let source backend handle this 1782 FlatpakRemote *remote = installSource(resource); 1783 if (remote) { 1784 resource->setState(AbstractResource::Installed); 1785 auto name = flatpak_remote_get_name(remote); 1786 g_autoptr(FlatpakRemote) remote = flatpak_installation_get_remote_by_name(resource->installation(), name, m_cancellable, nullptr); 1787 loadRemote(resource->installation(), remote); 1788 } 1789 return nullptr; 1790 } 1791 1792 FlatpakJobTransaction *transaction = new FlatpakJobTransaction(resource, Transaction::InstallRole); 1793 connect(transaction, &FlatpakJobTransaction::repositoriesAdded, this, &FlatpakBackend::checkRepositories); 1794 connect(transaction, &FlatpakJobTransaction::statusChanged, this, [this, resource](Transaction::Status status) { 1795 if (status == Transaction::Status::DoneStatus) { 1796 if (auto tempSource = resource->temporarySource()) { 1797 auto source = findSource(resource->installation(), resource->origin()); 1798 if (!source) { 1799 // It could mean that it's still integrating after checkRepositories It should update itself 1800 return; 1801 } 1802 resource->setTemporarySource({}); 1803 const auto id = resource->uniqueId(); 1804 source->m_resources.insert(id, resource); 1805 1806 tempSource->m_resources.remove(id); 1807 if (tempSource->m_resources.isEmpty()) { 1808 const bool removed = m_flatpakSources.removeAll(tempSource) || m_flatpakLoadingSources.removeAll(tempSource); 1809 Q_ASSERT(removed); 1810 } 1811 } 1812 updateAppState(resource); 1813 } 1814 }); 1815 return transaction; 1816 } 1817 1818 Transaction *FlatpakBackend::installApplication(AbstractResource *app) 1819 { 1820 return installApplication(app, {}); 1821 } 1822 1823 Transaction *FlatpakBackend::removeApplication(AbstractResource *app) 1824 { 1825 FlatpakResource *resource = qobject_cast<FlatpakResource *>(app); 1826 1827 if (resource->resourceType() == FlatpakResource::Source) { 1828 // Let source backend handle this 1829 if (m_sources->removeSource(resource->flatpakName())) { 1830 resource->setState(AbstractResource::None); 1831 } 1832 return nullptr; 1833 } 1834 1835 FlatpakJobTransaction *transaction = new FlatpakJobTransaction(resource, Transaction::RemoveRole); 1836 connect(transaction, &FlatpakJobTransaction::repositoriesAdded, this, &FlatpakBackend::checkRepositories); 1837 connect(transaction, &FlatpakJobTransaction::statusChanged, this, [this, resource](Transaction::Status status) { 1838 if (status == Transaction::Status::DoneStatus) { 1839 updateAppSize(resource); 1840 } 1841 }); 1842 return transaction; 1843 } 1844 1845 void FlatpakBackend::checkForUpdates() 1846 { 1847 disconnect(this, &FlatpakBackend::initialized, m_checkForUpdatesTimer, qOverload<>(&QTimer::start)); 1848 for (const auto &source : std::as_const(m_flatpakSources)) { 1849 if (source->remote()) { 1850 Q_ASSERT(!m_refreshAppstreamMetadataJobs.contains(source->remote())); 1851 m_refreshAppstreamMetadataJobs.insert(source->remote()); 1852 checkForRemoteUpdates(source->installation(), source->remote()); 1853 } 1854 } 1855 } 1856 1857 void FlatpakBackend::checkForRemoteUpdates(FlatpakInstallation *installation, FlatpakRemote *remote) 1858 { 1859 Q_ASSERT(remote); 1860 const bool needsIntegration = m_refreshAppstreamMetadataJobs.contains(remote); 1861 if (flatpak_remote_get_disabled(remote) || flatpak_remote_get_noenumerate(remote)) { 1862 if (needsIntegration) { 1863 integrateRemote(installation, remote); 1864 } 1865 return; 1866 } 1867 1868 FlatpakRefreshAppstreamMetadataJob *job = new FlatpakRefreshAppstreamMetadataJob(installation, remote); 1869 if (needsIntegration) { 1870 connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFinished, this, &FlatpakBackend::integrateRemote); 1871 } 1872 connect(job, &FlatpakRefreshAppstreamMetadataJob::finished, this, [=] { 1873 acquireFetching(false); 1874 }); 1875 1876 acquireFetching(true); 1877 job->start(); 1878 } 1879 1880 QString FlatpakBackend::displayName() const 1881 { 1882 return QStringLiteral("Flatpak"); 1883 } 1884 1885 InlineMessage *FlatpakBackend::explainDysfunction() const 1886 { 1887 if (m_flatpakSources.isEmpty()) { 1888 return new InlineMessage(InlineMessage::Error, QStringLiteral("emblem-error"), i18n("There are no Flatpak sources."), m_sources->actions()); 1889 } 1890 for (const auto &source : m_flatpakSources) { 1891 if (source->m_pool && !source->m_pool->lastError().isEmpty()) { 1892 return new InlineMessage(InlineMessage::Error, QStringLiteral("emblem-error"), i18n("Failed to load \"%1\" source", source->name())); 1893 } 1894 } 1895 return AbstractResourcesBackend::explainDysfunction(); 1896 } 1897 1898 bool FlatpakBackend::extends(const QString &extends) const 1899 { 1900 return std::any_of(m_flatpakSources.constBegin(), m_flatpakSources.constEnd(), [extends](const auto &source) { 1901 return source->m_pool && !source->m_pool->lastError().isEmpty() && !source->m_pool->componentsByExtends(extends).isEmpty(); 1902 }); 1903 } 1904 1905 #include "FlatpakBackend.moc"