FlatpakBackend.cpp 59.1 KB
Newer Older
1
2
3
4
5
6
/*
 *   SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
 *   SPDX-FileCopyrightText: 2017 Jan Grulich <jgrulich@redhat.com>
 *
 *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
 */
Jan Grulich's avatar
Jan Grulich committed
7
8

#include "FlatpakBackend.h"
9
#include "FlatpakFetchDataJob.h"
10
#include "FlatpakJobTransaction.h"
Alexander Lohnau's avatar
Alexander Lohnau committed
11
#include "FlatpakSourcesBackend.h"
Jan Grulich's avatar
Jan Grulich committed
12

13
#include <ReviewsBackend/Rating.h>
Jan Grulich's avatar
Jan Grulich committed
14
#include <Transaction/Transaction.h>
15
#include <appstream/AppStreamIntegration.h>
16
#include <appstream/AppStreamUtils.h>
Alexander Lohnau's avatar
Alexander Lohnau committed
17
18
19
20
#include <appstream/OdrsReviewsBackend.h>
#include <resources/SourcesModel.h>
#include <resources/StandardBackendUpdater.h>
#include <utils.h>
Jan Grulich's avatar
Jan Grulich committed
21

22
#include <AppStreamQt/bundle.h>
Jan Grulich's avatar
Jan Grulich committed
23
#include <AppStreamQt/icon.h>
24
#include <AppStreamQt/metadata.h>
25
#include <AppStreamQt/pool.h>
26

Jan Grulich's avatar
Jan Grulich committed
27
#include <KAboutData>
Alexander Lohnau's avatar
Alexander Lohnau committed
28
#include <KConfigGroup>
Jan Grulich's avatar
Jan Grulich committed
29
30
31
32
33
34
35
#include <KLocalizedString>
#include <KPluginFactory>
#include <KSharedConfig>

#include <QDebug>
#include <QDir>
#include <QFile>
36
#include <QFileInfo>
37
#include <QFutureWatcher>
Alexander Lohnau's avatar
Alexander Lohnau committed
38
#include <QNetworkAccessManager>
39
#include <QSettings>
Alexander Lohnau's avatar
Alexander Lohnau committed
40
41
#include <QTemporaryFile>
#include <QTextStream>
Jan Grulich's avatar
Jan Grulich committed
42
43
#include <QThread>
#include <QTimer>
Alexander Lohnau's avatar
Alexander Lohnau committed
44
#include <QtConcurrentRun>
Jan Grulich's avatar
Jan Grulich committed
45

46
#include <QRegularExpression>
Alexander Lohnau's avatar
Alexander Lohnau committed
47
#include <glib.h>
48

49
50
#include <sys/stat.h>

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
51
DISCOVER_BACKEND_PLUGIN(FlatpakBackend)
Jan Grulich's avatar
Jan Grulich committed
52

53
54
55
class FlatpakSource
{
public:
56
57
58
59
60
61
62
63
    FlatpakSource(FlatpakBackend *backend, FlatpakInstallation *installation)
        : m_remote(nullptr)
        , m_installation(installation)
        , m_backend(backend)
    {
        g_object_ref(m_installation);
    }

64
65
66
67
    FlatpakSource(FlatpakBackend *backend, FlatpakInstallation *installation, FlatpakRemote *remote)
        : m_remote(remote)
        , m_installation(installation)
        , m_backend(backend)
68
        , m_appstreamIconsDir(appstreamDir() + QLatin1String("/icons"))
69
70
71
72
73
74
75
    {
        g_object_ref(m_remote);
        g_object_ref(m_installation);
    }

    ~FlatpakSource()
    {
76
77
78
        if (m_remote) {
            g_object_unref(m_remote);
        }
79
80
81
        g_object_unref(m_installation);
    }

82
83
84
85
86
    QString url() const
    {
        return m_remote ? flatpak_remote_get_url(m_remote) : QString();
    }

87
88
    bool isEnabled() const
    {
89
        return m_remote && !flatpak_remote_get_disabled(m_remote);
90
91
    }

92
93
94
95
    QString appstreamIconsDir() const {
        return m_appstreamIconsDir;
    }
    QString appstreamDir() const {
96
        Q_ASSERT(m_remote);
97
98
99
100
101
102
103
104
105
106
107
        g_autoptr(GFile) appstreamDir = flatpak_remote_get_appstream_dir(m_remote, nullptr);
        if (!appstreamDir) {
            qWarning() << "No appstream dir for" << flatpak_remote_get_name(m_remote);
            return {};
        }
        g_autofree char *path_str = g_file_get_path(appstreamDir);
        return QString::fromUtf8(path_str);
    }

    QString name() const
    {
108
        return m_remote ? QString::fromUtf8(flatpak_remote_get_name(m_remote)) : QString();
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
    }

    FlatpakInstallation *installation() const
    {
        return m_installation;
    }

    void addResource(FlatpakResource *resource)
    {
        // Update app with all possible information we have
        if (!m_backend->parseMetadataFromAppBundle(resource)) {
            qWarning() << "Failed to parse metadata from app bundle for" << resource->name();
        }

        m_backend->updateAppState(resource);

        m_resources.insert(resource->uniqueId(), resource);
        if (!resource->extends().isEmpty()) {
            m_backend->m_extends.append(resource->extends());
            m_backend->m_extends.removeDuplicates();
        }

        QObject::connect(resource, &FlatpakResource::sizeChanged, m_backend, [this, resource] {
            if (!m_backend->isFetching())
                Q_EMIT m_backend->resourcesChanged(resource, {"size", "sizeDescription"});
        });
    }

