[gnome-software/wip/ubuntu-3-20: 2/31] Add a Snap plugin
- From: Robert Ancell <rancell src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-software/wip/ubuntu-3-20: 2/31] Add a Snap plugin
- Date: Wed, 13 Dec 2017 22:03:20 +0000 (UTC)
commit 0d520f02d07643561b543a28e5e7214d4c9c5f0e
Author: Robert Ancell <robert ancell canonical com>
Date: Fri Jun 16 16:54:22 2017 +1200
Add a Snap plugin
This commit is a combination of many changes by many people.
configure.ac | 24 +
po/POTFILES.in | 1 +
src/Makefile.am | 3 +
src/gnome-software-local-file.desktop.in | 2 +-
src/org.gnome.Software.desktop.in | 2 +-
src/plugins/Makefile.am | 26 +
src/plugins/gs-plugin-icons.c | 1 +
src/plugins/gs-plugin-snap.c | 757 +++++++++++++++++++++++++
src/plugins/gs-snapd.c | 896 ++++++++++++++++++++++++++++++
src/plugins/gs-snapd.h | 69 +++
10 files changed, 1779 insertions(+), 2 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index a53a8c4..5d85c1a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -70,6 +70,7 @@ PKG_CHECK_MODULES(SOUP, libsoup-2.4 >= 2.51.92)
PKG_CHECK_MODULES(GSETTINGS_DESKTOP_SCHEMAS, gsettings-desktop-schemas >= 3.11.5)
PKG_CHECK_MODULES(LIBSECRET, libsecret-1)
PKG_CHECK_MODULES(GNOME_DESKTOP, gnome-desktop-3.0 >= 3.17.92)
+PKG_CHECK_MODULES(OAUTH, oauth)
AC_PATH_PROG(APPSTREAM_UTIL, [appstream-util], [unfound])
AC_ARG_ENABLE(man,
[AS_HELP_STRING([--enable-man],
@@ -248,6 +249,28 @@ GS_PLUGIN_API_VERSION=9
AC_SUBST(GS_PLUGIN_API_VERSION)
AC_DEFINE_UNQUOTED([GS_PLUGIN_API_VERSION], "$GS_PLUGIN_API_VERSION", [the plugin API version])
+# Snap
+AC_ARG_ENABLE(snap,
+ [AS_HELP_STRING([--enable-snap],
+ [enable Snap support [default=auto]])],,
+ enable_snap=maybe)
+AS_IF([test "x$enable_snap" != "xno"], [
+ PKG_CHECK_MODULES(SNAP,
+ [snapd-glib],
+ [have_snap=yes],
+ [have_snap=no])
+], [
+ have_snap=no
+])
+AS_IF([test "x$have_snap" = "xyes"], [
+ AC_DEFINE(HAVE_SNAP,1,[Build Snap support])
+], [
+ AS_IF([test "x$enable_snap" = "xyes"], [
+ AC_MSG_ERROR([Snap support requested but 'snapd-glib' was not found])
+ ])
+])
+AM_CONDITIONAL(HAVE_SNAP, test "$have_snap" != no)
+
GLIB_TESTS
dnl ---------------------------------------------------------------------------
@@ -294,4 +317,5 @@ echo "
XDG-APP support: ${have_xdg_app}
ODRS support: ${enable_odrs}
Ubuntu Reviews support: ${enable_ubuntu_reviews}
+ Snap support: ${have_snap}
"
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 325993b..91c990a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -61,5 +61,6 @@ src/gs-utils.c
src/org.gnome.Software.desktop.in
src/plugins/gs-ubuntuone-dialog.c
[type: gettext/glade]src/plugins/gs-ubuntuone-dialog.ui
+src/plugins/gs-plugin-snap.c
src/plugins/menu-spec-common.c
[type: gettext/glade]src/gs-popular-tile.ui
diff --git a/src/Makefile.am b/src/Makefile.am
index e8919b1..6fd94e2 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -10,6 +10,7 @@ AM_CPPFLAGS = \
$(GNOME_DESKTOP_CFLAGS) \
$(POLKIT_CFLAGS) \
$(LIBSECRET_CFLAGS) \
+ $(JSON_GLIB_CFLAGS) \
-DG_LOG_DOMAIN=\"Gs\" \
-DI_KNOW_THE_PACKAGEKIT_GLIB2_API_IS_SUBJECT_TO_CHANGE \
-DGS_MODULESETDIR=\"$(datadir)/gnome-software/modulesets.d\" \
@@ -104,6 +105,7 @@ gnome_software_cmd_LDADD = \
$(SOUP_LIBS) \
$(LIBSECRET_LIBS) \
$(GLIB_LIBS) \
+ $(JSON_GLIB_LIBS) \
$(GTK_LIBS)
gnome_software_cmd_CFLAGS = \
@@ -238,6 +240,7 @@ gnome_software_LDADD = \
$(PACKAGEKIT_LIBS) \
$(GNOME_DESKTOP_LIBS) \
$(POLKIT_LIBS) \
+ $(JSON_GLIB_LIBS) \
-lm
gnome_software_CFLAGS = \
diff --git a/src/gnome-software-local-file.desktop.in b/src/gnome-software-local-file.desktop.in
index 1e3a284..f7289e2 100644
--- a/src/gnome-software-local-file.desktop.in
+++ b/src/gnome-software-local-file.desktop.in
@@ -8,4 +8,4 @@ Type=Application
Icon=system-software-install
StartupNotify=true
NoDisplay=true
-MimeType=application/x-rpm;application/x-redhat-package-manager;application/x-deb;application/x-app-package;application/vnd.ms-cab-compressed;application/vnd.xdgapp;x-scheme-handler/apt;
+MimeType=application/x-rpm;application/x-redhat-package-manager;application/x-deb;application/x-app-package;application/vnd.ms-cab-compressed;application/vnd.xdgapp;x-scheme-handler/apt;application/vnd.snap;
diff --git a/src/org.gnome.Software.desktop.in b/src/org.gnome.Software.desktop.in
index 26762c4..40feab2 100644
--- a/src/org.gnome.Software.desktop.in
+++ b/src/org.gnome.Software.desktop.in
@@ -8,7 +8,7 @@ Type=Application
Categories=GNOME;GTK;System;PackageManager;
_Keywords=Updates;Upgrade;Sources;Repositories;Preferences;Install;Uninstall;Program;Software;App;Store;
StartupNotify=true
-MimeType=x-scheme-handler/appstream;
+MimeType=x-scheme-handler/appstream;x-scheme-handler/snap;
X-GNOME-Bugzilla-Bugzilla=GNOME
X-GNOME-Bugzilla-Product=gnome-software
X-GNOME-Bugzilla-Component=gnome-software
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
index 389bb09..f0655d8 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -13,6 +13,9 @@ AM_CPPFLAGS = \
$(JSON_GLIB_CFLAGS) \
$(LIMBA_CFLAGS) \
$(XDG_APP_CFLAGS) \
+ $(OAUTH_CFLAGS) \
+ $(LIBSECRET_CFLAGS) \
+ $(SNAP_CFLAGS) \
-DBINDIR=\"$(bindir)\" \
-DDATADIR=\"$(datadir)\" \
-DGS_MODULESETDIR=\"$(datadir)/gnome-software/modulesets.d\" \
@@ -45,6 +48,10 @@ plugin_LTLIBRARIES += \
libgs_plugin_apt.la
endif
+if HAVE_SNAP
+plugin_LTLIBRARIES += libgs_plugin_snap.la
+endif
+
if HAVE_PACKAGEKIT
plugin_LTLIBRARIES += \
libgs_plugin_systemd-updates.la \
@@ -289,6 +296,25 @@ libgs_plugin_packagekit_proxy_la_LIBADD = $(GS_PLUGIN_LIBS)
libgs_plugin_packagekit_proxy_la_LDFLAGS = -module -avoid-version
libgs_plugin_packagekit_proxy_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+if HAVE_SNAP
+libgs_plugin_snap_la_SOURCES = \
+ gs-plugin-snap.c \
+ gs-ubuntuone.h \
+ gs-ubuntuone.c \
+ gs-ubuntuone-dialog.h \
+ gs-ubuntuone-dialog.c \
+ gs-snapd.h \
+ gs-snapd.c
+libgs_plugin_snap_la_LIBADD = \
+ $(GS_PLUGIN_LIBS) \
+ $(SNAP_LIBS) \
+ $(SOUP_LIBS) \
+ $(JSON_GLIB_LIBS) \
+ $(LIBSECRET_LIBS)
+libgs_plugin_snap_la_LDFLAGS = -module -avoid-version
+libgs_plugin_snap_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS) -DUSE_SNAPD
+endif
+
check_PROGRAMS = \
gs-self-test
diff --git a/src/plugins/gs-plugin-icons.c b/src/plugins/gs-plugin-icons.c
index d952974..06e2c89 100644
--- a/src/plugins/gs-plugin-icons.c
+++ b/src/plugins/gs-plugin-icons.c
@@ -55,6 +55,7 @@ gs_plugin_order_after (GsPlugin *plugin)
static const gchar *deps[] = {
"appstream", /* needs remote icons downloaded */
"epiphany", /* "" */
+ "snap", /* "" */
NULL };
return deps;
}
diff --git a/src/plugins/gs-plugin-snap.c b/src/plugins/gs-plugin-snap.c
new file mode 100644
index 0000000..ff62e30
--- /dev/null
+++ b/src/plugins/gs-plugin-snap.c
@@ -0,0 +1,757 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <gs-utils.h>
+#include <gs-plugin.h>
+
+#include <glib/gi18n.h>
+#include <json-glib/json-glib.h>
+#include "gs-snapd.h"
+#include "gs-ubuntuone.h"
+
+struct GsPluginPrivate {
+ gboolean system_is_confined;
+ gchar *store_name;
+ GMutex store_snaps_lock;
+ GHashTable *store_snaps;
+};
+
+typedef gboolean (*AppFilterFunc)(const gchar *id, JsonObject *object, gpointer data);
+
+const gchar *
+gs_plugin_get_name (void)
+{
+ return "snap";
+}
+
+const gchar **
+gs_plugin_order_after (GsPlugin *plugin)
+{
+ static const gchar *deps[] = {
+ "appstream", /* Override hardcoded popular apps */
+ "hardcoded-featured", /* Override hardcoded popular apps */
+ NULL };
+ return deps;
+}
+
+void
+gs_plugin_initialize (GsPlugin *plugin)
+{
+ g_autoptr(JsonObject) system_information = NULL;
+
+ /* create private area */
+ plugin->priv = GS_PLUGIN_GET_PRIVATE (GsPluginPrivate);
+
+ system_information = gs_snapd_get_system_info (NULL, NULL);
+ if (system_information == NULL) {
+ g_debug ("disabling '%s' as snapd not running",
+ gs_plugin_get_name ());
+ gs_plugin_set_enabled (plugin, FALSE);
+ }
+ plugin->priv->system_is_confined = g_strcmp0 (json_object_get_string_member (system_information,
"confinement"), "strict") == 0;
+ if (json_object_has_member (system_information, "store"))
+ plugin->priv->store_name = g_strdup (json_object_get_string_member (system_information,
"store"));
+ else
+ plugin->priv->store_name = g_strdup (/* TRANSLATORS: default snap store name */
+ _("Snap Store"));
+
+ g_mutex_init (&plugin->priv->store_snaps_lock);
+ plugin->priv->store_snaps = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, (GDestroyNotify) json_object_unref);
+}
+
+static gboolean
+gs_plugin_snap_set_app_pixbuf_from_data (GsApp *app, const gchar *buf, gsize count, GError **error)
+{
+ g_autoptr(GdkPixbufLoader) loader = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ loader = gdk_pixbuf_loader_new ();
+ if (!gdk_pixbuf_loader_write (loader, buf, count, &error_local)) {
+ g_debug ("icon_data[%" G_GSIZE_FORMAT "]=%s", count, buf);
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Failed to write: %s",
+ error_local->message);
+ return FALSE;
+ }
+ if (!gdk_pixbuf_loader_close (loader, &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Failed to close: %s",
+ error_local->message);
+ return FALSE;
+ }
+ gs_app_set_pixbuf (app, gdk_pixbuf_loader_get_pixbuf (loader));
+ return TRUE;
+}
+
+static JsonObject *
+store_snap_cache_lookup (GsPlugin *plugin, const gchar *name)
+{
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&plugin->priv->store_snaps_lock);
+ return g_hash_table_lookup (plugin->priv->store_snaps, name);
+}
+
+static void
+store_snap_cache_update (GsPlugin *plugin, JsonArray *snaps)
+{
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&plugin->priv->store_snaps_lock);
+ guint i;
+
+ for (i = 0; i < json_array_get_length (snaps); i++) {
+ JsonObject *snap = json_array_get_object_element (snaps, i);
+ g_hash_table_insert (plugin->priv->store_snaps, g_strdup (json_object_get_string_member
(snap, "name")), json_object_ref (snap));
+ }
+}
+
+static JsonArray *
+find_snaps (GsPlugin *plugin, const gchar *section, gboolean match_name, const gchar *query, GCancellable
*cancellable, GError **error)
+{
+ g_autoptr(JsonArray) snaps = NULL;
+
+ snaps = gs_snapd_find (section, match_name, query, cancellable, error);
+ if (snaps == NULL)
+ return NULL;
+
+ store_snap_cache_update (plugin, snaps);
+
+ return g_steal_pointer (&snaps);
+}
+
+static const gchar *
+get_snap_title (JsonObject *snap)
+{
+ const gchar *name = NULL;
+
+ if (json_object_has_member (snap, "title"))
+ name = json_object_get_string_member (snap, "title");
+ if (name == NULL || g_strcmp0 (name, "") == 0)
+ name = json_object_get_string_member (snap, "name");
+
+ return name;
+}
+
+static GsApp *
+snap_to_app (GsPlugin *plugin, JsonObject *snap)
+{
+ GsApp *app;
+ const gchar *type, *confinement;
+
+ /* create a unique ID for deduplication, TODO: branch? */
+ app = gs_app_new (json_object_get_string_member (snap, "name"));
+ type = json_object_get_string_member (snap, "type");
+ if (g_strcmp0 (type, "app") == 0) {
+ gs_app_set_kind (app, AS_APP_KIND_DESKTOP);
+ } else if (g_strcmp0 (type, "gadget") == 0 || g_strcmp0 (type, "os") == 0) {
+ gs_app_set_kind (app, AS_APP_KIND_RUNTIME);
+ gs_app_add_quirk (app, AS_APP_QUIRK_NOT_LAUNCHABLE);
+ }
+ gs_app_set_management_plugin (app, "snap");
+ gs_app_add_quirk (app, AS_APP_QUIRK_NOT_REVIEWABLE);
+ gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, get_snap_title (snap));
+ if (gs_plugin_check_distro_id (plugin, "ubuntu"))
+ gs_app_add_quirk (app, AS_APP_QUIRK_PROVENANCE);
+ confinement = json_object_get_string_member (snap, "confinement");
+ gs_app_set_metadata (app, "snap::confinement", confinement);
+ if (plugin->priv->system_is_confined && g_strcmp0 (confinement, "strict") == 0)
+ gs_app_add_kudo (app, GS_APP_KUDO_SANDBOXED);
+
+ return app;
+}
+
+gboolean
+gs_plugin_url_to_app (GsPlugin *plugin,
+ GList **list,
+ const gchar *url,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *scheme = NULL;
+ g_autoptr(JsonArray) snaps = NULL;
+ JsonObject *snap;
+ g_autofree gchar *path = NULL;
+ g_autoptr(GsApp) app = NULL;
+
+ /* not us */
+ scheme = gs_utils_get_url_scheme (url);
+ if (g_strcmp0 (scheme, "snap") != 0)
+ return TRUE;
+
+ /* create app */
+ path = gs_utils_get_url_path (url);
+ snaps = find_snaps (plugin, NULL, TRUE, path, cancellable, NULL);
+ if (snaps == NULL || json_array_get_length (snaps) < 1)
+ return TRUE;
+
+ snap = json_array_get_object_element (snaps, 0);
+ app = snap_to_app (plugin, snap);
+ gs_plugin_add_app (list, app);
+
+ return TRUE;
+}
+
+void
+gs_plugin_destroy (GsPlugin *plugin)
+{
+ g_free (plugin->priv->store_name);
+ g_hash_table_unref (plugin->priv->store_snaps);
+ g_mutex_clear (&plugin->priv->store_snaps_lock);
+}
+
+static gboolean
+is_banner_image (const gchar *filename)
+{
+ /* Check if this screenshot was uploaded as "banner.png" or "banner.jpg".
+ * The server optionally adds a 7 character suffix onto it if it would collide with
+ * an existing name, e.g. "banner_MgEy4MI.png"
+ * See https://forum.snapcraft.io/t/improve-method-for-setting-featured-snap-banner-image-in-store/
+ */
+ return g_regex_match_simple ("^banner(?:_[a-zA-Z0-9]{7})?\\.(?:png|jpg)$", filename, 0, 0);
+}
+
+static gboolean
+is_banner_icon_image (const gchar *filename)
+{
+ /* Check if this screenshot was uploaded as "banner-icon.png" or "banner-icon.jpg".
+ * The server optionally adds a 7 character suffix onto it if it would collide with
+ * an existing name, e.g. "banner-icon_Ugn6pmj.png"
+ * See https://forum.snapcraft.io/t/improve-method-for-setting-featured-snap-banner-image-in-store/
+ */
+ return g_regex_match_simple ("^banner-icon(?:_[a-zA-Z0-9]{7})?\\.(?:png|jpg)$", filename, 0, 0);
+}
+
+static gboolean
+remove_cb (GsApp *app, gpointer user_data)
+{
+ return FALSE;
+}
+
+gboolean
+gs_plugin_add_featured (GsPlugin *plugin,
+ GList **list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(JsonArray) snaps = NULL;
+ JsonObject *snap;
+ g_autoptr(GsApp) app = NULL;
+ const gchar *banner_url = NULL, *icon_url = NULL;
+ g_autoptr(GString) background_css = NULL;
+
+ snaps = find_snaps (plugin, "featured", FALSE, NULL, cancellable, error);
+
+ if (snaps == NULL)
+ return FALSE;
+
+ if (json_array_get_length (snaps) < 1)
+ return TRUE;
+
+ /* use first snap as the featured app */
+ snap = json_array_get_object_element (snaps, 0);
+ app = snap_to_app (plugin, snap);
+
+ /* if has a sceenshot called 'banner.png' or 'banner-icon.png' then use them for the banner */
+ if (json_object_has_member (snap, "screenshots")) {
+ JsonArray *screenshots;
+ guint i;
+
+ screenshots = json_object_get_array_member (snap, "screenshots");
+ for (i = 0; i < json_array_get_length (screenshots); i++) {
+ JsonObject *screenshot = json_array_get_object_element (screenshots, i);
+ const gchar *url;
+ g_autofree gchar *filename = NULL;
+
+ url = json_object_get_string_member (screenshot, "url");
+ filename = g_path_get_basename (url);
+ if (is_banner_image (filename))
+ banner_url = url;
+ else if (is_banner_icon_image (filename))
+ icon_url = url;
+ }
+ }
+
+ background_css = g_string_new ("");
+ if (icon_url != NULL)
+ g_string_append_printf (background_css,
+ "url('%s') left center / auto 100%% no-repeat, ",
+ icon_url);
+ else
+ g_string_append_printf (background_css,
+ "url('%s') left center / auto 100%% no-repeat, ",
+ json_object_get_string_member (snap, "icon"));
+ if (banner_url != NULL)
+ g_string_append_printf (background_css,
+ "url('%s') center / cover no-repeat;",
+ banner_url);
+ else
+ g_string_append_printf (background_css, "#FFFFFF;");
+ gs_app_add_kudo (app, GS_APP_KUDO_FEATURED_RECOMMENDED);
+ gs_app_set_metadata (app, "Featured::text-color", "#000000");
+ gs_app_set_metadata (app, "Featured::background", background_css->str);
+ gs_app_set_metadata (app, "Featured::stroke-color", "#000000");
+ gs_app_set_metadata (app, "Featured::text-shadow", "0 1px 1px rgba(255,255,255,0.5)");
+
+ /* replace any other featured apps with our one */
+ gs_plugin_list_filter (list, remove_cb, NULL);
+ gs_plugin_add_app (list, app);
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_popular (GsPlugin *plugin,
+ GList **list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(JsonArray) snaps = NULL;
+ guint i;
+
+ snaps = find_snaps (plugin, "featured", FALSE, NULL, cancellable, error);
+ if (snaps == NULL)
+ return FALSE;
+
+ /* replace any other popular apps with our one */
+ gs_plugin_list_filter (list, remove_cb, NULL);
+
+ /* skip first snap - it is used as the featured app */
+ for (i = 1; i < json_array_get_length (snaps); i++) {
+ g_autoptr(GsApp) app = snap_to_app (plugin, json_array_get_object_element (snaps, i));
+ gs_plugin_add_app (list, app);
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_installed (GsPlugin *plugin,
+ GList **list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(JsonArray) snaps = NULL;
+ guint i;
+
+ snaps = gs_snapd_list (cancellable, error);
+ if (snaps == NULL)
+ return FALSE;
+
+ for (i = 0; i < json_array_get_length (snaps); i++) {
+ JsonObject *snap = json_array_get_object_element (snaps, i);
+ g_autoptr(GsApp) app = NULL;
+ const gchar *status;
+
+ status = json_object_get_string_member (snap, "status");
+ if (g_strcmp0 (status, "active") != 0)
+ continue;
+
+ app = snap_to_app (plugin, snap);
+ gs_plugin_add_app (list, app);
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_search (GsPlugin *plugin,
+ gchar **values,
+ GList **list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *query = NULL;
+ g_autoptr(JsonArray) snaps = NULL;
+ guint i;
+
+ query = g_strjoinv (" ", values);
+ snaps = find_snaps (plugin, NULL, FALSE, query, cancellable, error);
+ if (snaps == NULL)
+ return FALSE;
+
+ for (i = 0; i < json_array_get_length (snaps); i++) {
+ g_autoptr(GsApp) app = snap_to_app (plugin, json_array_get_object_element (snaps, i));
+ gs_plugin_add_app (list, app);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+load_icon (GsPlugin *plugin, GsApp *app, const gchar *icon_url, GCancellable *cancellable, GError **error)
+{
+ if (icon_url == NULL || g_strcmp0 (icon_url, "") == 0) {
+ g_autoptr(AsIcon) icon = as_icon_new ();
+ as_icon_set_kind (icon, AS_ICON_KIND_STOCK);
+ as_icon_set_name (icon, "package-x-generic");
+ gs_app_set_icon (app, icon);
+ return TRUE;
+ }
+
+ /* icon is optional, either loaded from snapd or from a URL */
+ if (g_str_has_prefix (icon_url, "/")) {
+ g_autofree gchar *icon_data = NULL;
+ gsize icon_data_length;
+
+ icon_data = gs_snapd_get_resource (icon_url, &icon_data_length, cancellable, error);
+ if (icon_data == NULL)
+ return FALSE;
+
+ if (!gs_plugin_snap_set_app_pixbuf_from_data (app,
+ icon_data, icon_data_length,
+ error)) {
+ g_prefix_error (error, "Failed to load %s: ", icon_url);
+ return FALSE;
+ }
+ } else {
+ g_autofree gchar *basename_tmp = NULL;
+ g_autofree gchar *hash = NULL;
+ g_autofree gchar *basename = NULL;
+ g_autofree gchar *cache_dir = NULL;
+ g_autofree gchar *cache_fn = NULL;
+ g_autoptr(SoupMessage) message = NULL;
+ g_autoptr(GdkPixbufLoader) loader = NULL;
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(AsIcon) icon = NULL;
+
+ icon = as_icon_new ();
+ gs_app_set_icon (app, icon);
+
+ /* attempt to load from cache */
+ basename_tmp = g_path_get_basename (icon_url);
+ hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1, icon_url, -1);
+ basename = g_strdup_printf ("%s-%s", hash, basename_tmp);
+ cache_dir = gs_utils_get_cachedir ("snap-icons", error);
+ cache_fn = g_build_filename (cache_dir, basename, NULL);
+ as_icon_set_filename (icon, cache_fn);
+ if (g_file_test (cache_fn, G_FILE_TEST_EXISTS)) {
+ as_icon_set_kind (icon, AS_ICON_KIND_LOCAL);
+ if (gs_app_load_icon (app, plugin->scale, &local_error))
+ return TRUE;
+
+ g_warning ("Failed to load cached icon: %s", local_error->message);
+ }
+
+ as_icon_set_kind (icon, AS_ICON_KIND_REMOTE);
+ as_icon_set_url (icon, icon_url);
+ }
+
+ return TRUE;
+}
+
+static JsonObject *
+get_store_snap (GsPlugin *plugin, const gchar *name, GCancellable *cancellable, GError **error)
+{
+ JsonObject *snap = NULL;
+ g_autoptr(JsonArray) snaps = NULL;
+
+ /* use cached version if available */
+ snap = store_snap_cache_lookup (plugin, name);
+ if (snap != NULL)
+ return json_object_ref (snap);
+
+ snaps = find_snaps (plugin, NULL, TRUE, name, cancellable, error);
+ if (snaps == NULL || json_array_get_length (snaps) < 1)
+ return NULL;
+
+ return json_object_ref (json_array_get_object_element (snaps, 0));
+}
+
+gboolean
+gs_plugin_refine_app (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *id, *icon_url = NULL;
+ g_autoptr(JsonObject) local_snap = NULL;
+ g_autoptr(JsonObject) store_snap = NULL;
+
+ /* not us */
+ if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0)
+ return TRUE;
+
+ id = gs_app_get_id (app);
+ if (id == NULL)
+ id = gs_app_get_source_default (app);
+ if (id == NULL)
+ return TRUE;
+
+ /* get information from installed snaps */
+ local_snap = gs_snapd_list_one (id, cancellable, NULL);
+ if (local_snap != NULL) {
+ JsonArray *apps;
+ g_autoptr(GDateTime) install_date = NULL;
+ const gchar *launch_name = NULL;
+
+ if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN)
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+
+ gs_app_set_name (app, GS_APP_QUALITY_NORMAL, get_snap_title (local_snap));
+ gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, json_object_get_string_member (local_snap,
"summary"));
+ gs_app_set_description (app, GS_APP_QUALITY_NORMAL, json_object_get_string_member
(local_snap, "description"));
+ if (json_object_has_member (local_snap, "license"))
+ gs_app_set_license (app, GS_APP_QUALITY_NORMAL, json_object_get_string_member
(local_snap, "license"));
+ gs_app_set_version (app, json_object_get_string_member (local_snap, "version"));
+ if (json_object_has_member (local_snap, "installed-size"))
+ gs_app_set_size (app, json_object_get_int_member (local_snap, "installed-size"));
+ if (json_object_has_member (local_snap, "install-date"))
+ install_date = gs_snapd_parse_date (json_object_get_string_member (local_snap,
"install-date"));
+ if (install_date != NULL)
+ gs_app_set_install_date (app, g_date_time_to_unix (install_date));
+ if (json_object_has_member (local_snap, "developer"))
+ gs_app_set_developer_name (app, json_object_get_string_member (local_snap,
"developer"));
+ icon_url = json_object_get_string_member (local_snap, "icon");
+ if (g_strcmp0 (icon_url, "") == 0)
+ icon_url = NULL;
+
+ apps = json_object_get_array_member (local_snap, "apps");
+ if (apps && json_array_get_length (apps) > 0)
+ launch_name = json_object_get_string_member (json_array_get_object_element (apps, 0),
"name");
+
+ if (launch_name)
+ gs_app_set_metadata (app, "snap::launch-name", launch_name);
+ else
+ gs_app_add_quirk (app, AS_APP_QUIRK_NOT_LAUNCHABLE);
+ }
+
+ /* get information from snap store */
+ store_snap = get_store_snap (plugin, id, cancellable, NULL);
+ if (store_snap != NULL) {
+ if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN)
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+
+ gs_app_set_name (app, GS_APP_QUALITY_NORMAL, get_snap_title (store_snap));
+ gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, json_object_get_string_member (store_snap,
"summary"));
+ gs_app_set_description (app, GS_APP_QUALITY_NORMAL, json_object_get_string_member
(store_snap, "description"));
+ if (json_object_has_member (store_snap, "license"))
+ gs_app_set_license (app, GS_APP_QUALITY_NORMAL, json_object_get_string_member
(store_snap, "license"));
+ gs_app_set_version (app, json_object_get_string_member (store_snap, "version"));
+ if (gs_app_get_size (app) == GS_APP_SIZE_UNKNOWN && json_object_has_member (store_snap,
"download-size"))
+ gs_app_set_size (app, json_object_get_int_member (store_snap, "download-size"));
+ if (json_object_has_member (store_snap, "developer"))
+ gs_app_set_developer_name (app, json_object_get_string_member (store_snap,
"developer"));
+ if (icon_url == NULL) {
+ icon_url = json_object_get_string_member (store_snap, "icon");
+ if (g_strcmp0 (icon_url, "") == 0)
+ icon_url = NULL;
+ }
+
+ if (json_object_has_member (store_snap, "screenshots") && gs_app_get_screenshots (app)->len
== 0) {
+ JsonArray *screenshots;
+ guint i;
+
+ screenshots = json_object_get_array_member (store_snap, "screenshots");
+ for (i = 0; i < json_array_get_length (screenshots); i++) {
+ JsonObject *screenshot = json_array_get_object_element (screenshots, i);
+ const gchar *url;
+ g_autofree gchar *filename = NULL;
+ g_autoptr(AsScreenshot) ss = NULL;
+ g_autoptr(AsImage) image = NULL;
+
+ /* skip sceenshots used for banner when app is featured */
+ url = json_object_get_string_member (screenshot, "url");
+ filename = g_path_get_basename (url);
+ if (is_banner_image (filename) || is_banner_icon_image (filename))
+ continue;
+
+ ss = as_screenshot_new ();
+ as_screenshot_set_kind (ss, AS_SCREENSHOT_KIND_NORMAL);
+ image = as_image_new ();
+ as_image_set_url (image, json_object_get_string_member (screenshot, "url"));
+ as_image_set_kind (image, AS_IMAGE_KIND_SOURCE);
+ if (json_object_has_member (screenshot, "width"))
+ as_image_set_width (image, json_object_get_int_member (screenshot,
"width"));
+ if (json_object_has_member (screenshot, "height"))
+ as_image_set_height (image, json_object_get_int_member (screenshot,
"height"));
+ as_screenshot_add_image (ss, image);
+ gs_app_add_screenshot (app, ss);
+ }
+ }
+
+ gs_app_set_origin (app, plugin->priv->store_name);
+ }
+
+ /* load icon if requested */
+ if (gs_app_get_pixbuf (app) == NULL && gs_app_get_icon (app) == NULL) {
+ if (!load_icon (plugin, app, icon_url, cancellable, error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+typedef struct
+{
+ GsPlugin *plugin;
+ GsApp *app;
+} ProgressData;
+
+static void
+progress_cb (JsonObject *result, gpointer user_data)
+{
+ ProgressData *data = user_data;
+ JsonArray *tasks;
+ GList *task_list, *l;
+ gint64 done = 0, total = 0;
+
+ tasks = json_object_get_array_member (result, "tasks");
+ task_list = json_array_get_elements (tasks);
+
+ for (l = task_list; l != NULL; l = l->next) {
+ JsonObject *task, *progress;
+ gint64 task_done, task_total;
+
+ task = json_node_get_object (l->data);
+ progress = json_object_get_object_member (task, "progress");
+ task_done = json_object_get_int_member (progress, "done");
+ task_total = json_object_get_int_member (progress, "total");
+
+ done += task_done;
+ total += task_total;
+ }
+
+ gs_plugin_progress_update (data->plugin, data->app, 100 * done / total);
+
+ g_list_free (task_list);
+}
+
+gboolean
+gs_plugin_app_install (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ProgressData data;
+ gboolean classic = FALSE;
+
+ /* We can only install apps we know of */
+ if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0)
+ return TRUE;
+
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ if (g_strcmp0 (gs_app_get_metadata_item (app, "snap::confinement"), "classic") == 0)
+ classic = TRUE;
+ data.plugin = plugin;
+ data.app = app;
+ if (!gs_snapd_install (gs_app_get_id (app), classic, progress_cb, &data, cancellable, error)) {
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ return FALSE;
+ }
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ return TRUE;
+}
+
+// Check if an app is graphical by checking if it uses a known GUI interface.
+// This doesn't necessarily mean that every binary uses this interfaces, but is probably true.
+// https://bugs.launchpad.net/bugs/1595023
+static gboolean
+is_graphical (GsApp *app, GCancellable *cancellable)
+{
+ g_autoptr(JsonObject) result = NULL;
+ JsonArray *plugs;
+ guint i;
+ g_autoptr(GError) error = NULL;
+
+ result = gs_snapd_get_interfaces (cancellable, &error);
+ if (result == NULL) {
+ g_warning ("Failed to check interfaces: %s", error->message);
+ return FALSE;
+ }
+
+ plugs = json_object_get_array_member (result, "plugs");
+ for (i = 0; i < json_array_get_length (plugs); i++) {
+ JsonObject *plug = json_array_get_object_element (plugs, i);
+ const gchar *interface;
+
+ // Only looks at the plugs for this snap
+ if (g_strcmp0 (json_object_get_string_member (plug, "snap"), gs_app_get_id (app)) != 0)
+ continue;
+
+ interface = json_object_get_string_member (plug, "interface");
+ if (interface == NULL)
+ continue;
+
+ if (g_strcmp0 (interface, "unity7") == 0 || g_strcmp0 (interface, "x11") == 0 || g_strcmp0
(interface, "mir") == 0)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+gboolean
+gs_plugin_launch (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *launch_name;
+ g_autofree gchar *binary_name = NULL;
+ GAppInfoCreateFlags flags = G_APP_INFO_CREATE_NONE;
+ g_autoptr(GAppInfo) info = NULL;
+
+ /* We can only launch apps we know of */
+ if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0)
+ return TRUE;
+
+ launch_name = gs_app_get_metadata_item (app, "snap::launch-name");
+ if (!launch_name)
+ return TRUE;
+
+ if (g_strcmp0 (launch_name, gs_app_get_id (app)) == 0)
+ binary_name = g_strdup_printf ("/snap/bin/%s", launch_name);
+ else
+ binary_name = g_strdup_printf ("/snap/bin/%s.%s", gs_app_get_id (app), launch_name);
+
+ if (!is_graphical (app, cancellable))
+ flags |= G_APP_INFO_CREATE_NEEDS_TERMINAL;
+ info = g_app_info_create_from_commandline (binary_name, NULL, flags, error);
+ if (info == NULL)
+ return FALSE;
+
+ return g_app_info_launch (info, NULL, NULL, error);
+}
+
+gboolean
+gs_plugin_app_remove (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ProgressData data;
+
+ /* We can only remove apps we know of */
+ if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0)
+ return TRUE;
+
+ gs_app_set_state (app, AS_APP_STATE_REMOVING);
+ data.plugin = plugin;
+ data.app = app;
+ if (!gs_snapd_remove (gs_app_get_id (app), progress_cb, &data, cancellable, error)) {
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ return FALSE;
+ }
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ return TRUE;
+}
diff --git a/src/plugins/gs-snapd.c b/src/plugins/gs-snapd.c
new file mode 100644
index 0000000..eff7f12
--- /dev/null
+++ b/src/plugins/gs-snapd.c
@@ -0,0 +1,896 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <gs-plugin.h>
+#include <libsoup/soup.h>
+#include <gio/gunixsocketaddress.h>
+#include "gs-snapd.h"
+#include "gs-ubuntuone.h"
+
+// snapd API documentation is at https://github.com/snapcore/snapd/blob/master/docs/rest.md
+
+#define SNAPD_SOCKET "/run/snapd.socket"
+
+static GSocket *
+open_snapd_socket (GCancellable *cancellable, GError **error)
+{
+ GSocket *socket;
+ g_autoptr(GSocketAddress) address = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ socket = g_socket_new (G_SOCKET_FAMILY_UNIX,
+ G_SOCKET_TYPE_STREAM,
+ G_SOCKET_PROTOCOL_DEFAULT,
+ &error_local);
+ if (!socket) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to open snapd socket: %s",
+ error_local->message);
+ return NULL;
+ }
+ address = g_unix_socket_address_new (SNAPD_SOCKET);
+ if (!g_socket_connect (socket, address, cancellable, &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to connect snapd socket: %s",
+ error_local->message);
+ g_object_unref (socket);
+ return NULL;
+ }
+
+ return socket;
+}
+
+static gboolean
+read_from_snapd (GSocket *socket,
+ GByteArray *buffer,
+ gsize *read_offset,
+ gsize size,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gssize n_read;
+
+ if (*read_offset + size > buffer->len)
+ g_byte_array_set_size (buffer, *read_offset + size + 1);
+ n_read = g_socket_receive (socket,
+ (gchar*) (buffer->data + *read_offset),
+ size,
+ cancellable,
+ error);
+ if (n_read < 0)
+ return FALSE;
+ *read_offset += (gsize) n_read;
+ buffer->data[*read_offset] = '\0';
+
+ return TRUE;
+}
+
+static gboolean
+send_request (const gchar *method,
+ const gchar *path,
+ const gchar *content,
+ gboolean authenticate,
+ const gchar *macaroon_,
+ gchar **discharges_,
+ gboolean retry_after_login,
+ gchar **out_macaroon,
+ gchar ***out_discharges,
+ guint *status_code,
+ gchar **reason_phrase,
+ gchar **response_type,
+ gchar **response,
+ gsize *response_length,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr (GSocket) socket = NULL;
+ g_autoptr (GString) request = NULL;
+ gssize n_written;
+ g_autoptr (GByteArray) buffer = NULL;
+ gsize data_length = 0, body_offset = 0;
+ g_autoptr (SoupMessageHeaders) headers = NULL;
+ g_autofree gchar *macaroon = NULL;
+ g_auto(GStrv) discharges = NULL;
+ gsize chunk_length = 0, n_required, chunk_offset;
+ guint code;
+ gboolean ret;
+
+ macaroon = g_strdup (macaroon_);
+ discharges = g_strdupv (discharges_);
+ if (macaroon == NULL && authenticate) {
+ gs_ubuntuone_get_macaroon (TRUE, FALSE, &macaroon, &discharges, NULL);
+ }
+
+ // NOTE: Would love to use libsoup but it doesn't support unix sockets
+ // https://bugzilla.gnome.org/show_bug.cgi?id=727563
+
+ socket = open_snapd_socket (cancellable, error);
+ if (socket == NULL)
+ return FALSE;
+
+ request = g_string_new ("");
+ g_string_append_printf (request, "%s %s HTTP/1.1\r\n", method, path);
+ g_string_append (request, "Host:\r\n");
+ if (macaroon != NULL) {
+ gint i;
+
+ g_string_append_printf (request, "Authorization: Macaroon root=\"%s\"", macaroon);
+ for (i = 0; discharges[i] != NULL; i++)
+ g_string_append_printf (request, ",discharge=\"%s\"", discharges[i]);
+ g_string_append (request, "\r\n");
+ }
+ if (content)
+ g_string_append_printf (request, "Content-Length: %zu\r\n", strlen (content));
+ g_string_append (request, "X-Allow-Interaction: true\r\n");
+ g_string_append (request, "\r\n");
+ if (content)
+ g_string_append (request, content);
+
+ g_debug ("begin snapd request: %s", request->str);
+
+ /* send HTTP request */
+ n_written = g_socket_send (socket, request->str, request->len, cancellable, error);
+ if (n_written < 0)
+ return FALSE;
+
+ /* read HTTP headers */
+ buffer = g_byte_array_new ();
+ while (TRUE) {
+ const gchar *divider;
+
+ gsize n_read = data_length;
+ if (!read_from_snapd (socket,
+ buffer,
+ &data_length,
+ 1024,
+ cancellable,
+ error))
+ return FALSE;
+
+ if (n_read == data_length) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to find header separator in snapd response");
+ return FALSE;
+ }
+
+ divider = strstr ((const char *) buffer->data, "\r\n\r\n");
+ if (divider != NULL) {
+ body_offset = ((guint8*) divider - buffer->data) + 4;
+ break;
+ }
+ }
+
+ /* parse headers */
+ headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
+ if (!soup_headers_parse_response ((const char *) buffer->data, (gint) body_offset, headers,
+ NULL, &code, reason_phrase)) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd response HTTP headers not parseable");
+ return FALSE;
+ }
+
+ if (status_code != NULL)
+ *status_code = code;
+
+ if ((code == 401 || code == 403) && retry_after_login) {
+ g_socket_close (socket, NULL);
+
+ gs_ubuntuone_clear_macaroon ();
+
+ g_clear_pointer (&macaroon, g_free);
+ g_clear_pointer (&discharges, g_strfreev);
+ gs_ubuntuone_get_macaroon (FALSE, TRUE, &macaroon, &discharges, NULL);
+
+ if (macaroon == NULL) {
+ g_set_error_literal (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
+ "failed to authenticate");
+ return FALSE;
+ }
+
+ ret = send_request (method,
+ path,
+ content,
+ TRUE,
+ macaroon, discharges,
+ FALSE,
+ NULL, NULL,
+ status_code,
+ reason_phrase,
+ response_type,
+ response,
+ response_length,
+ cancellable,
+ error);
+
+ if (ret && out_macaroon != NULL) {
+ *out_macaroon = g_steal_pointer (&macaroon);
+ *out_discharges = g_steal_pointer (&discharges);
+ }
+
+ return ret;
+ }
+
+ /* read content */
+ switch (soup_message_headers_get_encoding (headers)) {
+ case SOUP_ENCODING_EOF:
+ chunk_offset = body_offset;
+ while (TRUE) {
+ gsize n_read = data_length;
+ if (!read_from_snapd (socket,
+ buffer,
+ &data_length,
+ 1024,
+ cancellable,
+ error))
+ return FALSE;
+ if (n_read == data_length)
+ break;
+ chunk_length += data_length - n_read;
+ }
+ break;
+ case SOUP_ENCODING_CHUNKED:
+ // FIXME: support multiple chunks
+ while (TRUE) {
+ const gchar *divider;
+ gsize n_read;
+
+ divider = strstr ((const char *) (buffer->data + body_offset), "\r\n");
+ if (divider) {
+ chunk_length = strtoul ((const char *) (buffer->data + body_offset), NULL,
16);
+ chunk_offset = ((guint8*) divider - buffer->data) + 2;
+ break;
+ }
+
+ n_read = data_length;
+ if (!read_from_snapd (socket,
+ buffer,
+ &data_length,
+ 1024,
+ cancellable,
+ error))
+ return FALSE;
+
+ if (n_read == data_length) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to find chunk header in "
+ "snapd response");
+ return FALSE;
+ }
+ }
+
+ /* check if enough space to read chunk */
+ n_required = chunk_offset + chunk_length;
+ while (data_length < n_required)
+ if (!read_from_snapd (socket,
+ buffer,
+ &data_length,
+ n_required - data_length,
+ cancellable,
+ error))
+ return FALSE;
+ break;
+ case SOUP_ENCODING_CONTENT_LENGTH:
+ chunk_offset = body_offset;
+ chunk_length = soup_message_headers_get_content_length (headers);
+ n_required = chunk_offset + chunk_length;
+ while (data_length < n_required) {
+ if (!read_from_snapd (socket,
+ buffer,
+ &data_length,
+ n_required - data_length,
+ cancellable,
+ error))
+ return FALSE;
+ }
+ break;
+ default:
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to determine content "
+ "length of snapd response");
+ return FALSE;
+ }
+
+
+ if (out_macaroon != NULL) {
+ *out_macaroon = g_steal_pointer (&macaroon);
+ *out_discharges = g_steal_pointer (&discharges);
+ }
+ if (response_type)
+ *response_type = g_strdup (soup_message_headers_get_content_type (headers, NULL));
+ if (response != NULL) {
+ *response = g_malloc (chunk_length + 1);
+ memcpy (*response, buffer->data + chunk_offset, chunk_length + 1);
+ g_debug ("snapd status %u: %s", code, *response);
+ }
+ if (response_length)
+ *response_length = chunk_length;
+
+ return TRUE;
+}
+
+static JsonParser *
+parse_result (const gchar *response, const gchar *response_type, GError **error)
+{
+ g_autoptr(JsonParser) parser = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ if (response_type == NULL) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned no content type");
+ return NULL;
+ }
+ if (g_strcmp0 (response_type, "application/json") != 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned unexpected content type %s", response_type);
+ return NULL;
+ }
+
+ parser = json_parser_new ();
+ if (!json_parser_load_from_data (parser, response, -1, &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to parse snapd response: %s",
+ error_local->message);
+ return NULL;
+ }
+ if (!JSON_NODE_HOLDS_OBJECT (json_parser_get_root (parser))) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd response does is not a valid JSON object");
+ return NULL;
+ }
+
+ return g_object_ref (parser);
+}
+
+JsonObject *
+gs_snapd_get_system_info (GCancellable *cancellable, GError **error)
+{
+ guint status_code;
+ g_autofree gchar *reason_phrase = NULL;
+ g_autofree gchar *response_type = NULL;
+ g_autofree gchar *response = NULL;
+ g_autoptr(JsonParser) parser = NULL;
+ JsonObject *root, *result;
+
+ if (!send_request ("GET", "/v2/system-info", NULL,
+ TRUE, NULL, NULL,
+ TRUE, NULL, NULL,
+ &status_code, &reason_phrase,
+ &response_type, &response, NULL,
+ cancellable, error))
+ return NULL;
+
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %u: %s",
+ status_code, reason_phrase);
+ return NULL;
+ }
+
+ parser = parse_result (response, response_type, error);
+ if (parser == NULL)
+ return NULL;
+ root = json_node_get_object (json_parser_get_root (parser));
+ result = json_object_get_object_member (root, "result");
+ if (result == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned no system information");
+ return NULL;
+ }
+
+ return json_object_ref (result);
+}
+
+JsonObject *
+gs_snapd_list_one (const gchar *name,
+ GCancellable *cancellable, GError **error)
+{
+ g_autofree gchar *path = NULL;
+ guint status_code;
+ g_autofree gchar *reason_phrase = NULL;
+ g_autofree gchar *response_type = NULL;
+ g_autofree gchar *response = NULL;
+ g_autoptr(JsonParser) parser = NULL;
+ JsonObject *root, *result;
+
+ path = g_strdup_printf ("/v2/snaps/%s", name);
+ if (!send_request ("GET", path, NULL,
+ TRUE, NULL, NULL,
+ TRUE, NULL, NULL,
+ &status_code, &reason_phrase,
+ &response_type, &response, NULL,
+ cancellable, error))
+ return NULL;
+
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %u: %s",
+ status_code, reason_phrase);
+ return NULL;
+ }
+
+ parser = parse_result (response, response_type, error);
+ if (parser == NULL)
+ return NULL;
+ root = json_node_get_object (json_parser_get_root (parser));
+ result = json_object_get_object_member (root, "result");
+ if (result == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned no results for %s", name);
+ return NULL;
+ }
+
+ return json_object_ref (result);
+}
+
+JsonArray *
+gs_snapd_list (GCancellable *cancellable, GError **error)
+{
+ guint status_code;
+ g_autofree gchar *reason_phrase = NULL;
+ g_autofree gchar *response_type = NULL;
+ g_autofree gchar *response = NULL;
+ g_autoptr(JsonParser) parser = NULL;
+ JsonObject *root;
+ JsonArray *result;
+
+ if (!send_request ("GET", "/v2/snaps", NULL,
+ TRUE, NULL, NULL,
+ TRUE, NULL, NULL,
+ &status_code, &reason_phrase,
+ &response_type, &response, NULL,
+ cancellable, error))
+ return NULL;
+
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %u: %s",
+ status_code, reason_phrase);
+ return NULL;
+ }
+
+ parser = parse_result (response, response_type, error);
+ if (parser == NULL)
+ return NULL;
+ root = json_node_get_object (json_parser_get_root (parser));
+ result = json_object_get_array_member (root, "result");
+ if (result == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned no result");
+ return NULL;
+ }
+
+ return json_array_ref (result);
+}
+
+JsonArray *
+gs_snapd_find (const gchar *section, gboolean match_name, const gchar *query,
+ GCancellable *cancellable, GError **error)
+{
+ g_autoptr(GString) path = NULL;
+ guint status_code;
+ g_autofree gchar *reason_phrase = NULL;
+ g_autofree gchar *response_type = NULL;
+ g_autofree gchar *response = NULL;
+ g_autoptr(JsonParser) parser = NULL;
+ JsonObject *root;
+ JsonArray *result;
+
+ path = g_string_new ("/v2/find?");
+ if (section != NULL) {
+ g_string_append_printf (path, "section=%s", section);
+ }
+ if (query != NULL) {
+ g_autofree gchar *escaped = NULL;
+
+ escaped = soup_uri_encode (query, NULL);
+ if (section != NULL)
+ g_string_append (path, "&");
+ if (match_name)
+ g_string_append (path, "name=");
+ else
+ g_string_append (path, "q=");
+ g_string_append (path, escaped);
+ }
+ if (!send_request ("GET", path->str, NULL,
+ TRUE, NULL, NULL,
+ TRUE, NULL, NULL,
+ &status_code, &reason_phrase,
+ &response_type, &response, NULL,
+ cancellable, error))
+ return NULL;
+
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %u: %s",
+ status_code, reason_phrase);
+ return NULL;
+ }
+
+ parser = parse_result (response, response_type, error);
+ if (parser == NULL)
+ return NULL;
+ root = json_node_get_object (json_parser_get_root (parser));
+ result = json_object_get_array_member (root, "result");
+ if (result == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned no result");
+ return NULL;
+ }
+
+ return json_array_ref (result);
+}
+
+JsonObject *
+gs_snapd_get_interfaces (GCancellable *cancellable, GError **error)
+{
+ guint status_code;
+ g_autofree gchar *reason_phrase = NULL;
+ g_autofree gchar *response_type = NULL;
+ g_autofree gchar *response = NULL;
+ g_autoptr(JsonParser) parser = NULL;
+ JsonObject *root;
+ JsonObject *result;
+
+ if (!send_request ("GET", "/v2/interfaces", NULL,
+ TRUE, NULL, NULL,
+ TRUE, NULL, NULL,
+ &status_code, &reason_phrase,
+ &response_type, &response, NULL,
+ cancellable, error))
+ return NULL;
+
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %u: %s",
+ status_code, reason_phrase);
+ return NULL;
+ }
+
+ parser = parse_result (response, response_type, error);
+ if (parser == NULL)
+ return NULL;
+ root = json_node_get_object (json_parser_get_root (parser));
+ result = json_object_get_object_member (root, "result");
+ if (result == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned no result");
+ return NULL;
+ }
+
+ return json_object_ref (result);
+}
+
+static JsonObject *
+get_changes (const gchar *macaroon, gchar **discharges,
+ const gchar *change_id,
+ GCancellable *cancellable, GError **error)
+{
+ g_autofree gchar *path = NULL;
+ guint status_code;
+ g_autofree gchar *reason_phrase = NULL;
+ g_autofree gchar *response_type = NULL;
+ g_autofree gchar *response = NULL;
+ g_autoptr(JsonParser) parser = NULL;
+ JsonObject *root, *result;
+
+ path = g_strdup_printf ("/v2/changes/%s", change_id);
+ if (!send_request ("GET", path, NULL,
+ TRUE, macaroon, discharges,
+ TRUE, NULL, NULL,
+ &status_code, &reason_phrase,
+ &response_type, &response, NULL,
+ cancellable, error))
+ return NULL;
+
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %u: %s",
+ status_code, reason_phrase);
+ return NULL;
+ }
+
+ parser = parse_result (response, response_type, error);
+ if (parser == NULL)
+ return NULL;
+ root = json_node_get_object (json_parser_get_root (parser));
+ result = json_object_get_object_member (root, "result");
+ if (result == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned no result");
+ return NULL;
+ }
+
+ return json_object_ref (result);
+}
+
+static gboolean
+send_package_action (const gchar *name,
+ const gchar *action,
+ gboolean classic,
+ GsSnapdProgressCallback callback,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GString) content = NULL;
+ g_autofree gchar *path = NULL;
+ guint status_code;
+ g_autofree gchar *macaroon = NULL;
+ g_auto(GStrv) discharges = NULL;
+ g_autofree gchar *reason_phrase = NULL;
+ g_autofree gchar *response_type = NULL;
+ g_autofree gchar *response = NULL;
+ g_autofree gchar *status = NULL;
+ g_autoptr(JsonParser) parser = NULL;
+ JsonObject *root, *result;
+ const gchar *type;
+
+ content = g_string_new ("{");
+ g_string_append_printf (content, "\"action\": \"%s\"", action);
+ if (classic)
+ g_string_append (content, ", \"classic\": true");
+ g_string_append (content, "}");
+ path = g_strdup_printf ("/v2/snaps/%s", name);
+ if (!send_request ("POST", path, content->str,
+ TRUE, NULL, NULL,
+ TRUE, &macaroon, &discharges,
+ &status_code, &reason_phrase,
+ &response_type, &response, NULL,
+ cancellable, error))
+ return FALSE;
+
+ if (status_code != SOUP_STATUS_ACCEPTED) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %u: %s",
+ status_code, reason_phrase);
+ return FALSE;
+ }
+
+ parser = parse_result (response, response_type, error);
+ if (parser == NULL)
+ return FALSE;
+
+ root = json_node_get_object (json_parser_get_root (parser));
+ type = json_object_get_string_member (root, "type");
+
+ if (g_strcmp0 (type, "async") == 0) {
+ const gchar *change_id;
+
+ change_id = json_object_get_string_member (root, "change");
+
+ while (TRUE) {
+ /* Wait for a little bit before polling */
+ g_usleep (100 * 1000);
+
+ result = get_changes (macaroon, discharges, change_id, cancellable, error);
+ if (result == NULL)
+ return FALSE;
+
+ status = g_strdup (json_object_get_string_member (result, "status"));
+
+ if (g_strcmp0 (status, "Done") == 0)
+ break;
+
+ callback (result, user_data);
+ }
+ }
+
+ if (g_strcmp0 (status, "Done") != 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd operation finished with status %s", status);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_snapd_install (const gchar *name, gboolean classic,
+ GsSnapdProgressCallback callback, gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ return send_package_action (name, "install", classic, callback, user_data, cancellable, error);
+}
+
+gboolean
+gs_snapd_remove (const gchar *name,
+ GsSnapdProgressCallback callback, gpointer user_data,
+ GCancellable *cancellable, GError **error)
+{
+ return send_package_action (name, "remove", FALSE, callback, user_data, cancellable, error);
+}
+
+gchar *
+gs_snapd_get_resource (const gchar *path,
+ gsize *data_length,
+ GCancellable *cancellable, GError **error)
+{
+ guint status_code;
+ g_autofree gchar *reason_phrase = NULL;
+ g_autofree gchar *response_type = NULL;
+ g_autofree gchar *data = NULL;
+
+ if (!send_request ("GET", path, NULL,
+ TRUE, NULL, NULL,
+ TRUE, NULL, NULL,
+ &status_code, &reason_phrase,
+ NULL, &data, data_length,
+ cancellable, error))
+ return NULL;
+
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %u: %s",
+ status_code, reason_phrase);
+ return NULL;
+ }
+
+ return g_steal_pointer (&data);
+}
+
+static gboolean
+parse_date (const gchar *date_string, gint *year, gint *month, gint *day)
+{
+ /* Example: 2016-05-17 */
+ if (strchr (date_string, '-') != NULL) {
+ g_auto(GStrv) tokens = NULL;
+
+ tokens = g_strsplit (date_string, "-", -1);
+ if (g_strv_length (tokens) != 3)
+ return FALSE;
+
+ *year = atoi (tokens[0]);
+ *month = atoi (tokens[1]);
+ *day = atoi (tokens[2]);
+
+ return TRUE;
+ }
+ /* Example: 20160517 */
+ else if (strlen (date_string) == 8) {
+ // FIXME: Implement
+ return FALSE;
+ }
+ else
+ return FALSE;
+}
+
+static gboolean
+parse_time (const gchar *time_string, gint *hour, gint *minute, gdouble *seconds)
+{
+ /* Example: 09:36:53.682 or 09:36:53 or 09:36 */
+ if (strchr (time_string, ':') != NULL) {
+ g_auto(GStrv) tokens = NULL;
+
+ tokens = g_strsplit (time_string, ":", 3);
+ *hour = atoi (tokens[0]);
+ if (tokens[1] == NULL)
+ return FALSE;
+ *minute = atoi (tokens[1]);
+ if (tokens[2] != NULL)
+ *seconds = g_ascii_strtod (tokens[2], NULL);
+ else
+ *seconds = 0.0;
+
+ return TRUE;
+ }
+ /* Example: 093653.682 or 093653 or 0936 */
+ else {
+ // FIXME: Implement
+ return FALSE;
+ }
+}
+
+static gboolean
+is_timezone_prefix (gchar c)
+{
+ return c == '+' || c == '-' || c == 'Z';
+}
+
+GDateTime *
+gs_snapd_parse_date (const gchar *value)
+{
+ g_auto(GStrv) tokens = NULL;
+ g_autoptr(GTimeZone) timezone = NULL;
+ gint year = 0, month = 0, day = 0, hour = 0, minute = 0;
+ gdouble seconds = 0.0;
+
+ if (value == NULL)
+ return NULL;
+
+ /* Example: 2016-05-17T09:36:53+12:00 */
+ tokens = g_strsplit (value, "T", 2);
+ if (!parse_date (tokens[0], &year, &month, &day))
+ return NULL;
+ if (tokens[1] != NULL) {
+ gchar *timezone_start;
+
+ /* Timezone is either Z (UTC) +hh:mm or -hh:mm */
+ timezone_start = tokens[1];
+ while (*timezone_start != '\0' && !is_timezone_prefix (*timezone_start))
+ timezone_start++;
+ if (*timezone_start != '\0')
+ timezone = g_time_zone_new (timezone_start);
+
+ /* Strip off timezone */
+ *timezone_start = '\0';
+
+ if (!parse_time (tokens[1], &hour, &minute, &seconds))
+ return NULL;
+ }
+
+ if (timezone == NULL)
+ timezone = g_time_zone_new_local ();
+
+ return g_date_time_new (timezone, year, month, day, hour, minute, seconds);
+}
diff --git a/src/plugins/gs-snapd.h b/src/plugins/gs-snapd.h
new file mode 100644
index 0000000..5941ebd
--- /dev/null
+++ b/src/plugins/gs-snapd.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_SNAPD_H__
+#define __GS_SNAPD_H__
+
+#include <gio/gio.h>
+#include <json-glib/json-glib.h>
+
+typedef void (*GsSnapdProgressCallback) (JsonObject *object, gpointer user_data);
+
+JsonObject *gs_snapd_get_system_info (GCancellable *cancellable,
+ GError **error);
+
+JsonObject *gs_snapd_list_one (const gchar *name,
+ GCancellable *cancellable,
+ GError **error);
+
+JsonArray *gs_snapd_list (GCancellable *cancellable,
+ GError **error);
+
+JsonArray *gs_snapd_find (const gchar *section,
+ gboolean match_name,
+ const gchar *query,
+ GCancellable *cancellable,
+ GError **error);
+
+JsonObject *gs_snapd_get_interfaces (GCancellable *cancellable,
+ GError **error);
+
+gboolean gs_snapd_install (const gchar *name,
+ gboolean classic,
+ GsSnapdProgressCallback callback,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error);
+
+gboolean gs_snapd_remove (const gchar *name,
+ GsSnapdProgressCallback callback,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error);
+
+gchar *gs_snapd_get_resource (const gchar *path,
+ gsize *data_length,
+ GCancellable *cancellable,
+ GError **error);
+
+GDateTime *gs_snapd_parse_date (const gchar *value);
+
+#endif /* __GS_SNAPD_H__ */
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]