/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* camel-message-cache.c: Class for a Camel cache. * * Copyright (C) 1999-2008 Novell, Inc. (www.novell.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 . * * Authors: Michael Zucchi */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include "camel-data-cache.h" #include "camel-object.h" #include "camel-object-bag.h" #include "camel-stream-mem.h" #include "camel-file-utils.h" #define d(x) #define CAMEL_DATA_CACHE_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), CAMEL_TYPE_DATA_CACHE, CamelDataCachePrivate)) /* how many 'bits' of hash are used to key the toplevel directory */ #define CAMEL_DATA_CACHE_BITS (6) #define CAMEL_DATA_CACHE_MASK ((1 << CAMEL_DATA_CACHE_BITS)-1) /* timeout before a cache dir is checked again for expired entries, * once an hour should be enough */ #define CAMEL_DATA_CACHE_CYCLE_TIME (60*60) struct _CamelDataCachePrivate { CamelObjectBag *busy_bag; gchar *path; time_t expire_age; time_t expire_access; time_t expire_last[1 << CAMEL_DATA_CACHE_BITS]; }; enum { PROP_0, PROP_PATH }; G_DEFINE_TYPE (CamelDataCache, camel_data_cache, G_TYPE_OBJECT) static void data_cache_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_PATH: camel_data_cache_set_path ( CAMEL_DATA_CACHE (object), g_value_get_string (value)); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void data_cache_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_PATH: g_value_set_string ( value, camel_data_cache_get_path ( CAMEL_DATA_CACHE (object))); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void data_cache_finalize (GObject *object) { CamelDataCachePrivate *priv; priv = CAMEL_DATA_CACHE_GET_PRIVATE (object); camel_object_bag_destroy (priv->busy_bag); g_free (priv->path); /* Chain up to parent's finalize() method. */ G_OBJECT_CLASS (camel_data_cache_parent_class)->finalize (object); } static void camel_data_cache_class_init (CamelDataCacheClass *class) { GObjectClass *object_class; g_type_class_add_private (class, sizeof (CamelDataCachePrivate)); object_class = G_OBJECT_CLASS (class); object_class->set_property = data_cache_set_property; object_class->get_property = data_cache_get_property; object_class->finalize = data_cache_finalize; g_object_class_install_property ( object_class, PROP_PATH, g_param_spec_string ( "path", "Path", NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); } static void camel_data_cache_init (CamelDataCache *data_cache) { CamelObjectBag *busy_bag; busy_bag = camel_object_bag_new ( g_str_hash, g_str_equal, (CamelCopyFunc) g_strdup, (GFreeFunc) g_free); data_cache->priv = CAMEL_DATA_CACHE_GET_PRIVATE (data_cache); data_cache->priv->busy_bag = busy_bag; data_cache->priv->expire_age = -1; data_cache->priv->expire_access = -1; } /** * camel_data_cache_new: * @path: Base path of cache, subdirectories will be created here. * @error: return location for a #GError, or %NULL * * Create a new data cache. * * Returns: A new cache object, or NULL if the base path cannot * be written to. **/ CamelDataCache * camel_data_cache_new (const gchar *path, GError **error) { g_return_val_if_fail (path != NULL, NULL); if (g_mkdir_with_parents (path, 0700) == -1) { g_set_error ( error, CAMEL_ERROR, CAMEL_ERROR_GENERIC, _("Unable to create cache path")); return NULL; } return g_object_new (CAMEL_TYPE_DATA_CACHE, "path", path, NULL); } /** * camel_data_cache_get_path: * @cdc: a #CamelDataCache * * Returns the path to the data cache. * * Returns: the path to the data cache * * Since: 2.32 **/ const gchar * camel_data_cache_get_path (CamelDataCache *cdc) { g_return_val_if_fail (CAMEL_IS_DATA_CACHE (cdc), NULL); return cdc->priv->path; } /** * camel_data_cache_set_path: * @cdc: a #CamelDataCache * @path: path to the data cache * * Sets the path to the data cache. * * Since: 2.32 **/ void camel_data_cache_set_path (CamelDataCache *cdc, const gchar *path) { g_return_if_fail (CAMEL_IS_DATA_CACHE (cdc)); g_return_if_fail (path != NULL); if (g_strcmp0 (cdc->priv->path, path) == 0) return; g_free (cdc->priv->path); cdc->priv->path = g_strdup (path); g_object_notify (G_OBJECT (cdc), "path"); } /** * camel_data_cache_set_expire_age: * @cdc: A #CamelDataCache * @when: Timeout for age expiry, or -1 to disable. * * Set the cache expiration policy for aged entries. * * Items in the cache older than @when seconds may be * flushed at any time. Items are expired in a lazy * manner, so it is indeterminate when the items will * physically be removed. * * Note you can set both an age and an access limit. The * age acts as a hard limit on cache entries. **/ void camel_data_cache_set_expire_age (CamelDataCache *cdc, time_t when) { cdc->priv->expire_age = when; } /** * camel_data_cache_set_expire_access: * @cdc: A #CamelDataCache * @when: Timeout for access, or -1 to disable access expiry. * * Set the cache expiration policy for access times. * * Items in the cache which haven't been accessed for @when * seconds may be expired at any time. Items are expired in a lazy * manner, so it is indeterminate when the items will * physically be removed. * * Note you can set both an age and an access limit. The * age acts as a hard limit on cache entries. **/ void camel_data_cache_set_expire_access (CamelDataCache *cdc, time_t when) { cdc->priv->expire_access = when; } static void data_cache_expire (CamelDataCache *cdc, const gchar *path, const gchar *keep, time_t now, gboolean expire_all) { GDir *dir; const gchar *dname; struct stat st; GIOStream *stream; dir = g_dir_open (path, 0, NULL); if (dir == NULL) return; while ((dname = g_dir_read_name (dir))) { gchar *dpath; if (keep && strcmp (dname, keep) == 0) continue; dpath = g_build_filename (path, dname, NULL); if (g_stat (dpath, &st) == 0 && S_ISREG (st.st_mode) && (expire_all || (cdc->priv->expire_age != -1 && st.st_mtime + cdc->priv->expire_age < now) || (cdc->priv->expire_access != -1 && st.st_atime + cdc->priv->expire_access < now))) { g_unlink (dpath); stream = camel_object_bag_get (cdc->priv->busy_bag, dpath); if (stream) { camel_object_bag_remove (cdc->priv->busy_bag, stream); g_object_unref (stream); } } g_free (dpath); } g_dir_close (dir); } /* Since we have to stat the directory anyway, we use this opportunity to * lazily expire old data. * If it is this directories 'turn', and we haven't done it for CYCLE_TIME seconds, * then we perform an expiry run */ static gchar * data_cache_path (CamelDataCache *cdc, gint create, const gchar *path, const gchar *key) { gchar *dir, *real, *tmp; gsize dir_len; guint32 hash; hash = g_str_hash (key); hash = (hash >> 5) &CAMEL_DATA_CACHE_MASK; dir_len = strlen (cdc->priv->path) + strlen (path) + 8; dir = alloca (dir_len); g_snprintf (dir, dir_len, "%s/%s/%02x", cdc->priv->path, path, hash); if (g_access (dir, F_OK) == -1) { if (create) g_mkdir_with_parents (dir, 0700); } else if (cdc->priv->expire_age != -1 || cdc->priv->expire_access != -1) { time_t now; /* This has a race, but at worst we re-run an expire cycle which is safe */ now = time (NULL); if (cdc->priv->expire_last[hash] + CAMEL_DATA_CACHE_CYCLE_TIME < now) { cdc->priv->expire_last[hash] = now; data_cache_expire (cdc, dir, key, now, FALSE); } } tmp = camel_file_util_safe_filename (key); real = g_strdup_printf ("%s/%s", dir, tmp); g_free (tmp); return real; } /** * camel_data_cache_add: * @cdc: A #CamelDataCache * @path: Relative path of item to add. * @key: Key of item to add. * @error: return location for a #GError, or %NULL * * Add a new item to the cache, returning a #GIOStream to the new item. * * The key and the path combine to form a unique key used to store the item. * * Potentially, expiry processing will be performed while this call is * executing. * * The returned #GIOStream is referenced for thread-safety and must be * unreferenced with g_object_unref() when finished with it. * * Returns: a #GIOStream for the new cache item, or %NULL **/ GIOStream * camel_data_cache_add (CamelDataCache *cdc, const gchar *path, const gchar *key, GError **error) { gchar *real; GFileIOStream *stream; GFile *file; real = data_cache_path (cdc, TRUE, path, key); /* need to loop 'cause otherwise we can call bag_add/bag_abort * after bag_reserve returned a pointer, which is an invalid * sequence. */ do { stream = camel_object_bag_reserve (cdc->priv->busy_bag, real); if (stream) { g_unlink (real); camel_object_bag_remove (cdc->priv->busy_bag, stream); g_object_unref (stream); } } while (stream != NULL); file = g_file_new_for_path (real); stream = g_file_replace_readwrite ( file, NULL, FALSE, G_FILE_CREATE_PRIVATE, NULL, error); g_object_unref (file); if (stream != NULL) camel_object_bag_add (cdc->priv->busy_bag, real, stream); else camel_object_bag_abort (cdc->priv->busy_bag, real); g_free (real); return G_IO_STREAM (stream); } /** * camel_data_cache_get: * @cdc: A #CamelDataCache * @path: Path to the (sub) cache the item exists in. * @key: Key for the cache item. * @error: return location for a #GError, or %NULL * * Lookup an item in the cache. If the item exists, a #GIOStream is returned * for the item. The stream may be shared by multiple callers, so ensure the * stream is in a valid state through external locking. * * The returned #GIOStream is referenced for thread-safety and must be * unreferenced with g_object_unref() when finished with it. * * Returns: a #GIOStream for the requested cache item, or %NULL **/ GIOStream * camel_data_cache_get (CamelDataCache *cdc, const gchar *path, const gchar *key, GError **error) { GFileIOStream *stream; GFile *file; struct stat st; gchar *real; real = data_cache_path (cdc, FALSE, path, key); stream = camel_object_bag_reserve (cdc->priv->busy_bag, real); if (stream != NULL) goto exit; /* An empty cache file is useless. Return an error. */ if (g_stat (real, &st) == 0 && st.st_size == 0) { g_set_error ( error, CAMEL_ERROR, CAMEL_ERROR_GENERIC, "%s: %s", _("Empty cache file"), real); camel_object_bag_abort (cdc->priv->busy_bag, real); goto exit; } file = g_file_new_for_path (real); stream = g_file_open_readwrite (file, NULL, error); g_object_unref (file); if (stream != NULL) camel_object_bag_add (cdc->priv->busy_bag, real, stream); else camel_object_bag_abort (cdc->priv->busy_bag, real); exit: g_free (real); return G_IO_STREAM (stream); } /** * camel_data_cache_get_filename: * @cdc: A #CamelDataCache * @path: Path to the (sub) cache the item exists in. * @key: Key for the cache item. * * Lookup the filename for an item in the cache * * Returns: The filename for a cache item * * Since: 2.26 **/ gchar * camel_data_cache_get_filename (CamelDataCache *cdc, const gchar *path, const gchar *key) { return data_cache_path (cdc, FALSE, path, key); } /** * camel_data_cache_remove: * @cdc: A #CamelDataCache * @path: * @key: * @error: return location for a #GError, or %NULL * * Remove/expire a cache item. * * Returns: **/ gint camel_data_cache_remove (CamelDataCache *cdc, const gchar *path, const gchar *key, GError **error) { GIOStream *stream; gchar *real; gint ret; real = data_cache_path (cdc, FALSE, path, key); stream = camel_object_bag_get (cdc->priv->busy_bag, real); if (stream) { camel_object_bag_remove (cdc->priv->busy_bag, stream); g_object_unref (stream); } /* maybe we were a mem stream */ if (g_unlink (real) == -1 && errno != ENOENT) { g_set_error ( error, G_IO_ERROR, g_io_error_from_errno (errno), _("Could not remove cache entry: %s: %s"), real, g_strerror (errno)); ret = -1; } else { ret = 0; } g_free (real); return ret; } /** * camel_data_cache_clear: * @cdc: a #CamelDataCache * @path: Path to the (sub) cache the item exists in. * * Clear cache's content in @path. * * Since: 3.2 **/ void camel_data_cache_clear (CamelDataCache *cdc, const gchar *path) { gchar *base_dir; GDir *dir; const gchar *dname; struct stat st; g_return_if_fail (cdc != NULL); g_return_if_fail (path != NULL); base_dir = g_build_filename (cdc->priv->path, path, NULL); dir = g_dir_open (base_dir, 0, NULL); if (dir == NULL) { g_free (base_dir); return; } while ((dname = g_dir_read_name (dir))) { gchar *dpath; dpath = g_build_filename (base_dir, dname, NULL); if (g_stat (dpath, &st) == 0 && S_ISDIR (st.st_mode) && !g_str_equal (dname, ".") && !g_str_equal (dname, "..")) { data_cache_expire (cdc, dpath, NULL, -1, TRUE); } g_free (dpath); } g_dir_close (dir); g_free (base_dir); }