    AppStream::Pool *m_pool = nullptr;
    QHash<FlatpakResource::Id, FlatpakResource *> m_resources;

private:
    FlatpakRemote *const m_remote;
    FlatpakInstallation *const m_installation;
    FlatpakBackend *const m_backend;
144
    const QString m_appstreamIconsDir;
145
146
};

Alexander Lohnau's avatar
Alexander Lohnau committed
147
QDebug operator<<(QDebug debug, const FlatpakResource::Id &id)
148
149
150
151
152
153
154
155
156
157
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "FlatpakResource::Id(";
    debug.nospace() << "name:" << id.id << ',';
    debug.nospace() << "branch:" << id.branch << ',';
    debug.nospace() << "type:" << id.type;
    debug.nospace() << ')';
    return debug;
}

158
159
static FlatpakResource::Id idForRefString(const QStringView &ref)
{
160
161
    Q_ASSERT(!ref.isEmpty());

162
163
164
165
166
167
168
169
170
171
    auto parts = ref.split('/');
    // app/app.getspace.Space/x86_64/stable
    return {
        parts[0] == QLatin1String("app") ? FlatpakResource::DesktopApp : FlatpakResource::Runtime,
        parts[1].toString(),
        parts[3].toString(),
        parts[2].toString(),
    };
}

172
static FlatpakResource::Id idForInstalledRef(FlatpakInstalledRef *ref, const QString &postfix)
173
{
174
175
176
    const FlatpakResource::ResourceType appType = (flatpak_ref_get_kind(FLATPAK_REF(ref)) == FLATPAK_REF_KIND_APP //
                                                       ? FlatpakResource::DesktopApp
                                                       : FlatpakResource::Runtime);
177
    const QString appId = QLatin1String(flatpak_ref_get_name(FLATPAK_REF(ref))) + postfix;
178
179
180
    const QString arch = QString::fromUtf8(flatpak_ref_get_arch(FLATPAK_REF(ref)));
    const QString branch = QString::fromUtf8(flatpak_ref_get_branch(FLATPAK_REF(ref)));

181
    return {appType, appId, branch, arch};
182
183
}

Alexander Lohnau's avatar
Alexander Lohnau committed
184
FlatpakBackend::FlatpakBackend(QObject *parent)
Jan Grulich's avatar
Jan Grulich committed
185
186
    : AbstractResourcesBackend(parent)
    , m_updater(new StandardBackendUpdater(this))
187
    , m_reviews(AppStreamIntegration::global()->reviews())
188
    , m_refreshAppstreamMetadataJobs(0)
189
    , m_cancellable(g_cancellable_new())
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
190
    , m_threadPool(new QThreadPool(this))
Jan Grulich's avatar
Jan Grulich committed
191
192
193
{
    g_autoptr(GError) error = nullptr;

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

Jan Grulich's avatar
Jan Grulich committed
196
    // Load flatpak installation
197
    if (!setupFlatpakInstallations(&error)) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
198
        qWarning() << "Failed to setup flatpak installations:" << error->message;
Jan Grulich's avatar
Jan Grulich committed
199
    } else {
200
        loadAppsFromAppstreamData();
201

202
        m_sources = new FlatpakSourcesBackend(m_installations, this);
203
        SourcesModel::global()->addSourcesBackend(m_sources);
Jan Grulich's avatar
Jan Grulich committed
204
205
    }

206
    connect(m_reviews.data(), &OdrsReviewsBackend::ratingsReady, this, [this] {
207
208
        m_reviews->emitRatingFetched(this, kAppend<QList<AbstractResource *>>(m_flatpakSources, [](const auto &source) {
                                         return kTransform<QList<AbstractResource *>>(source->m_resources.values());
Alexander Lohnau's avatar
Alexander Lohnau committed
209
                                     }));
210
    });
