/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com) * * This library is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation. * * This library 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 Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . */ /** * SECTION: e-cache * @include: libebackend/libebackend.h * @short_description: An SQLite data cache * * The #ECache is an abstract class which consists of the common * parts which can be used by its descendants. It also allows * storing offline state for the stored objects. * * The API is thread safe, with special considerations to be made * around e_cache_lock() and e_cache_unlock() for * the sake of isolating transactions across threads. **/ #include "evolution-data-server-config.h" #include #include #include #include #include #include #include #include "e-sqlite3-vfs.h" #include "e-cache.h" #define E_CACHE_KEY_VERSION "version" #define E_CACHE_KEY_REVISION "revision" /* The number of SQLite virtual machine instructions that are * evaluated at a time, the user passed GCancellable is * checked between each batch of evaluated instructions. */ #define E_CACHE_CANCEL_BATCH_SIZE 200 /* How many rows to read when e_cache_foreach_update() */ #define E_CACHE_UPDATE_BATCH_SIZE 100 struct _ECachePrivate { gchar *filename; sqlite3 *db; GRecMutex lock; /* Main API lock */ guint32 in_transaction; /* Nested transaction counter */ ECacheLockType lock_type; /* The lock type acquired for the current transaction */ GCancellable *cancellable; /* User passed GCancellable, we abort an operation if cancelled */ guint32 revision_change_frozen; gint revision_counter; gint64 last_revision_time; gboolean needs_revision_change; }; enum { BEFORE_PUT, BEFORE_REMOVE, REVISION_CHANGED, LAST_SIGNAL }; static guint signals[LAST_SIGNAL]; G_DEFINE_QUARK (e-cache-error-quark, e_cache_error) G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (ECache, e_cache, G_TYPE_OBJECT) G_DEFINE_BOXED_TYPE (ECacheColumnValues, e_cache_column_values, e_cache_column_values_copy, e_cache_column_values_free) G_DEFINE_BOXED_TYPE (ECacheOfflineChange, e_cache_offline_change, e_cache_offline_change_copy, e_cache_offline_change_free) G_DEFINE_BOXED_TYPE (ECacheColumnInfo, e_cache_column_info, e_cache_column_info_copy, e_cache_column_info_free) /** * e_cache_column_values_new: * * Creates a new #ECacheColumnValues to store values for additional columns. * The column names are compared case insensitively. * * Returns: (transfer full): a new #ECacheColumnValues. Free with e_cache_column_values_free(), * when no longer needed. * * Since: 3.26 **/ ECacheColumnValues * e_cache_column_values_new (void) { return (ECacheColumnValues *) g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, g_free); } /** * e_cache_column_values_copy: * @other_columns: (nullable): an #ECacheColumnValues * * Returns: (transfer full): Copy of the @other_columns. Free with * e_cache_column_values_free(), when no longer needed. * * Since: 3.26 **/ ECacheColumnValues * e_cache_column_values_copy (ECacheColumnValues *other_columns) { GHashTableIter iter; gpointer name, value; ECacheColumnValues *copy; if (!other_columns) return NULL; copy = e_cache_column_values_new (); e_cache_column_values_init_iter (other_columns, &iter); while (g_hash_table_iter_next (&iter, &name, &value)) { e_cache_column_values_put (copy, name, value); } return copy; } /** * e_cache_column_values_free: * @other_columns: (nullable): an #ECacheColumnValues * * Frees previously allocated @other_columns with * e_cache_column_values_new() or e_cache_column_values_copy(). * * Since: 3.26 **/ void e_cache_column_values_free (ECacheColumnValues *other_columns) { if (other_columns) g_hash_table_destroy ((GHashTable *) other_columns); } /** * e_cache_column_values_put: * @other_columns: an #ECacheColumnValues * @name: a column name * @value: (nullable): a column value * * Puts the @value for column @name. If contains a value for the same * column, then it is replaced. This creates a copy of both @name * and @value. * * Since: 3.26 **/ void e_cache_column_values_put (ECacheColumnValues *other_columns, const gchar *name, const gchar *value) { GHashTable *hash_table = (GHashTable *) other_columns; g_return_if_fail (other_columns != NULL); g_return_if_fail (name != NULL); g_hash_table_insert (hash_table, g_strdup (name), g_strdup (value)); } /** * e_cache_column_values_take_value: * @other_columns: an #ECacheColumnValues * @name: a column name * @value: (nullable) (in) (transfer full): a column value * * Puts the @value for column @name. If contains a value for the same * column, then it is replaced. This creates a copy of the @name, but * takes owner ship of the @value. * * Since: 3.26 **/ void e_cache_column_values_take_value (ECacheColumnValues *other_columns, const gchar *name, gchar *value) { GHashTable *hash_table = (GHashTable *) other_columns; g_return_if_fail (other_columns != NULL); g_return_if_fail (name != NULL); g_hash_table_insert (hash_table, g_strdup (name), value); } /** * e_cache_column_values_take: * @other_columns: an #ECacheColumnValues * @name: (in) (transfer full): a column name * @value: (nullable) (in) (transfer full): a column value * * Puts the @value for column @name. If contains a value for the same * column, then it is replaced. This creates takes ownership of both * the @name and the @value. * * Since: 3.26 **/ void e_cache_column_values_take (ECacheColumnValues *other_columns, gchar *name, gchar *value) { GHashTable *hash_table = (GHashTable *) other_columns; g_return_if_fail (other_columns != NULL); g_return_if_fail (name != NULL); g_hash_table_insert (hash_table, name, value); } /** * e_cache_column_values_contains: * @other_columns: an #ECacheColumnValues * @name: a column name * * Returns: Whether @other_columns contains column named @name. * * Since: 3.26 **/ gboolean e_cache_column_values_contains (ECacheColumnValues *other_columns, const gchar *name) { GHashTable *hash_table = (GHashTable *) other_columns; g_return_val_if_fail (other_columns != NULL, FALSE); g_return_val_if_fail (name != NULL, FALSE); return g_hash_table_contains (hash_table, name); } /** * e_cache_column_values_remove: * @other_columns: an #ECacheColumnValues * @name: a column name * * Removes value for the column named @name from @other_columns. * * Returns: Whether such column existed and had been removed. * * Since: 3.26 **/ gboolean e_cache_column_values_remove (ECacheColumnValues *other_columns, const gchar *name) { GHashTable *hash_table = (GHashTable *) other_columns; g_return_val_if_fail (other_columns != NULL, FALSE); g_return_val_if_fail (name != NULL, FALSE); return g_hash_table_remove (hash_table, name); } /** * e_cache_column_values_remove_all: * @other_columns: an #ECacheColumnValues * * Removes all values from the @other_columns, leaving it empty. * * Since: 3.26 **/ void e_cache_column_values_remove_all (ECacheColumnValues *other_columns) { GHashTable *hash_table = (GHashTable *) other_columns; g_return_if_fail (other_columns != NULL); g_hash_table_remove_all (hash_table); } /** * e_cache_column_values_lookup: * @other_columns: an #ECacheColumnValues * @name: a column name * * Looks up currently stored value for the column named @name. * As the values can be %NULL one cannot distinguish between * a column which doesn't have stored any value and a column * which has stored %NULL value. Use e_cache_column_values_contains() * to check whether such column exitst in the @other_columns. * The returned pointer is owned by @other_columns and is valid until * the value is overwritten of the @other_columns freed. * * Returns: (nullable): Stored value for the column named @name, * or %NULL, if no such column values is stored. * * Since: 3.26 **/ const gchar * e_cache_column_values_lookup (ECacheColumnValues *other_columns, const gchar *name) { GHashTable *hash_table = (GHashTable *) other_columns; g_return_val_if_fail (other_columns != NULL, NULL); g_return_val_if_fail (name != NULL, NULL); return g_hash_table_lookup (hash_table, name); } /** * e_cache_column_values_get_size: * @other_columns: an #ECacheColumnValues * * Returns: How many columns are stored in the @other_columns. * * Since: 3.26 **/ guint e_cache_column_values_get_size (ECacheColumnValues *other_columns) { GHashTable *hash_table = (GHashTable *) other_columns; g_return_val_if_fail (other_columns != NULL, 0); return g_hash_table_size (hash_table); } /** * e_cache_column_values_init_iter: * @other_columns: an #ECacheColumnValues * @iter: a #GHashTableIter * * Initialized the @iter, thus the @other_columns can be traversed * with g_hash_table_iter_next(). The key is a column name and * the value is the corresponding column value. * * Since: 3.26 **/ void e_cache_column_values_init_iter (ECacheColumnValues *other_columns, GHashTableIter *iter) { GHashTable *hash_table = (GHashTable *) other_columns; g_return_if_fail (other_columns != NULL); g_return_if_fail (iter != NULL); g_hash_table_iter_init (iter, hash_table); } /** * e_cache_offline_change_new: * @uid: a unique object identifier * @revision: (nullable): a revision of the object * @object: (nullable): object itself * @state: an #EOfflineState * * Creates a new #ECacheOfflineChange with the offline @state * information for the given @uid. * * Returns: (transfer full): A new #ECacheOfflineChange. Free it with * e_cache_offline_change_free() when no longer needed. * * Since: 3.26 **/ ECacheOfflineChange * e_cache_offline_change_new (const gchar *uid, const gchar *revision, const gchar *object, EOfflineState state) { ECacheOfflineChange *change; g_return_val_if_fail (uid != NULL, NULL); change = g_slice_new0 (ECacheOfflineChange); change->uid = g_strdup (uid); change->revision = g_strdup (revision); change->object = g_strdup (object); change->state = state; return change; } /** * e_cache_offline_change_copy: * @change: (nullable): a source #ECacheOfflineChange to copy, or %NULL * * Returns: (transfer full) (nullable): Copy of the given @change. * Free it with e_cache_offline_change_free() when no longer * needed. If the @change is %NULL, then returns %NULL as well. * * Since: 3.26 **/ ECacheOfflineChange * e_cache_offline_change_copy (const ECacheOfflineChange *change) { if (!change) return NULL; return e_cache_offline_change_new (change->uid, change->revision, change->object, change->state); } /** * e_cache_offline_change_free: * @change: (nullable): an #ECacheOfflineChange * * Frees the @change structure, previously allocated with e_cache_offline_change_new() * or e_cache_offline_change_copy(). * * Since: 3.26 **/ void e_cache_offline_change_free (gpointer change) { ECacheOfflineChange *chng = change; if (chng) { g_free (chng->uid); g_free (chng->revision); g_free (chng->object); g_slice_free (ECacheOfflineChange, chng); } } /** * e_cache_column_info_new: * @name: a column name * @type: a column type * @index_name: (nullable): an index name for this column, or %NULL * * Returns: (transfer full): A new #ECacheColumnInfo. Free it with * e_cache_column_info_free() when no longer needed. * * Since: 3.26 **/ ECacheColumnInfo * e_cache_column_info_new (const gchar *name, const gchar *type, const gchar *index_name) { ECacheColumnInfo *info; g_return_val_if_fail (name != NULL, NULL); g_return_val_if_fail (type != NULL, NULL); info = g_slice_new0 (ECacheColumnInfo); info->name = g_strdup (name); info->type = g_strdup (type); info->index_name = g_strdup (index_name); return info; } /** * e_cache_column_info_copy: * @info: (nullable): a source #ECacheColumnInfo to copy, or %NULL * * Returns: (transfer full) (nullable): Copy of the given @info. * Free it with e_cache_column_info_free() when no longer needed. * If the @info is %NULL, then returns %NULL as well. * * Since: 3.26 **/ ECacheColumnInfo * e_cache_column_info_copy (const ECacheColumnInfo *info) { if (!info) return NULL; return e_cache_column_info_new (info->name, info->type, info->index_name); } /** * e_cache_column_info_free: * @info: (nullable): an #ECacheColumnInfo * * Frees the @info structure, previously allocated with e_cache_column_info_new() * or e_cache_column_info_copy(). * * Since: 3.26 **/ void e_cache_column_info_free (gpointer info) { ECacheColumnInfo *nfo = info; if (nfo) { g_free (nfo->name); g_free (nfo->type); g_free (nfo->index_name); g_slice_free (ECacheColumnInfo, nfo); } } struct CacheSQLiteExecData { ECache *cache; ECacheSelectFunc callback; gpointer user_data; }; static gint e_cache_sqlite_exec_cb (gpointer user_data, gint ncols, gchar **column_values, gchar **column_names) { struct CacheSQLiteExecData *cse = user_data; g_return_val_if_fail (cse != NULL, SQLITE_MISUSE); g_return_val_if_fail (cse->callback != NULL, SQLITE_MISUSE); if (!cse->callback (cse->cache, ncols, (const gchar **) column_names, (const gchar **) column_values, cse->user_data)) return SQLITE_ABORT; return SQLITE_OK; } static gboolean e_cache_sqlite_exec_internal (ECache *cache, const gchar *stmt, ECacheSelectFunc callback, gpointer user_data, GCancellable *cancellable, GError **error) { struct CacheSQLiteExecData cse; GCancellable *previous_cancellable; gchar *errmsg = NULL; gint ret = -1, retries = 0; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (stmt != NULL, FALSE); g_rec_mutex_lock (&cache->priv->lock); previous_cancellable = cache->priv->cancellable; if (cancellable) cache->priv->cancellable = cancellable; cse.cache = cache; cse.callback = callback; cse.user_data = user_data; ret = sqlite3_exec (cache->priv->db, stmt, callback ? e_cache_sqlite_exec_cb : NULL, &cse, &errmsg); while (ret == SQLITE_BUSY || ret == SQLITE_LOCKED || ret == -1) { /* try for ~15 seconds, then give up */ if (retries > 150) break; retries++; g_clear_pointer (&errmsg, sqlite3_free); g_thread_yield (); g_usleep (100 * 1000); /* Sleep for 100 ms */ ret = sqlite3_exec (cache->priv->db, stmt, callback ? e_cache_sqlite_exec_cb : NULL, &cse, &errmsg); } cache->priv->cancellable = previous_cancellable; g_rec_mutex_unlock (&cache->priv->lock); if (ret != SQLITE_OK) { if (ret == SQLITE_CONSTRAINT) { g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_CONSTRAINT, errmsg); } else if (ret == SQLITE_ABORT || ret == SQLITE_INTERRUPT) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Operation cancelled: %s", errmsg); } else if (ret == SQLITE_CORRUPT && cache->priv->filename && *cache->priv->filename) { g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_CORRUPT, "%s (%s)", errmsg, cache->priv->filename); } else { gchar *valid_utf8 = e_util_utf8_make_valid (stmt); g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE, "SQLite error code '%d': %s (statement:%s)", ret, errmsg, valid_utf8 ? valid_utf8 : stmt); g_free (valid_utf8); } sqlite3_free (errmsg); return FALSE; } if (errmsg) sqlite3_free (errmsg); return TRUE; } static gboolean e_cache_sqlite_exec_printf (ECache *cache, const gchar *format, ECacheSelectFunc callback, gpointer user_data, GCancellable *cancellable, GError **error, ...) { gboolean success; va_list args; gchar *stmt; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (format != NULL, FALSE); va_start (args, error); stmt = sqlite3_vmprintf (format, args); success = e_cache_sqlite_exec_internal (cache, stmt, callback, user_data, cancellable, error); sqlite3_free (stmt); va_end (args); return success; } static gboolean e_cache_read_key_value (ECache *cache, gint ncols, const gchar **column_names, const gchar **column_values, gpointer user_data) { gchar **pvalue = user_data; g_return_val_if_fail (ncols == 1, FALSE); g_return_val_if_fail (column_names != NULL, FALSE); g_return_val_if_fail (column_values != NULL, FALSE); g_return_val_if_fail (pvalue != NULL, FALSE); if (!*pvalue) *pvalue = g_strdup (column_values[0]); return TRUE; } static gchar * e_cache_build_user_key (const gchar *key) { return g_strconcat ("user::", key, NULL); } static gboolean e_cache_set_key_internal (ECache *cache, gboolean is_user_key, const gchar *key, const gchar *value, GError **error) { gchar *tmp = NULL; const gchar *usekey; gboolean success; if (is_user_key) { tmp = e_cache_build_user_key (key); usekey = tmp; } else { usekey = key; } if (value) { success = e_cache_sqlite_exec_printf (cache, "INSERT or REPLACE INTO " E_CACHE_TABLE_KEYS " (key, value) VALUES (%Q, %Q)", NULL, NULL, NULL, error, usekey, value); } else { success = e_cache_sqlite_exec_printf (cache, "DELETE FROM " E_CACHE_TABLE_KEYS " WHERE key = %Q", NULL, NULL, NULL, error, usekey); } g_free (tmp); return success; } static gchar * e_cache_dup_key_internal (ECache *cache, gboolean is_user_key, const gchar *key, GError **error) { gchar *tmp = NULL; const gchar *usekey; gchar *value = NULL; if (is_user_key) { tmp = e_cache_build_user_key (key); usekey = tmp; } else { usekey = key; } if (!e_cache_sqlite_exec_printf (cache, "SELECT value FROM " E_CACHE_TABLE_KEYS " WHERE key = %Q", e_cache_read_key_value, &value, NULL, error, usekey)) { g_warn_if_fail (value == NULL); } g_free (tmp); return value; } static gint e_cache_check_cancelled_cb (gpointer user_data) { ECache *cache = user_data; /* Do not use E_IS_CACHE() here, for performance reasons */ g_return_val_if_fail (cache != NULL, SQLITE_ABORT); if (cache->priv->cancellable && g_cancellable_is_cancelled (cache->priv->cancellable)) { return SQLITE_ABORT; } return SQLITE_OK; } static gboolean e_cache_init_sqlite (ECache *cache, const gchar *filename, GCancellable *cancellable, GError **error) { gint ret; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (filename != NULL, FALSE); g_return_val_if_fail (cache->priv->filename == NULL, FALSE); cache->priv->filename = g_strdup (filename); ret = sqlite3_open (filename, &cache->priv->db); if (ret != SQLITE_OK) { if (!cache->priv->db) { g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_LOAD, _("Out of memory")); } else { const gchar *errmsg = sqlite3_errmsg (cache->priv->db); g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE, _("Can’t open database %s: %s"), filename, errmsg); sqlite3_close (cache->priv->db); cache->priv->db = NULL; } return FALSE; } /* Handle GCancellable */ sqlite3_progress_handler ( cache->priv->db, E_CACHE_CANCEL_BATCH_SIZE, e_cache_check_cancelled_cb, cache); return e_cache_sqlite_exec_internal (cache, "ATTACH DATABASE ':memory:' AS mem", NULL, NULL, cancellable, error) && e_cache_sqlite_exec_internal (cache, "PRAGMA foreign_keys = ON", NULL, NULL, cancellable, error) && e_cache_sqlite_exec_internal (cache, "PRAGMA case_sensitive_like = ON", NULL, NULL, cancellable, error); } static gboolean e_cache_garther_column_names_cb (ECache *cache, gint ncols, const gchar *column_names[], const gchar *column_values[], gpointer user_data) { GHashTable *known_columns = user_data; gint ii; g_return_val_if_fail (known_columns != NULL, FALSE); g_return_val_if_fail (column_names != NULL, FALSE); g_return_val_if_fail (column_values != NULL, FALSE); for (ii = 0; ii < ncols; ii++) { if (column_names[ii] && camel_strcase_equal (column_names[ii], "name")) { if (column_values[ii]) g_hash_table_insert (known_columns, g_strdup (column_values[ii]), NULL); break; } } return TRUE; } static gboolean e_cache_init_tables (ECache *cache, const GSList *other_columns, GCancellable *cancellable, GError **error) { GHashTable *known_columns; GString *objects_stmt; const GSList *link; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (cache->priv->db != NULL, FALSE); if (!e_cache_sqlite_exec_internal (cache, "CREATE TABLE IF NOT EXISTS " E_CACHE_TABLE_KEYS " (" "key TEXT PRIMARY KEY," "value TEXT)", NULL, NULL, cancellable, error)) { return FALSE; } objects_stmt = g_string_new (""); g_string_append (objects_stmt, "CREATE TABLE IF NOT EXISTS " E_CACHE_TABLE_OBJECTS " (" E_CACHE_COLUMN_UID " TEXT PRIMARY KEY," E_CACHE_COLUMN_REVISION " TEXT," E_CACHE_COLUMN_OBJECT " TEXT," E_CACHE_COLUMN_STATE " INTEGER"); for (link = other_columns; link; link = g_slist_next (link)) { const ECacheColumnInfo *info = link->data; if (!info) continue; g_string_append_c (objects_stmt, ','); g_string_append (objects_stmt, info->name); g_string_append_c (objects_stmt, ' '); g_string_append (objects_stmt, info->type); } g_string_append_c (objects_stmt, ')'); if (!e_cache_sqlite_exec_internal (cache, objects_stmt->str, NULL, NULL, cancellable, error)) { g_string_free (objects_stmt, TRUE); return FALSE; } g_string_free (objects_stmt, TRUE); /* Verify that all other columns are there and remove those unused */ known_columns = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, NULL); if (!e_cache_sqlite_exec_internal (cache, "PRAGMA table_info (" E_CACHE_TABLE_OBJECTS ")", e_cache_garther_column_names_cb, known_columns, cancellable, error)) { g_string_free (objects_stmt, TRUE); return FALSE; } g_hash_table_remove (known_columns, E_CACHE_COLUMN_UID); g_hash_table_remove (known_columns, E_CACHE_COLUMN_REVISION); g_hash_table_remove (known_columns, E_CACHE_COLUMN_OBJECT); g_hash_table_remove (known_columns, E_CACHE_COLUMN_STATE); for (link = other_columns; link; link = g_slist_next (link)) { const ECacheColumnInfo *info = link->data; if (!info) continue; if (g_hash_table_remove (known_columns, info->name)) continue; if (!e_cache_sqlite_exec_printf (cache, "ALTER TABLE " E_CACHE_TABLE_OBJECTS " ADD COLUMN %Q %s", NULL, NULL, cancellable, error, info->name, info->type)) { g_hash_table_destroy (known_columns); return FALSE; } } g_hash_table_destroy (known_columns); for (link = other_columns; link; link = g_slist_next (link)) { const ECacheColumnInfo *info = link->data; if (!info || !info->index_name) continue; if (!e_cache_sqlite_exec_printf (cache, "CREATE INDEX IF NOT EXISTS %Q ON " E_CACHE_TABLE_OBJECTS " (%s)", NULL, NULL, cancellable, error, info->index_name, info->name)) { return FALSE; } } return TRUE; } /** * e_cache_initialize_sync: * @cache: an #ECache * @filename: a filename of an SQLite database to use * @other_columns: (element-type ECacheColumnInfo) (nullable): an optional * #GSList with additional columns to add to the objects table * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Initializes the @cache and opens the @filename database. * This should be called by the descendant. * * The @other_columns are added to the objects table (@E_CACHE_TABLE_OBJECTS). * Values for these columns are returned by e_cache_get() * and can be stored with e_cache_put(). * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_initialize_sync (ECache *cache, const gchar *filename, const GSList *other_columns, GCancellable *cancellable, GError **error) { gchar *dirname; gboolean success; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (cache->priv->filename == NULL, FALSE); /* Ensure existance of the directories leading up to 'filename' */ dirname = g_path_get_dirname (filename); if (g_mkdir_with_parents (dirname, 0777) < 0) { g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_LOAD, _("Can not make parent directory: %s"), g_strerror (errno)); g_free (dirname); return FALSE; } g_free (dirname); g_rec_mutex_lock (&cache->priv->lock); success = e_cache_init_sqlite (cache, filename, cancellable, error) && e_cache_init_tables (cache, other_columns, cancellable, error); g_rec_mutex_unlock (&cache->priv->lock); return success; } /** * e_cache_get_filename: * @cache: an #ECache * * Returns: a filename of the @cache, with which it had been initialized. * * Since: 3.26 **/ const gchar * e_cache_get_filename (ECache *cache) { g_return_val_if_fail (E_IS_CACHE (cache), NULL); return cache->priv->filename; } /** * e_cache_get_version: * @cache: an #ECache * * Returns: A cache data version. This is meant to be used by the descendants. * * Since: 3.26 **/ gint e_cache_get_version (ECache *cache) { gchar *value; gint version = -1; g_return_val_if_fail (E_IS_CACHE (cache), -1); value = e_cache_dup_key_internal (cache, FALSE, E_CACHE_KEY_VERSION, NULL); if (value) { version = g_ascii_strtoll (value, NULL, 10); g_free (value); } return version; } /** * e_cache_set_version: * @cache: an #ECache * @version: a cache data version to set * * Sets a cache data version. This is meant to be used by the descendants. * The @version should be greater than zero. * * Since: 3.26 **/ void e_cache_set_version (ECache *cache, gint version) { gchar *value; g_return_if_fail (E_IS_CACHE (cache)); g_return_if_fail (version > 0); value = g_strdup_printf ("%d", version); e_cache_set_key_internal (cache, FALSE, E_CACHE_KEY_VERSION, value, NULL); g_free (value); } /** * e_cache_dup_revision: * @cache: an #ECache * * Returns: (transfer full): A revision of the whole @cache. This is meant to be * used by the descendants. Free the returned pointer with g_free(), when no * longer needed. * * Since: 3.26 **/ gchar * e_cache_dup_revision (ECache *cache) { g_return_val_if_fail (E_IS_CACHE (cache), NULL); return e_cache_dup_key_internal (cache, FALSE, E_CACHE_KEY_REVISION, NULL); } /** * e_cache_set_revision: * @cache: an #ECache * @revision: (nullable): a revision to set; use %NULL to unset it * * Sets the @revision of the whole @cache. This is not meant to be * used by the descendants, because the revision is updated automatically * when needed. The descendants can listen to "revision-changed" signal. * * Since: 3.26 **/ void e_cache_set_revision (ECache *cache, const gchar *revision) { g_return_if_fail (E_IS_CACHE (cache)); e_cache_set_key_internal (cache, FALSE, E_CACHE_KEY_REVISION, revision, NULL); g_signal_emit (cache, signals[REVISION_CHANGED], 0, NULL); } /** * e_cache_change_revision: * @cache: an #ECache * * Instructs the @cache to change its revision. In case the revision * change is frozen with e_cache_freeze_revision_change() it notes to * change the revision once the revision change is fully thaw. * * Since: 3.26 **/ void e_cache_change_revision (ECache *cache) { g_return_if_fail (E_IS_CACHE (cache)); g_rec_mutex_lock (&cache->priv->lock); if (e_cache_is_revision_change_frozen (cache)) { cache->priv->needs_revision_change = TRUE; } else { gchar time_string[100] = { 0 }; const struct tm *tm = NULL; time_t t; gint64 revision_time; gchar *revision; revision_time = g_get_real_time () / (1000 * 1000); t = (time_t) revision_time; if (revision_time != cache->priv->last_revision_time) { cache->priv->revision_counter = 0; cache->priv->last_revision_time = revision_time; } tm = gmtime (&t); if (tm) strftime (time_string, 100, "%Y-%m-%dT%H:%M:%SZ", tm); revision = g_strdup_printf ("%s(%d)", time_string, cache->priv->revision_counter++); e_cache_set_revision (cache, revision); g_free (revision); } g_rec_mutex_unlock (&cache->priv->lock); } /** * e_cache_freeze_revision_change: * @cache: an #ECache * * Freezes automatic revision change for the @cache. The function * can be called multiple times, but each such call requires its * pair function e_cache_thaw_revision_change() call. See also * e_cache_change_revision(). * * Since: 3.26 **/ void e_cache_freeze_revision_change (ECache *cache) { g_return_if_fail (E_IS_CACHE (cache)); g_rec_mutex_lock (&cache->priv->lock); cache->priv->revision_change_frozen++; g_warn_if_fail (cache->priv->revision_change_frozen != 0); g_rec_mutex_unlock (&cache->priv->lock); } /** * e_cache_thaw_revision_change: * @cache: an #ECache * * Thaws automatic revision change for the @cache. It's the pair * function of e_cache_freeze_revision_change(). * * Since: 3.26 **/ void e_cache_thaw_revision_change (ECache *cache) { g_return_if_fail (E_IS_CACHE (cache)); g_rec_mutex_lock (&cache->priv->lock); if (!cache->priv->revision_change_frozen) { g_warn_if_fail (cache->priv->revision_change_frozen > 0); } else { cache->priv->revision_change_frozen--; if (!cache->priv->revision_change_frozen && cache->priv->needs_revision_change) { cache->priv->needs_revision_change = FALSE; e_cache_change_revision (cache); } } g_rec_mutex_unlock (&cache->priv->lock); } /** * e_cache_is_revision_change_frozen: * @cache: an #ECache * * Returns: Whether automatic revision change for the @cache * is currently frozen. * * Since: 3.26 **/ gboolean e_cache_is_revision_change_frozen (ECache *cache) { gboolean frozen; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_rec_mutex_lock (&cache->priv->lock); frozen = cache->priv->revision_change_frozen > 0; g_rec_mutex_unlock (&cache->priv->lock); return frozen; } /** * e_cache_erase: * @cache: an #ECache * * Erases the cache and all of its content from the disk. * The only valid operation after this is to free the @cache. * * Since: 3.26 **/ void e_cache_erase (ECache *cache) { ECacheClass *klass; g_return_if_fail (E_IS_CACHE (cache)); if (!cache->priv->db) return; klass = E_CACHE_GET_CLASS (cache); g_return_if_fail (klass != NULL); if (klass->erase) klass->erase (cache); sqlite3_close (cache->priv->db); cache->priv->db = NULL; g_unlink (cache->priv->filename); g_free (cache->priv->filename); cache->priv->filename = NULL; } static gboolean e_cache_count_rows_cb (ECache *cache, gint ncols, const gchar **column_names, const gchar **column_values, gpointer user_data) { guint *pnrows = user_data; g_return_val_if_fail (pnrows != NULL, FALSE); *pnrows = (*pnrows) + 1; return TRUE; } /** * e_cache_contains: * @cache: an #ECache * @uid: a unique identifier of an object * @deleted_flag: one of #ECacheDeletedFlag enum * * Checkes whether the @cache contains an object with * the given @uid. * * Returns: Whether the object had been found. * * Since: 3.26 **/ gboolean e_cache_contains (ECache *cache, const gchar *uid, ECacheDeletedFlag deleted_flag) { guint nrows = 0; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (uid != NULL, FALSE); if (deleted_flag == E_CACHE_INCLUDE_DELETED) { e_cache_sqlite_exec_printf (cache, "SELECT " E_CACHE_COLUMN_UID " FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_UID " = %Q" " LIMIT 2", e_cache_count_rows_cb, &nrows, NULL, NULL, uid); } else { e_cache_sqlite_exec_printf (cache, "SELECT " E_CACHE_COLUMN_UID " FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_UID " = %Q AND " E_CACHE_COLUMN_STATE " != %d" " LIMIT 2", e_cache_count_rows_cb, &nrows, NULL, NULL, uid, E_OFFLINE_STATE_LOCALLY_DELETED); } g_warn_if_fail (nrows <= 1); return nrows > 0; } struct GetObjectData { gchar *object; gchar **out_revision; ECacheColumnValues **out_other_columns; }; static gboolean e_cache_get_object_cb (ECache *cache, gint ncols, const gchar **column_names, const gchar **column_values, gpointer user_data) { struct GetObjectData *gd = user_data; gint ii; g_return_val_if_fail (gd != NULL, FALSE); g_return_val_if_fail (column_names != NULL, FALSE); g_return_val_if_fail (column_values != NULL, FALSE); for (ii = 0; ii < ncols; ii++) { if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) == 0 || g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_STATE) == 0) { /* Skip these two */ } else if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_REVISION) == 0) { if (gd->out_revision) *gd->out_revision = g_strdup (column_values[ii]); } else if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_OBJECT) == 0) { gd->object = g_strdup (column_values[ii]); } else if (gd->out_other_columns) { if (!*gd->out_other_columns) *gd->out_other_columns = e_cache_column_values_new (); e_cache_column_values_put (*gd->out_other_columns, column_names[ii], column_values[ii]); } else if (gd->object && (!gd->out_revision || *gd->out_revision)) { /* Short-break the cycle when the other columns are not requested and the object/revision values were already read. */ break; } } return TRUE; } static gchar * e_cache_get_object_internal (ECache *cache, gboolean include_deleted, const gchar *uid, gchar **out_revision, ECacheColumnValues **out_other_columns, GCancellable *cancellable, GError **error) { struct GetObjectData gd; gboolean success; g_return_val_if_fail (E_IS_CACHE (cache), NULL); g_return_val_if_fail (uid != NULL, NULL); if (out_revision) *out_revision = NULL; if (out_other_columns) *out_other_columns = NULL; gd.object = NULL; gd.out_revision = out_revision; gd.out_other_columns = out_other_columns; if (include_deleted) { success = e_cache_sqlite_exec_printf (cache, "SELECT * FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_UID " = %Q", e_cache_get_object_cb, &gd, cancellable, error, uid); } else { success = e_cache_sqlite_exec_printf (cache, "SELECT * FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_UID " = %Q AND " E_CACHE_COLUMN_STATE " != %d", e_cache_get_object_cb, &gd, cancellable, error, uid, E_OFFLINE_STATE_LOCALLY_DELETED); } if (success && !gd.object) g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid); return gd.object; } /** * e_cache_get: * @cache: an #ECache * @uid: a unique identifier of an object * @out_revision: (out) (nullable) (transfer full): an out variable for a revision * of the object, or %NULL to ignore * @out_other_columns: (out) (nullable) (transfer full): an out * variable for #ECacheColumnValues other columns, as defined when creating the @cache, or %NULL to ignore * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Returns an object with the given @uid. This function does not consider locally * deleted objects. The @out_revision is set to the object revision, if not %NULL. * Free it with g_free() when no longer needed. Similarly the @out_other_columns * contains a column name to column value strings for additional columns which had * been requested when calling e_cache_initialize_sync(), if not %NULL. * Free the returned #ECacheColumnValues with e_cache_column_values_free(), when * no longer needed. * * Returns: (nullable) (transfer full): An object with the given @uid. Free it * with g_free(), when no longer needed. Returns %NULL on error, like when * the object could not be found. * * Since: 3.26 **/ gchar * e_cache_get (ECache *cache, const gchar *uid, gchar **out_revision, ECacheColumnValues **out_other_columns, GCancellable *cancellable, GError **error) { g_return_val_if_fail (E_IS_CACHE (cache), NULL); g_return_val_if_fail (uid != NULL, NULL); return e_cache_get_object_internal (cache, FALSE, uid, out_revision, out_other_columns, cancellable, error); } /** * e_cache_get_object_include_deleted: * @cache: an #ECache * @uid: a unique identifier of an object * @out_revision: (out) (nullable) (transfer full): an out variable for a revision * of the object, or %NULL to ignore * @out_other_columns: (out) (nullable) (transfer full): an out * variable for #ECacheColumnValues other columns, as defined when creating the @cache, or %NULL to ignore * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * The same as e_cache_get(), only considers also locally deleted objects. * * Returns: (nullable) (transfer full): An object with the given @uid. Free it * with g_free(), when no longer needed. Returns %NULL on error, like when * the object could not be found. * * Since: 3.30 **/ gchar * e_cache_get_object_include_deleted (ECache *cache, const gchar *uid, gchar **out_revision, ECacheColumnValues **out_other_columns, GCancellable *cancellable, GError **error) { g_return_val_if_fail (E_IS_CACHE (cache), NULL); g_return_val_if_fail (uid != NULL, NULL); return e_cache_get_object_internal (cache, TRUE, uid, out_revision, out_other_columns, cancellable, error); } static gboolean e_cache_put_locked (ECache *cache, const gchar *uid, const gchar *revision, const gchar *object, ECacheColumnValues *other_columns, EOfflineState offline_state, gboolean is_replace, GCancellable *cancellable, GError **error) { ECacheColumnValues *my_other_columns = NULL; gboolean success = TRUE; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (uid != NULL, FALSE); g_return_val_if_fail (object != NULL, FALSE); if (!other_columns) { my_other_columns = e_cache_column_values_new (); other_columns = my_other_columns; } g_signal_emit (cache, signals[BEFORE_PUT], 0, uid, revision, object, other_columns, is_replace, cancellable, error, &success); if (success) { ECacheClass *klass; klass = E_CACHE_GET_CLASS (cache); g_return_val_if_fail (klass != NULL, FALSE); g_return_val_if_fail (klass->put_locked != NULL, FALSE); success = klass->put_locked (cache, uid, revision, object, other_columns, offline_state, is_replace, cancellable, error); if (success) e_cache_change_revision (cache); } e_cache_column_values_free (my_other_columns); return success; } /** * e_cache_put: * @cache: an #ECache * @uid: a unique identifier of an object * @revision: (nullable): a revision of the object * @object: the object itself * @other_columns: (nullable): an #ECacheColumnValues with other columns to set; can be %NULL * @offline_flag: one of #ECacheOfflineFlag, whether putting this object in offline * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Stores an object into the cache. Depending on @offline_flag, this update * the object's offline state accordingly. When the @offline_flag is set * to %E_CACHE_IS_ONLINE, then it's set to #E_OFFLINE_STATE_SYNCED, like * to be fully synchronized with the server, regardless of its previous * offline state. Overwriting locally deleted object behaves like an addition * of a completely new object. * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_put (ECache *cache, const gchar *uid, const gchar *revision, const gchar *object, ECacheColumnValues *other_columns, ECacheOfflineFlag offline_flag, GCancellable *cancellable, GError **error) { EOfflineState offline_state; gboolean success = TRUE, is_replace; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (uid != NULL, FALSE); g_return_val_if_fail (object != NULL, FALSE); e_cache_lock (cache, E_CACHE_LOCK_WRITE); if (offline_flag == E_CACHE_IS_ONLINE) { is_replace = e_cache_contains (cache, uid, E_CACHE_EXCLUDE_DELETED); offline_state = E_OFFLINE_STATE_SYNCED; } else { is_replace = e_cache_contains (cache, uid, E_CACHE_INCLUDE_DELETED); if (is_replace) { GError *local_error = NULL; offline_state = e_cache_get_offline_state (cache, uid, cancellable, &local_error); if (local_error) { success = FALSE; g_propagate_error (error, local_error); } else if (offline_state != E_OFFLINE_STATE_LOCALLY_CREATED) { offline_state = E_OFFLINE_STATE_LOCALLY_MODIFIED; } } else { offline_state = E_OFFLINE_STATE_LOCALLY_CREATED; } } success = success && e_cache_put_locked (cache, uid, revision, object, other_columns, offline_state, is_replace, cancellable, error); e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK); return success; } /** * e_cache_remove: * @cache: an #ECache * @uid: a unique identifier of an object * @offline_flag: one of #ECacheOfflineFlag, whether removing the object in offline * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Removes the object with the given @uid from the @cache. Based on the @offline_flag, * it can remove also any information about locally made offline changes. Removing * the object with %E_CACHE_IS_OFFLINE will still remember it for later use * with e_cache_get_offline_changes(). * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_remove (ECache *cache, const gchar *uid, ECacheOfflineFlag offline_flag, GCancellable *cancellable, GError **error) { ECacheClass *klass; gboolean success = TRUE; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (uid != NULL, FALSE); klass = E_CACHE_GET_CLASS (cache); g_return_val_if_fail (klass != NULL, FALSE); g_return_val_if_fail (klass->remove_locked != NULL, FALSE); e_cache_lock (cache, E_CACHE_LOCK_WRITE); if (offline_flag == E_CACHE_IS_ONLINE) { success = klass->remove_locked (cache, uid, cancellable, error); } else { EOfflineState offline_state; offline_state = e_cache_get_offline_state (cache, uid, cancellable, error); if (offline_state == E_OFFLINE_STATE_UNKNOWN) { success = FALSE; } else if (offline_state == E_OFFLINE_STATE_LOCALLY_CREATED) { success = klass->remove_locked (cache, uid, cancellable, error); } else { g_signal_emit (cache, signals[BEFORE_REMOVE], 0, uid, cancellable, error, &success); if (success) { success = e_cache_set_offline_state (cache, uid, E_OFFLINE_STATE_LOCALLY_DELETED, cancellable, error); } } } if (success) e_cache_change_revision (cache); e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK); return success; } /** * e_cache_remove_all: * @cache: an #ECache * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Removes all objects from the @cache in one call. * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_remove_all (ECache *cache, GCancellable *cancellable, GError **error) { ECacheClass *klass; GSList *uids = NULL; gboolean success; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); klass = E_CACHE_GET_CLASS (cache); g_return_val_if_fail (klass != NULL, FALSE); g_return_val_if_fail (klass->remove_all_locked != NULL, FALSE); e_cache_lock (cache, E_CACHE_LOCK_WRITE); success = e_cache_get_uids (cache, E_CACHE_INCLUDE_DELETED, &uids, NULL, cancellable, error); if (success && uids) success = klass->remove_all_locked (cache, uids, cancellable, error); if (success) { e_cache_sqlite_maybe_vacuum (cache, cancellable, NULL); e_cache_change_revision (cache); } e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK); g_slist_free_full (uids, g_free); return success; } static gboolean e_cache_get_uint64_cb (ECache *cache, gint ncols, const gchar **column_names, const gchar **column_values, gpointer user_data) { guint64 *pui64 = user_data; g_return_val_if_fail (pui64 != NULL, FALSE); if (ncols == 1) { *pui64 = column_values[0] ? g_ascii_strtoull (column_values[0], NULL, 10) : 0; } else { *pui64 = 0; } return TRUE; } static gboolean e_cache_get_int64_cb (ECache *cache, gint ncols, const gchar **column_names, const gchar **column_values, gpointer user_data) { gint64 *pi64 = user_data; g_return_val_if_fail (pi64 != NULL, FALSE); if (ncols == 1) { *pi64 = column_values[0] ? g_ascii_strtoll (column_values[0], NULL, 10) : 0; } else { *pi64 = 0; } return TRUE; } /** * e_cache_get_count: * @cache: an #ECache * @deleted_flag: one of #ECacheDeletedFlag enum * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Returns: Count of objects stored in the @cache. * * Since: 3.26 **/ guint e_cache_get_count (ECache *cache, ECacheDeletedFlag deleted_flag, GCancellable *cancellable, GError **error) { guint64 nobjects = 0; g_return_val_if_fail (E_IS_CACHE (cache), 0); if (deleted_flag == E_CACHE_INCLUDE_DELETED) { e_cache_sqlite_exec_printf (cache, "SELECT COUNT(*) FROM " E_CACHE_TABLE_OBJECTS, e_cache_get_uint64_cb, &nobjects, cancellable, error); } else { e_cache_sqlite_exec_printf (cache, "SELECT COUNT(*) FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_STATE " != %d", e_cache_get_uint64_cb, &nobjects, NULL, NULL, E_OFFLINE_STATE_LOCALLY_DELETED); } return nobjects; } struct GatherRowsData { GSList **out_uids; GSList **out_revisions; GSList **out_objects; }; static gboolean e_cache_gather_rows_data_cb (ECache *cache, const gchar *uid, const gchar *revision, const gchar *object, EOfflineState offline_state, gint ncols, const gchar *column_names[], const gchar *column_values[], gpointer user_data) { struct GatherRowsData *gd = user_data; g_return_val_if_fail (gd != NULL, FALSE); if (gd->out_uids) *gd->out_uids = g_slist_prepend (*gd->out_uids, g_strdup (uid)); if (gd->out_revisions) *gd->out_revisions = g_slist_prepend (*gd->out_revisions, g_strdup (revision)); if (gd->out_objects) *gd->out_objects = g_slist_prepend (*gd->out_objects, g_strdup (object)); return TRUE; } /** * e_cache_get_uids: * @cache: an #ECache * @deleted_flag: one of #ECacheDeletedFlag enum * @out_uids: (out) (transfer full) (element-type utf8): a pointer to #GSList to store the found uid to * @out_revisions: (out) (transfer full) (element-type utf8) (nullable): a pointer to #GSList to store * the found revisions to, or %NULL * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Gets a list of unique object identifiers stored in the @cache, optionally * together with their revisions. The uids are not returned in any particular * order, but the position between @out_uids and @out_revisions matches * the same object. * * Both @out_uids and @out_revisions contain newly allocated #GSList, which * should be freed with g_slist_free_full (slist, g_free); when no longer needed. * * Returns: Whether succeeded. It doesn't necessarily mean that there was * any object stored in the @cache. * * Since: 3.26 **/ gboolean e_cache_get_uids (ECache *cache, ECacheDeletedFlag deleted_flag, GSList **out_uids, GSList **out_revisions, GCancellable *cancellable, GError **error) { struct GatherRowsData gr; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (out_uids, FALSE); gr.out_uids = out_uids; gr.out_revisions = out_revisions; gr.out_objects = NULL; return e_cache_foreach (cache, deleted_flag, NULL, e_cache_gather_rows_data_cb, &gr, cancellable, error); } /** * e_cache_get_objects: * @cache: an #ECache * @deleted_flag: one of #ECacheDeletedFlag enum * @out_objects: (out) (transfer full) (element-type utf8): a pointer to #GSList to store the found objects to * @out_revisions: (out) (transfer full) (element-type utf8) (nullable): a pointer to #GSList to store * the found revisions to, or %NULL * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Gets a list of objects stored in the @cache, optionally together with * their revisions. The uids are not returned in any particular order, * but the position between @out_objects and @out_revisions matches * the same object. * * Both @out_objects and @out_revisions contain newly allocated #GSList, which * should be freed with g_slist_free_full (slist, g_free); when no longer needed. * * Returns: Whether succeeded. It doesn't necessarily mean that there was * any object stored in the @cache. * * Since: 3.26 **/ gboolean e_cache_get_objects (ECache *cache, ECacheDeletedFlag deleted_flag, GSList **out_objects, GSList **out_revisions, GCancellable *cancellable, GError **error) { struct GatherRowsData gr; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (out_objects, FALSE); gr.out_uids = NULL; gr.out_revisions = out_revisions; gr.out_objects = out_objects; return e_cache_foreach (cache, deleted_flag, NULL, e_cache_gather_rows_data_cb, &gr, cancellable, error); } struct ForeachData { gint uid_index; gint revision_index; gint object_index; gint state_index; ECacheForeachFunc func; gpointer user_data; }; static gboolean e_cache_foreach_cb (ECache *cache, gint ncols, const gchar *column_names[], const gchar *column_values[], gpointer user_data) { struct ForeachData *fe = user_data; EOfflineState offline_state; g_return_val_if_fail (fe != NULL, FALSE); g_return_val_if_fail (fe->func != NULL, FALSE); g_return_val_if_fail (column_names != NULL, FALSE); g_return_val_if_fail (column_values != NULL, FALSE); if (fe->uid_index == -1 || fe->revision_index == -1 || fe->object_index == -1 || fe->state_index == -1) { gint ii; for (ii = 0; ii < ncols && (fe->uid_index == -1 || fe->revision_index == -1 || fe->object_index == -1 || fe->state_index == -1); ii++) { if (!column_names[ii]) continue; if (fe->uid_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) == 0) { fe->uid_index = ii; } else if (fe->revision_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_REVISION) == 0) { fe->revision_index = ii; } else if (fe->object_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_OBJECT) == 0) { fe->object_index = ii; } else if (fe->state_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_STATE) == 0) { fe->state_index = ii; } } } g_return_val_if_fail (fe->uid_index >= 0 && fe->uid_index < ncols, FALSE); g_return_val_if_fail (fe->revision_index >= 0 && fe->revision_index < ncols, FALSE); g_return_val_if_fail (fe->object_index >= 0 && fe->object_index < ncols, FALSE); g_return_val_if_fail (fe->state_index >= 0 && fe->state_index < ncols, FALSE); if (!column_values[fe->state_index]) offline_state = E_OFFLINE_STATE_UNKNOWN; else offline_state = g_ascii_strtoull (column_values[fe->state_index], NULL, 10); return fe->func (cache, column_values[fe->uid_index], column_values[fe->revision_index], column_values[fe->object_index], offline_state, ncols, column_names, column_values, fe->user_data); } /** * e_cache_foreach: * @cache: an #ECache * @deleted_flag: one of #ECacheDeletedFlag enum * @where_clause: (nullable): an optional SQLite WHERE clause part, or %NULL * @func: (scope call): an #ECacheForeachFunc function to call for each object * @user_data: user data for the @func * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Calls @func for each found object, which satisfies the criteria * for both @deleted_flag and @where_clause. * * Note the @func should not call any SQLite commands, because it's invoked * within a SELECT statement execution. * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_foreach (ECache *cache, ECacheDeletedFlag deleted_flag, const gchar *where_clause, ECacheForeachFunc func, gpointer user_data, GCancellable *cancellable, GError **error) { struct ForeachData fe; GString *stmt; gboolean success; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (func, FALSE); stmt = g_string_new ("SELECT * FROM " E_CACHE_TABLE_OBJECTS); if (where_clause) { g_string_append (stmt, " WHERE "); if (deleted_flag == E_CACHE_INCLUDE_DELETED) { g_string_append (stmt, where_clause); } else { g_string_append_printf (stmt, E_CACHE_COLUMN_STATE "!=%d AND (%s)", E_OFFLINE_STATE_LOCALLY_DELETED, where_clause); } } else if (deleted_flag != E_CACHE_INCLUDE_DELETED) { g_string_append_printf (stmt, " WHERE " E_CACHE_COLUMN_STATE "!=%d", E_OFFLINE_STATE_LOCALLY_DELETED); } fe.func = func; fe.user_data = user_data; fe.uid_index = -1; fe.revision_index = -1; fe.object_index = -1; fe.state_index = -1; success = e_cache_sqlite_exec_internal (cache, stmt->str, e_cache_foreach_cb, &fe, cancellable, error); g_string_free (stmt, TRUE); return success; } struct ForeachUpdateRowData { gchar *uid; gchar *revision; gchar *object; EOfflineState offline_state; gint ncols; GPtrArray *column_values; }; static void foreach_update_row_data_free (gpointer ptr) { struct ForeachUpdateRowData *fr = ptr; if (fr) { g_free (fr->uid); g_free (fr->revision); g_free (fr->object); g_ptr_array_free (fr->column_values, TRUE); g_slice_free (struct ForeachUpdateRowData, fr); } } struct ForeachUpdateData { gint uid_index; gint revision_index; gint object_index; gint state_index; GSList *rows; /* struct ForeachUpdateRowData * */ GPtrArray *column_names; }; static gboolean e_cache_foreach_update_cb (ECache *cache, gint ncols, const gchar *column_names[], const gchar *column_values[], gpointer user_data) { struct ForeachUpdateData *fu = user_data; struct ForeachUpdateRowData *rd; EOfflineState offline_state; GPtrArray *cnames, *cvalues; gint ii; g_return_val_if_fail (fu != NULL, FALSE); g_return_val_if_fail (column_names != NULL, FALSE); g_return_val_if_fail (column_values != NULL, FALSE); if (fu->uid_index == -1 || fu->revision_index == -1 || fu->object_index == -1 || fu->state_index == -1) { gint ii; for (ii = 0; ii < ncols && (fu->uid_index == -1 || fu->revision_index == -1 || fu->object_index == -1 || fu->state_index == -1); ii++) { if (!column_names[ii]) continue; if (fu->uid_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) == 0) { fu->uid_index = ii; } else if (fu->revision_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_REVISION) == 0) { fu->revision_index = ii; } else if (fu->object_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_OBJECT) == 0) { fu->object_index = ii; } else if (fu->state_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_STATE) == 0) { fu->state_index = ii; } } } g_return_val_if_fail (fu->uid_index >= 0 && fu->uid_index < ncols, FALSE); g_return_val_if_fail (fu->revision_index >= 0 && fu->revision_index < ncols, FALSE); g_return_val_if_fail (fu->object_index >= 0 && fu->object_index < ncols, FALSE); g_return_val_if_fail (fu->state_index >= 0 && fu->state_index < ncols, FALSE); if (!column_values[fu->state_index]) offline_state = E_OFFLINE_STATE_UNKNOWN; else offline_state = g_ascii_strtoull (column_values[fu->state_index], NULL, 10); cnames = fu->column_names ? NULL : g_ptr_array_new_full (ncols, g_free); cvalues = g_ptr_array_new_full (ncols, g_free); for (ii = 0; ii < ncols; ii++) { if (fu->uid_index == ii || fu->revision_index == ii || fu->object_index == ii || fu->state_index == ii) { continue; } if (cnames) g_ptr_array_add (cnames, g_strdup (column_names[ii])); g_ptr_array_add (cvalues, g_strdup (column_values[ii])); } rd = g_slice_new0 (struct ForeachUpdateRowData); rd->uid = g_strdup (column_values[fu->uid_index]); rd->revision = g_strdup (column_values[fu->revision_index]); rd->object = g_strdup (column_values[fu->object_index]); rd->offline_state = offline_state; rd->ncols = cvalues->len; rd->column_values = cvalues; if (cnames) fu->column_names = cnames; fu->rows = g_slist_prepend (fu->rows, rd); g_return_val_if_fail (fu->column_names && (gint) fu->column_names->len == rd->ncols, FALSE); return TRUE; } /** * e_cache_foreach_update: * @cache: an #ECache * @deleted_flag: one of #ECacheDeletedFlag enum * @where_clause: (nullable): an optional SQLite WHERE clause part, or %NULL * @func: (scope call): an #ECacheUpdateFunc function to call for each object * @user_data: user data for the @func * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Calls @func for each found object, which satisfies the criteria for both * @deleted_flag and @where_clause, letting the caller update values where * necessary. The return value of @func is used to determine whether the call * was successful, not whether there are any changes to be saved. If anything * fails during the call then the all changes are reverted. * * When there are requested any changes by the @func, this function also * calls e_cache_copy_missing_to_column_values() to ensure no descendant * column data is lost. * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_foreach_update (ECache *cache, ECacheDeletedFlag deleted_flag, const gchar *where_clause, ECacheUpdateFunc func, gpointer user_data, GCancellable *cancellable, GError **error) { GString *stmt_begin; gchar *uid = NULL; gint n_results; gboolean has_where = TRUE; gboolean success; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (func, FALSE); e_cache_lock (cache, E_CACHE_LOCK_WRITE); stmt_begin = g_string_new ("SELECT * FROM " E_CACHE_TABLE_OBJECTS); if (where_clause) { g_string_append (stmt_begin, " WHERE "); if (deleted_flag == E_CACHE_INCLUDE_DELETED) { g_string_append (stmt_begin, where_clause); } else { g_string_append_printf (stmt_begin, E_CACHE_COLUMN_STATE "!=%d AND (%s)", E_OFFLINE_STATE_LOCALLY_DELETED, where_clause); } } else if (deleted_flag != E_CACHE_INCLUDE_DELETED) { g_string_append_printf (stmt_begin, " WHERE " E_CACHE_COLUMN_STATE "!=%d", E_OFFLINE_STATE_LOCALLY_DELETED); } else { has_where = FALSE; } do { GString *stmt; GSList *link; struct ForeachUpdateData fu; fu.uid_index = -1; fu.revision_index = -1; fu.object_index = -1; fu.state_index = -1; fu.rows = NULL; fu.column_names = NULL; stmt = g_string_new (stmt_begin->str); if (uid) { if (has_where) g_string_append (stmt, " AND "); else g_string_append (stmt, " WHERE "); e_cache_sqlite_stmt_append_printf (stmt, E_CACHE_COLUMN_UID ">%Q", uid); } g_string_append_printf (stmt, " ORDER BY " E_CACHE_COLUMN_UID " ASC LIMIT %d", E_CACHE_UPDATE_BATCH_SIZE); success = e_cache_sqlite_exec_internal (cache, stmt->str, e_cache_foreach_update_cb, &fu, cancellable, error); g_string_free (stmt, TRUE); if (success) { n_results = 0; fu.rows = g_slist_reverse (fu.rows); for (link = fu.rows; success && link; link = g_slist_next (link), n_results++) { struct ForeachUpdateRowData *fr = link->data; success = fr && fr->column_values && fu.column_names; if (success) { gchar *new_revision = NULL; gchar *new_object = NULL; EOfflineState new_offline_state = fr->offline_state; ECacheColumnValues *new_other_columns = NULL; success = func (cache, fr->uid, fr->revision, fr->object, fr->offline_state, fr->ncols, (const gchar **) fu.column_names->pdata, (const gchar **) fr->column_values->pdata, &new_revision, &new_object, &new_offline_state, &new_other_columns, user_data); if (success && ( (new_revision && g_strcmp0 (new_revision, fr->revision) != 0) || (new_object && g_strcmp0 (new_object, fr->object) != 0) || (new_offline_state != fr->offline_state) || (new_other_columns && e_cache_column_values_get_size (new_other_columns) > 0))) { if (!new_other_columns) new_other_columns = e_cache_column_values_new (); e_cache_copy_missing_to_column_values (cache, fr->ncols, (const gchar **) fu.column_names->pdata, (const gchar **) fr->column_values->pdata, new_other_columns); success = e_cache_put_locked (cache, fr->uid, new_revision ? new_revision : fr->revision, new_object ? new_object : fr->object, new_other_columns, new_offline_state, TRUE, cancellable, error); } g_free (new_revision); g_free (new_object); e_cache_column_values_free (new_other_columns); if (!g_slist_next (link)) { g_free (uid); uid = g_strdup (fr->uid); } } } } g_slist_free_full (fu.rows, foreach_update_row_data_free); if (fu.column_names) g_ptr_array_free (fu.column_names, TRUE); } while (success && n_results == E_CACHE_UPDATE_BATCH_SIZE); g_string_free (stmt_begin, TRUE); g_free (uid); e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK); return success; } /** * e_cache_copy_missing_to_column_values: * @cache: an #ECache * @ncols: count of columns, items in column_names and column_values * @column_names: (array length=ncols) (element-type utf8): column names * @column_values: (array length=ncols) (element-type utf8): column values * @other_columns: (inout): an #ECacheColumnValues to fill * * Adds every column value which is not part of the @other_columns to it, * except of E_CACHE_COLUMN_UID, E_CACHE_COLUMN_REVISION, E_CACHE_COLUMN_OBJECT * and E_CACHE_COLUMN_STATE columns. * * This can be used within the callback of e_cache_foreach_update(). * * Since: 3.32 **/ void e_cache_copy_missing_to_column_values (ECache *cache, gint ncols, const gchar *column_names[], const gchar *column_values[], ECacheColumnValues *other_columns) { gint ii; g_return_if_fail (E_IS_CACHE (cache)); g_return_if_fail (column_names != NULL); g_return_if_fail (column_values != NULL); g_return_if_fail (other_columns != NULL); for (ii = 0; ii < ncols; ii++) { if (column_names[ii] && column_values[ii] && !e_cache_column_values_contains (other_columns, column_names[ii]) && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) != 0 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_REVISION) != 0 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_OBJECT) != 0 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_STATE) != 0) { e_cache_column_values_put (other_columns, column_names[ii], column_values[ii]); } } } /** * e_cache_get_offline_state: * @cache: an #ECache * @uid: a unique identifier of an object * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Returns: Current offline state #EOfflineState for the given object. * It returns %E_OFFLINE_STATE_UNKNOWN when the object could not be * found or other error happened. * * Since: 3.26 **/ EOfflineState e_cache_get_offline_state (ECache *cache, const gchar *uid, GCancellable *cancellable, GError **error) { EOfflineState offline_state = E_OFFLINE_STATE_UNKNOWN; gint64 value = offline_state; g_return_val_if_fail (E_IS_CACHE (cache), E_OFFLINE_STATE_UNKNOWN); g_return_val_if_fail (uid != NULL, E_OFFLINE_STATE_UNKNOWN); if (!e_cache_contains (cache, uid, E_CACHE_INCLUDE_DELETED)) { g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid); return offline_state; } if (e_cache_sqlite_exec_printf (cache, "SELECT " E_CACHE_COLUMN_STATE " FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_UID " = %Q", e_cache_get_int64_cb, &value, cancellable, error, uid)) { offline_state = value; } return offline_state; } /** * e_cache_set_offline_state: * @cache: an #ECache * @uid: a unique identifier of an object * @state: an #EOfflineState to set * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Sets an offline @state for the object identified by @uid. * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_set_offline_state (ECache *cache, const gchar *uid, EOfflineState state, GCancellable *cancellable, GError **error) { g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (uid != NULL, FALSE); if (!e_cache_contains (cache, uid, E_CACHE_INCLUDE_DELETED)) { g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid); return FALSE; } return e_cache_sqlite_exec_printf (cache, "UPDATE " E_CACHE_TABLE_OBJECTS " SET " E_CACHE_COLUMN_STATE "=%d" " WHERE " E_CACHE_COLUMN_UID " = %Q", NULL, NULL, cancellable, error, state, uid); } static gboolean e_cache_get_offline_changes_cb (ECache *cache, const gchar *uid, const gchar *revision, const gchar *object, EOfflineState offline_state, gint ncols, const gchar *column_names[], const gchar *column_values[], gpointer user_data) { GSList **pchanges = user_data; g_return_val_if_fail (pchanges != NULL, FALSE); if (offline_state == E_OFFLINE_STATE_LOCALLY_CREATED || offline_state == E_OFFLINE_STATE_LOCALLY_MODIFIED || offline_state == E_OFFLINE_STATE_LOCALLY_DELETED) { *pchanges = g_slist_prepend (*pchanges, e_cache_offline_change_new (uid, revision, object, offline_state)); } return TRUE; } /** * e_cache_get_offline_changes: * @cache: an #ECache * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Gathers the list of all offline changes being done so far. * The returned #GSList contains #ECacheOfflineChange structure. * Use e_cache_clear_offline_changes() to clear all offline * changes at once. * * Returns: (transfer full) (element-type ECacheOfflineChange): A newly allocated list of all * offline changes. Free it with g_slist_free_full (slist, e_cache_offline_change_free); * when no longer needed. * * Since: 3.26 **/ GSList * e_cache_get_offline_changes (ECache *cache, GCancellable *cancellable, GError **error) { GSList *changes = NULL; gchar *stmt; g_return_val_if_fail (E_IS_CACHE (cache), NULL); stmt = e_cache_sqlite_stmt_printf (E_CACHE_COLUMN_STATE "!=%d", E_OFFLINE_STATE_SYNCED); if (!e_cache_foreach (cache, E_CACHE_INCLUDE_DELETED, stmt, e_cache_get_offline_changes_cb, &changes, cancellable, error)) { g_slist_free_full (changes, e_cache_offline_change_free); changes = NULL; } e_cache_sqlite_stmt_free (stmt); return changes; } /** * e_cache_clear_offline_changes: * @cache: an #ECache * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Marks all objects as being fully synchronized with the server and * removes those which are marked as locally deleted. * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_clear_offline_changes (ECache *cache, GCancellable *cancellable, GError **error) { ECacheClass *klass; gboolean success; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); klass = E_CACHE_GET_CLASS (cache); g_return_val_if_fail (klass != NULL, FALSE); g_return_val_if_fail (klass->clear_offline_changes_locked != NULL, FALSE); e_cache_lock (cache, E_CACHE_LOCK_WRITE); success = klass->clear_offline_changes_locked (cache, cancellable, error); e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK); return success; } /** * e_cache_set_key: * @cache: an #ECache * @key: a key name * @value: (nullable): a value to set, or %NULL to delete the key * @error: return location for a #GError, or %NULL * * Sets a @value of the user @key, or deletes it, if the @value is %NULL. * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_set_key (ECache *cache, const gchar *key, const gchar *value, GError **error) { g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (key != NULL, FALSE); return e_cache_set_key_internal (cache, TRUE, key, value, error); } /** * e_cache_dup_key: * @cache: an #ECache * @key: a key name * @error: return location for a #GError, or %NULL * * Returns: (transfer full): a value of the @key. Free the returned string * with g_free(), when no longer needed. * * Since: 3.26 **/ gchar * e_cache_dup_key (ECache *cache, const gchar *key, GError **error) { g_return_val_if_fail (E_IS_CACHE (cache), NULL); g_return_val_if_fail (key != NULL, NULL); return e_cache_dup_key_internal (cache, TRUE, key, error); } /** * e_cache_set_key_int: * @cache: an #ECache * @key: a key name * @value: an integer value to set * @error: return location for a #GError, or %NULL * * Sets an integer @value for the user @key. * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_set_key_int (ECache *cache, const gchar *key, gint value, GError **error) { gchar *str_value; gboolean success; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (key != NULL, FALSE); str_value = g_strdup_printf ("%d", value); success = e_cache_set_key (cache, key, str_value, error); g_free (str_value); return success; } /** * e_cache_get_key_int: * @cache: an #ECache * @key: a key name * @error: return location for a #GError, or %NULL * * Reads the user @key value as an integer. * * Returns: The user @key value or -1 on error. * * Since: 3.26 **/ gint e_cache_get_key_int (ECache *cache, const gchar *key, GError **error) { gchar *str_value; gint value; g_return_val_if_fail (E_IS_CACHE (cache), -1); str_value = e_cache_dup_key (cache, key, error); if (!str_value) return -1; value = g_ascii_strtoll (str_value, NULL, 10); g_free (str_value); return value; } /** * e_cache_lock: * @cache: an #ECache * @lock_type: an #ECacheLockType * * Locks the @cache thus other threads cannot use it. * This can be called recursively within one thread. * Each call should have its pair e_cache_unlock(). * * Since: 3.26 **/ void e_cache_lock (ECache *cache, ECacheLockType lock_type) { g_return_if_fail (E_IS_CACHE (cache)); g_rec_mutex_lock (&cache->priv->lock); cache->priv->in_transaction++; g_return_if_fail (cache->priv->in_transaction > 0); if (cache->priv->in_transaction == 1) { /* It's important to make the distinction between a * transaction which will read or one which will write. * * While it's not well documented, when receiving the SQLITE_BUSY * error status, one can only safely retry at the beginning of * the transaction. * * If a transaction is 'upgraded' to require a writer lock * half way through the transaction and SQLITE_BUSY is returned, * the whole transaction would need to be retried from the beginning. */ cache->priv->lock_type = lock_type; switch (lock_type) { case E_CACHE_LOCK_READ: e_cache_sqlite_exec_internal (cache, "BEGIN", NULL, NULL, NULL, NULL); break; case E_CACHE_LOCK_WRITE: e_cache_sqlite_exec_internal (cache, "BEGIN IMMEDIATE", NULL, NULL, NULL, NULL); break; } } else { /* Warn about cases where a read transaction might be upgraded */ if (lock_type == E_CACHE_LOCK_WRITE && cache->priv->lock_type == E_CACHE_LOCK_READ) g_warning ( "A nested transaction wants to write, " "but the outermost transaction was started " "without a writer lock."); } } /** * e_cache_unlock: * @cache: an #ECache * @action: an #ECacheUnlockAction * * Unlocks the cache which was previouly locked with e_cache_lock(). * The cache locked with #E_CACHE_LOCK_WRITE should use either * @action #E_CACHE_UNLOCK_COMMIT or #E_CACHE_UNLOCK_ROLLBACK, * while the #E_CACHE_LOCK_READ should use #E_CACHE_UNLOCK_NONE @action. * * Since: 3.26 **/ void e_cache_unlock (ECache *cache, ECacheUnlockAction action) { g_return_if_fail (E_IS_CACHE (cache)); g_return_if_fail (cache->priv->in_transaction > 0); cache->priv->in_transaction--; if (cache->priv->in_transaction == 0) { switch (action) { case E_CACHE_UNLOCK_NONE: case E_CACHE_UNLOCK_COMMIT: e_cache_sqlite_exec_internal (cache, "COMMIT", NULL, NULL, NULL, NULL); break; case E_CACHE_UNLOCK_ROLLBACK: e_cache_sqlite_exec_internal (cache, "ROLLBACK", NULL, NULL, NULL, NULL); break; } } g_rec_mutex_unlock (&cache->priv->lock); } /** * e_cache_get_sqlitedb: * @cache: an #ECache * * Returns: (transfer none): An SQLite3 database pointer. It is owned by the @cache. * * Since: 3.26 **/ gpointer e_cache_get_sqlitedb (ECache *cache) { g_return_val_if_fail (E_IS_CACHE (cache), NULL); return cache->priv->db; } /** * e_cache_sqlite_exec: * @cache: an #ECache * @sql_stmt: an SQLite statement to execute * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Executes an SQLite statement. Use e_cache_sqlite_select() for * SELECT statements. * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_sqlite_exec (ECache *cache, const gchar *sql_stmt, GCancellable *cancellable, GError **error) { g_return_val_if_fail (E_IS_CACHE (cache), FALSE); return e_cache_sqlite_exec_internal (cache, sql_stmt, NULL, NULL, cancellable, error); } /** * e_cache_sqlite_select: * @cache: an #ECache * @sql_stmt: an SQLite SELECT statement to execute * @func: (scope call): an #ECacheSelectFunc function to call for each row * @user_data: user data for @func * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Executes a SELECT statement @sql_stmt and calls @func for each row of the result. * Use e_cache_sqlite_exec() for statements which do not return row sets. * * Returns: Whether succeeded. * * Since: 3.26 **/ gboolean e_cache_sqlite_select (ECache *cache, const gchar *sql_stmt, ECacheSelectFunc func, gpointer user_data, GCancellable *cancellable, GError **error) { g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (sql_stmt, FALSE); g_return_val_if_fail (func, FALSE); return e_cache_sqlite_exec_internal (cache, sql_stmt, func, user_data, cancellable, error); } /** * e_cache_sqlite_stmt_append_printf: * @stmt: a #GString statement to append to * @format: a printf-like format * @...: arguments for the @format * * Appends an SQLite statement fragment based on the @format and * its arguments to the @stmt. * The @format can contain any values recognized by sqlite3_mprintf(). * * Since: 3.26 **/ void e_cache_sqlite_stmt_append_printf (GString *stmt, const gchar *format, ...) { va_list args; gchar *tmp_stmt; g_return_if_fail (stmt != NULL); g_return_if_fail (format != NULL); va_start (args, format); tmp_stmt = sqlite3_vmprintf (format, args); va_end (args); g_string_append (stmt, tmp_stmt); sqlite3_free (tmp_stmt); } /** * e_cache_sqlite_stmt_printf: * @format: a printf-like format * @...: arguments for the @format * * Creates an SQLite statement based on the @format and its arguments. * The @format can contain any values recognized by sqlite3_mprintf(). * * Returns: (transfer full): A new SQLite statement. Free the returned * string with e_cache_sqlite_stmt_free() when no longer needed. * * Since: 3.26 **/ gchar * e_cache_sqlite_stmt_printf (const gchar *format, ...) { va_list args; gchar *stmt; g_return_val_if_fail (format != NULL, NULL); va_start (args, format); stmt = sqlite3_vmprintf (format, args); va_end (args); return stmt; } /** * e_cache_sqlite_stmt_free: * @stmt: a statement to free * * Frees a statement previously constructed with e_cache_sqlite_stmt_printf(). * * Since: 3.26 **/ void e_cache_sqlite_stmt_free (gchar *stmt) { if (stmt) sqlite3_free (stmt); } /** * e_cache_sqlite_maybe_vacuum: * @cache: an #ECache * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Runs vacuum (compacts the database file), if needed. * * Returns: Whether succeeded. It doesn't mean that the vacuum had been run, * only that no error happened during the call. * * Since: 3.26 **/ gboolean e_cache_sqlite_maybe_vacuum (ECache *cache, GCancellable *cancellable, GError **error) { guint64 page_count = 0, page_size = 0, freelist_count = 0; gboolean success = FALSE; GError *local_error = NULL; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_rec_mutex_lock (&cache->priv->lock); if (e_cache_sqlite_exec_internal (cache, "PRAGMA page_count;", e_cache_get_uint64_cb, &page_count, cancellable, &local_error) && e_cache_sqlite_exec_internal (cache, "PRAGMA page_size;", e_cache_get_uint64_cb, &page_size, cancellable, &local_error) && e_cache_sqlite_exec_internal (cache, "PRAGMA freelist_count;", e_cache_get_uint64_cb, &freelist_count, cancellable, &local_error)) { /* Vacuum, if there's more than 5% of the free pages, or when free pages use more than 10MB */ success = !page_count || !freelist_count || (freelist_count * page_size < 1024 * 1024 * 10 && freelist_count * 1000 / page_count <= 50) || e_cache_sqlite_exec_internal (cache, "vacuum;", NULL, NULL, cancellable, &local_error); } g_rec_mutex_unlock (&cache->priv->lock); if (local_error) { g_propagate_error (error, local_error); success = FALSE; } return success; } static gboolean e_cache_put_locked_default (ECache *cache, const gchar *uid, const gchar *revision, const gchar *object, ECacheColumnValues *other_columns, EOfflineState offline_state, gboolean is_replace, GCancellable *cancellable, GError **error) { GString *statement, *other_names = NULL, *other_values = NULL; gboolean success; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (uid != NULL, FALSE); g_return_val_if_fail (object != NULL, FALSE); statement = g_string_sized_new (255); e_cache_sqlite_stmt_append_printf (statement, "INSERT OR REPLACE INTO %Q (" E_CACHE_COLUMN_UID "," E_CACHE_COLUMN_REVISION "," E_CACHE_COLUMN_OBJECT "," E_CACHE_COLUMN_STATE, E_CACHE_TABLE_OBJECTS); if (other_columns) { GHashTableIter iter; gpointer key, value; e_cache_column_values_init_iter (other_columns, &iter); while (g_hash_table_iter_next (&iter, &key, &value)) { if (!other_names) other_names = g_string_new (""); g_string_append_c (other_names, ','); e_cache_sqlite_stmt_append_printf (other_names, "%Q", key); if (!other_values) other_values = g_string_new (""); g_string_append_c (other_values, ','); if (value) { e_cache_sqlite_stmt_append_printf (other_values, "%Q", value); } else { g_string_append (other_values, "NULL"); } } } if (other_names) g_string_append (statement, other_names->str); g_string_append (statement, ") VALUES ("); e_cache_sqlite_stmt_append_printf (statement, "%Q,%Q,%Q,%d", uid, revision ? revision : "", object, offline_state); if (other_values) g_string_append (statement, other_values->str); g_string_append_c (statement, ')'); success = e_cache_sqlite_exec_internal (cache, statement->str, NULL, NULL, cancellable, error); if (other_names) g_string_free (other_names, TRUE); if (other_values) g_string_free (other_values, TRUE); g_string_free (statement, TRUE); return success; } static gboolean e_cache_remove_locked_default (ECache *cache, const gchar *uid, GCancellable *cancellable, GError **error) { gboolean success = TRUE; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); g_return_val_if_fail (uid != NULL, FALSE); g_signal_emit (cache, signals[BEFORE_REMOVE], 0, uid, cancellable, error, &success); success = success && e_cache_sqlite_exec_printf (cache, "DELETE FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_UID " = %Q", NULL, NULL, cancellable, error, uid); return success; } static gboolean e_cache_remove_all_locked_default (ECache *cache, const GSList *uids, GCancellable *cancellable, GError **error) { const GSList *link; gboolean success = TRUE; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); for (link = uids; link && success; link = g_slist_next (link)) { const gchar *uid = link->data; g_signal_emit (cache, signals[BEFORE_REMOVE], 0, uid, cancellable, error, &success); } if (success) { success = e_cache_sqlite_exec_printf (cache, "DELETE FROM " E_CACHE_TABLE_OBJECTS, NULL, NULL, cancellable, error); } return success; } static gboolean e_cache_clear_offline_changes_locked_default (ECache *cache, GCancellable *cancellable, GError **error) { gboolean success; g_return_val_if_fail (E_IS_CACHE (cache), FALSE); success = e_cache_sqlite_exec_printf (cache, "DELETE FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_STATE "=%d", NULL, NULL, cancellable, error, E_OFFLINE_STATE_LOCALLY_DELETED); success = success && e_cache_sqlite_exec_printf (cache, "UPDATE " E_CACHE_TABLE_OBJECTS " SET " E_CACHE_COLUMN_STATE "=%d" " WHERE " E_CACHE_COLUMN_STATE "!=%d", NULL, NULL, cancellable, error, E_OFFLINE_STATE_SYNCED, E_OFFLINE_STATE_SYNCED); return success; } static gboolean e_cache_signals_accumulator (GSignalInvocationHint *ihint, GValue *return_accu, const GValue *handler_return, gpointer data) { gboolean handler_result; handler_result = g_value_get_boolean (handler_return); g_value_set_boolean (return_accu, handler_result); return handler_result; } static gboolean e_cache_before_put_default (ECache *cache, const gchar *uid, const gchar *revision, const gchar *object, ECacheColumnValues *other_columns, gboolean is_replace, GCancellable *cancellable, GError **error) { return TRUE; } static gboolean e_cache_before_remove_default (ECache *cache, const gchar *uid, GCancellable *cancellable, GError **error) { return TRUE; } static void e_cache_finalize (GObject *object) { ECache *cache = E_CACHE (object); g_free (cache->priv->filename); cache->priv->filename = NULL; g_clear_pointer (&cache->priv->db, sqlite3_close); g_rec_mutex_clear (&cache->priv->lock); g_warn_if_fail (cache->priv->cancellable == NULL); g_clear_object (&cache->priv->cancellable); /* Chain up to parent's method. */ G_OBJECT_CLASS (e_cache_parent_class)->finalize (object); } static void e_cache_class_init (ECacheClass *klass) { GObjectClass *object_class; object_class = G_OBJECT_CLASS (klass); object_class->finalize = e_cache_finalize; klass->put_locked = e_cache_put_locked_default; klass->remove_locked = e_cache_remove_locked_default; klass->remove_all_locked = e_cache_remove_all_locked_default; klass->clear_offline_changes_locked = e_cache_clear_offline_changes_locked_default; klass->before_put = e_cache_before_put_default; klass->before_remove = e_cache_before_remove_default; signals[BEFORE_PUT] = g_signal_new ( "before-put", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (ECacheClass, before_put), e_cache_signals_accumulator, NULL, g_cclosure_marshal_generic, G_TYPE_BOOLEAN, 7, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, E_TYPE_CACHE_COLUMN_VALUES, G_TYPE_BOOLEAN, G_TYPE_CANCELLABLE, G_TYPE_POINTER); signals[BEFORE_REMOVE] = g_signal_new ( "before-remove", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (ECacheClass, before_remove), e_cache_signals_accumulator, NULL, g_cclosure_marshal_generic, G_TYPE_BOOLEAN, 3, G_TYPE_STRING, G_TYPE_CANCELLABLE, G_TYPE_POINTER); signals[REVISION_CHANGED] = g_signal_new ( "revision-changed", G_OBJECT_CLASS_TYPE (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (ECacheClass, revision_changed), NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 0, G_TYPE_NONE); e_sqlite3_vfs_init (); } static void e_cache_init (ECache *cache) { cache->priv = e_cache_get_instance_private (cache); cache->priv->filename = NULL; cache->priv->db = NULL; cache->priv->cancellable = NULL; cache->priv->in_transaction = 0; cache->priv->revision_change_frozen = 0; cache->priv->revision_counter = 0; cache->priv->last_revision_time = 0; cache->priv->needs_revision_change = FALSE; g_rec_mutex_init (&cache->priv->lock); }