FlatpakBackend.cpp 51.9 KB
Newer Older
Jan Grulich's avatar
Jan Grulich committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/***************************************************************************
 *   Copyright © 2013 Aleix Pol Gonzalez <aleixpol@blue-systems.com>       *
 *   Copyright © 2017 Jan Grulich <jgrulich@redhat.com>                    *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or         *
 *   modify it under the terms of the GNU General Public License as        *
 *   published by the Free Software Foundation; either version 2 of        *
 *   the License or (at your option) version 3 or any later version        *
 *   accepted by the membership of KDE e.V. (or its successor approved     *
 *   by the membership of KDE e.V.), which shall act as a proxy            *
 *   defined in Section 14 of version 3 of the license.                    *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
 ***************************************************************************/

#include "FlatpakBackend.h"
23
#include "FlatpakFetchDataJob.h"
Jan Grulich's avatar
Jan Grulich committed
24
25
#include "FlatpakResource.h"
#include "FlatpakSourcesBackend.h"
26
#include "FlatpakJobTransaction.h"
Jan Grulich's avatar
Jan Grulich committed
27

28
#include <utils.h>
Jan Grulich's avatar
Jan Grulich committed
29
30
31
#include <resources/StandardBackendUpdater.h>
#include <resources/SourcesModel.h>
#include <Transaction/Transaction.h>
32
33
#include <appstream/OdrsReviewsBackend.h>
#include <appstream/AppStreamIntegration.h>
34
#include <appstream/AppStreamUtils.h>
Jan Grulich's avatar
Jan Grulich committed
35

36
37
#include <AppStreamQt/bundle.h>
#include <AppStreamQt/component.h>
Jan Grulich's avatar
Jan Grulich committed
38
#include <AppStreamQt/icon.h>
39
#include <AppStreamQt/metadata.h>
40

Jan Grulich's avatar
Jan Grulich committed
41
42
43
44
45
46
47
#include <KAboutData>
#include <KLocalizedString>
#include <KPluginFactory>
#include <KConfigGroup>
#include <KSharedConfig>

#include <QAction>
48
#include <QtConcurrentRun>
Jan Grulich's avatar
Jan Grulich committed
49
50
51
#include <QDebug>
#include <QDir>
#include <QFile>
52
#include <QFileInfo>
53
#include <QFutureWatcher>
54
#include <QSettings>
Jan Grulich's avatar
Jan Grulich committed
55
56
#include <QThread>
#include <QTimer>
57
58
#include <QTextStream>
#include <QTemporaryFile>
59
#include <QNetworkAccessManager>
Jan Grulich's avatar
Jan Grulich committed
60

61
#include <glib.h>
62
#include <QRegularExpression>
63

64
65
#include <sys/stat.h>

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
66
DISCOVER_BACKEND_PLUGIN(FlatpakBackend)
Jan Grulich's avatar
Jan Grulich committed
67

68
69
70
71
72
73
74
75
76
77
78
79
QDebug operator<<(QDebug debug, const FlatpakResource::Id& id)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "FlatpakResource::Id(";
    debug.nospace() << "name:" << id.id << ',';
    debug.nospace() << "branch:" << id.branch << ',';
    debug.nospace() << "origin:" << id.origin << ',';
    debug.nospace() << "type:" << id.type;
    debug.nospace() << ')';
    return debug;
}

80
static FlatpakResource::Id idForInstalledRef(FlatpakInstallation *installation, FlatpakInstalledRef *ref, const QString &postfix)
81
82
{
    const FlatpakResource::ResourceType appType = flatpak_ref_get_kind(FLATPAK_REF(ref)) == FLATPAK_REF_KIND_APP ? FlatpakResource::DesktopApp : FlatpakResource::Runtime;
83
    const QString appId = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))) + postfix;
84
85
86
    const QString arch = QString::fromUtf8(flatpak_ref_get_arch(FLATPAK_REF(ref)));
    const QString branch = QString::fromUtf8(flatpak_ref_get_branch(FLATPAK_REF(ref)));

87
    return { installation, QString::fromUtf8(flatpak_installed_ref_get_origin(ref)), appType, appId, branch, arch };
88
89
}

Jan Grulich's avatar
Jan Grulich committed
90
91
92
FlatpakBackend::FlatpakBackend(QObject* parent)
    : AbstractResourcesBackend(parent)
    , m_updater(new StandardBackendUpdater(this))
93
    , m_reviews(AppStreamIntegration::global()->reviews())
94
    , m_refreshAppstreamMetadataJobs(0)
95
    , m_cancellable(g_cancellable_new())
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
96
    , m_threadPool(new QThreadPool(this))