211
212
213
214
215
216
217

    /* 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
218
219
}

220
221
FlatpakBackend::~FlatpakBackend()
{
222
    g_cancellable_cancel(m_cancellable);
Alexander Lohnau's avatar
Alexander Lohnau committed
223
    for (auto inst : qAsConst(m_installations))
224
        g_object_unref(inst);
225
226
227
228
    if (!m_threadPool.waitForDone(200)) {
        qDebug() << "could not kill them all" << m_threadPool.activeThreadCount();
    }
    m_threadPool.clear();
229

230
    g_object_unref(m_cancellable);
231
232
}

233
234
bool FlatpakBackend::isValid() const
{
235
    return m_sources && !m_installations.isEmpty();
236
237
}

238
239
class FlatpakFetchRemoteResourceJob : public QNetworkAccessManager
{
Alexander Lohnau's avatar
Alexander Lohnau committed
240
    Q_OBJECT
241
public:
242
    FlatpakFetchRemoteResourceJob(const QUrl &url, ResultsStream *stream, FlatpakBackend *backend)
243
244
        : QNetworkAccessManager(backend)
        , m_backend(backend)
245
        , m_stream(stream)
246
247
        , m_url(url)
    {
248
        connect(stream, &ResultsStream::destroyed, this, &QObject::deleteLater);
249
250
251
252
    }

    void start()
    {
253
        if (m_url.isLocalFile()) {
254
255
256
            QTimer::singleShot(0, m_stream, [this] {
                processFile(m_url);
            });
257
258
259
            return;
        }

260
261
262
        QNetworkRequest req(m_url);
        req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
        auto replyGet = get(req);
263
        connect(replyGet, &QNetworkReply::finished, this, [this, replyGet] {
Laurent Montel's avatar
Laurent Montel committed
264
            QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> replyPtr(replyGet);
265
            if (replyGet->error() != QNetworkReply::NoError) {
266
                qWarning() << "couldn't download" << m_url << replyGet->errorString();
267
                m_stream->finish();
268
269
                return;
            }
270
            const QUrl fileUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) //
271
                                                     + QLatin1Char('/') + m_url.fileName());
272
            auto replyPut = put(QNetworkRequest(fileUrl), replyGet->readAll());
273
            connect(replyPut, &QNetworkReply::finished, this, [this, fileUrl, replyPut]() {
Laurent Montel's avatar
Laurent Montel committed
274
                QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> replyPtr(replyPut);
275
                if (replyPut->error() != QNetworkReply::NoError) {
276
                    qWarning() << "couldn't save" << m_url << replyPut->errorString();
277
                    m_stream->finish();
278
279
280
                    return;
                }
                if (!fileUrl.isLocalFile()) {
281
                    m_stream->finish();
282
283
284
                    return;
                }

285
                processFile(fileUrl);
Alexander Lohnau's avatar
Alexander Lohnau committed
286
            });
287
288
289
        });
    }

290
291
292
293
    void processFile(const QUrl &fileUrl)
    {
        const auto path = fileUrl.toLocalFile();
        if (path.endsWith(QLatin1String(".flatpak"))) {
294
            m_backend->addAppFromFlatpakBundle(fileUrl, m_stream);
295
        } else if (path.endsWith(QLatin1String(".flatpakref"))) {
296
            m_backend->addAppFromFlatpakRef(fileUrl, m_stream);
297
        } else if (path.endsWith(QLatin1String(".flatpakrepo"))) {
298
            m_backend->addSourceFromFlatpakRepo(fileUrl, m_stream);
299
300
301
302
303
        } else {
            qWarning() << "unrecognized format" << fileUrl;
        }
    }

304
private:
Alexander Lohnau's avatar
Alexander Lohnau committed
305
    FlatpakBackend *const m_backend;
306
    ResultsStream *const m_stream;
307
    const QUrl m_url;
308
309
};

Alexander Lohnau's avatar
Alexander Lohnau committed
310
FlatpakRemote *FlatpakBackend::getFlatpakRemoteByUrl(const QString &url, FlatpakInstallation *installation) const
Jan Grulich's avatar
Jan Grulich committed
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
{
    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;
}

328
FlatpakInstalledRef *FlatpakBackend::getInstalledRefForApp(const FlatpakResource *resource) const
329
{
330
    Q_ASSERT(resource->resourceType() != FlatpakResource::Source);
331
332
    g_autoptr(GError) localError = nullptr;

333
    const auto type = resource->resourceType() == FlatpakResource::DesktopApp ? FLATPAK_REF_KIND_APP : FLATPAK_REF_KIND_RUNTIME;
334

335
    FlatpakInstalledRef *ref = flatpak_installation_get_installed_ref(resource->installation(),
Alexander Lohnau's avatar
Alexander Lohnau committed
336
337
338
339
340
341
                                                                      type,
                                                                      resource->flatpakName().toUtf8().constData(),
                                                                      resource->arch().toUtf8().constData(),
                                                                      resource->branch().toUtf8().constData(),
                                                                      m_cancellable,
                                                                      &localError);
342
    return ref;
343
344
}

345
346
347
348
349
350
351
QString refToBundleId(FlatpakRef *ref)
{
    return QString(flatpak_ref_get_kind(ref) == FLATPAK_REF_KIND_APP ? "app/" : "runtime/") + flatpak_ref_get_name(ref) + '/' + flatpak_ref_get_arch(ref) + '/'
        + flatpak_ref_get_branch(ref);
}

FlatpakResource *FlatpakBackend::getAppForInstalledRef(FlatpakInstallation *installation, FlatpakInstalledRef *ref) const
Jan Grulich's avatar
Jan Grulich committed
352
{
353
    auto id = idForInstalledRef(ref, {});
354
355
356
357
358
359
    for (const auto &source : m_flatpakSources) {
        auto ret = source->m_resources.value(id);
        if (ret) {
            return ret;
        }
    }
360
    auto id2 = idForInstalledRef(ref, QStringLiteral(".desktop"));
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
    for (const auto &source : m_flatpakSources) {
        auto ret = source->m_resources.value(id2);
        if (ret) {
            return ret;
        }
    }

    const QLatin1String name(flatpak_ref_get_name(FLATPAK_REF(ref)));

    const QString origin = QString::fromUtf8(flatpak_installed_ref_get_origin(ref));
    auto source = findSource(installation, origin);
    const QString pathExports = FlatpakResource::installationPath(installation) + QLatin1String("/exports/");
    const QString pathApps = pathExports + QLatin1String("share/applications/");
    AppStream::Component cid;
    if (source && source->m_pool) {
        QList<AppStream::Component> comps = source->m_pool->componentsById(name);
        if (comps.isEmpty()) {
            comps = source->m_pool->componentsById(name + ".desktop");
        }

        if (comps.isEmpty()) {
            const QString bundleId = refToBundleId(FLATPAK_REF(ref));
            comps = kFilter<QList<AppStream::Component>>(comps, [&bundleId](const AppStream::Component &comp) -> bool {
                return comp.bundle(AppStream::Bundle::Kind::KindFlatpak).id() == bundleId;
            });
        }

        if (comps.count() >= 1) {
            Q_ASSERT(comps.count() == 1);
            cid = comps.constFirst();
        }
    }

    if (!cid.isValid()) {
        AppStream::Metadata metadata;
        const QString fnDesktop = pathApps + name + QLatin1String(".desktop");
        AppStream::Metadata::MetadataError error = metadata.parseFile(fnDesktop, AppStream::Metadata::FormatKindDesktopEntry);
        if (error != AppStream::Metadata::MetadataErrorNoError) {
            if (QFile::exists(fnDesktop))
                qDebug() << "Failed to parse appstream metadata:" << error << fnDesktop;

            cid.setId(QString::fromLatin1(flatpak_ref_get_name(FLATPAK_REF(ref))));
#if FLATPAK_CHECK_VERSION(1, 1, 2)
            cid.setName(QString::fromUtf8(flatpak_installed_ref_get_appdata_name(ref)));
#endif
        } else
            cid = metadata.component();
    }

410
411
    FlatpakResource *resource = new FlatpakResource(cid, source->installation(), const_cast<FlatpakBackend *>(this));
    resource->setOrigin(source->name());
412
413
    resource->setIconPath(pathExports);
    resource->updateFromRef(FLATPAK_REF(ref));
414
    resource->setState(AbstractResource::Installed);
415
416
417
418
419
420
421
422
423
424
425
    source->addResource(resource);
    return resource;
}

QSharedPointer<FlatpakSource> FlatpakBackend::findSource(FlatpakInstallation *installation, const QString &origin) const
{
    for (const auto &source : m_flatpakSources) {
        if (source->installation() == installation && source->name() == origin) {
            return source;
        }
    }
426
427
428
429
430
    for (const auto &source : m_flatpakLoadingSources) {
        if (source->installation() == installation && source->name() == origin) {
            return source;
        }
    }
431
432

    Q_UNREACHABLE();
Jan Grulich's avatar
Jan Grulich committed
433
434
}

Alexander Lohnau's avatar
Alexander Lohnau committed
435
FlatpakResource *FlatpakBackend::getRuntimeForApp(FlatpakResource *resource) const
436
437
{
    FlatpakResource *runtime = nullptr;
438
439
    const QString runtimeName = resource->runtime();
    const auto runtimeInfo = runtimeName.splitRef(QLatin1Char('/'));
440
441
442
443
444

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

445
446
447
448
449
450
451
    for (const auto &source : m_flatpakSources) {
        for (auto it = source->m_resources.constBegin(), itEnd = source->m_resources.constEnd(); it != itEnd; ++it) {
            const auto &id = it.key();
            if (id.type == FlatpakResource::Runtime && id.id == runtimeInfo.at(0) && id.branch == runtimeInfo.at(2)) {
                runtime = *it;
                break;
            }
452
453
454
455
        }
    }

    // TODO if runtime wasn't found, create a new one from available info
456
    if (!runtime) {
457
        qWarning() << "could not find runtime" << runtimeName << resource;
458
    }
459
460
461
462

    return runtime;
}

463
void FlatpakBackend::addAppFromFlatpakBundle(const QUrl &url, ResultsStream *stream)
Jan Grulich's avatar
Jan Grulich committed
464
{
465
466
467
    auto x = qScopeGuard([stream] {
        stream->finish();
    });
Jan Grulich's avatar
Jan Grulich committed
468
469
470
471
    g_autoptr(GBytes) appstreamGz = nullptr;
    g_autoptr(GError) localError = nullptr;
    g_autoptr(GFile) file = nullptr;
    g_autoptr(FlatpakBundleRef) bundleRef = nullptr;
472
    AppStream::Component asComponent;
Jan Grulich's avatar
Jan Grulich committed
473

474
    file = g_file_new_for_path(url.toLocalFile().toUtf8().constData());
Jan Grulich's avatar
Jan Grulich committed
475
476
477
    bundleRef = flatpak_bundle_ref_new(file, &localError);

    if (!bundleRef) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
478
        qWarning() << "Failed to load bundle:" << localError->message;
479
        return;
Jan Grulich's avatar
Jan Grulich committed
480
481
    }

482
    gsize len = 0;
483
    g_autoptr(GBytes) metadata = flatpak_bundle_ref_get_metadata(bundleRef);
484
485
    const QByteArray metadataContent((char *)g_bytes_get_data(metadata, &len));

Jan Grulich's avatar
Jan Grulich committed
486
487
488
489
490
491
492
493
    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 */
