/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * pikadataloaderfactory.c * Copyright (C) 2001-2018 Michael Natterer * * 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 3 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, see . */ #include "config.h" #include #include #include #include "libpikabase/pikabase.h" #include "core-types.h" #include "pika.h" #include "pika-utils.h" #include "pikacontainer.h" #include "pikadata.h" #include "pikadataloaderfactory.h" #include "pika-intl.h" /* Data files that have this string in their path are considered * obsolete and are only kept around for backwards compatibility */ #define PIKA_OBSOLETE_DATA_DIR_NAME "pika-obsolete-files" typedef struct _PikaDataLoader PikaDataLoader; struct _PikaDataLoader { gchar *name; PikaDataLoadFunc load_func; gchar *extension; gboolean writable; }; struct _PikaDataLoaderFactoryPrivate { GList *loaders; PikaDataLoader *fallback; }; #define GET_PRIVATE(obj) (((PikaDataLoaderFactory *) (obj))->priv) static void pika_data_loader_factory_finalize (GObject *object); static void pika_data_loader_factory_data_init (PikaDataFactory *factory, PikaContext *context); static void pika_data_loader_factory_data_refresh (PikaDataFactory *factory, PikaContext *context); static PikaDataLoader * pika_data_loader_factory_get_loader (PikaDataFactory *factory, GFile *file); static void pika_data_loader_factory_load (PikaDataFactory *factory, PikaContext *context, GHashTable *cache); static void pika_data_loader_factory_load_directory (PikaDataFactory *factory, PikaContext *context, GHashTable *cache, gboolean dir_writable, GFile *directory, GFile *top_directory); static void pika_data_loader_factory_load_data (PikaDataFactory *factory, PikaContext *context, GHashTable *cache, gboolean dir_writable, GFile *file, GFileInfo *info, GFile *top_directory); static PikaDataLoader * pika_data_loader_new (const gchar *name, PikaDataLoadFunc load_func, const gchar *extension, gboolean writable); static void pika_data_loader_free (PikaDataLoader *loader); G_DEFINE_TYPE_WITH_PRIVATE (PikaDataLoaderFactory, pika_data_loader_factory, PIKA_TYPE_DATA_FACTORY) #define parent_class pika_data_loader_factory_parent_class static void pika_data_loader_factory_class_init (PikaDataLoaderFactoryClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); PikaDataFactoryClass *factory_class = PIKA_DATA_FACTORY_CLASS (klass); object_class->finalize = pika_data_loader_factory_finalize; factory_class->data_init = pika_data_loader_factory_data_init; factory_class->data_refresh = pika_data_loader_factory_data_refresh; } static void pika_data_loader_factory_init (PikaDataLoaderFactory *factory) { factory->priv = pika_data_loader_factory_get_instance_private (factory); } static void pika_data_loader_factory_finalize (GObject *object) { PikaDataLoaderFactoryPrivate *priv = GET_PRIVATE (object); g_list_free_full (priv->loaders, (GDestroyNotify) pika_data_loader_free); priv->loaders = NULL; g_clear_pointer (&priv->fallback, pika_data_loader_free); G_OBJECT_CLASS (parent_class)->finalize (object); } static void pika_data_loader_factory_data_init (PikaDataFactory *factory, PikaContext *context) { pika_data_loader_factory_load (factory, context, NULL); } static void pika_data_loader_factory_refresh_cache_add (PikaDataFactory *factory, PikaData *data, gpointer user_data) { GFile *file = pika_data_get_file (data); if (file) { PikaContainer *container = pika_data_factory_get_container (factory); GHashTable *cache = user_data; GList *list; g_object_ref (data); pika_container_remove (container, PIKA_OBJECT (data)); list = g_hash_table_lookup (cache, file); list = g_list_prepend (list, data); g_hash_table_insert (cache, file, list); } } static gboolean pika_data_loader_factory_refresh_cache_remove (gpointer key, gpointer value, gpointer user_data) { GList *list; for (list = value; list; list = list->next) g_object_unref (list->data); g_list_free (value); return TRUE; } static void pika_data_loader_factory_data_refresh (PikaDataFactory *factory, PikaContext *context) { PikaContainer *container = pika_data_factory_get_container (factory); GHashTable *cache; pika_container_freeze (container); /* First, save all dirty data objects */ pika_data_factory_data_save (factory); cache = g_hash_table_new (g_file_hash, (GEqualFunc) g_file_equal); pika_data_factory_data_foreach (factory, TRUE, pika_data_loader_factory_refresh_cache_add, cache); /* Now the cache contains a GFile => list-of-objects mapping of * the old objects. So we should now traverse the directory and for * each file load it only if its mtime is newer. * * Once a file was added, it is removed from the cache, so the only * objects remaining there will be those that are not present on * the disk (that have to be destroyed) */ pika_data_loader_factory_load (factory, context, cache); /* Now all the data is loaded. Free what remains in the cache */ g_hash_table_foreach_remove (cache, pika_data_loader_factory_refresh_cache_remove, NULL); g_hash_table_destroy (cache); pika_container_thaw (container); } /* public functions */ PikaDataFactory * pika_data_loader_factory_new (Pika *pika, GType data_type, const gchar *path_property_name, const gchar *writable_property_name, const gchar *ext_property_name, PikaDataNewFunc new_func, PikaDataGetStandardFunc get_standard_func) { g_return_val_if_fail (PIKA_IS_PIKA (pika), NULL); g_return_val_if_fail (g_type_is_a (data_type, PIKA_TYPE_DATA), NULL); g_return_val_if_fail (path_property_name != NULL, NULL); g_return_val_if_fail (writable_property_name != NULL, NULL); g_return_val_if_fail (ext_property_name != NULL, NULL); return g_object_new (PIKA_TYPE_DATA_LOADER_FACTORY, "pika", pika, "data-type", data_type, "path-property-name", path_property_name, "writable-property-name", writable_property_name, "ext-property-name", ext_property_name, "new-func", new_func, "get-standard-func", get_standard_func, NULL); } void pika_data_loader_factory_add_loader (PikaDataFactory *factory, const gchar *name, PikaDataLoadFunc load_func, const gchar *extension, gboolean writable) { PikaDataLoaderFactoryPrivate *priv; PikaDataLoader *loader; g_return_if_fail (PIKA_IS_DATA_LOADER_FACTORY (factory)); g_return_if_fail (name != NULL); g_return_if_fail (load_func != NULL); g_return_if_fail (extension != NULL); priv = GET_PRIVATE (factory); loader = pika_data_loader_new (name, load_func, extension, writable); priv->loaders = g_list_append (priv->loaders, loader); } void pika_data_loader_factory_add_fallback (PikaDataFactory *factory, const gchar *name, PikaDataLoadFunc load_func) { PikaDataLoaderFactoryPrivate *priv; g_return_if_fail (PIKA_IS_DATA_LOADER_FACTORY (factory)); g_return_if_fail (name != NULL); g_return_if_fail (load_func != NULL); priv = GET_PRIVATE (factory); g_clear_pointer (&priv->fallback, pika_data_loader_free); priv->fallback = pika_data_loader_new (name, load_func, NULL, FALSE); } /* private functions */ static PikaDataLoader * pika_data_loader_factory_get_loader (PikaDataFactory *factory, GFile *file) { PikaDataLoaderFactoryPrivate *priv = GET_PRIVATE (factory); GList *list; for (list = priv->loaders; list; list = g_list_next (list)) { PikaDataLoader *loader = list->data; if (pika_file_has_extension (file, loader->extension)) return loader; } return priv->fallback; } static void pika_data_loader_factory_load (PikaDataFactory *factory, PikaContext *context, GHashTable *cache) { const GList *ext_path; GList *path; GList *writable_path; GList *list; path = pika_data_factory_get_data_path (factory); writable_path = pika_data_factory_get_data_path_writable (factory); ext_path = pika_data_factory_get_data_path_ext (factory); for (list = (GList *) ext_path; list; list = g_list_next (list)) { /* Adding data from extensions. * Consider these always non-writable (even when the directory is * writable, since writability of extension is only taken into * account for extension update). */ pika_data_loader_factory_load_directory (factory, context, cache, FALSE, list->data, list->data); } for (list = path; list; list = g_list_next (list)) { gboolean dir_writable = FALSE; if (g_list_find_custom (writable_path, list->data, (GCompareFunc) pika_file_compare)) dir_writable = TRUE; pika_data_loader_factory_load_directory (factory, context, cache, dir_writable, list->data, list->data); } g_list_free_full (path, (GDestroyNotify) g_object_unref); g_list_free_full (writable_path, (GDestroyNotify) g_object_unref); } static void pika_data_loader_factory_load_directory (PikaDataFactory *factory, PikaContext *context, GHashTable *cache, gboolean dir_writable, GFile *directory, GFile *top_directory) { GFileEnumerator *enumerator; enumerator = g_file_enumerate_children (directory, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN "," G_FILE_ATTRIBUTE_STANDARD_TYPE "," G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (enumerator) { GFileInfo *info; while ((info = g_file_enumerator_next_file (enumerator, NULL, NULL))) { GFileType file_type; GFile *child; if (g_file_info_get_is_hidden (info)) { g_object_unref (info); continue; } file_type = g_file_info_get_file_type (info); child = g_file_enumerator_get_child (enumerator, info); if (file_type == G_FILE_TYPE_DIRECTORY) { pika_data_loader_factory_load_directory (factory, context, cache, dir_writable, child, top_directory); } else if (file_type == G_FILE_TYPE_REGULAR) { pika_data_loader_factory_load_data (factory, context, cache, dir_writable, child, info, top_directory); } g_object_unref (child); g_object_unref (info); } g_object_unref (enumerator); } } static void pika_data_loader_factory_load_data (PikaDataFactory *factory, PikaContext *context, GHashTable *cache, gboolean dir_writable, GFile *file, GFileInfo *info, GFile *top_directory) { PikaDataLoader *loader; PikaContainer *container; PikaContainer *container_obsolete; GList *data_list = NULL; GInputStream *input; guint64 mtime; GError *error = NULL; loader = pika_data_loader_factory_get_loader (factory, file); if (! loader) return; container = pika_data_factory_get_container (factory); container_obsolete = pika_data_factory_get_container_obsolete (factory); if (pika_data_factory_get_pika (factory)->be_verbose) g_print (" Loading %s\n", pika_file_get_utf8_name (file)); mtime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED); if (cache) { GList *cached_data = g_hash_table_lookup (cache, file); if (cached_data && pika_data_get_mtime (cached_data->data) != 0 && pika_data_get_mtime (cached_data->data) == mtime) { GList *list; for (list = cached_data; list; list = g_list_next (list)) pika_container_add (container, list->data); return; } } input = G_INPUT_STREAM (g_file_read (file, NULL, &error)); if (input) { GInputStream *buffered = g_buffered_input_stream_new (input); data_list = loader->load_func (context, file, buffered, &error); if (error) { g_prefix_error (&error, _("Error loading '%s': "), pika_file_get_utf8_name (file)); } else if (! data_list) { g_set_error (&error, PIKA_DATA_ERROR, PIKA_DATA_ERROR_READ, _("Error loading '%s'"), pika_file_get_utf8_name (file)); } g_object_unref (buffered); g_object_unref (input); } else { g_prefix_error (&error, _("Could not open '%s' for reading: "), pika_file_get_utf8_name (file)); } if (G_LIKELY (data_list)) { GList *list; gchar *uri; gboolean obsolete; gboolean writable = FALSE; gboolean deletable = FALSE; uri = g_file_get_uri (file); obsolete = (strstr (uri, PIKA_OBSOLETE_DATA_DIR_NAME) != 0); g_free (uri); /* obsolete files are immutable, don't check their writability */ if (! obsolete) { deletable = (g_list_length (data_list) == 1 && dir_writable); writable = (deletable && loader->writable); } for (list = data_list; list; list = g_list_next (list)) { PikaData *data = list->data; pika_data_set_file (data, file, writable, deletable); pika_data_set_mtime (data, mtime); pika_data_clean (data); if (obsolete) { pika_container_add (container_obsolete, PIKA_OBJECT (data)); } else { pika_data_set_folder_tags (data, top_directory); pika_container_add (container, PIKA_OBJECT (data)); } g_object_unref (data); } g_list_free (data_list); } /* not else { ... } because loader->load_func() can return a list * of data objects *and* an error message if loading failed after * something was already loaded */ if (G_UNLIKELY (error)) { pika_message (pika_data_factory_get_pika (factory), NULL, PIKA_MESSAGE_ERROR, _("Failed to load data:\n\n%s"), error->message); g_clear_error (&error); } } static PikaDataLoader * pika_data_loader_new (const gchar *name, PikaDataLoadFunc load_func, const gchar *extension, gboolean writable) { PikaDataLoader *loader = g_slice_new (PikaDataLoader); loader->name = g_strdup (name); loader->load_func = load_func; loader->extension = g_strdup (extension); loader->writable = writable ? TRUE : FALSE; return loader; } static void pika_data_loader_free (PikaDataLoader *loader) { g_free (loader->name); g_free (loader->extension); g_slice_free (PikaDataLoader, loader); }