diff options
author | Xan Lopez <xan@gnome.org> | 2009-05-07 23:52:12 +0300 |
---|---|---|
committer | Xan Lopez <xan@gnome.org> | 2009-05-07 23:52:12 +0300 |
commit | 5e49d5438a07fdaed7cd58365c9c79548fc55805 (patch) | |
tree | 5ae7e62957096a89810a7e6a404029e726beaee1 | |
parent | b0a27affd574e8e2b2673aa9652cd1afcbc04fe0 (diff) | |
download | libsoup-5e49d5438a07fdaed7cd58365c9c79548fc55805.tar.gz |
Forgot to add files.
-rw-r--r-- | libsoup/soup-cache.c | 724 | ||||
-rw-r--r-- | libsoup/soup-cache.h | 46 |
2 files changed, 770 insertions, 0 deletions
diff --git a/libsoup/soup-cache.c b/libsoup/soup-cache.c new file mode 100644 index 00000000..8796c57a --- /dev/null +++ b/libsoup/soup-cache.c @@ -0,0 +1,724 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * soup-cache.c + * + * Copyright (C) 2009 Igalia S.L. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "soup-cache.h" +#include "soup-date.h" +#include "soup-headers.h" +#include "soup-message.h" +#include "soup-session-feature.h" +#include "soup-uri.h" + +#include <gio/gio.h> + +static void soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data); + +typedef struct _SoupCacheEntry +{ + char *key; + char *filename; + guint freshness_lifetime; + gboolean must_revalidate; + guint date; + guint length; + SoupMessageHeaders *headers; + GOutputStream *stream; + gboolean dirty; +} SoupCacheEntry; + +struct _SoupCachePrivate { + char *cache_dir; + GHashTable *cache; +}; + +enum { + PROP_0, + PROP_CACHE_DIR +}; + +typedef enum { + SOUP_CACHE_CACHEABLE = (1 << 0), + SOUP_CACHE_UNCACHEABLE = (1 << 1), + SOUP_CACHE_INVALIDATES = (1 << 2) +} SoupCacheability; + +#define SOUP_CACHE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_CACHE, SoupCachePrivate)) + +G_DEFINE_TYPE_WITH_CODE (SoupCache, soup_cache, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE, + soup_cache_session_feature_init)) + +static SoupCacheability +get_cacheability (SoupMessage *msg) +{ + SoupCacheability cacheability; + const char *cache_control; + + /* 1. The request method must be cacheable */ + if (msg->method == SOUP_METHOD_GET) + cacheability = SOUP_CACHE_CACHEABLE; + else if (msg->method == SOUP_METHOD_HEAD || + msg->method == SOUP_METHOD_TRACE || + msg->method == SOUP_METHOD_CONNECT) + return SOUP_CACHE_UNCACHEABLE; + else + return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES); + + cache_control = soup_message_headers_get (msg->response_headers, "Cache-Control"); + if (cache_control) { + GHashTable *hash; + + hash = soup_header_parse_param_list (cache_control); + /* 2. The 'no-store' cache directive does not appear in the + headers */ + if (g_hash_table_lookup (hash, "no-store")) { + soup_header_free_param_list (hash); + return SOUP_CACHE_UNCACHEABLE; + } + + /* This does not appear in section 2.1, but I think it makes + sense to check it too? */ + if (g_hash_table_lookup (hash, "no-cache")) { + soup_header_free_param_list (hash); + return SOUP_CACHE_UNCACHEABLE; + } + } + + switch (msg->status_code) { + case SOUP_STATUS_PARTIAL_CONTENT: + case SOUP_STATUS_NOT_MODIFIED: + /* We don't cache partial responses, but they only + * invalidate cached full responses if the headers + * don't match. Likewise with 304 Not Modified. + */ + cacheability = SOUP_CACHE_UNCACHEABLE; + break; + + case SOUP_STATUS_MULTIPLE_CHOICES: + case SOUP_STATUS_MOVED_PERMANENTLY: + case SOUP_STATUS_GONE: + /* FIXME: cacheable unless indicated otherwise */ + cacheability = SOUP_CACHE_UNCACHEABLE; + break; + + case SOUP_STATUS_FOUND: + case SOUP_STATUS_TEMPORARY_REDIRECT: + /* FIXME: cacheable if explicitly indicated */ + cacheability = SOUP_CACHE_UNCACHEABLE; + break; + + case SOUP_STATUS_SEE_OTHER: + case SOUP_STATUS_FORBIDDEN: + case SOUP_STATUS_NOT_FOUND: + case SOUP_STATUS_METHOD_NOT_ALLOWED: + return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES); + + default: + /* Any 5xx status or any 4xx status not handled above + * is uncacheable but doesn't break the cache. + */ + if ((msg->status_code >= SOUP_STATUS_BAD_REQUEST && + msg->status_code <= SOUP_STATUS_FAILED_DEPENDENCY) || + msg->status_code >= SOUP_STATUS_INTERNAL_SERVER_ERROR) + return SOUP_CACHE_UNCACHEABLE; + + /* An unrecognized 2xx, 3xx, or 4xx response breaks + * the cache. + */ + if ((msg->status_code > SOUP_STATUS_PARTIAL_CONTENT && + msg->status_code < SOUP_STATUS_MULTIPLE_CHOICES) || + (msg->status_code > SOUP_STATUS_TEMPORARY_REDIRECT && + msg->status_code < SOUP_STATUS_INTERNAL_SERVER_ERROR)) + return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES); + break; + } + + return cacheability; +} + +static void +soup_cache_entry_free (SoupCacheEntry *entry) +{ + g_free (entry->filename); + entry->filename = NULL; + g_free (entry->key); + entry->key = NULL; + soup_message_headers_free (entry->headers); + entry->headers = NULL; + g_slice_free (SoupCacheEntry, entry); +} + +static void +copy_headers (const char *name, const char *value, SoupMessageHeaders *headers) +{ + soup_message_headers_append (headers, name, value); +} + +static guint +soup_cache_entry_get_current_age (SoupCacheEntry *entry) +{ + time_t now = time (NULL); + + return now - entry->date; +} + +static gboolean +soup_cache_entry_is_fresh_enough (SoupCacheEntry *entry, int min_fresh) +{ + int limit = (min_fresh == -1) ? soup_cache_entry_get_current_age (entry) : min_fresh; + + g_debug ("IS ENTRY FRESH ENOUGH? LIFETIME %d, CURRENT AGE %d, MIN-FRESH %d, FRESH: %d (Key: %s)", entry->freshness_lifetime, soup_cache_entry_get_current_age (entry), min_fresh, (gboolean) entry->freshness_lifetime > limit, entry->key); + return entry->freshness_lifetime > limit; +} + +static char * +soup_message_get_cache_key (SoupMessage *msg) +{ + SoupURI *uri; + + uri = soup_message_get_uri (msg); + return soup_uri_to_string (uri, FALSE); +} + +static void +soup_cache_entry_set_freshness (SoupCacheEntry *entry, SoupMessage *msg) +{ + const char *cache_control; + const char *max_age, *expires, *date, *last_modified; + GHashTable *hash; + + hash = NULL; + + cache_control = soup_message_headers_get (entry->headers, "Cache-Control"); + if (cache_control) { + hash = soup_header_parse_param_list (cache_control); + + /* Should we re-validate the entry when it goes stale */ + entry->must_revalidate = (gboolean)g_hash_table_lookup (hash, "must-revalidate"); + + /* If 'max-age' cache directive is present, use that */ + max_age = g_hash_table_lookup (hash, "max-age"); + if (max_age) { + gint64 freshness_lifetime; + + freshness_lifetime = g_ascii_strtoll (max_age, NULL, 10); + if (freshness_lifetime) { + entry->freshness_lifetime = (guint) MIN (freshness_lifetime, G_MAXUINT32); + soup_header_free_param_list (hash); + return; + } + } + } + + if (hash != NULL) + soup_header_free_param_list (hash); + + /* If the 'Expires' response header is present, use its value + minus the value of the 'Date' response header */ + expires = soup_message_headers_get (entry->headers, "Expires"); + date = soup_message_headers_get (entry->headers, "Date"); + if (expires && date) { + SoupDate *expires_d, *date_d; + time_t expires_t, date_t; + + expires_d = soup_date_new_from_string (expires); + date_d = soup_date_new_from_string (date); + + expires_t = soup_date_to_time_t (expires_d); + date_t = soup_date_to_time_t (date_d); + + soup_date_free (expires_d); + soup_date_free (date_d); + + if (expires_t && date_t) { + entry->freshness_lifetime = (guint) MAX (expires_t - date_t, G_MAXUINT32); + return; + } + } + + /* Otherwise an heuristic may be used */ + + last_modified = soup_message_headers_get (entry->headers, "Last-Modified"); + if (last_modified && + (msg->status_code == SOUP_STATUS_OK || + msg->status_code == SOUP_STATUS_NON_AUTHORITATIVE || + /*msg->status_code == SOUP_STATUS_PARTIAL_CONTENT || */ + msg->status_code == SOUP_STATUS_MULTIPLE_CHOICES || + msg->status_code == SOUP_STATUS_MOVED_PERMANENTLY || + msg->status_code == SOUP_STATUS_GONE)) { + SoupDate *soup_date; + time_t now, last_modified_t; + + soup_date = soup_date_new_from_string (last_modified); + last_modified_t = soup_date_to_time_t (soup_date); + now = time (NULL); + +#define HEURISTIC_FACTOR 0.1 /* From Section 2.3.1.1 */ + + entry->freshness_lifetime = MAX (0, (now - last_modified_t) * HEURISTIC_FACTOR); + soup_date_free (soup_date); + return; + } + + /* If all else fails, make the entry expire immediately */ + entry->freshness_lifetime = 0; +} + +static SoupCacheEntry * +soup_cache_entry_new (SoupCache *cache, SoupMessage *msg) +{ + SoupCacheEntry *entry; + SoupMessageHeaders *headers; + const char *date; + char *md5; + + entry = g_slice_new (SoupCacheEntry); + entry->length = 0; + entry->dirty = TRUE; + + /* key & filename */ + entry->key = soup_message_get_cache_key (msg); + md5 = g_compute_checksum_for_string (G_CHECKSUM_MD5, entry->key, -1); + entry->filename = g_build_filename (cache->priv->cache_dir, md5, NULL); + g_free (md5); + + /* Headers */ + headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE); + soup_message_headers_foreach (msg->response_headers, + (SoupMessageHeadersForeachFunc)copy_headers, + headers); + entry->headers = headers; + + /* Section 2.3.1, Freshness Lifetime */ + soup_cache_entry_set_freshness (entry, msg); + + /* Section 2.3.2, Calculating Age */ + date = soup_message_headers_get (entry->headers, "Date"); + + if (date) { + SoupDate *soup_date; + soup_date = soup_date_new_from_string (date); + + entry->date = soup_date_to_time_t (soup_date); + soup_date_free (soup_date); + } else { + entry->date = time (NULL); + } + + return entry; +} + +static SoupCacheEntry * +soup_cache_lookup_uri (SoupCache *cache, const char *uri) +{ + SoupCachePrivate *priv; + SoupCacheEntry *entry; + + priv = cache->priv; + + entry = g_hash_table_lookup (priv->cache, uri); + + return entry; +} + +static void +msg_got_chunk_cb (SoupMessage *msg, SoupBuffer *chunk, SoupCacheEntry *entry) +{ + g_return_if_fail (chunk->data && chunk->length); + g_return_if_fail (entry); + g_return_if_fail (G_IS_OUTPUT_STREAM (entry->stream)); + + g_output_stream_write (entry->stream, chunk->data, chunk->length, NULL, NULL); + entry->length += chunk->length; +} + +static void +msg_got_body_cb (SoupMessage *msg, SoupCacheEntry *entry) +{ + g_return_if_fail (entry); + g_return_if_fail (G_IS_OUTPUT_STREAM (entry->stream)); + + g_output_stream_close (entry->stream, NULL, NULL); + g_object_unref (entry->stream); + + entry->stream = NULL; + entry->dirty = FALSE; +} + +static void +soup_cache_entry_delete_by_key (SoupCache *cache, const char *key) +{ + GFile *file; + char *md5, *filename; + + /* Delete cache file */ + md5 = g_compute_checksum_for_string (G_CHECKSUM_MD5, key, -1); + filename = g_build_filename (cache->priv->cache_dir, md5, NULL); + file = g_file_new_for_path (filename); + g_file_delete (file, NULL, NULL); + g_free (md5); + g_free (filename); + g_object_unref (file); + + /* Remove from cache */ + g_hash_table_remove (cache->priv->cache, key); + +} + +static void +msg_restarted_cb (SoupMessage *msg, SoupCacheEntry *entry) +{ + /* FIXME: What should we do here exactly? */ +} + +static void +msg_got_headers_cb (SoupMessage *msg, SoupCache *cache) +{ + SoupCacheability cacheable; + + cacheable = get_cacheability (msg); + + if (cacheable & SOUP_CACHE_CACHEABLE) { + SoupCacheEntry *entry; + char *key; + GFile *file; + + /* Check if we are already caching this resource */ + key = soup_message_get_cache_key (msg); + entry = soup_cache_lookup_uri (cache, key); + g_free (key); + + if (entry && entry->dirty) { + g_debug ("ALREADY GOT AN ENTRY, IS IT DIRTY?: %d (%s)", entry->dirty, entry->key); + return; + } + + /* Create a new entry, deleting any old one if + present */ + entry = soup_cache_entry_new (cache, msg); + soup_cache_entry_delete_by_key (cache, entry->key); + + g_hash_table_insert (cache->priv->cache, g_strdup (entry->key), entry); + + /* Prepare entry */ + file = g_file_new_for_path (entry->filename); + entry->stream = (GOutputStream*)g_file_append_to (file, 0, NULL, NULL); + g_object_unref (file); + + g_signal_connect (msg, "got-chunk", G_CALLBACK (msg_got_chunk_cb), entry); + g_signal_connect (msg, "got-body", G_CALLBACK (msg_got_body_cb), entry); + g_signal_connect (msg, "restarted", G_CALLBACK (msg_restarted_cb), entry); + } else if (cacheable & SOUP_CACHE_INVALIDATES) { + char *key; + + key = soup_message_get_cache_key (msg); + soup_cache_entry_delete_by_key (cache, key); + g_free (key); + } +} + +gboolean +soup_cache_has_response (SoupCache *cache, SoupSession *session, SoupMessage *msg) +{ + char *key; + SoupCacheEntry *entry; + const char *cache_control; + GHashTable *hash; + gpointer value; + gboolean must_revalidate; + int max_age, max_stale, min_fresh; + SoupURI *uri = soup_message_get_uri (msg); + + key = soup_message_get_cache_key (msg); + entry = soup_cache_lookup_uri (cache, key); + + /* 1. The presented Request-URI and that of stored response + match */ + if (!entry) return FALSE; + + if (entry->dirty) return FALSE; + + /* 2. The request method associated with the stored response + allows it to be used for the presented request */ + + /* In practice this means we only return our resource for GET, + cacheability for other methods is a TODO in the RFC + (TODO: although we could return the headers for HEAD + probably). */ + if (msg->method != SOUP_METHOD_GET) return FALSE; + + /* 3. Selecting request-headers nominated by the stored + response (if any) match those presented. */ + + /* TODO */ + + /* 4. The presented request and stored response are free from + directives that would prevent its use. */ + + must_revalidate = FALSE; + max_age = max_stale = min_fresh = -1; + + cache_control = soup_message_headers_get (msg->request_headers, "Cache-Control"); + g_debug ("Cache-Control for %s: %s", soup_uri_to_string (uri, FALSE), cache_control); + if (cache_control) { + hash = soup_header_parse_param_list (cache_control); + + if (g_hash_table_lookup_extended (hash, "no-store", NULL, NULL)) { + g_debug ("NO-STORE, OUT"); + g_hash_table_destroy (hash); + return FALSE; + } + + if (g_hash_table_lookup_extended (hash, "no-cache", NULL, NULL)) { + entry->must_revalidate = TRUE; + } + + value = g_hash_table_lookup (hash, "max-age"); + if (value) { + SoupURI *uri = soup_message_get_uri (msg); + max_age = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32); + g_debug ("Set max-age to %d (resource %s)", max_age, soup_uri_to_string (uri, FALSE)); + } + + /* max-stale can have no value set, we need to use _extended */ + if (g_hash_table_lookup_extended (hash, "max-stale", NULL, &value)) { + if (value) + max_stale = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32); + else + max_stale = G_MAXINT32; + } + + value = g_hash_table_lookup (hash, "min-fresh"); + if (value) + min_fresh = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32); + + g_hash_table_destroy (hash); + + if (max_age != -1) { + guint current_age = soup_cache_entry_get_current_age (entry); + + /* If we are over max-age and max-stale is not set, do + not use the value from the cache */ + if (max_age <= current_age && max_stale == -1) { + g_debug ("OVER MAX-AGE (%d) AND NO MAX-STALE (%d), OUT", max_age, max_stale); + return FALSE; + } + } + } + + /* 5. The stored response is either: fresh, allowed to be + served stale or succesfully validated */ + if (entry->must_revalidate) { + return FALSE; + } + + if (!soup_cache_entry_is_fresh_enough (entry, min_fresh)) { + /* Not fresh, can it be served stale? */ + if (max_stale != -1) { + /* G_MAXINT32 means we accept any staleness */ + if (max_stale == G_MAXINT32) + return TRUE; + + if ((soup_cache_entry_get_current_age (entry) - entry->freshness_lifetime) <= max_stale) + return TRUE; + } + + return FALSE; + } + + g_debug ("RETURNING FROM CACHE: %s (lifetime %d)", soup_uri_to_string (uri, FALSE), entry->freshness_lifetime); + return TRUE; +} + +void +soup_cache_send_response (SoupCache *cache, SoupSession *session, SoupMessage *msg) +{ + char *key; + SoupCacheEntry *entry; + SoupBuffer *buffer; + char *current_age; + + key = soup_message_get_cache_key (msg); + entry = soup_cache_lookup_uri (cache, key); + g_return_if_fail (entry); + + /* Headers */ + soup_message_headers_foreach (entry->headers, + (SoupMessageHeadersForeachFunc)copy_headers, + msg->response_headers); + + /* Add 'Age' header with the current age */ + current_age = g_strdup_printf ("%d", soup_cache_entry_get_current_age (entry)); + soup_message_headers_replace (msg->response_headers, + "Age", + current_age); + g_free (current_age); + + g_signal_emit_by_name (msg, "got-headers", NULL); + + /* Data */ + /* Do not try to read anything if the length of the + resource is 0 */ + if (entry->length) { + char *data; + gsize length; + + g_file_get_contents (entry->filename, &data, &length, NULL); + + if (data && length) { + buffer = soup_buffer_new (SOUP_MEMORY_TEMPORARY, data, length); + soup_message_body_append_buffer (msg->response_body, buffer); + g_signal_emit_by_name (msg, "got-chunk", buffer, NULL); + soup_buffer_free (buffer); + } + } + + soup_message_got_body (msg); + soup_message_finished (msg); +} + +static void +request_started (SoupSessionFeature *feature, SoupSession *session, + SoupMessage *msg, SoupSocket *socket) +{ + char *key; + key = soup_message_get_cache_key (msg); + g_debug ("REQUEST-STARTED %s", key); + g_free (key); + + g_signal_connect (msg, "got-headers", G_CALLBACK (msg_got_headers_cb), feature); +} + +static void +soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface, + gpointer interface_data) +{ + feature_interface->request_started = request_started; +} + +static void +soup_cache_init (SoupCache *cache) +{ + SoupCachePrivate *priv; + + priv = cache->priv = SOUP_CACHE_GET_PRIVATE (cache); + + priv->cache = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify)g_free, + (GDestroyNotify)soup_cache_entry_free); +} + +static void +soup_cache_finalize (GObject *object) +{ + SoupCachePrivate *priv; + + priv = SOUP_CACHE (object)->priv; + + g_hash_table_destroy (priv->cache); + g_free (priv->cache_dir); + + G_OBJECT_CLASS (soup_cache_parent_class)->finalize (object); +} + +static void +soup_cache_set_property (GObject *object, guint prop_id, + const GValue *value, GParamSpec *pspec) +{ + SoupCachePrivate *priv = SOUP_CACHE (object)->priv; + + switch (prop_id) { + case PROP_CACHE_DIR: + priv->cache_dir = g_value_dup_string (value); + /* Create directory if it does not exist (FIXME: should we?) */ + if (!g_file_test (priv->cache_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) + g_mkdir_with_parents (priv->cache_dir, 0700); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +soup_cache_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + SoupCachePrivate *priv = SOUP_CACHE (object)->priv; + + switch (prop_id) { + case PROP_CACHE_DIR: + g_value_set_string (value, priv->cache_dir); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +soup_cache_constructed (GObject *object) +{ + SoupCachePrivate *priv; + + priv = SOUP_CACHE (object)->priv; + + if (!priv->cache_dir) { + /* Set a default cache dir, different for each user */ + priv->cache_dir = g_build_filename (g_get_user_cache_dir (), + "httpcache", + NULL); + if (!g_file_test (priv->cache_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) + g_mkdir_with_parents (priv->cache_dir, 0700); + } + + if (G_OBJECT_CLASS (soup_cache_parent_class)->constructed) + G_OBJECT_CLASS (soup_cache_parent_class)->constructed (object); +} + +static void +soup_cache_class_init (SoupCacheClass *cache_class) +{ + GObjectClass *gobject_class = (GObjectClass*)cache_class; + + gobject_class->finalize = soup_cache_finalize; + gobject_class->constructed = soup_cache_constructed; + gobject_class->set_property = soup_cache_set_property; + gobject_class->get_property = soup_cache_get_property; + + g_object_class_install_property(gobject_class, PROP_CACHE_DIR, + g_param_spec_string("cache-dir", + "Cache directory", + "The directory to store the cache files", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + + g_type_class_add_private(cache_class, sizeof (SoupCachePrivate)); +} + +/** + * soup_cache_new: + * @cache_dir: the directory to store the cached data, or %NULL to use the default one + * + * Creates a new #SoupCache. + * + * Returns: a new #SoupCache + * + * Since: 2.28 + **/ +SoupCache * +soup_cache_new (const char *cache_dir) +{ + return g_object_new (SOUP_TYPE_CACHE, + "cache-dir", cache_dir, + NULL); +} + diff --git a/libsoup/soup-cache.h b/libsoup/soup-cache.h new file mode 100644 index 00000000..18e03fe4 --- /dev/null +++ b/libsoup/soup-cache.h @@ -0,0 +1,46 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2009 Igalia S.L. + */ + +#ifndef SOUP_CACHE_H +#define SOUP_CACHE_H 1 + +#include <libsoup/soup-types.h> + +G_BEGIN_DECLS + +#define SOUP_TYPE_CACHE (soup_cache_get_type ()) +#define SOUP_CACHE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOUP_TYPE_CACHE, SoupCache)) +#define SOUP_CACHE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SOUP_TYPE_CACHE, SoupCacheClass)) +#define SOUP_IS_CACHE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SOUP_TYPE_CACHE)) +#define SOUP_IS_CACHE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), SOUP_TYPE_CACHE)) +#define SOUP_CACHE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SOUP_TYPE_CACHE, SoupCacheClass)) + +typedef struct _SoupCachePrivate SoupCachePrivate; + +typedef struct { + GObject parent_instance; + + SoupCachePrivate *priv; +} SoupCache; + +typedef struct { + GObjectClass parent_class; + + /* Padding for future expansion */ + void (*_libsoup_reserved1) (void); + void (*_libsoup_reserved2) (void); + void (*_libsoup_reserved3) (void); +} SoupCacheClass; + +GType soup_cache_get_type (void); +SoupCache *soup_cache_new (const char *cache_dir); +gboolean soup_cache_has_response (SoupCache *cache, SoupSession *session, SoupMessage *msg); +void soup_cache_send_response (SoupCache *cache, SoupSession *session, SoupMessage *msg); + +G_END_DECLS + + +#endif /* SOUP_CACHE_H */ + |