Alexander Lohnau's avatar
Alexander Lohnau committed
494
495
        decompressor = g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_GZIP);
        streamGz = g_memory_input_stream_new_from_bytes(appstreamGz);
Jan Grulich's avatar
Jan Grulich committed
496
        if (!streamGz) {
497
            return;
Jan Grulich's avatar
Jan Grulich committed
498
499
        }

Alexander Lohnau's avatar
Alexander Lohnau committed
500
        streamData = g_converter_input_stream_new(streamGz, G_CONVERTER(decompressor));
Jan Grulich's avatar
Jan Grulich committed
501

Alexander Lohnau's avatar
Alexander Lohnau committed
502
        appstream = g_input_stream_read_bytes(streamData, 0x100000, m_cancellable, &localError);
Jan Grulich's avatar
Jan Grulich committed
503
        if (!appstream) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
504
            qWarning() << "Failed to extract appstream metadata from bundle:" << localError->message;
505
            return;
Jan Grulich's avatar
Jan Grulich committed
506
507
        }

508
509
510
        gsize len = 0;
        gconstpointer data = g_bytes_get_data(appstream, &len);

511
512
        AppStream::Metadata metadata;
        metadata.setFormatStyle(AppStream::Metadata::FormatStyleCollection);
Alexander Lohnau's avatar
Alexander Lohnau committed
513
        AppStream::Metadata::MetadataError error = metadata.parse(QString::fromUtf8((char *)data, len), AppStream::Metadata::FormatKindXml);