Jan Grulich's avatar
Jan Grulich committed
97
98
99
{
    g_autoptr(GError) error = nullptr;

Jan Grulich's avatar
Jan Grulich committed
100
101
    connect(m_updater, &StandardBackendUpdater::updatesCountChanged, this, &FlatpakBackend::updatesCountChanged);

Jan Grulich's avatar
Jan Grulich committed
102
    // Load flatpak installation
103
    if (!setupFlatpakInstallations(&error)) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
104
        qWarning() << "Failed to setup flatpak installations:" << error->message;
Jan Grulich's avatar
Jan Grulich committed
105
    } else {
106
        loadAppsFromAppstreamData();
107

108
        m_sources = new FlatpakSourcesBackend(m_installations, this);
109
        SourcesModel::global()->addSourcesBackend(m_sources);
Jan Grulich's avatar
Jan Grulich committed
110
111
    }

112
113
114
    connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, [this] {
        m_reviews->emitRatingFetched(this, kTransform<QList<AbstractResource*>>(m_resources.values(), [] (AbstractResource* r) { return r; }));
    });
115
116
117
118
119
120
121

    /* Override the umask to 022 to make it possible to share files between
     * the plasma-discover process and flatpak system helper process.
     *
     * See https://github.com/flatpak/flatpak/pull/2856/
     */
    umask(022);
Jan Grulich's avatar
Jan Grulich committed
122
123
}

124
125
FlatpakBackend::~FlatpakBackend()
{
126
    g_cancellable_cancel(m_cancellable);
127
    m_threadPool.waitForDone(200);
128
    m_threadPool.clear();
129
    for(auto inst : qAsConst(m_installations))
130
131
        g_object_unref(inst);

132
    g_object_unref(m_cancellable);
133
134
}

135
136
bool FlatpakBackend::isValid() const
{
137
    return m_sources && !m_installations.isEmpty();
138
139
}

140
141
142
143
144
145
146
147
148
149
150
151
152
class FlatpakFetchRemoteResourceJob : public QNetworkAccessManager
{
Q_OBJECT
public:
    FlatpakFetchRemoteResourceJob(const QUrl &url, FlatpakBackend *backend)
        : QNetworkAccessManager(backend)
        , m_backend(backend)
        , m_url(url)
    {
    }

    void start()
    {
153
154
155
        QNetworkRequest req(m_url);
        req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
        auto replyGet = get(req);
156
        connect(replyGet, &QNetworkReply::finished, this, [this, replyGet] {
Laurent Montel's avatar
Laurent Montel committed
157
            QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> replyPtr(replyGet);
158
159
160
161
162
163
164
165
166
167
            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]() {
Laurent Montel's avatar
Laurent Montel committed
168
                QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> replyPtr(replyPut);
169
                if (replyPut->error() != QNetworkReply::NoError) {
170
171
                    qWarning() << "couldn't save" << originalUrl << replyPut->errorString();
                    Q_EMIT jobFinished(false, nullptr);
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
                    return;
                }
                if (!fileUrl.isLocalFile()) {
                    Q_EMIT jobFinished(false, nullptr);
                    return;
                }

                FlatpakResource *resource = nullptr;
                if (fileUrl.path().endsWith(QLatin1String(".flatpak"))) {
                    resource = m_backend->addAppFromFlatpakBundle(fileUrl);
                } else if (fileUrl.path().endsWith(QLatin1String(".flatpakref"))) {
                    resource = m_backend->addAppFromFlatpakRef(fileUrl);
                } else if (fileUrl.path().endsWith(QLatin1String(".flatpakrepo"))) {
                    resource = m_backend->addSourceFromFlatpakRepo(fileUrl);
                }

                if (resource) {
                    resource->setResourceFile(originalUrl);
                    Q_EMIT jobFinished(true, resource);
                } else {
                    qWarning() << "couldn't create resource from" << fileUrl.toLocalFile();
                    Q_EMIT jobFinished(false, nullptr);
194
                }
195
196
            }
            );
197
198
199
200
201
202
203
        });
    }

Q_SIGNALS:
    void jobFinished(bool success, FlatpakResource *resource);

private:
204
205
    FlatpakBackend  *const m_backend;
    const QUrl m_url;
206
207
};

Jan Grulich's avatar
Jan Grulich committed
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
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;
}

226
FlatpakInstalledRef * FlatpakBackend::getInstalledRefForApp(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource) const
227
228
229
230
231
232
233
234
{
    FlatpakInstalledRef *ref = nullptr;
    g_autoptr(GError) localError = nullptr;

    if (!flatpakInstallation) {
        return ref;
    }

235
    const auto type = resource->resourceType() == FlatpakResource::DesktopApp ? FLATPAK_REF_KIND_APP : FLATPAK_REF_KIND_RUNTIME;
236
237
238

    return flatpak_installation_get_installed_ref(flatpakInstallation,
                                                 type,
239
240
241
                                                 resource->flatpakName().toUtf8().constData(),
                                                 resource->arch().toUtf8().constData(),
                                                 resource->branch().toUtf8().constData(),
242
                                                 m_cancellable, &localError);
243
244
}

