diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:57:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:57:27 +0000 |
commit | 6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18 (patch) | |
tree | d423850ae901365e582137bdf2b5cbdffd7ca266 /lib/gs-remote-icon.c | |
parent | Initial commit. (diff) | |
download | gnome-software-6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18.tar.xz gnome-software-6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18.zip |
Adding upstream version 43.5.upstream/43.5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/gs-remote-icon.c')
-rw-r--r-- | lib/gs-remote-icon.c | 375 |
1 files changed, 375 insertions, 0 deletions
diff --git a/lib/gs-remote-icon.c b/lib/gs-remote-icon.c new file mode 100644 index 0000000..84b071b --- /dev/null +++ b/lib/gs-remote-icon.c @@ -0,0 +1,375 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation, Inc + * + * Author: Philip Withnall <pwithnall@endlessos.org> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-remote-icon + * @short_description: A #GIcon implementation for remote icons + * + * #GsRemoteIcon is a #GIcon implementation which represents remote icons — + * icons which have an HTTP or HTTPS URI. It provides a well-known local filename + * for a cached copy of the icon, accessible as #GFileIcon:file, and a method + * to download the icon to the cache, gs_remote_icon_ensure_cached(). + * + * Constructing a #GsRemoteIcon does not guarantee that the icon is cached. Call + * gs_remote_icon_ensure_cached() for that. + * + * #GsRemoteIcon is immutable after construction and hence is entirely thread + * safe. + * + * FIXME: Currently does no cache invalidation. + * + * Since: 40 + */ + +#include "config.h" + +#include <gio/gio.h> +#include <glib.h> +#include <glib-object.h> +#include <glib/gstdio.h> +#include <fcntl.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <libsoup/soup.h> + +#include "gs-remote-icon.h" +#include "gs-utils.h" + +/* FIXME: Work around the fact that GFileIcon is not derivable, by deriving from + * it anyway by copying its `struct GFileIcon` definition inline here. This will + * work as long as the size of `struct GFileIcon` doesn’t change within GIO. + * There’s no way of knowing if that’s the case. + * + * See https://gitlab.gnome.org/GNOME/glib/-/issues/2345 for why this is + * necessary. */ +struct _GsRemoteIcon +{ + /* struct GFileIcon { */ + GObject grandparent; + GFile *file; + /* } */ + + gchar *uri; /* (owned), immutable after construction */ +}; + +G_DEFINE_TYPE (GsRemoteIcon, gs_remote_icon, G_TYPE_FILE_ICON) + +typedef enum { + PROP_URI = 1, +} GsRemoteIconProperty; + +static GParamSpec *obj_props[PROP_URI + 1] = { NULL, }; + +static void +gs_remote_icon_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsRemoteIcon *self = GS_REMOTE_ICON (object); + + switch ((GsRemoteIconProperty) prop_id) { + case PROP_URI: + g_value_set_string (value, gs_remote_icon_get_uri (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_remote_icon_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsRemoteIcon *self = GS_REMOTE_ICON (object); + + switch ((GsRemoteIconProperty) prop_id) { + case PROP_URI: + /* Construct only */ + g_assert (self->uri == NULL); + self->uri = g_value_dup_string (value); + g_assert (g_str_has_prefix (self->uri, "http:") || + g_str_has_prefix (self->uri, "https:")); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_remote_icon_finalize (GObject *object) +{ + GsRemoteIcon *self = GS_REMOTE_ICON (object); + + g_free (self->uri); + + G_OBJECT_CLASS (gs_remote_icon_parent_class)->finalize (object); +} + +static void +gs_remote_icon_class_init (GsRemoteIconClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = gs_remote_icon_get_property; + object_class->set_property = gs_remote_icon_set_property; + object_class->finalize = gs_remote_icon_finalize; + + /** + * GsRemoteIcon:uri: (not nullable) + * + * Remote URI of the icon. This must be an HTTP or HTTPS URI; it is a + * programmer error to provide other URI schemes. + * + * Since: 40 + */ + obj_props[PROP_URI] = + g_param_spec_string ("uri", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props); +} + +static void +gs_remote_icon_init (GsRemoteIcon *self) +{ +} + +/* Use a hash-prefixed filename to avoid cache clashes. + * This can only fail if @create_directory is %TRUE. */ +static gchar * +gs_remote_icon_get_cache_filename (const gchar *uri, + gboolean create_directory, + GError **error) +{ + g_autofree gchar *uri_checksum = NULL; + g_autofree gchar *uri_basename = NULL; + g_autofree gchar *cache_basename = NULL; + GsUtilsCacheFlags flags; + + uri_checksum = g_compute_checksum_for_string (G_CHECKSUM_SHA1, + uri, + -1); + uri_basename = g_path_get_basename (uri); + + /* convert filename from jpg to png, as we always convert to PNG on + * download */ + if (g_str_has_suffix (uri_basename, ".jpg")) + memcpy (uri_basename + strlen (uri_basename) - 4, ".png", 4); + + cache_basename = g_strdup_printf ("%s-%s", uri_checksum, uri_basename); + + flags = GS_UTILS_CACHE_FLAG_WRITEABLE; + if (create_directory) + flags |= GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY; + + return gs_utils_get_cache_filename ("icons", + cache_basename, + flags, + error); +} + +/** + * gs_remote_icon_new: + * @uri: remote URI of the icon + * + * Create a new #GsRemoteIcon representing @uri. The #GFileIcon:file of the + * resulting icon will represent the local cache location for the icon. + * + * Returns: (transfer full): a new remote icon + * Since: 40 + */ +GIcon * +gs_remote_icon_new (const gchar *uri) +{ + g_autofree gchar *cache_filename = NULL; + g_autoptr(GFile) file = NULL; + + g_return_val_if_fail (uri != NULL, NULL); + + /* The file is the expected cached location of the icon, once it’s + * downloaded. By setting it as the #GFileIcon:file property, existing + * code (particularly in GTK) which operates on #GFileIcons will work + * transparently with this. + * + * Ideally, #GFileIcon would be an interface rather than a class, which + * would make this implementation cleaner, but this is what we’re stuck + * with. + * + * See https://gitlab.gnome.org/GNOME/glib/-/issues/2345 */ + cache_filename = gs_remote_icon_get_cache_filename (uri, FALSE, NULL); + g_assert (cache_filename != NULL); + file = g_file_new_for_path (cache_filename); + + return g_object_new (GS_TYPE_REMOTE_ICON, + "file", file, + "uri", uri, + NULL); +} + +/** + * gs_remote_icon_get_uri: + * @self: a #GsRemoteIcon + * + * Gets the value of #GsRemoteIcon:uri. + * + * Returns: (not nullable): remote URI of the icon + * Since: 40 + */ +const gchar * +gs_remote_icon_get_uri (GsRemoteIcon *self) +{ + g_return_val_if_fail (GS_IS_REMOTE_ICON (self), NULL); + + return self->uri; +} + +static GdkPixbuf * +gs_icon_download (SoupSession *session, + const gchar *uri, + const gchar *destination_path, + guint max_size, + GCancellable *cancellable, + GError **error) +{ + guint status_code; + g_autoptr(SoupMessage) msg = NULL; + g_autoptr(GInputStream) stream = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autoptr(GdkPixbuf) scaled_pixbuf = NULL; + + /* Create the request */ + msg = soup_message_new (SOUP_METHOD_GET, uri); + if (msg == NULL) { + g_set_error_literal (error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "Icon has an invalid URL"); + return NULL; + } + + /* Send request synchronously and start reading the response. */ + stream = soup_session_send (session, msg, cancellable, error); + +#if SOUP_CHECK_VERSION(3, 0, 0) + status_code = soup_message_get_status (msg); +#else + status_code = msg->status_code; +#endif + if (stream == NULL) { + return NULL; + } else if (status_code != SOUP_STATUS_OK) { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Failed to download icon %s: %s", + uri, soup_status_get_phrase (status_code)); + return NULL; + } + + /* Typically these icons are 64x64px PNG files. If not, resize down + * so it’s at most @max_size square, to minimise the size of the on-disk + * cache.*/ + pixbuf = gdk_pixbuf_new_from_stream (stream, cancellable, error); + if (pixbuf == NULL) + return NULL; + + if ((guint) gdk_pixbuf_get_height (pixbuf) <= max_size && + (guint) gdk_pixbuf_get_width (pixbuf) <= max_size) { + scaled_pixbuf = g_object_ref (pixbuf); + } else { + scaled_pixbuf = gdk_pixbuf_scale_simple (pixbuf, max_size, max_size, + GDK_INTERP_BILINEAR); + } + + /* write file */ + if (!gdk_pixbuf_save (scaled_pixbuf, destination_path, "png", error, NULL)) + return NULL; + + return g_steal_pointer (&scaled_pixbuf); +} + +/** + * gs_remote_icon_ensure_cached: + * @self: a #GsRemoteIcon + * @soup_session: a #SoupSession to use to download the icon + * @maximum_icon_size: maximum size (in device pixels) of the icon to save + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError, or %NULL + * + * Ensure the given icon is present in the local cache, potentially downloading + * it from its remote server if needed. This will do network and disk I/O. + * + * @maximum_icon_size specifies the maximum size (in device pixels) of the icon + * which should be saved to the cache. This is the maximum size that the icon + * can ever be used at, as icons can be downscaled but never upscaled. Typically + * this will be 160px multiplied by the device scale + * (`gtk_widget_get_scale_factor()`). + * + * This can be called from any thread, as #GsRemoteIcon is immutable and hence + * thread-safe. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 40 + */ +gboolean +gs_remote_icon_ensure_cached (GsRemoteIcon *self, + SoupSession *soup_session, + guint maximum_icon_size, + GCancellable *cancellable, + GError **error) +{ + const gchar *uri; + g_autofree gchar *cache_filename = NULL; + g_autoptr(GdkPixbuf) cached_pixbuf = NULL; + GStatBuf stat_buf; + + g_return_val_if_fail (GS_IS_REMOTE_ICON (self), FALSE); + g_return_val_if_fail (SOUP_IS_SESSION (soup_session), FALSE); + g_return_val_if_fail (maximum_icon_size > 0, FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + uri = gs_remote_icon_get_uri (self); + + /* Work out cache filename. */ + cache_filename = gs_remote_icon_get_cache_filename (uri, TRUE, error); + if (cache_filename == NULL) + return FALSE; + + /* Already in cache and not older than 30 days */ + if (g_stat (cache_filename, &stat_buf) != -1 && + S_ISREG (stat_buf.st_mode) && + (g_get_real_time () / G_USEC_PER_SEC) - stat_buf.st_mtim.tv_sec < (60 * 60 * 24 * 30)) { + gint width = 0, height = 0; + /* Ensure the downloaded image dimensions are stored on the icon */ + if (!g_object_get_data (G_OBJECT (self), "width") && + gdk_pixbuf_get_file_info (cache_filename, &width, &height)) { + g_object_set_data (G_OBJECT (self), "width", GINT_TO_POINTER (width)); + g_object_set_data (G_OBJECT (self), "height", GINT_TO_POINTER (height)); + } + return TRUE; + } + + cached_pixbuf = gs_icon_download (soup_session, uri, cache_filename, maximum_icon_size, cancellable, error); + if (cached_pixbuf == NULL) + return FALSE; + + /* Ensure the dimensions are set correctly on the icon. */ + g_object_set_data (G_OBJECT (self), "width", GUINT_TO_POINTER (gdk_pixbuf_get_width (cached_pixbuf))); + g_object_set_data (G_OBJECT (self), "height", GUINT_TO_POINTER (gdk_pixbuf_get_height (cached_pixbuf))); + + return TRUE; +} |