514
515
        if (error != AppStream::Metadata::MetadataErrorNoError) {
            qWarning() << "Failed to parse appstream metadata: " << error;
516
            return;
Jan Grulich's avatar
Jan Grulich committed
517
518
        }

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
519
        const QList<AppStream::Component> components = metadata.components();
520
        if (components.size()) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
521
            asComponent = components.first();
Jan Grulich's avatar
Jan Grulich committed
522
523
        } else {
            qWarning() << "Failed to parse appstream metadata";
524
            return;
Jan Grulich's avatar
Jan Grulich committed
525
526
527
        }
    } else {
        qWarning() << "No appstream metadata in bundle";
528
529
530
531
532

        QTemporaryFile tempFile;
        tempFile.setAutoRemove(false);
        if (!tempFile.open()) {
            qWarning() << "Failed to get metadata file";
533
            return;
534
535
536
537
538
539
540
541
542
543
544
545
        }

        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
546
547
    }

548
    FlatpakResource *resource = new FlatpakResource(asComponent, preferredInstallation(), this);
Jan Grulich's avatar
Jan Grulich committed
549
    if (!updateAppMetadata(resource, metadataContent)) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
550
        delete resource;
Jan Grulich's avatar
Jan Grulich committed
551
        qWarning() << "Failed to update metadata from app bundle";
552
        return;
Jan Grulich's avatar
Jan Grulich committed
553
554
    }

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
555
    g_autoptr(GBytes) iconData = flatpak_bundle_ref_get_icon(bundleRef, 128);
Jan Grulich's avatar
Jan Grulich committed
556
557
558
559
560
561
    if (!iconData) {
        iconData = flatpak_bundle_ref_get_icon(bundleRef, 64);
    }

    if (iconData) {
        gsize len = 0;
Alexander Lohnau's avatar
Alexander Lohnau committed
562
        char *data = (char *)g_bytes_get_data(iconData, &len);
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
563
564
565

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

569
570
    const QString origin = QString::fromUtf8(flatpak_bundle_ref_get_origin(bundleRef));

571
    resource->setDownloadSize(0);
Jan Grulich's avatar
Jan Grulich committed
572
    resource->setInstalledSize(flatpak_bundle_ref_get_installed_size(bundleRef));
573
574
    resource->setPropertyState(FlatpakResource::DownloadSize, FlatpakResource::AlreadyKnown);
    resource->setPropertyState(FlatpakResource::InstalledSize, FlatpakResource::AlreadyKnown);
575
    resource->setFlatpakFileType(FlatpakResource::FileFlatpak);
576
    resource->setOrigin(origin.isEmpty() ? i18n("Local bundle") : origin);
Jan Grulich's avatar
Jan Grulich committed
577
578
579
580
    resource->setResourceFile(url);
    resource->setState(FlatpakResource::None);
    resource->setType(FlatpakResource::DesktopApp);

581
    if (!m_localSource) {
582
        m_localSource.reset(new FlatpakSource(this, preferredInstallation()));
583
584
585
        m_flatpakSources += m_localSource;
    }
    m_localSource->addResource(resource);
586
    stream->resourcesFound({resource});
Jan Grulich's avatar
Jan Grulich committed
587
588
}