245
FlatpakResource * FlatpakBackend::getAppForInstalledRef(FlatpakInstallation *flatpakInstallation, FlatpakInstalledRef *ref) const
Jan Grulich's avatar
Jan Grulich committed
246
{
247
248
249
250
    auto r = m_resources.value(idForInstalledRef(flatpakInstallation, ref, {}));
    if (!r)
        r = m_resources.value(idForInstalledRef(flatpakInstallation, ref, QStringLiteral(".desktop")));

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
251
252
253
//     if (!r) {
//         qDebug() << "no" << flatpak_ref_get_name(FLATPAK_REF(ref));
//     }
254
    return r;
Jan Grulich's avatar
Jan Grulich committed
255
256
}

257
FlatpakResource * FlatpakBackend::getRuntimeForApp(FlatpakResource *resource) const
258
259
{
    FlatpakResource *runtime = nullptr;
260
261
    const QString runtimeName = resource->runtime();
    const auto runtimeInfo = runtimeName.splitRef(QLatin1Char('/'));
262
263
264
265
266

    if (runtimeInfo.count() != 3) {
        return runtime;
    }

267
    for(auto it = m_resources.constBegin(), itEnd = m_resources.constEnd(); it!=itEnd; ++it) {
268
        const auto& id = it.key();
269
        if (id.type == FlatpakResource::Runtime && id.id == runtimeInfo.at(0) && id.branch == runtimeInfo.at(2)) {
270
            runtime = *it;
271
272
273
274
275
            break;
        }
    }

    // TODO if runtime wasn't found, create a new one from available info
276
    if (!runtime) {
277
        qWarning() << "could not find runtime" << runtimeInfo << resource;
278
    }
279
280
281
282

    return runtime;
}

Jan Grulich's avatar
Jan Grulich committed
283
284
285
286
287
288
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;
289
    AppStream::Component asComponent;
Jan Grulich's avatar
Jan Grulich committed
290

291
    file = g_file_new_for_path(url.toLocalFile().toUtf8().constData());
Jan Grulich's avatar
Jan Grulich committed
292
293
294
    bundleRef = flatpak_bundle_ref_new(file, &localError);

    if (!bundleRef) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
295
        qWarning() << "Failed to load bundle:" << localError->message;
Jan Grulich's avatar
Jan Grulich committed
296
297
298
        return nullptr;
    }

299
    g_autoptr(GBytes) metadata = flatpak_bundle_ref_get_metadata(bundleRef);
Jan Grulich's avatar
Jan Grulich committed
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
    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) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
318
            qWarning() << "Failed to extract appstream metadata from bundle:" << localError->message;
Jan Grulich's avatar
Jan Grulich committed
319
320
321
            return nullptr;
        }

322
323
324
        gsize len = 0;
        gconstpointer data = g_bytes_get_data(appstream, &len);

325
326
        AppStream::Metadata metadata;
        metadata.setFormatStyle(AppStream::Metadata::FormatStyleCollection);
327
        AppStream::Metadata::MetadataError error = metadata.parse(QString::fromUtf8((char*)data, len), AppStream::Metadata::FormatKindXml);
328
329
        if (error != AppStream::Metadata::MetadataErrorNoError) {
            qWarning() << "Failed to parse appstream metadata: " << error;
Jan Grulich's avatar
Jan Grulich committed
330
331
332
            return nullptr;
        }

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
333
        const QList<AppStream::Component> components = metadata.components();
334
335
        if (components.size()) {
            asComponent = AppStream::Component(components.first());
Jan Grulich's avatar
Jan Grulich committed
336
337
338
339
340
341
        } else {
            qWarning() << "Failed to parse appstream metadata";
            return nullptr;
        }
    } else {
        qWarning() << "No appstream metadata in bundle";
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361

        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();
Jan Grulich's avatar
Jan Grulich committed
362
363
    }

364
    FlatpakResource *resource = new FlatpakResource(asComponent, preferredInstallation(), this);
Jan Grulich's avatar
Jan Grulich committed
365

366
    gsize len = 0;
Jan Grulich's avatar
Jan Grulich committed
367
368
    QByteArray metadataContent = QByteArray((char *)g_bytes_get_data(metadata, &len));
    if (!updateAppMetadata(resource, metadataContent)) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
369
        delete resource;
Jan Grulich's avatar
Jan Grulich committed
370
371
372
373
        qWarning() << "Failed to update metadata from app bundle";
        return nullptr;
    }

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
374
    g_autoptr(GBytes) iconData = flatpak_bundle_ref_get_icon(bundleRef, 128);
Jan Grulich's avatar
Jan Grulich committed
375
376
377
378
379
380
381
    if (!iconData) {
        iconData = flatpak_bundle_ref_get_icon(bundleRef, 64);
    }

    if (iconData) {
        gsize len = 0;
        char * data = (char *)g_bytes_get_data(iconData, &len);
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
382
383
384

        QPixmap pixmap;
        pixmap.loadFromData(QByteArray(data, len), "PNG");
Jan Grulich's avatar
Jan Grulich committed
385
386
387
        resource->setBundledIcon(pixmap);
    }

