[gnome-software/wip/ubuntu-3-20: 27/57] Add a Snap plugin



commit 6795a7a907cd1a4b669e3bdf198140b6df780ecc
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             |  726 ++++++++++++++++++++++++
 src/plugins/gs-snapd.c                   |  890 ++++++++++++++++++++++++++++++
 src/plugins/gs-snapd.h                   |   68 +++
 10 files changed, 1741 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..bf7bc29
--- /dev/null
+++ b/src/plugins/gs-plugin-snap.c
@@ -0,0 +1,726 @@
+/* -*- 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 {
+       gchar           *store_name;
+       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);
+       }
+       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"));
+
+       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 JsonArray *
+find_snaps (GsPlugin *plugin, const gchar *section, gboolean match_name, const gchar *query, GCancellable 
*cancellable, GError **error)
+{
+       g_autoptr(JsonArray) snaps = NULL;
+       guint i;
+
+       snaps = gs_snapd_find (section, match_name, query, cancellable, error);
+       if (snaps == NULL)
+               return NULL;
+
+       /* cache results */
+       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));
+       }
+
+       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;
+
+       /* 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);
+
+       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);
+       gs_plugin_add_app (list, snap_to_app (plugin, snap));
+
+       return TRUE;
+}
+
+void
+gs_plugin_destroy (GsPlugin *plugin)
+{
+       g_free (plugin->priv->store_name);
+       g_hash_table_unref (plugin->priv->store_snaps);
+}
+
+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++) {
+               JsonObject *snap = json_array_get_object_element (snaps, i);
+               gs_plugin_add_app (list, snap_to_app (plugin, snap));
+       }
+
+       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);
+               const gchar *status;
+
+               status = json_object_get_string_member (snap, "status");
+               if (g_strcmp0 (status, "active") != 0)
+                       continue;
+
+               gs_plugin_add_app (list, snap_to_app (plugin, snap));
+       }
+
+       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++) {
+               JsonObject *snap = json_array_get_object_element (snaps, i);
+               gs_plugin_add_app (list, snap_to_app (plugin, snap));
+       }
+
+       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 = g_hash_table_lookup (plugin->priv->store_snaps, 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);
+
+       /* 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;
+
+       /* 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);
+       data.plugin = plugin;
+       data.app = app;
+       if (!gs_snapd_install (gs_app_get_id (app), 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..261af42
--- /dev/null
+++ b/src/plugins/gs-snapd.c
@@ -0,0 +1,890 @@
+/* -*- 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,
+                    GsSnapdProgressCallback callback,
+                    gpointer user_data,
+                    GCancellable *cancellable,
+                    GError **error)
+{
+       g_autofree gchar *content = NULL, *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_strdup_printf ("{\"action\": \"%s\"}", action);
+       path = g_strdup_printf ("/v2/snaps/%s", name);
+       if (!send_request ("POST", path, content,
+                          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,
+                 GsSnapdProgressCallback callback, gpointer user_data,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       return send_package_action (name, "install", 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", 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..c75cdf2
--- /dev/null
+++ b/src/plugins/gs-snapd.h
@@ -0,0 +1,68 @@
+/* -*- 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,
+                                        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]