589
void FlatpakBackend::addAppFromFlatpakRef(const QUrl &url, ResultsStream *stream)
Jan Grulich's avatar
Jan Grulich committed
590
{
591
    Q_ASSERT(url.isLocalFile());
Jan Grulich's avatar
Jan Grulich committed
592
593
    QSettings settings(url.toLocalFile(), QSettings::NativeFormat);
    const QString refurl = settings.value(QStringLiteral("Flatpak Ref/Url")).toString();
594
    const QString name = settings.value(QStringLiteral("Flatpak Ref/Name")).toString();
595
596
    const QString remoteName = settings.value(QStringLiteral("Flatpak Ref/SuggestRemoteName")).toString();
    g_autoptr(GError) error = nullptr;
597

598
599
600
601
602
603
604
605
606
607
    // If we already added the remote, just go with it
    g_autoptr(FlatpakRemote) remote = flatpak_installation_get_remote_by_name(preferredInstallation(), remoteName.toUtf8().constData(), m_cancellable, &error);
    if (remote && flatpak_remote_get_url(remote) != refurl) {
        remote = nullptr;
    }
    if (remote) {
        m_refreshAppstreamMetadataJobs++;
        auto source = integrateRemote(preferredInstallation(), remote);
        if (source) {
            auto searchComponent = [this, stream, source, name] {
608
609
610
611
612
                auto comps = source->m_pool->componentsById(name);
                if (comps.isEmpty()) {
                    const QString nameWithDesktop = name + QLatin1String(".desktop");
                    comps = source->m_pool->componentsById(nameWithDesktop);
                }
613
614
615
616
                auto resources = kTransform<QVector<AbstractResource *>>(comps, [this, source](const auto &comp) {
                    return resourceForComponent(comp, source);
                });
                stream->resourcesFound(resources);
617
                stream->finish();
618
619
620
621
622
            };
            if (source->m_pool) {
                QTimer::singleShot(0, this, searchComponent);
            } else {
                connect(this, &FlatpakBackend::initialized, stream, searchComponent);
623
            }
624
            return;
625
626
        }
    }
Jan Grulich's avatar
Jan Grulich committed
627
628
629
630
631

    g_autoptr(FlatpakRemoteRef) remoteRef = nullptr;
    {
        QFile f(url.toLocalFile());
        if (!f.open(QFile::ReadOnly | QFile::Text)) {
632
633
            stream->finish();
            return;
Jan Grulich's avatar
Jan Grulich committed
634
635
636
637
        }

        QByteArray contents = f.readAll();

Alexander Lohnau's avatar
Alexander Lohnau committed
638
        g_autoptr(GBytes) bytes = g_bytes_new(contents.data(), contents.size());
Jan Grulich's avatar
Jan Grulich committed
639

Alexander Lohnau's avatar
Alexander Lohnau committed
640
        remoteRef = flatpak_installation_install_ref_file(preferredInstallation(), bytes, m_cancellable, &error);
Jan Grulich's avatar
Jan Grulich committed
641
        if (!remoteRef) {
642
            qWarning() << "Failed to create install ref file:" << error->message;
643
644
645
646
647
            AbstractResourcesBackend::Filters filter;
            filter.resourceUrl = QUrl(QLatin1String("appstream://") + name);
            auto streamKnown = search(filter);
            connect(streamKnown, &ResultsStream::resourcesFound, stream, &ResultsStream::resourcesFound);
            connect(streamKnown, &ResultsStream::destroyed, stream, &ResultsStream::finish);
648
            return;
Jan Grulich's avatar
Jan Grulich committed
649
650
651
652
653
        }
    }

    auto ref = FLATPAK_REF(remoteRef);

654
655
656
657
658
    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());
659
    asComponent.setId(name);
660

661
662
663
664
665
    AppStream::Bundle b;
    b.setKind(AppStream::Bundle::KindFlatpak);
    b.setId(flatpak_ref_format_ref(ref));
    asComponent.addBundle(b);

Jan Grulich's avatar
Jan Grulich committed
666
667
    const QString iconUrl = settings.value(QStringLiteral("Flatpak Ref/Icon")).toString();
    if (!iconUrl.isEmpty()) {
668
669
670
671
        AppStream::Icon icon;
        icon.setKind(AppStream::Icon::KindRemote);
        icon.setUrl(QUrl(iconUrl));
        asComponent.addIcon(icon);
Jan Grulich's avatar
Jan Grulich committed
672
673
    }

674
    auto resource = new FlatpakResource(asComponent, preferredInstallation(), this);
675
    resource->setFlatpakFileType(FlatpakResource::FileFlatpakRef);
676
    resource->setOrigin(remoteName);
Jan Grulich's avatar
Jan Grulich committed
677
    resource->updateFromRef(ref);
678
679

    QUrl runtimeUrl = QUrl(settings.value(QStringLiteral("Flatpak Ref/RuntimeRepo")).toString());
680
    auto refSource = QSharedPointer<FlatpakSource>::create(this, preferredInstallation());
681
    m_flatpakSources += refSource;
682
683
    if (!runtimeUrl.isEmpty()) {
        // We need to fetch metadata to find information about required runtime
684
        auto fw = new QFutureWatcher<QByteArray>(this);
685
        connect(fw, &QFutureWatcher<QByteArray>::finished, this, [this, resource, fw, runtimeUrl, stream, refSource]() {
686
            fw->deleteLater();
687
            const auto metadata = fw->result();
688
            // Even when we failed to fetch information about runtime we still want to show the application
689
            if (metadata.isEmpty()) {
690
                Q_EMIT onFetchMetadataFinished(resource, metadata);
691
            } else {
692
693
694
695
                updateAppMetadata(resource, metadata);

                auto runtime = getRuntimeForApp(resource);
                if (!runtime || (runtime && !runtime->isInstalled())) {
696
                    auto repoStream = new ResultsStream(QLatin1String("FlatpakStream-searchrepo-") + runtimeUrl.toString());
697
698
699
700
701
702
703
704
705
706
707
                    connect(repoStream,
                            &ResultsStream::resourcesFound,
                            this,
                            [this, resource, stream, refSource](const QVector<AbstractResource *> &resources) {
                                for (auto res : resources) {
                                    installApplication(res);
                                }
                                refSource->addResource(resource);
                                stream->resourcesFound({resource});
                                stream->finish();
                            });
708

709
                    auto fetchRemoteResource = new FlatpakFetchRemoteResourceJob(runtimeUrl, repoStream, this);
710
711
712
                    fetchRemoteResource->start();
                    return;
                } else {
713
                    refSource->addResource(resource);
714
                }
715
            }
716
717
            stream->resourcesFound({resource});
            stream->finish();
718
        });
719
        fw->setFuture(QtConcurrent::run(&m_threadPool, &FlatpakRunnables::fetchMetadata, resource, m_cancellable));
720
    } else {
721
        refSource->addResource(resource);
722
723
        stream->resourcesFound({resource});
        stream->finish();
724
725
    }