388
389
    const QString origin = QString::fromUtf8(flatpak_bundle_ref_get_origin(bundleRef));

390
    resource->setDownloadSize(0);
Jan Grulich's avatar
Jan Grulich committed
391
    resource->setInstalledSize(flatpak_bundle_ref_get_installed_size(bundleRef));
392
393
    resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::AlreadyKnown);
    resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::AlreadyKnown);
Jan Grulich's avatar
Jan Grulich committed
394
    resource->setFlatpakFileType(QStringLiteral("flatpak"));
395
    resource->setOrigin(origin.isEmpty() ? i18n("Local bundle") : origin);
Jan Grulich's avatar
Jan Grulich committed
396
397
398
399
400
401
402
403
404
405
406
407
    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();
408
409
410
411
412
413
414
415
416
417
418
    const QString name = settings.value(QStringLiteral("Flatpak Ref/Name")).toString();

    auto item = m_sources->sourceByUrl(refurl);
    if (item) {
        const auto resources = resourcesByAppstreamName(name);
        for (auto resource : resources) {
            if (resource->origin() == item->data(AbstractSourcesBackend::IdRole)) {
                return static_cast<FlatpakResource*>(resource);
            }
        }
    }
Jan Grulich's avatar
Jan Grulich committed
419

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
420
    g_autoptr(GError) error = nullptr;
Jan Grulich's avatar
Jan Grulich committed
421
422
423
424
425
426
427
428
429
430
431
    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());

432
        remoteRef = flatpak_installation_install_ref_file (preferredInstallation(), bytes, m_cancellable, &error);
Jan Grulich's avatar
Jan Grulich committed
433
        if (!remoteRef) {
434
            qWarning() << "Failed to create install ref file:" << error->message;
435
            const auto resources = resourcesByAppstreamName(name);
436
437
438
            if (!resources.isEmpty()) {
                return qobject_cast<FlatpakResource*>(resources.constFirst());
            }
Jan Grulich's avatar
Jan Grulich committed
439
440
441
442
443
444
445
446
            return nullptr;
        }
    }

    const auto remoteName = flatpak_remote_ref_get_remote_name(remoteRef);

    auto ref = FLATPAK_REF(remoteRef);

447
448
449
450
451
    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());
452
    asComponent.setId(name);
453

Jan Grulich's avatar
Jan Grulich committed
454
455
    const QString iconUrl = settings.value(QStringLiteral("Flatpak Ref/Icon")).toString();
    if (!iconUrl.isEmpty()) {
456
457
458
459
        AppStream::Icon icon;
        icon.setKind(AppStream::Icon::KindRemote);
        icon.setUrl(QUrl(iconUrl));
        asComponent.addIcon(icon);
Jan Grulich's avatar
Jan Grulich committed
460
461
    }

462
    auto resource = new FlatpakResource(asComponent, preferredInstallation(), this);
Jan Grulich's avatar
Jan Grulich committed
463
464
465
    resource->setFlatpakFileType(QStringLiteral("flatpakref"));
    resource->setOrigin(QString::fromUtf8(remoteName));
    resource->updateFromRef(ref);
466
467
468

    QUrl runtimeUrl = QUrl(settings.value(QStringLiteral("Flatpak Ref/RuntimeRepo")).toString());
    if (!runtimeUrl.isEmpty()) {
469
        auto installation = preferredInstallation();
470
        // We need to fetch metadata to find information about required runtime
471
472
473
        auto fw = new QFutureWatcher<QByteArray>(this);
        connect(fw, &QFutureWatcher<QByteArray>::finished, this, [this, installation, resource, fw, runtimeUrl]() {
            const auto metadata = fw->result();
474
            // Even when we failed to fetch information about runtime we still want to show the application
475
476
            if (metadata.isEmpty()) {
                onFetchMetadataFinished(installation, resource, metadata);
477
            } else {
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
                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);
                }
494
            }
495
            fw->deleteLater();
496
        });
497
        fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, installation, resource));
498
499
500
501
    } else {
        addResource(resource);
    }

Jan Grulich's avatar
Jan Grulich committed
502
503
504
    return resource;
}

505
506
FlatpakResource * FlatpakBackend::addSourceFromFlatpakRepo(const QUrl &url)
{
507
    Q_ASSERT(url.isLocalFile());
508
509
510
511
512
513
514
515
516
517
    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;
    }

518
    if (gpgKey.startsWith(QLatin1String("http://")) || gpgKey.startsWith(QLatin1String("https://"))) {
519
520
521
        return nullptr;
    }

522
523
524
525
526
527
528
    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());

529
    const QString iconUrl = settings.value(QStringLiteral("Flatpak Repo/Icon")).toString();
