Commit f364bf80 authored by Aleix Pol Gonzalez's avatar Aleix Pol Gonzalez 🐧

Initial port to snapd-glib library

CCMAIL: robert.ancell@canonical.com
parent ae25ff80
......@@ -30,6 +30,8 @@ find_package(KF5NewStuff 5.23)
pkg_check_modules(FLATPAK flatpak>=0.6.12)
pkg_check_modules(APPSTREAM appstream)
find_package(Snapd)
configure_file(DiscoverVersion.h.in DiscoverVersion.h)
add_subdirectory(libdiscover)
......@@ -69,5 +71,10 @@ set_package_properties(AppStreamQt PROPERTIES
URL "http://www.freedesktop.org"
PURPOSE "Required to build the PackageKit backend"
TYPE OPTIONAL)
set_package_properties(Snapd PROPERTIES
DESCRIPTION "Library that exposes Snapd"
URL "http://www.snapcraft.io"
PURPOSE "Required to build the Snap backend"
TYPE OPTIONAL)
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
add_subdirectory(tests)
add_subdirectory(libsnapclient)
add_library(snap-backend MODULE SnapResource.cpp SnapBackend.cpp SnapReviewsBackend.cpp SnapTransaction.cpp)
target_link_libraries(snap-backend Qt5::Core KF5::CoreAddons KF5::ConfigCore Discover::Common Discover::SnapClient)
target_link_libraries(snap-backend Qt5::Core KF5::CoreAddons KF5::ConfigCore Discover::Common Snapd::Core)
install(TARGETS snap-backend DESTINATION ${PLUGIN_INSTALL_DIR}/discover)
install(FILES snap-backend-categories.xml DESTINATION ${DATA_INSTALL_DIR}/libdiscover/categories)
# add_library(SnapNotifier MODULE SnapNotifier.cpp)
# target_link_libraries(SnapNotifier Discover::Notifiers)
# set_target_properties(SnapNotifier PROPERTIES INSTALL_RPATH ${CMAKE_INSTALL_FULL_LIBDIR}/plasma-discover)
#
# install(TARGETS SnapNotifier DESTINATION ${PLUGIN_INSTALL_DIR}/discover-notifier)
......@@ -44,6 +44,10 @@ SnapBackend::SnapBackend(QObject* parent)
, m_updater(new StandardBackendUpdater(this))
, m_reviews(new SnapReviewsBackend(this))
{
{
auto request = m_client.connect();
request->runSync();
}
connect(m_reviews, &SnapReviewsBackend::ratingsReady, this, &AbstractResourcesBackend::emitRatingsReady);
}
......@@ -59,53 +63,60 @@ ResultsStream * SnapBackend::search(const AbstractResourcesBackend::Filters& fil
if (filters.category && filters.category->isAddons())
return voidStream();
if (filters.state >= AbstractResource::Installed) {
return populate(m_socket.snaps(), AbstractResource::Installed);
return populate(m_client.list(), AbstractResource::Installed);
} else {
return populate(m_socket.find(filters.search), AbstractResource::None);
return populate(m_client.find(QSnapdClient::FindFlag::None, filters.search), AbstractResource::None);
}
return voidStream();
}
ResultsStream * SnapBackend::findResourceByPackageName(const QUrl& search)
{
return search.scheme() == QLatin1String("snap") ? populate(m_socket.snapByName(search.host()), AbstractResource::Installed) : voidStream();
return search.scheme() == QLatin1String("snap") ? populate(m_client.find(QSnapdClient::MatchName, search.host()), AbstractResource::Installed) : voidStream();
}
ResultsStream* SnapBackend::populate(SnapJob* job, AbstractResource::State state)
template <class T>
ResultsStream* SnapBackend::populate(T* job, AbstractResource::State state)
{
auto stream = new ResultsStream(QStringLiteral("Snap-populate"));
connect(job, &SnapJob::finished, stream, [stream, this, state](SnapJob* job) {
if (!job->isSuccessful()) {
stream->finish();
return;
connect(job, &QSnapdFindRequest::complete, stream, [stream, this, state, job]() {
QSet<SnapResource*> higher;
if (state == AbstractResource::Installed) {
for(auto res: m_resources) {
if (res->state()>=state)
higher += res;
}
}
const auto snaps = job->result().toArray();
QVector<AbstractResource*> ret;
QSet<SnapResource*> resources;
for(const auto& snap: snaps) {
const auto snapObj = snap.toObject();
const auto snapid = snapObj.value(QLatin1String("name")).toString();
SnapResource* res = m_resources.value(snapid);
for (int i=0, c=job->snapCount(); i<c; ++i) {
const auto snap = job->snap(i);
const auto snapname = snap->name();
SnapResource* res = m_resources.value(snapname);
if (!res) {
res = new SnapResource(snapObj, state, this);
Q_ASSERT(res->packageName() == snapid);
res = new SnapResource(snap, state, this);
Q_ASSERT(res->packageName() == snapname);
resources += res;
} else {
res->setState(state);
higher.remove(res);
}
ret += res;
}
if (!resources.isEmpty()) {
foreach(SnapResource* res, resources)
m_resources[res->packageName()] = res;
foreach(SnapResource* res, resources)
m_resources[res->packageName()] = res;
for(auto res: higher) {
res->setState(AbstractResource::None);
}
if (!ret.isEmpty())
stream->resourcesFound(ret);
stream->finish();
});
job->start();
job->runAsync();
return stream;
}
......@@ -138,15 +149,13 @@ Transaction* SnapBackend::installApplication(AbstractResource* app, const AddonL
Transaction* SnapBackend::installApplication(AbstractResource* _app)
{
auto app = qobject_cast<SnapResource*>(_app);
auto job = m_socket.snapAction(app->packageName(), SnapSocket::Install);
return new SnapTransaction(app, job, &m_socket, Transaction::InstallRole);
return new SnapTransaction(app, m_client.install(app->packageName()), Transaction::InstallRole);
}
Transaction* SnapBackend::removeApplication(AbstractResource* _app)
{
auto app = qobject_cast<SnapResource*>(_app);
auto job = m_socket.snapAction(app->packageName(), SnapSocket::Remove);
return new SnapTransaction(app, job, &m_socket, Transaction::RemoveRole);
return new SnapTransaction(app, m_client.remove(app->packageName()), Transaction::RemoveRole);
}
QString SnapBackend::displayName() const
......@@ -154,4 +163,9 @@ QString SnapBackend::displayName() const
return QStringLiteral("Snap");
}
void SnapBackend::refreshStates()
{
populate(m_client.list(), AbstractResource::Installed);
}
#include "SnapBackend.moc"
......@@ -24,7 +24,7 @@
#include <resources/AbstractResource.h>
#include <resources/AbstractResourcesBackend.h>
#include <QVariantList>
#include "SnapSocket.h"
#include <Snapd/Client>
class QAction;
class SnapReviewsBackend;
......@@ -50,20 +50,22 @@ public:
Transaction* installApplication(AbstractResource* app, const AddonList& addons) override;
Transaction* removeApplication(AbstractResource* app) override;
bool isFetching() const override { return m_fetching; }
SnapSocket* socket() { return &m_socket; }
void checkForUpdates() override {}
bool hasApplications() const override { return true; }
QSnapdClient* client() { return &m_client; }
void refreshStates();
private:
void setFetching(bool fetching);
ResultsStream* populate(SnapJob* snaps, AbstractResource::State state);
template <class T>
ResultsStream* populate(T* snaps, AbstractResource::State state);
QHash<QString, SnapResource*> m_resources;
StandardBackendUpdater* m_updater;
SnapReviewsBackend* m_reviews;
SnapSocket m_socket;
bool m_fetching = false;
QSnapdClient m_client;
};
#endif // SNAPBACKEND_H
......@@ -23,10 +23,10 @@
#include <QDebug>
#include <QProcess>
SnapResource::SnapResource(QJsonObject data, AbstractResource::State state, AbstractResourcesBackend* parent)
SnapResource::SnapResource(QSnapdSnap* snap, AbstractResource::State state, SnapBackend* parent)
: AbstractResource(parent)
, m_state(state)
, m_data(std::move(data))
, m_snap(snap)
{
}
......@@ -42,12 +42,12 @@ QStringList SnapResource::categories()
QString SnapResource::comment()
{
return m_data.value(QLatin1String("summary")).toString();
return m_snap->summary();
}
int SnapResource::size()
{
return m_data.value(QLatin1String("installed-size")).toInt();
return m_snap->installedSize();
}
QUrl SnapResource::homepage()
......@@ -57,12 +57,12 @@ QUrl SnapResource::homepage()
QVariant SnapResource::icon() const
{
return QUrl(m_data.value(QLatin1String("icon")).toString());
return QUrl(m_snap->icon());
}
QString SnapResource::installedVersion() const
{
return m_data.value(QLatin1String("version")).toString();
return m_snap->version();
}
QString SnapResource::license()
......@@ -72,22 +72,22 @@ QString SnapResource::license()
QString SnapResource::longDescription()
{
return m_data.value(QLatin1String("description")).toString();
return m_snap->description();
}
QString SnapResource::name()
{
return m_data.value(QLatin1String("name")).toString();
return m_snap->name();
}
QString SnapResource::origin() const
{
return QStringLiteral("snappy:") + m_data.value(QLatin1String("channel")).toString();
return QStringLiteral("snappy:") + m_snap->channel();
}
QString SnapResource::packageName() const
{
return m_data.value(QLatin1String("name")).toString();
return m_snap->name();
}
QUrl SnapResource::screenshotUrl()
......@@ -110,6 +110,14 @@ AbstractResource::State SnapResource::state()
return m_state;
}
void SnapResource::setState(AbstractResource::State state)
{
if (m_state != state) {
m_state = state;
Q_EMIT stateChanged();
}
}
void SnapResource::fetchChangelog()
{
QString log;
......@@ -123,22 +131,10 @@ void SnapResource::fetchScreenshots()
void SnapResource::invokeApplication() const
{
QProcess::startDetached(m_data[QLatin1String("resource")].toString());
// QProcess::startDetached(m_snap->price());
}
bool SnapResource::isTechnical() const
{
return m_data.value(QLatin1String("type")) == QLatin1String("os") || m_data.value(QLatin1String("private")).toBool();
}
void SnapResource::refreshState()
{
auto b = qobject_cast<SnapBackend*>(backend());
SnapSocket* socket = b->socket();
auto job = socket->snapByName(packageName());
connect(job, &SnapJob::finished, this, [this](SnapJob* job){
m_state = job->isSuccessful() ? AbstractResource::Installed : AbstractResource::None;
Q_EMIT stateChanged();
qDebug() << "refreshed!!" << job->result() << job->statusCode() << m_state;
});
return m_snap->snapType() == QLatin1String("os") || m_snap->isPrivate();
}
......@@ -23,13 +23,15 @@
#include <resources/AbstractResource.h>
#include <QJsonObject>
#include <Snapd/Snap>
class SnapBackend;
class AddonList;
class SnapResource : public AbstractResource
{
Q_OBJECT
public:
explicit SnapResource(QJsonObject data, AbstractResource::State state, AbstractResourcesBackend* parent);
explicit SnapResource(QSnapdSnap* snap, AbstractResource::State state, SnapBackend* parent);
QString section() override;
QString origin() const override;
......@@ -54,11 +56,12 @@ public:
void fetchScreenshots() override;
QList<PackageState> addonsInformation() override { return {}; }
void refreshState();
void setState(AbstractResource::State state);
public:
AbstractResource::State m_state;
QJsonObject m_data;
QSnapdSnap* m_snap;
};
#endif // SNAPRESOURCE_H
......@@ -21,64 +21,69 @@
#include "SnapTransaction.h"
#include "SnapBackend.h"
#include "SnapResource.h"
#include "SnapSocket.h"
#include <Snapd/Request>
#include <QTimer>
#include <QProcess>
#include <QJsonDocument>
#include <QJsonArray>
#include "libsnapclient/config-paths.h"
#include "utils.h"
SnapTransaction::SnapTransaction(SnapResource* app, SnapJob* job, SnapSocket* socket, Role role)
SnapTransaction::SnapTransaction(SnapResource* app, QSnapdRequest* request, Role role)
: Transaction(app, app, role)
, m_app(app)
, m_socket(socket)
, m_request(request)
{
setStatus(DownloadingStatus);
setCancellable(false);
connect(job, &SnapJob::finished, this, &SnapTransaction::transactionStarted);
connect(request, &QSnapdRequest::progress, this, &SnapTransaction::progressed);
connect(request, &QSnapdRequest::complete, this, &SnapTransaction::finishTransaction);
setStatus(SetupStatus);
// Only needed until /v2/events is fixed upstream
m_timer = new QTimer(this);
m_timer->setInterval(50);
connect(m_timer, &QTimer::timeout, this, [this](){
auto job = m_socket->changes(m_changeId);
connect(job, &SnapJob::finished, this, &SnapTransaction::iterateTransaction);
});
job->start();
request->runAsync();
}
void SnapTransaction::transactionStarted(SnapJob* job)
void SnapTransaction::cancel()
{
Q_ASSERT(m_changeId.isEmpty());
if (!job->isSuccessful()) {
qWarning() << "non-successful transaction" << job->statusCode();
setStatus(DoneStatus);
return;
}
auto data = job->data();
m_changeId = data.value(QLatin1String("change")).toString();
setStatus(DownloadingStatus);
m_timer->start();
m_request->cancel();
}
void SnapTransaction::iterateTransaction(SnapJob* job)
void SnapTransaction::finishTransaction()
{
auto res = job->result().toObject();
switch(m_request->error()) {
case QSnapdRequest::NoError:
static_cast<SnapBackend*>(m_app->backend())->refreshStates();
break;
case QSnapdRequest::AuthDataRequired: {
QProcess* p = new QProcess;
p->setProgram(QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR "/discover/SnapMacaroonDialog"));
p->start();
if (res.value(QLatin1String("ready")).toBool()) {
finishTransaction();
connect(p, static_cast<void(QProcess::*)(int)>(&QProcess::finished), this, [this, p] (int code) {
p->deleteLater();
if (code != 0) {
qWarning() << "login failed..." << p->readAll();
Q_EMIT passiveMessage(m_request->errorString());
return;
}
const auto doc = QJsonDocument::fromJson(p->readAllStandardOutput());
const auto result = doc.object();
const auto macaroon = result[QStringLiteral("macaroon")].toString();
const auto discharges = kTransform<QStringList>(result[QStringLiteral("discharges")].toArray(), [](const QJsonValue& val) { return val.toString(); });
static_cast<SnapBackend*>(m_app->backend())->client()->setAuthData(new QSnapdAuthData(macaroon, discharges));
m_request->runAsync();
});
} return;
default:
Q_EMIT passiveMessage(m_request->errorString());
break;
}
}
void SnapTransaction::cancel()
{
Q_UNREACHABLE();
setStatus(DoneStatus);
}
void SnapTransaction::finishTransaction()
void SnapTransaction::progressed()
{
delete m_timer;
m_app->refreshState();
setStatus(DoneStatus);
// setProgress(m_request->change()->???);
}
......@@ -24,29 +24,25 @@
#include <Transaction/Transaction.h>
#include <QPointer>
class QTimer;
class SnapJob;
class SnapSocket;
class SnapResource;
class QSnapdRequest;
class SnapTransaction : public Transaction
{
Q_OBJECT
public:
SnapTransaction(SnapResource* app, SnapJob* job, SnapSocket* socket, Role role);
SnapTransaction(SnapResource* app, QSnapdRequest* request, Role role);
void cancel() override;
private Q_SLOTS:
void transactionStarted(SnapJob* job);
void iterateTransaction(SnapJob* job);
void finishTransaction();
private:
void progressed();
SnapResource * const m_app;
SnapSocket * m_socket;
QString m_changeId;
QPointer<QTimer> m_timer;
QSnapdRequest* const m_request;
};
#endif // SNAPTRANSACTION_H
add_library(DiscoverSnapClient SnapSocket.cpp)
target_link_libraries(DiscoverSnapClient PUBLIC Qt5::Network KF5::Auth)
add_library(Discover::SnapClient ALIAS DiscoverSnapClient)
install(TARGETS DiscoverSnapClient DESTINATION ${INSTALL_TARGETS_DEFAULT_ARGS})
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-paths.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-paths.h)
ki18n_wrap_ui(SnapMacaroonDialog_SRCS SnapMacaroonDialog.ui)
add_executable(SnapMacaroonDialog SnapMacaroonDialog.cpp ${SnapMacaroonDialog_SRCS})
target_link_libraries(SnapMacaroonDialog Qt5::Network Qt5::Widgets KF5::Auth KF5::I18n Snapd::Core)
install(TARGETS SnapMacaroonDialog DESTINATION ${KDE_INSTALL_LIBEXECDIR}/discover)
add_executable(libsnap_helper SnapAuthHelper.cpp)
target_link_libraries(libsnap_helper Qt5::Network KF5::Auth)
target_link_libraries(libsnap_helper Qt5::Network KF5::Auth Snapd::Core)
install(TARGETS libsnap_helper DESTINATION ${KAUTH_HELPER_INSTALL_DIR})
kauth_install_actions(org.kde.discover.libsnapclient org.kde.discover.libsnapclient.actions)
......
......@@ -20,32 +20,52 @@
#include <QProcess>
#include <QDebug>
#include <QLocalSocket>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <unistd.h>
#include <stdlib.h>
#include <kauthhelpersupport.h>
#include <kauthactionreply.h>
#include <Snapd/Client>
using namespace KAuth;
class SnapAuthHelper : public QObject
{
Q_OBJECT
QSnapdClient m_client;
public:
SnapAuthHelper() {
m_client.connect()->runAsync();
}
public Q_SLOTS:
ActionReply modify(const QVariantMap &args)
{
QLocalSocket socket;
socket.connectToServer(QStringLiteral("/run/snapd.socket"), QIODevice::ReadWrite);
const bool b = socket.waitForConnected();
Q_ASSERT(b);
const QByteArray request = args[QStringLiteral("request")].toByteArray();
socket.write(request);
socket.waitForReadyRead();
const auto replyData = socket.readAll();
ActionReply reply = ActionReply::SuccessReply();
reply.setData({ { QStringLiteral("reply"), replyData } });
const QString user = args[QStringLiteral("user")].toString()
, pass = args[QStringLiteral("password")].toString()
, otp = args[QStringLiteral("otp")].toString();
QSnapdLoginRequest* req = otp.isEmpty() ? m_client.login(user, pass)
: m_client.login(user, pass, otp);
req->runSync();
auto auth = req->authData();
const QByteArray replyData = QJsonDocument(QJsonObject{
{QStringLiteral("macaroon"), auth->macaroon()},
{QStringLiteral("discharges"), QJsonArray::fromStringList(auth->discharges())}
}).toJson();
ActionReply reply = req->error() == QSnapdRequest::NoError ? ActionReply::SuccessReply() : ActionReply::InvalidActionReply();
bool otpMode = req->error() == QSnapdConnectRequest::TwoFactorRequired;
reply.setData({
{ QStringLiteral("reply"), replyData },
{ QStringLiteral("errorString"), req->errorString() },
{ QStringLiteral("otpMode"), otpMode }
});
return reply;
}
};
......
/***************************************************************************
* Copyright © 2016 Aleix Pol Gonzalez <aleixpol@blue-systems.com> *
* Copyright © 2017 Aleix Pol Gonzalez <aleixpol@blue-systems.com> *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
......@@ -18,94 +18,95 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
***************************************************************************/
#ifndef SNAPSOCKET_H
#define SNAPSOCKET_H
#include <QObject>
#include <QApplication>
#include <QBuffer>
#include <QPointer>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QLocalSocket>
#include <KAuthExecuteJob>
#include <Snapd/Client>
#include "ui_SnapMacaroonDialog.h"
class Q_DECL_EXPORT SnapJob : public QObject
class MacaroonDialog : public QDialog
{
Q_OBJECT
public:
SnapJob(QObject* parent = nullptr);
QJsonValue result() const { return m_data.value(QLatin1String("result")); }
int statusCode() const { return m_data.value(QLatin1String("status-code")).toInt(); }
QString status() const { return m_data.value(QLatin1String("status")).toString(); }
QString type() const { return m_data.value(QLatin1String("type")).toString(); }
bool isSuccessful() const { return statusCode()>=200 && statusCode()<300; }
QJsonObject data() const { return m_data; }
virtual void start() = 0;
virtual bool exec() = 0;
Q_SIGNALS:
void finished(SnapJob* job);
protected:
void processReply(QIODevice* device);
QJsonObject m_data;
MacaroonDialog()
: QDialog()
{
{
auto request = m_client.connect();
request->runSync();
}
m_ui.setupUi(this);
connect(this, &QDialog::accepted, this, &MacaroonDialog::startLogin);
connect(this, &QDialog::rejected, this, []() {
qApp->exit(1);
});
setOtpMode(false);
}
void startLogin()
{
login(m_ui.username->text(), m_ui.password->text(), m_ui.otp->text());
}
void login(const QString& username, const QString& password, const QString& otp = {})
{
KAuth::Action snapAction(QStringLiteral("org.kde.discover.libsnapclient.login"));
snapAction.setHelperId(QStringLiteral("org.kde.discover.libsnapclient"));
snapAction.setArguments({
{ QStringLiteral("user"), username },
{ QStringLiteral("password"), password },
{ QStringLiteral("otp"), otp }
});
// qDebug() << "snap" << snapAction.isValid() << snapAction.status();
Q_ASSERT(snapAction.isValid());
KAuth::ExecuteJob *reply = snapAction.execute();
connect(reply, &KAuth::ExecuteJob::finished, this, &MacaroonDialog::replied);
}
void setOtpMode(bool enabled)
{