Jan Grulich's avatar
Jan Grulich committed
726
727
}

728
void FlatpakBackend::addSourceFromFlatpakRepo(const QUrl &url, ResultsStream *stream)
729
{
730
731
732
    auto x = qScopeGuard([stream] {
        stream->finish();
    });
733
    Q_ASSERT(url.isLocalFile());
734
735
736
737
738
739
740
    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()) {
741
        return;
742
743
    }

744
    if (gpgKey.startsWith(QLatin1String("http://")) || gpgKey.startsWith(QLatin1String("https://"))) {
745
        return;
746
747
    }

748
749
750
751
752
753
754
    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());

755
    const QString iconUrl = settings.value(QStringLiteral("Flatpak Repo/Icon")).toString();
756
    if (!iconUrl.isEmpty()) {
757
758
759
760
        AppStream::Icon icon;
        icon.setKind(AppStream::Icon::KindRemote);
        icon.setUrl(QUrl(iconUrl));
        asComponent.addIcon(icon);
761
762
    }

763
    auto resource = new FlatpakResource(asComponent, preferredInstallation(), this);
764
765
766
767
768
769
770
    // 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);

771
772
    g_autoptr(FlatpakRemote) repo =
        flatpak_installation_get_remote_by_name(preferredInstallation(), resource->flatpakName().toUtf8().constData(), m_cancellable, nullptr);
773
774
775
776
777
778
    if (!repo) {
        resource->setState(AbstractResource::State::None);
    } else {
        resource->setState(AbstractResource::State::Installed);
    }

779
    stream->resourcesFound({resource});
780
781
}

782
783
784
785
void FlatpakBackend::loadAppsFromAppstreamData()
{
    for (auto installation : qAsConst(m_installations)) {
        // Load applications from appstream metadata
786
787
788
        if (g_cancellable_is_cancelled(m_cancellable))
            break;

789
790
791
792
793
794
        if (!loadAppsFromAppstreamData(installation)) {
            qWarning() << "Failed to load packages from appstream data from installation" << installation;
        }
    }
}

795
796
797
798
bool FlatpakBackend::loadAppsFromAppstreamData(FlatpakInstallation *flatpakInstallation)
{
    Q_ASSERT(flatpakInstallation);

799
    g_autoptr(GPtrArray) remotes = flatpak_installation_list_remotes(flatpakInstallation, m_cancellable, nullptr);
Jan Grulich's avatar
Jan Grulich committed
800
801
802
803
    if (!remotes) {
        return false;
    }

804
805
    m_refreshAppstreamMetadataJobs += remotes->len;

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

810
        g_autofree char *path_str = g_file_get_path(fileTimestamp);
811
        QFileInfo fileInfo(QFile::encodeName(path_str));
812
813
814
815
816
817
        // 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);
        }
818
819
820
    }
    return true;
}
Jan Grulich's avatar
Jan Grulich committed
821

822
823
824
825
826
827
828
829
void FlatpakBackend::metadataRefreshed()
{
    m_refreshAppstreamMetadataJobs--;
    if (m_refreshAppstreamMetadataJobs == 0) {
        checkForUpdates();
    }
}