530
    if (!iconUrl.isEmpty()) {
531
532
533
534
        AppStream::Icon icon;
        icon.setKind(AppStream::Icon::KindRemote);
        icon.setUrl(QUrl(iconUrl));
        asComponent.addIcon(icon);
535
536
    }

537
    auto resource = new FlatpakResource(asComponent, preferredInstallation(), this);
538
539
540
541
542
543
544
    // 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);

545
    auto repo = flatpak_installation_get_remote_by_name(preferredInstallation(), resource->flatpakName().toUtf8().constData(), m_cancellable, nullptr);
546
547
548
549
550
551
552
553
554
    if (!repo) {
        resource->setState(AbstractResource::State::None);
    } else {
        resource->setState(AbstractResource::State::Installed);
    }

    return resource;
}

555
556
557
558
void FlatpakBackend::addResource(FlatpakResource *resource)
{
    // Update app with all possible information we have
    if (!parseMetadataFromAppBundle(resource)) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
559
        qWarning() << "Failed to parse metadata from app bundle for" << resource->name();
560
561
    }

562
    auto installation = resource->installation();
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
563
    updateAppState(installation, resource);
564

565
    // This will update also metadata (required runtime)
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
566
    updateAppSize(installation, resource);
567
568

    m_resources.insert(resource->uniqueId(), resource);
569
570
571
572
    if (!resource->extends().isEmpty()) {
        m_extends.append(resource->extends());
        m_extends.removeDuplicates();
    }
573
574
}

575
class FlatpakSource
Jan Grulich's avatar
Jan Grulich committed
576
{
577
578
public:
    FlatpakSource(FlatpakRemote* remote) : m_remote(remote) {}
Jan Grulich's avatar
Jan Grulich committed
579

580
581
582
    bool isEnabled() const
    {
        return !flatpak_remote_get_disabled(m_remote);
Jan Grulich's avatar
Jan Grulich committed
583
584
    }

585
586
587
588
    QString appstreamDir() const
    {
        g_autoptr(GFile) appstreamDir = flatpak_remote_get_appstream_dir(m_remote, nullptr);
        if (!appstreamDir) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
589
            qWarning() << "No appstream dir for" << flatpak_remote_get_name(m_remote);
590
591
592
593
594
595
596
597
598
599
600
601
602
603
            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;
};

604
605
606
607
void FlatpakBackend::loadAppsFromAppstreamData()
{
    for (auto installation : qAsConst(m_installations)) {
        // Load applications from appstream metadata
608
609
610
        if (g_cancellable_is_cancelled(m_cancellable))
            break;

611
612
613
614
615
616
        if (!loadAppsFromAppstreamData(installation)) {
            qWarning() << "Failed to load packages from appstream data from installation" << installation;
        }
    }
}

617
618
619
620
bool FlatpakBackend::loadAppsFromAppstreamData(FlatpakInstallation *flatpakInstallation)
{
    Q_ASSERT(flatpakInstallation);

621
    GPtrArray *remotes = flatpak_installation_list_remotes(flatpakInstallation, m_cancellable, nullptr);
Jan Grulich's avatar
Jan Grulich committed
622
623
624
625
    if (!remotes) {
        return false;
    }

626
627
    m_refreshAppstreamMetadataJobs += remotes->len;

Jan Grulich's avatar
Jan Grulich committed
628
629
    for (uint i = 0; i < remotes->len; i++) {
        FlatpakRemote *remote = FLATPAK_REMOTE(g_ptr_array_index(remotes, i));
630
        g_autoptr(GFile) fileTimestamp = flatpak_remote_get_appstream_timestamp(remote, flatpak_get_default_arch());
631

632
633
634
635
636
637
638
        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);
        }
639
640
641
    }
    return true;
}
Jan Grulich's avatar
Jan Grulich committed
642

643
644
645
646
647
648
649
650
651
void FlatpakBackend::metadataRefreshed()
{
    m_refreshAppstreamMetadataJobs--;
    if (m_refreshAppstreamMetadataJobs == 0) {
        loadInstalledApps();
        checkForUpdates();
    }
}

652
653
void FlatpakBackend::integrateRemote(FlatpakInstallation *flatpakInstallation, FlatpakRemote *remote)
{
654
    Q_ASSERT(m_refreshAppstreamMetadataJobs != 0);
655

656
657
    FlatpakSource source(remote);
    if (!source.isEnabled() || flatpak_remote_get_noenumerate(remote)) {
658
        metadataRefreshed();
659
660
        return;
    }
Jan Grulich's avatar
Jan Grulich committed
661

662
    const QString appstreamDirPath = source.appstreamDir();
663
    const QString appstreamIconsPath = source.appstreamDir() + QLatin1String("/icons/");
664
665
    const QString appDirFileName = appstreamDirPath + QLatin1String("/appstream.xml.gz");
    if (!QFile::exists(appDirFileName)) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
666
        qWarning() << "No" << appDirFileName << "appstream metadata found for" << source.name();
667
        metadataRefreshed();
668
        return;
Jan Grulich's avatar
Jan Grulich committed
669
670
    }

671
672
673
674
675
676
677
678
679
680
    auto fw = new QFutureWatcher<QList<AppStream::Component>>(this);
    const auto sourceName = source.name();
    connect(fw, &QFutureWatcher<QList<AppStream::Component>>::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);
        }
681

682
        metadataRefreshed();
683
        acquireFetching(false);
684
685
        fw->deleteLater();
    });
686
687
688
689
690
691
692
693
694
695
696
697
    acquireFetching(true);
    fw->setFuture(QtConcurrent::run(&m_threadPool, [appDirFileName]() -> QList<AppStream::Component> {
        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();
    }));
698
699
700
701
702
703
704
705
706
707
}

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;
        }
    }
Jan Grulich's avatar
Jan Grulich committed
708
709
}

710
bool FlatpakBackend::loadInstalledApps(FlatpakInstallation *flatpakInstallation)
Jan Grulich's avatar
Jan Grulich committed
711
{
712
    Q_ASSERT(flatpakInstallation);
Jan Grulich's avatar
Jan Grulich committed
713

714
715
716
717
718
719
720
    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;
    }

721
722
    const QString pathExports = FlatpakResource::installationPath(flatpakInstallation) + QLatin1String("/exports/");
    const QString pathApps = pathExports + QLatin1String("share/applications/");
Jan Grulich's avatar
Jan Grulich committed
723

724
725
    for (uint i = 0; i < refs->len; i++) {
        FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i));
Jan Grulich's avatar
Jan Grulich committed
726

727
728
729
730
        const auto name = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref)));
        if (name.endsWith(QLatin1String(".Debug")) || name.endsWith(QLatin1String(".Locale")) || name.endsWith(QLatin1String(".BaseApp")) || name.endsWith(QLatin1String(".Docs")))
            continue;

731
732
733
734
735
        const auto res = getAppForInstalledRef(flatpakInstallation, ref);
        if (res) {
            res->setState(AbstractResource::Installed);
            continue;
        }
736

737
        AppStream::Component cid;
738
        AppStream::Metadata metadata;
739
        const QString fnDesktop = pathApps + name + QLatin1String(".desktop");
740
        AppStream::Metadata::MetadataError error = metadata.parseFile(fnDesktop, AppStream::Metadata::FormatKindDesktopEntry);
741
        if (error != AppStream::Metadata::MetadataErrorNoError) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
742
743
            if (QFile::exists(fnDesktop))
                qDebug() << "Failed to parse appstream metadata:" << error << fnDesktop;
744
745

            cid.setId(QString::fromLatin1(flatpak_ref_get_name(FLATPAK_REF(ref))));
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
746
#if FLATPAK_CHECK_VERSION(1,1,2)
747
            cid.setName(QString::fromLatin1(flatpak_installed_ref_get_appdata_name(ref)));
748
#endif
749
750
        } else
            cid = metadata.component();
751

752
        FlatpakResource *resource = new FlatpakResource(cid, flatpakInstallation, this);
753
754
755
756
757
758

        resource->setIconPath(pathExports);
        resource->setState(AbstractResource::Installed);
        resource->setOrigin(QString::fromUtf8(flatpak_installed_ref_get_origin(ref)));
        resource->updateFromRef(FLATPAK_REF(ref));

759
        addResource(resource);
Jan Grulich's avatar
Jan Grulich committed
760
761
762
763
764
    }

    return true;
}

Jan Grulich's avatar
Jan Grulich committed
765
766
767
768
769
770
771
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) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
772
        qWarning() << "Failed to get list of installed refs for listing updates:" << localError->message;
Jan Grulich's avatar
Jan Grulich committed
773
774
775
776
777
        return;
    }

    for (uint i = 0; i < refs->len; i++) {
        FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(refs, i));
778

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
779
        const gchar *latestCommit = flatpak_installed_ref_get_latest_commit(ref);
Jan Grulich's avatar
Jan Grulich committed
780
781

        if (!latestCommit) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
782
            qWarning() << "Couldn't get latest commit for" << flatpak_ref_get_name(FLATPAK_REF(ref));
783
            continue;
Jan Grulich's avatar
Jan Grulich committed
784
785
        }

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
786
        const gchar *commit = flatpak_ref_get_commit(FLATPAK_REF(ref));
Jan Grulich's avatar
Jan Grulich committed
787
788
789
790
791
792
793
        if (g_strcmp0(commit, latestCommit) == 0) {
            continue;
        }

        FlatpakResource *resource = getAppForInstalledRef(flatpakInstallation, ref);
        if (resource) {
            resource->setState(AbstractResource::Upgradeable);
794
            updateAppSize(flatpakInstallation, resource);
Jan Grulich's avatar
Jan Grulich committed
795
796
797
798
        }
    }
}