830
QSharedPointer<FlatpakSource> FlatpakBackend::integrateRemote(FlatpakInstallation *flatpakInstallation, FlatpakRemote *remote)
831
{
832
    Q_ASSERT(m_refreshAppstreamMetadataJobs != 0);
833
834
835
836
837
838
839
840
841
842
843
844
845
846
    for (auto source : qAsConst(m_flatpakSources)) {
        if (source->url() == flatpak_remote_get_url(remote)) {
            qDebug() << "do not add a source twice" << source << remote;
            metadataRefreshed();
            return source;
        }
    }
    for (auto source : qAsConst(m_flatpakLoadingSources)) {
        if (source->url() == flatpak_remote_get_url(remote)) {
            qDebug() << "do not add a source twice" << source << remote;
            metadataRefreshed();
            return source;
        }
    }
847

848
    auto source = QSharedPointer<FlatpakSource>::create(this, flatpakInstallation, remote);
849
    if (!source->isEnabled() || flatpak_remote_get_noenumerate(remote)) {
850
        m_flatpakSources += source;
851
        metadataRefreshed();
852
        return {};
853
    }
Jan Grulich's avatar
Jan Grulich committed
854

855
    const QString appstreamDirPath = source->appstreamDir();
856
    if (!QFile::exists(appstreamDirPath)) {
857
        qWarning() << "No" << appstreamDirPath << "appstream metadata found for" << source->name();
858
        metadataRefreshed();
859
        return {};
Jan Grulich's avatar
Jan Grulich committed
860
861
    }

862
863
864
865
    AppStream::Pool *pool = new AppStream::Pool(this);
    auto fw = new QFutureWatcher<bool>(this);
    const auto sourceName = source->name();
    connect(fw, &QFutureWatcher<bool>::finished, this, [this, fw, pool, source]() {
866
867
        source->m_pool = pool;
        m_flatpakLoadingSources.removeAll(source);
868
869
870
871
        if (fw->result()) {
            m_flatpakSources += source;
        } else {
            qWarning() << "Could not open the AppStream metadata pool" << pool->lastError();
872
        }
873
        metadataRefreshed();
874
        acquireFetching(false);
875
876
        fw->deleteLater();
    });
877
    acquireFetching(true);
878
879
    pool->clearMetadataLocations();
    pool->addMetadataLocation(appstreamDirPath);
880
    pool->setFlags(AppStream::Pool::FlagReadCollection);
881
882
883
884
885
    pool->setCacheFlags(AppStream::Pool::CacheFlagUseUser);

    const QString subdir = flatpak_installation_get_id(flatpakInstallation) + QLatin1Char('/') + sourceName;
    pool->setCacheLocation(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/flatpak-appstream/" + subdir);
    QDir().mkpath(pool->cacheLocation());
886
    fw->setFuture(QtConcurrent::run(&m_threadPool, pool, &AppStream::Pool::load));
887
888
    m_flatpakLoadingSources << source;
    return source;
Jan Grulich's avatar
Jan Grulich committed
889
890
}

Jan Grulich's avatar
Jan Grulich committed
891
892
893
void FlatpakBackend::loadLocalUpdates(FlatpakInstallation *flatpakInstallation)
{
    g_autoptr(GError) localError = nullptr;
894
    g_autoptr(GPtrArray) refs = flatpak_installation_list_installed_refs(flatpakInstallation, m_cancellable, &localError);
Jan Grulich's avatar
Jan Grulich committed
895
    if (!refs) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
896
        qWarning() << "Failed to get list of installed refs for listing updates:" << localError->message;
Jan Grulich's avatar
Jan Grulich committed
897
898
899
900
901
        return;
    }

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

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
903
        const gchar *latestCommit = flatpak_installed_ref_get_latest_commit(ref);
Jan Grulich's avatar
Jan Grulich committed
904
905

        if (!latestCommit) {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
906
            qWarning() << "Couldn't get latest commit for" << flatpak_ref_get_name(FLATPAK_REF(ref));
907
            continue;
Jan Grulich's avatar
Jan Grulich committed
908
909
        }

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
910
        const gchar *commit = flatpak_ref_get_commit(FLATPAK_REF(ref));
Jan Grulich's avatar
Jan Grulich committed
911
912
913
914
915
916
917
        if (g_strcmp0(commit, latestCommit) == 0) {
            continue;
        }

        FlatpakResource *resource = getAppForInstalledRef(flatpakInstallation, ref);
        if (resource) {
            resource->setState(AbstractResource::Upgradeable);
918
            updateAppSize(resource);
Jan Grulich's avatar
Jan Grulich committed
919
920
921
922
        }
    }
}

923
924
925
bool FlatpakBackend::parseMetadataFromAppBundle(FlatpakResource *resource)
{
    g_autoptr(GError) localError = nullptr;
926
927
928
929
930
931
    g_autoptr(FlatpakRef) ref = flatpak_ref_parse(resource->ref().toUtf8().constData(), &localError);
    if (!ref) {
        qWarning() << "Failed to parse" << resource->ref() << localError->message;
        return false;
    } else {
        resource->updateFromRef(ref);
932
933
934
935
936
    }

    return true;
}

937
938
939
940
941
942
class FlatpakRefreshAppstreamMetadataJob : public QThread
{
    Q_OBJECT
public:
    FlatpakRefreshAppstreamMetadataJob(FlatpakInstallation *installation, FlatpakRemote *remote)
        : QThread()
943
        , m_cancellable(g_cancellable_new())
944
945
946
        , m_installation(installation)
        , m_remote(remote)
    {
947
        g_object_ref(m_remote);
948
        connect(this, &FlatpakRefreshAppstreamMetadataJob::finished, this, &QObject::deleteLater);
949
950
951
952
    }

    ~FlatpakRefreshAppstreamMetadataJob()
    {
953
        g_object_unref(m_remote);
954
955
956
957
958
959
960
961
962
963
964
965
        g_object_unref(m_cancellable);
    }

    void cancel()
    {
        g_cancellable_cancel(m_cancellable);
    }

    void run() override
    {
        g_autoptr(GError) localError = nullptr;

Alexander Lohnau's avatar
Alexander Lohnau committed
966
967
968
969
970
971
972
973
974
975
976
#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)) {
977
978
#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
979
#endif
980
981
982
            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);
Alexander Lohnau's avatar
Alexander Lohnau committed
983
        } else {
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
984
            Q_EMIT jobRefreshAppstreamMetadataFinished(m_installation, m_remote);
985
986
987
988
        }
    }

Q_SIGNALS:
989
    void jobRefreshAppstreamMetadataFailed(const QString &errorMessage);
990
991
992
993
994
995
996
997
998
999
1000
    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);