799
void FlatpakBackend::loadRemoteUpdates(FlatpakInstallation* installation)
Jan Grulich's avatar
Jan Grulich committed
800
{
801
    auto fw = new QFutureWatcher<GPtrArray *>(this);
802
    connect(fw, &QFutureWatcher<GPtrArray *>::finished, this, [this, installation, fw](){
803
        g_autoptr(GPtrArray) refs = fw->result();
804
805
        onFetchUpdatesFinished(installation, refs);
        fw->deleteLater();
806
        acquireFetching(false);
807
    });
808
    acquireFetching(true);
809
    fw->setFuture(QtConcurrent::run(&m_threadPool, [installation, this]() -> GPtrArray * {
810
        g_autoptr(GError) localError = nullptr;
811
812
813
814
        if (g_cancellable_is_cancelled(m_cancellable)) {
            qWarning() << "don't issue commands after cancelling";
            return {};
        }
815
        GPtrArray *refs = flatpak_installation_list_installed_refs_for_update(installation, m_cancellable, &localError);
816
817
818
819
820
        if (!refs) {
            qWarning() << "Failed to get list of installed refs for listing updates: " << localError->message;
        }
        return refs;
    }));
821
}
Jan Grulich's avatar
Jan Grulich committed
822

823
void FlatpakBackend::onFetchUpdatesFinished(FlatpakInstallation *flatpakInstallation, GPtrArray *fetchedUpdates)
824
{
825
    if (!fetchedUpdates) {
826
827
828
829
        qWarning() << "could not get updates for" << flatpakInstallation;
        return;
    }

830
831
    for (uint i = 0; i < fetchedUpdates->len; i++) {
        FlatpakInstalledRef *ref = FLATPAK_INSTALLED_REF(g_ptr_array_index(fetchedUpdates, i));
Jan Grulich's avatar
Jan Grulich committed
832
833
834
        FlatpakResource *resource = getAppForInstalledRef(flatpakInstallation, ref);
        if (resource) {
            resource->setState(AbstractResource::Upgradeable);
835
            updateAppSize(flatpakInstallation, resource);
836
        } else
837
            qWarning() << "could not find updated resource" << flatpak_ref_get_name(FLATPAK_REF(ref)) << m_resources.size();
Jan Grulich's avatar
Jan Grulich committed
838
839
840
    }
}

841
842
843
844
bool FlatpakBackend::parseMetadataFromAppBundle(FlatpakResource *resource)
{
    g_autoptr(FlatpakRef) ref = nullptr;
    g_autoptr(GError) localError = nullptr;
845
    AppStream::Bundle bundle = resource->appstreamComponent().bundle(AppStream::Bundle::KindFlatpak);
846
847

    // Get arch/branch/commit/name from FlatpakRef
848
    if (!bundle.isEmpty()) {
849
        ref = flatpak_ref_parse(bundle.id().toUtf8().constData(), &localError);
850
        if (!ref) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
851
            qWarning() << "Failed to parse" << bundle.id() << localError->message;
852
853
            return false;
        } else {
854
            resource->updateFromRef(ref);
855
856
857
858
859
860
        }
    }

    return true;
}

861
862
863
864
865
866
class FlatpakRefreshAppstreamMetadataJob : public QThread
{
    Q_OBJECT
public:
    FlatpakRefreshAppstreamMetadataJob(FlatpakInstallation *installation, FlatpakRemote *remote)
        : QThread()
867
        , m_cancellable(g_cancellable_new())
868
869
870
        , m_installation(installation)
        , m_remote(remote)
    {
871
        connect(this, &FlatpakRefreshAppstreamMetadataJob::finished, this, &QObject::deleteLater);
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
    }

    ~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)) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
894
#endif
895
896
897
            const QString error = localError ? QString::fromUtf8(localError->message) : QStringLiteral("<no error>");
            qWarning() << "Failed to refresh appstream metadata for " << flatpak_remote_get_name(m_remote) << ": " << error;
            Q_EMIT jobRefreshAppstreamMetadataFailed(error);
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
898
899
        } else  {
            Q_EMIT jobRefreshAppstreamMetadataFinished(m_installation, m_remote);
900
901
902
903
        }
    }

Q_SIGNALS:
904
    void jobRefreshAppstreamMetadataFailed(const QString &errorMessage);
905
906
907
908
909
910
911
912
913
914
915
    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);
916
    connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFailed, this, &FlatpakBackend::metadataRefreshed);
917
    connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFailed, this, [this] (const QString &errorMessage) { Q_EMIT passiveMessage(errorMessage); });
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
918
    connect(job, &FlatpakRefreshAppstreamMetadataJob::jobRefreshAppstreamMetadataFinished, this, &FlatpakBackend::integrateRemote);
919
920
921
    connect(job, &FlatpakRefreshAppstreamMetadataJob::finished, this, [this] { acquireFetching(false); });

    acquireFetching(true);
922
923
924
    job->start();
}

925
bool FlatpakBackend::setupFlatpakInstallations(GError **error)
Jan Grulich's avatar
Jan Grulich committed
926
{
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
927
928
929
930
931
932
933
934
    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;
    }

935
    GPtrArray *installations = flatpak_get_system_installations(m_cancellable, error);
936
937
938
939
    if (*error) {
        qWarning() << "Failed to call flatpak_get_system_installations:" << (*error)->message;
    }
    for (uint i = 0; installations && i < installations->len; i++) {
940
        m_installations << FLATPAK_INSTALLATION(g_ptr_array_index(installations, i));
Jan Grulich's avatar
Jan Grulich committed
941
942
    }

943
944
945
    auto user = flatpak_installation_new_user(m_cancellable, error);
    if (user) {
        m_installations << user;
Jan Grulich's avatar
Jan Grulich committed
946
947
    }

948
    return !m_installations.isEmpty();
Jan Grulich's avatar
Jan Grulich committed
949
950
}

951
void FlatpakBackend::updateAppInstalledMetadata(FlatpakInstalledRef *installedRef, FlatpakResource *resource)
Jan Grulich's avatar
Jan Grulich committed
952
{
953
    // Update the rest
954
    resource->updateFromRef(FLATPAK_REF(installedRef));
955
    resource->setInstalledSize(flatpak_installed_ref_get_installed_size(installedRef));
956
    resource->setOrigin(QString::fromUtf8(flatpak_installed_ref_get_origin(installedRef)));
957
958
    if (resource->state() < AbstractResource::Installed)
        resource->setState(AbstractResource::Installed);
959
960
}

961
bool FlatpakBackend::updateAppMetadata(FlatpakInstallation* flatpakInstallation, FlatpakResource *resource)
962
{
963
    if (resource->resourceType() != FlatpakResource::DesktopApp) {
964
        return true;
Jan Grulich's avatar
Jan Grulich committed
965
966
    }

967
    const QString path = resource->installPath() + QStringLiteral("/metadata");
Jan Grulich's avatar
Jan Grulich committed
968

969
    if (QFile::exists(path)) {
970
        return updateAppMetadata(resource, path);
971
    } else {
972
973
974
975
976
977
978
        auto fw = new QFutureWatcher<QByteArray>(this);
        connect(fw, &QFutureWatcher<QByteArray>::finished, this, [this, flatpakInstallation, resource, fw]() {
            const auto metadata = fw->result();
            if (!metadata.isEmpty())
                onFetchMetadataFinished(flatpakInstallation, resource, metadata);
            fw->deleteLater();
        });
979
        fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, flatpakInstallation, resource));
980

981
        // Return false to indicate we cannot continue (right now used only in updateAppSize())
982
983
        return false;
    }
984
985
986
987
988
}

void FlatpakBackend::onFetchMetadataFinished(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource, const QByteArray &metadata)
{
    updateAppMetadata(resource, metadata);
989

990
991
    // 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
992
    // distinguish between these calls
993
    updateAppSizeFromRemote(flatpakInstallation, resource);
Jan Grulich's avatar
Jan Grulich committed
994
995
}

996
997
998
999
1000
1001
1002
1003
1004
1005
1006
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;
}

Jan Grulich's avatar
Jan Grulich committed
1007
1008
bool FlatpakBackend::updateAppMetadata(FlatpakResource *resource, const QByteArray &data)
{
1009
1010
1011
1012
    //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()) {
1013
1014
1015
        return false;
    }

1016
    resource->setRuntime(match.captured(1));
1017
    return true;
Jan Grulich's avatar
Jan Grulich committed
1018
1019
}

1020
bool FlatpakBackend::updateAppSize(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource)
Jan Grulich's avatar
Jan Grulich committed
1021
{
1022
    // Check if the size is already set, we should also distinguish between download and installed size,
1023
1024
1025
    // 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

1026
    if (resource->state() == AbstractResource::Installed) {
1027
        // The size appears to be already set (from updateAppInstalledMetadata() apparently)
1028
        if (resource->installedSize() > 0) {
1029
1030
            return true;
        }
1031
1032
1033
1034
1035
    } else {
        if (resource->installedSize() > 0 && resource->downloadSize() > 0) {
            return true;
        }
    }
1036
1037
1038

    // Check if we know the needed runtime which is needed for calculating the size
    if (resource->runtime().isEmpty()) {
1039
        if (!updateAppMetadata(flatpakInstallation, resource)) {
1040
1041
            return false;
        }
Jan Grulich's avatar
Jan Grulich committed
1042
1043
    }

1044
1045
1046
1047
1048
    return updateAppSizeFromRemote(flatpakInstallation, resource);
}

bool FlatpakBackend::updateAppSizeFromRemote(FlatpakInstallation *flatpakInstallation, FlatpakResource *resource)
{
1049
    // Calculate the runtime size
1050
    if (resource->state() == AbstractResource::None && resource->resourceType() == FlatpakResource::DesktopApp) {