diff options
Diffstat (limited to 'libsoup/soup-cache.c')
-rw-r--r-- | libsoup/soup-cache.c | 811 |
1 files changed, 331 insertions, 480 deletions
diff --git a/libsoup/soup-cache.c b/libsoup/soup-cache.c index 8b26478d..c17e537c 100644 --- a/libsoup/soup-cache.c +++ b/libsoup/soup-cache.c @@ -31,15 +31,28 @@ #include <string.h> -#define LIBSOUP_USE_UNSTABLE_REQUEST_API - #include "soup-cache.h" +#include "soup-body-input-stream.h" +#include "soup-cache-input-stream.h" #include "soup-cache-private.h" +#include "soup-content-processor.h" +#include "soup-message-private.h" #include "soup.h" +#include "soup-message-private.h" + +/** + * SECTION:soup-cache + * @short_description: Caching support + * + * #SoupCache implements a file-based cache for HTTP resources. + */ static SoupSessionFeatureInterface *soup_cache_default_feature_interface; static void soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data); +static SoupContentProcessorInterface *soup_cache_default_content_processor_interface; +static void soup_cache_content_processor_init (SoupContentProcessorInterface *interface, gpointer interface_data); + #define DEFAULT_MAX_SIZE 50 * 1024 * 1024 #define MAX_ENTRY_DATA_PERCENTAGE 10 /* Percentage of the total size of the cache that can be @@ -65,6 +78,19 @@ static void soup_cache_session_feature_init (SoupSessionFeatureInterface *featur */ #define SOUP_CACHE_CURRENT_VERSION 5 +#define OLD_SOUP_CACHE_FILE "soup.cache" +#define SOUP_CACHE_FILE "soup.cache2" + +#define SOUP_CACHE_HEADERS_FORMAT "{ss}" +#define SOUP_CACHE_PHEADERS_FORMAT "(sbuuuuuqa" SOUP_CACHE_HEADERS_FORMAT ")" +#define SOUP_CACHE_ENTRIES_FORMAT "(qa" SOUP_CACHE_PHEADERS_FORMAT ")" + +/* Basically the same format than above except that some strings are + prepended with &. This way the GVariant returns a pointer to the + data instead of duplicating the string */ +#define SOUP_CACHE_DECODE_HEADERS_FORMAT "{&s&s}" + + typedef struct _SoupCacheEntry { guint32 key; char *uri; @@ -73,13 +99,9 @@ typedef struct _SoupCacheEntry { gsize length; guint32 corrected_initial_age; guint32 response_time; - SoupBuffer *current_writing_buffer; gboolean dirty; - gboolean got_body; gboolean being_validated; SoupMessageHeaders *headers; - GOutputStream *stream; - GError *error; guint32 hits; GCancellable *cancellable; guint16 status_code; @@ -97,17 +119,6 @@ struct _SoupCachePrivate { GList *lru_start; }; -typedef struct { - SoupCache *cache; - SoupCacheEntry *entry; - SoupMessage *msg; - gulong content_sniffed_handler; - gulong got_chunk_handler; - gulong got_body_handler; - gulong restarted_handler; - GQueue *buffer_queue; -} SoupCacheWritingFixture; - enum { PROP_0, PROP_CACHE_DIR, @@ -118,12 +129,13 @@ enum { G_DEFINE_TYPE_WITH_CODE (SoupCache, soup_cache, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE, - soup_cache_session_feature_init)) + soup_cache_session_feature_init) + G_IMPLEMENT_INTERFACE (SOUP_TYPE_CONTENT_PROCESSOR, + soup_cache_content_processor_init)) -static gboolean soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry); +static gboolean soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry, gboolean purge); static void make_room_for_new_entry (SoupCache *cache, guint length_to_add); static gboolean cache_accepts_entries_of_size (SoupCache *cache, guint length_to_add); -static gboolean write_next_buffer (SoupCacheEntry *entry, SoupCacheWritingFixture *fixture); static GFile * get_file_from_entry (SoupCache *cache, SoupCacheEntry *entry) @@ -260,17 +272,10 @@ get_cacheability (SoupCache *cache, SoupMessage *msg) * and also unref's the GFile object representing it. */ static void -soup_cache_entry_free (SoupCacheEntry *entry, GFile *file) +soup_cache_entry_free (SoupCacheEntry *entry) { - if (file) { - g_file_delete (file, NULL, NULL); - g_object_unref (file); - } - g_free (entry->uri); - g_clear_pointer (&entry->current_writing_buffer, soup_buffer_free); g_clear_pointer (&entry->headers, soup_message_headers_free); - g_clear_error (&entry->error); g_clear_object (&entry->cancellable); g_slice_free (SoupCacheEntry, entry); @@ -282,6 +287,12 @@ copy_headers (const char *name, const char *value, SoupMessageHeaders *headers) soup_message_headers_append (headers, name, value); } +static void +remove_headers (const char *name, const char *value, SoupMessageHeaders *headers) +{ + soup_message_headers_remove (headers, name); +} + static char *hop_by_hop_headers[] = {"Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailer", "Transfer-Encoding", "Upgrade"}; static void @@ -324,8 +335,14 @@ soup_cache_entry_set_freshness (SoupCacheEntry *entry, SoupMessage *msg, SoupCac const char *cache_control; const char *expires, *date, *last_modified; + /* Reset these values. We have to do this to ensure that + * revalidations overwrite previous values for the headers. + */ + entry->must_revalidate = FALSE; + entry->freshness_lifetime = 0; + cache_control = soup_message_headers_get_list (entry->headers, "Cache-Control"); - if (cache_control) { + if (cache_control && *cache_control) { const char *max_age, *s_maxage; gint64 freshness_lifetime = 0; GHashTable *hash; @@ -442,10 +459,7 @@ soup_cache_entry_new (SoupCache *cache, SoupMessage *msg, time_t request_time, t entry = g_slice_new0 (SoupCacheEntry); entry->dirty = FALSE; - entry->current_writing_buffer = NULL; - entry->got_body = FALSE; entry->being_validated = FALSE; - entry->error = NULL; entry->status_code = msg->status_code; entry->response_time = response_time; entry->uri = soup_uri_to_string (soup_message_get_uri (msg), FALSE); @@ -488,243 +502,11 @@ soup_cache_entry_new (SoupCache *cache, SoupMessage *msg, time_t request_time, t return entry; } -static void -soup_cache_writing_fixture_free (SoupCacheWritingFixture *fixture) -{ - /* Free fixture. And disconnect signals, we don't want to - listen to more SoupMessage events as we're finished with - this resource */ - if (g_signal_handler_is_connected (fixture->msg, fixture->content_sniffed_handler)) - g_signal_handler_disconnect (fixture->msg, fixture->content_sniffed_handler); - if (g_signal_handler_is_connected (fixture->msg, fixture->got_chunk_handler)) - g_signal_handler_disconnect (fixture->msg, fixture->got_chunk_handler); - if (g_signal_handler_is_connected (fixture->msg, fixture->got_body_handler)) - g_signal_handler_disconnect (fixture->msg, fixture->got_body_handler); - if (g_signal_handler_is_connected (fixture->msg, fixture->restarted_handler)) - g_signal_handler_disconnect (fixture->msg, fixture->restarted_handler); - g_queue_foreach (fixture->buffer_queue, (GFunc) soup_buffer_free, NULL); - g_queue_free (fixture->buffer_queue); - g_object_unref (fixture->msg); - g_object_unref (fixture->cache); - g_slice_free (SoupCacheWritingFixture, fixture); -} - -static void -msg_content_sniffed_cb (SoupMessage *msg, gchar *content_type, GHashTable *params, SoupCacheWritingFixture *fixture) -{ - soup_message_headers_set_content_type (fixture->entry->headers, content_type, params); -} - -static void -close_ready_cb (GObject *source, GAsyncResult *result, SoupCacheWritingFixture *fixture) -{ - SoupCacheEntry *entry = fixture->entry; - SoupCache *cache = fixture->cache; - GOutputStream *stream = G_OUTPUT_STREAM (source); - goffset content_length; - - g_warn_if_fail (entry->error == NULL); - - /* FIXME: what do we do on error ? */ - - if (stream) { - g_output_stream_close_finish (stream, result, NULL); - g_object_unref (stream); - } - entry->stream = NULL; - - content_length = soup_message_headers_get_content_length (entry->headers); - - /* If the process was cancelled, then delete the entry from - the cache. Do it also if the size of a chunked resource is - too much for the cache */ - if (g_cancellable_is_cancelled (entry->cancellable)) { - entry->dirty = FALSE; - soup_cache_entry_remove (cache, entry); - soup_cache_entry_free (entry, get_file_from_entry (cache, entry)); - entry = NULL; - } else if ((soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CHUNKED) || - entry->length != (gsize) content_length) { - /* Two options here: - * - * 1. "chunked" data, entry was temporarily added to - * cache (as content-length is 0) and now that we have - * the actual size we have to evaluate if we want it - * in the cache or not - * - * 2. Content-Length has a different value than actual - * length, means that the content was encoded for - * transmission (typically compressed) and thus we - * have to substract the content-length value that was - * added to the cache and add the unencoded length - */ - gint length_to_add = entry->length - content_length; - - /* Make room in cache if needed */ - if (cache_accepts_entries_of_size (cache, length_to_add)) { - make_room_for_new_entry (cache, length_to_add); - - cache->priv->size += length_to_add; - } else { - entry->dirty = FALSE; - soup_cache_entry_remove (cache, entry); - soup_cache_entry_free (entry, get_file_from_entry (cache, entry)); - entry = NULL; - } - } - - if (entry) { - entry->dirty = FALSE; - entry->got_body = FALSE; - - if (entry->current_writing_buffer) { - soup_buffer_free (entry->current_writing_buffer); - entry->current_writing_buffer = NULL; - } - - g_object_unref (entry->cancellable); - entry->cancellable = NULL; - } - - cache->priv->n_pending--; - - /* Frees */ - soup_cache_writing_fixture_free (fixture); -} - -static void -write_ready_cb (GObject *source, GAsyncResult *result, SoupCacheWritingFixture *fixture) -{ - GOutputStream *stream = G_OUTPUT_STREAM (source); - GError *error = NULL; - gssize write_size; - SoupCacheEntry *entry = fixture->entry; - - if (g_cancellable_is_cancelled (entry->cancellable)) { - g_output_stream_close_async (stream, - G_PRIORITY_LOW, - entry->cancellable, - (GAsyncReadyCallback)close_ready_cb, - fixture); - return; - } - - write_size = g_output_stream_write_finish (stream, result, &error); - if (write_size <= 0 || error) { - if (error) - entry->error = error; - g_output_stream_close_async (stream, - G_PRIORITY_LOW, - entry->cancellable, - (GAsyncReadyCallback)close_ready_cb, - fixture); - /* FIXME: We should completely stop caching the - resource at this point */ - } else { - /* Are we still writing and is there new data to write - already ? */ - if (fixture->buffer_queue->length > 0) - write_next_buffer (entry, fixture); - else { - soup_buffer_free (entry->current_writing_buffer); - entry->current_writing_buffer = NULL; - - if (entry->got_body) { - /* If we already received 'got-body' - and we have written all the data, - we can close the stream */ - g_output_stream_close_async (entry->stream, - G_PRIORITY_LOW, - entry->cancellable, - (GAsyncReadyCallback)close_ready_cb, - fixture); - } - } - } -} - static gboolean -write_next_buffer (SoupCacheEntry *entry, SoupCacheWritingFixture *fixture) -{ - SoupBuffer *buffer = g_queue_pop_head (fixture->buffer_queue); - - if (buffer == NULL) - return FALSE; - - /* Free the old buffer */ - if (entry->current_writing_buffer) { - soup_buffer_free (entry->current_writing_buffer); - entry->current_writing_buffer = NULL; - } - entry->current_writing_buffer = buffer; - - g_output_stream_write_async (entry->stream, buffer->data, buffer->length, - G_PRIORITY_LOW, entry->cancellable, - (GAsyncReadyCallback) write_ready_cb, - fixture); - return TRUE; -} - -static void -msg_got_chunk_cb (SoupMessage *msg, SoupBuffer *chunk, SoupCacheWritingFixture *fixture) -{ - SoupCacheEntry *entry = fixture->entry; - - /* Ignore this if the writing or appending was cancelled */ - if (!g_cancellable_is_cancelled (entry->cancellable)) { - g_queue_push_tail (fixture->buffer_queue, soup_buffer_copy (chunk)); - entry->length += chunk->length; - - if (!cache_accepts_entries_of_size (fixture->cache, entry->length)) { - /* Quickly cancel the caching of the resource */ - g_cancellable_cancel (entry->cancellable); - } - } - - /* FIXME: remove the error check when we cancel the caching at - the first write error */ - /* Only write if the entry stream is ready */ - if (entry->current_writing_buffer == NULL && entry->error == NULL && entry->stream) - write_next_buffer (entry, fixture); -} - -static void -msg_got_body_cb (SoupMessage *msg, SoupCacheWritingFixture *fixture) -{ - SoupCacheEntry *entry = fixture->entry; - g_return_if_fail (entry); - - entry->got_body = TRUE; - - if (!entry->stream && fixture->buffer_queue->length > 0) - /* The stream is not ready to be written but we still - have data to write, we'll write it when the stream - is opened for writing */ - return; - - - if (fixture->buffer_queue->length > 0) { - /* If we still have data to write, write it, - write_ready_cb will close the stream */ - if (entry->current_writing_buffer == NULL && entry->error == NULL && entry->stream) - write_next_buffer (entry, fixture); - return; - } - - if (entry->stream && entry->current_writing_buffer == NULL) - g_output_stream_close_async (entry->stream, - G_PRIORITY_LOW, - entry->cancellable, - (GAsyncReadyCallback)close_ready_cb, - fixture); -} - -static gboolean -soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry) +soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry, gboolean purge) { GList *lru_item; - /* if (entry->dirty && !g_cancellable_is_cancelled (entry->cancellable)) { */ if (entry->dirty) { g_cancellable_cancel (entry->cancellable); return FALSE; @@ -745,6 +527,14 @@ soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry) g_assert (g_list_length (cache->priv->lru_start) == g_hash_table_size (cache->priv->cache)); + /* Free resources */ + if (purge) { + GFile *file = get_file_from_entry (cache, entry); + g_file_delete (file, NULL, NULL); + g_object_unref (file); + } + soup_cache_entry_free (entry); + return TRUE; } @@ -803,10 +593,9 @@ make_room_for_new_entry (SoupCache *cache, guint length_to_add) /* Discard entries. Once cancelled resources will be * freed in close_ready_cb */ - if (soup_cache_entry_remove (cache, old_entry)) { - soup_cache_entry_free (old_entry, get_file_from_entry (cache, old_entry)); + if (soup_cache_entry_remove (cache, old_entry, TRUE)) lru_entry = cache->priv->lru_start; - } else + else lru_entry = g_list_next (lru_entry); } } @@ -822,7 +611,7 @@ soup_cache_entry_insert (SoupCache *cache, /* Fill the key */ entry->key = get_cache_key_from_uri ((const char *) entry->uri); - if (soup_message_headers_get_encoding (entry->headers) != SOUP_ENCODING_CHUNKED) + if (soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CONTENT_LENGTH) length_to_add = soup_message_headers_get_content_length (entry->headers); /* Check if we are going to store the resource depending on its size */ @@ -836,9 +625,7 @@ soup_cache_entry_insert (SoupCache *cache, /* Remove any previous entry */ if ((old_entry = g_hash_table_lookup (cache->priv->cache, GUINT_TO_POINTER (entry->key))) != NULL) { - if (soup_cache_entry_remove (cache, old_entry)) - soup_cache_entry_free (old_entry, get_file_from_entry (cache, old_entry)); - else + if (!soup_cache_entry_remove (cache, old_entry, TRUE)) return FALSE; } @@ -879,157 +666,12 @@ soup_cache_entry_lookup (SoupCache *cache, return entry; } -static void -msg_restarted_cb (SoupMessage *msg, SoupCacheEntry *entry) -{ - /* FIXME: What should we do here exactly? */ -} - -static void -replace_cb (GObject *source, GAsyncResult *result, SoupCacheWritingFixture *fixture) -{ - SoupCacheEntry *entry = fixture->entry; - GOutputStream *stream = (GOutputStream *) g_file_replace_finish (G_FILE (source), - result, &entry->error); - - if (g_cancellable_is_cancelled (entry->cancellable) || entry->error) { - if (stream) - g_object_unref (stream); - fixture->cache->priv->n_pending--; - entry->dirty = FALSE; - soup_cache_entry_remove (fixture->cache, entry); - soup_cache_entry_free (entry, get_file_from_entry (fixture->cache, entry)); - soup_cache_writing_fixture_free (fixture); - return; - } - - entry->stream = stream; - - /* If we already got all the data we have to initiate the - * writing here, since we won't get more 'got-chunk' - * signals - */ - if (!entry->got_body) - return; - - /* It could happen that reading the data from server - * was completed before this happens. In that case - * there is no data - */ - if (!write_next_buffer (entry, fixture)) - /* Could happen if the resource is empty */ - g_output_stream_close_async (stream, G_PRIORITY_LOW, entry->cancellable, - (GAsyncReadyCallback) close_ready_cb, - fixture); -} - -typedef struct { - time_t request_time; - SoupSessionFeature *feature; - gulong got_headers_handler; -} RequestHelper; - -static void -msg_got_headers_cb (SoupMessage *msg, gpointer user_data) -{ - SoupCache *cache; - SoupCacheability cacheable; - RequestHelper *helper; - time_t request_time, response_time; - SoupCacheEntry *entry; - - response_time = time (NULL); - - helper = (RequestHelper *)user_data; - cache = SOUP_CACHE (helper->feature); - request_time = helper->request_time; - g_signal_handlers_disconnect_by_func (msg, msg_got_headers_cb, user_data); - g_slice_free (RequestHelper, helper); - - cacheable = soup_cache_get_cacheability (cache, msg); - - if (cacheable & SOUP_CACHE_CACHEABLE) { - GFile *file; - SoupCacheWritingFixture *fixture; - - /* Check if we are already caching this resource */ - entry = soup_cache_entry_lookup (cache, msg); - - if (entry && (entry->dirty || entry->being_validated)) - return; - - /* Create a new entry, deleting any old one if present */ - if (entry) { - soup_cache_entry_remove (cache, entry); - soup_cache_entry_free (entry, get_file_from_entry (cache, entry)); - } - - entry = soup_cache_entry_new (cache, msg, request_time, response_time); - entry->hits = 1; - - /* Do not continue if it can not be stored */ - if (!soup_cache_entry_insert (cache, entry, TRUE)) { - soup_cache_entry_free (entry, get_file_from_entry (cache, entry)); - return; - } - - fixture = g_slice_new0 (SoupCacheWritingFixture); - fixture->cache = g_object_ref (cache); - fixture->entry = entry; - fixture->msg = g_object_ref (msg); - fixture->buffer_queue = g_queue_new (); - - /* We connect now to these signals and buffer the data - if it comes before the file is ready for writing */ - fixture->content_sniffed_handler = - g_signal_connect (msg, "content-sniffed", G_CALLBACK (msg_content_sniffed_cb), fixture); - fixture->got_chunk_handler = - g_signal_connect (msg, "got-chunk", G_CALLBACK (msg_got_chunk_cb), fixture); - fixture->got_body_handler = - g_signal_connect (msg, "got-body", G_CALLBACK (msg_got_body_cb), fixture); - fixture->restarted_handler = - g_signal_connect (msg, "restarted", G_CALLBACK (msg_restarted_cb), entry); - - /* Prepare entry */ - cache->priv->n_pending++; - - entry->dirty = TRUE; - entry->cancellable = g_cancellable_new (); - file = get_file_from_entry (cache, entry); - g_file_replace_async (file, NULL, FALSE, - G_FILE_CREATE_PRIVATE | G_FILE_CREATE_REPLACE_DESTINATION, - G_PRIORITY_LOW, entry->cancellable, - (GAsyncReadyCallback) replace_cb, fixture); - g_object_unref (file); - } else if (cacheable & SOUP_CACHE_INVALIDATES) { - entry = soup_cache_entry_lookup (cache, msg); - - if (entry) { - if (soup_cache_entry_remove (cache, entry)) - soup_cache_entry_free (entry, get_file_from_entry (cache, entry)); - } - } else if (cacheable & SOUP_CACHE_VALIDATES) { - entry = soup_cache_entry_lookup (cache, msg); - - /* It's possible to get a CACHE_VALIDATES with no - * entry in the hash table. This could happen if for - * example the soup client is the one creating the - * conditional request. - */ - if (entry) { - entry->being_validated = FALSE; - copy_end_to_end_headers (msg->response_headers, entry->headers); - soup_cache_entry_set_freshness (entry, msg, cache); - } - } -} - GInputStream * soup_cache_send_response (SoupCache *cache, SoupMessage *msg) { SoupCacheEntry *entry; char *current_age; - GInputStream *stream = NULL; + GInputStream *file_stream, *body_stream, *cache_stream; GFile *file; g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL); @@ -1038,18 +680,19 @@ soup_cache_send_response (SoupCache *cache, SoupMessage *msg) entry = soup_cache_entry_lookup (cache, msg); g_return_val_if_fail (entry, NULL); - /* TODO: the original idea was to save reads, but current code - assumes that a stream is always returned. Need to reach - some agreement here. Also we have to handle the situation - were the file was no longer there (for example files - removed without notifying the cache */ file = get_file_from_entry (cache, entry); - stream = G_INPUT_STREAM (g_file_read (file, NULL, NULL)); + file_stream = G_INPUT_STREAM (g_file_read (file, NULL, NULL)); g_object_unref (file); /* Do not change the original message if there is no resource */ - if (stream == NULL) - return stream; + if (!file_stream) + return NULL; + + body_stream = soup_body_input_stream_new (file_stream, SOUP_ENCODING_CONTENT_LENGTH, entry->length); + g_object_unref (file_stream); + + if (!body_stream) + return NULL; /* If we are told to send a response from cache any validation in course is over by now */ @@ -1068,19 +711,29 @@ soup_cache_send_response (SoupCache *cache, SoupMessage *msg) current_age); g_free (current_age); - return stream; + /* Create the cache stream. */ + soup_message_disable_feature (msg, SOUP_TYPE_CACHE); + cache_stream = soup_message_setup_body_istream (body_stream, msg, + cache->priv->session, + SOUP_STAGE_ENTITY_BODY); + g_object_unref (body_stream); + + return cache_stream; +} + +static void +msg_got_headers_cb (SoupMessage *msg, gpointer user_data) +{ + g_object_set_data (G_OBJECT (msg), "response-time", GINT_TO_POINTER (time (NULL))); + g_signal_handlers_disconnect_by_func (msg, msg_got_headers_cb, user_data); } static void request_started (SoupSessionFeature *feature, SoupSession *session, SoupMessage *msg, SoupSocket *socket) { - RequestHelper *helper = g_slice_new0 (RequestHelper); - helper->request_time = time (NULL); - helper->feature = feature; - helper->got_headers_handler = g_signal_connect (msg, "got-headers", - G_CALLBACK (msg_got_headers_cb), - helper); + g_object_set_data (G_OBJECT (msg), "request-time", GINT_TO_POINTER (time (NULL))); + g_signal_connect (msg, "got-headers", G_CALLBACK (msg_got_headers_cb), NULL); } static void @@ -1103,6 +756,138 @@ soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface, feature_interface->request_started = request_started; } +typedef struct { + SoupCache *cache; + SoupCacheEntry *entry; +} StreamHelper; + +static void +istream_caching_finished (SoupCacheInputStream *istream, + gsize bytes_written, + GError *error, + gpointer user_data) +{ + StreamHelper *helper = (StreamHelper *) user_data; + SoupCache *cache = helper->cache; + SoupCacheEntry *entry = helper->entry; + + --cache->priv->n_pending; + + entry->dirty = FALSE; + entry->length = bytes_written; + g_clear_object (&entry->cancellable); + + if (error) { + /* Update cache size */ + if (soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CONTENT_LENGTH) + cache->priv->size -= soup_message_headers_get_content_length (entry->headers); + + soup_cache_entry_remove (cache, entry, TRUE); + helper->entry = entry = NULL; + goto cleanup; + } + + if (soup_message_headers_get_encoding (entry->headers) != SOUP_ENCODING_CONTENT_LENGTH) { + + if (cache_accepts_entries_of_size (cache, entry->length)) { + make_room_for_new_entry (cache, entry->length); + cache->priv->size += entry->length; + } else { + soup_cache_entry_remove (cache, entry, TRUE); + helper->entry = entry = NULL; + } + } + + cleanup: + g_object_unref (helper->cache); + g_slice_free (StreamHelper, helper); +} + +static GInputStream* +soup_cache_content_processor_wrap_input (SoupContentProcessor *processor, + GInputStream *base_stream, + SoupMessage *msg, + GError **error) +{ + SoupCache *cache = (SoupCache*) processor; + SoupCacheEntry *entry; + SoupCacheability cacheability; + GInputStream *istream; + GFile *file; + StreamHelper *helper; + time_t request_time, response_time; + + /* First of all, check if we should cache the resource. */ + cacheability = soup_cache_get_cacheability (cache, msg); + entry = soup_cache_entry_lookup (cache, msg); + + if (cacheability & SOUP_CACHE_INVALIDATES) { + if (entry) + soup_cache_entry_remove (cache, entry, TRUE); + return NULL; + } + + if (cacheability & SOUP_CACHE_VALIDATES) { + /* It's possible to get a CACHE_VALIDATES with no + * entry in the hash table. This could happen if for + * example the soup client is the one creating the + * conditional request. + */ + if (entry) + soup_cache_update_from_conditional_request (cache, msg); + return NULL; + } + + if (!(cacheability & SOUP_CACHE_CACHEABLE)) + return NULL; + + /* Check if we are already caching this resource */ + if (entry && (entry->dirty || entry->being_validated)) + return NULL; + + /* Create a new entry, deleting any old one if present */ + if (entry) + soup_cache_entry_remove (cache, entry, TRUE); + + request_time = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "request-time")); + response_time = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "request-time")); + entry = soup_cache_entry_new (cache, msg, request_time, response_time); + entry->hits = 1; + entry->dirty = TRUE; + + /* Do not continue if it can not be stored */ + if (!soup_cache_entry_insert (cache, entry, TRUE)) { + soup_cache_entry_free (entry); + return NULL; + } + + entry->cancellable = g_cancellable_new (); + ++cache->priv->n_pending; + + helper = g_slice_new (StreamHelper); + helper->cache = g_object_ref (cache); + helper->entry = entry; + + file = get_file_from_entry (cache, entry); + istream = soup_cache_input_stream_new (base_stream, file); + g_object_unref (file); + + g_signal_connect (istream, "caching-finished", G_CALLBACK (istream_caching_finished), helper); + + return istream; +} + +static void +soup_cache_content_processor_init (SoupContentProcessorInterface *processor_interface, + gpointer interface_data) +{ + soup_cache_default_content_processor_interface = + g_type_default_interface_peek (SOUP_TYPE_CONTENT_PROCESSOR); + + processor_interface->processing_stage = SOUP_STAGE_ENTITY_BODY; + processor_interface->wrap_input = soup_cache_content_processor_wrap_input; +} + static void soup_cache_init (SoupCache *cache) { @@ -1127,11 +912,7 @@ static void remove_cache_item (gpointer data, gpointer user_data) { - SoupCache *cache = (SoupCache *) user_data; - SoupCacheEntry *entry = (SoupCacheEntry *) data; - - if (soup_cache_entry_remove (cache, entry)) - soup_cache_entry_free (entry, NULL); + soup_cache_entry_remove ((SoupCache *) user_data, (SoupCacheEntry *) data, FALSE); } static void @@ -1361,7 +1142,7 @@ soup_cache_has_response (SoupCache *cache, SoupMessage *msg) return SOUP_CACHE_RESPONSE_STALE; cache_control = soup_message_headers_get_list (msg->request_headers, "Cache-Control"); - if (cache_control) { + if (cache_control && *cache_control) { GHashTable *hash = soup_header_parse_param_list (cache_control); if (g_hash_table_lookup_extended (hash, "no-store", NULL, NULL)) { @@ -1470,6 +1251,9 @@ force_flush_timeout (gpointer data) * committed to disk. For doing so it will iterate the #GMainContext * associated with @cache's session as long as needed. * + * Contrast with soup_cache_dump(), which writes out the cache index + * file. + * * Since: 2.34 */ void @@ -1502,19 +1286,39 @@ static void clear_cache_item (gpointer data, gpointer user_data) { - SoupCache *cache = (SoupCache *) user_data; - SoupCacheEntry *entry = (SoupCacheEntry *) data; + soup_cache_entry_remove ((SoupCache *) user_data, (SoupCacheEntry *) data, TRUE); +} + +static void +clear_cache_files (SoupCache *cache) +{ + GFileInfo *file_info; + GFileEnumerator *file_enumerator; + GFile *cache_dir_file = g_file_new_for_path (cache->priv->cache_dir); + + file_enumerator = g_file_enumerate_children (cache_dir_file, G_FILE_ATTRIBUTE_STANDARD_NAME, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (file_enumerator) { + while ((file_info = g_file_enumerator_next_file (file_enumerator, NULL, NULL)) != NULL) { + const char *filename = g_file_info_get_name (file_info); - if (soup_cache_entry_remove (cache, entry)) - soup_cache_entry_free (entry, get_file_from_entry (cache, entry)); + if (strcmp (filename, SOUP_CACHE_FILE) != 0) { + GFile *cache_file = g_file_get_child (cache_dir_file, filename); + g_file_delete (cache_file, NULL, NULL); + g_object_unref (cache_file); + } + g_object_unref (file_info); + } + g_object_unref (file_enumerator); + } + g_object_unref (cache_dir_file); } /** * soup_cache_clear: * @cache: a #SoupCache * - * Will remove all entries in the @cache plus all the cache files - * associated with them. + * Will remove all entries in the @cache plus all the cache files. * * Since: 2.34 */ @@ -1530,6 +1334,9 @@ soup_cache_clear (SoupCache *cache) entries = g_hash_table_get_values (cache->priv->cache); g_list_foreach (entries, clear_cache_item, cache); g_list_free (entries); + + /* Remove also any file not associated with a cache entry. */ + clear_cache_files (cache); } SoupMessage * @@ -1539,6 +1346,8 @@ soup_cache_generate_conditional_request (SoupCache *cache, SoupMessage *original SoupURI *uri; SoupCacheEntry *entry; const char *last_modified, *etag; + SoupMessagePrivate *origpriv; + GSList *f; g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL); g_return_val_if_fail (SOUP_IS_MESSAGE (original), NULL); @@ -1558,11 +1367,16 @@ soup_cache_generate_conditional_request (SoupCache *cache, SoupMessage *original /* Copy the data we need from the original message */ uri = soup_message_get_uri (original); msg = soup_message_new_from_uri (original->method, uri); + soup_message_disable_feature (msg, SOUP_TYPE_CACHE); soup_message_headers_foreach (original->request_headers, (SoupMessageHeadersForeachFunc)copy_headers, msg->request_headers); + origpriv = SOUP_MESSAGE_GET_PRIVATE (original); + for (f = origpriv->disabled_features; f; f = f->next) + soup_message_disable_feature (msg, (GType) GPOINTER_TO_SIZE (f->data)); + if (last_modified) soup_message_headers_append (msg->request_headers, "If-Modified-Since", @@ -1575,17 +1389,38 @@ soup_cache_generate_conditional_request (SoupCache *cache, SoupMessage *original return msg; } -#define OLD_SOUP_CACHE_FILE "soup.cache" -#define SOUP_CACHE_FILE "soup.cache2" +void +soup_cache_cancel_conditional_request (SoupCache *cache, + SoupMessage *msg) +{ + SoupCacheEntry *entry; -#define SOUP_CACHE_HEADERS_FORMAT "{ss}" -#define SOUP_CACHE_PHEADERS_FORMAT "(sbuuuuuqa" SOUP_CACHE_HEADERS_FORMAT ")" -#define SOUP_CACHE_ENTRIES_FORMAT "(qa" SOUP_CACHE_PHEADERS_FORMAT ")" + entry = soup_cache_entry_lookup (cache, msg); + if (entry) + entry->being_validated = FALSE; -/* Basically the same format than above except that some strings are - prepended with &. This way the GVariant returns a pointer to the - data instead of duplicating the string */ -#define SOUP_CACHE_DECODE_HEADERS_FORMAT "{&s&s}" + soup_session_cancel_message (cache->priv->session, msg, SOUP_STATUS_CANCELLED); +} + +void +soup_cache_update_from_conditional_request (SoupCache *cache, + SoupMessage *msg) +{ + SoupCacheEntry *entry = soup_cache_entry_lookup (cache, msg); + if (!entry) + return; + + entry->being_validated = FALSE; + + if (msg->status_code == SOUP_STATUS_NOT_MODIFIED) { + soup_message_headers_foreach (msg->response_headers, + (SoupMessageHeadersForeachFunc) remove_headers, + entry->headers); + copy_end_to_end_headers (msg->response_headers, entry->headers); + + soup_cache_entry_set_freshness (entry, msg, cache); + } +} static void pack_entry (gpointer data, @@ -1597,7 +1432,7 @@ pack_entry (gpointer data, GVariantBuilder *entries_builder = (GVariantBuilder *)user_data; /* Do not store non-consolidated entries */ - if (entry->dirty || entry->current_writing_buffer != NULL || !entry->key) + if (entry->dirty || !entry->key) return; g_variant_builder_open (entries_builder, G_VARIANT_TYPE (SOUP_CACHE_PHEADERS_FORMAT)); @@ -1622,6 +1457,19 @@ pack_entry (gpointer data, g_variant_builder_close (entries_builder); /* SOUP_CACHE_PHEADERS_FORMAT */ } +/** + * soup_cache_dump: + * @cache: a #SoupCache + * + * Synchronously writes the cache index out to disk. Contrast with + * soup_cache_flush(), which writes pending cache + * <emphasis>entries</emphasis> to disk. + * + * You must call this before exiting if you want your cache data to + * persist between sessions. + * + * Since: 2.34. + */ void soup_cache_dump (SoupCache *cache) { @@ -1650,30 +1498,14 @@ soup_cache_dump (SoupCache *cache) g_variant_unref (cache_variant); } -static void -clear_cache_files (SoupCache *cache) -{ - GFileInfo *file_info; - GFileEnumerator *file_enumerator; - GFile *cache_dir_file = g_file_new_for_path (cache->priv->cache_dir); - - file_enumerator = g_file_enumerate_children (cache_dir_file, G_FILE_ATTRIBUTE_STANDARD_NAME, - G_FILE_QUERY_INFO_NONE, NULL, NULL); - if (file_enumerator) { - while ((file_info = g_file_enumerator_next_file (file_enumerator, NULL, NULL)) != NULL) { - const char *filename = g_file_info_get_name (file_info); - - if (strcmp (filename, SOUP_CACHE_FILE) != 0) { - GFile *cache_file = g_file_get_child (cache_dir_file, filename); - g_file_delete (cache_file, NULL, NULL); - g_object_unref (cache_file); - } - } - g_object_unref (file_enumerator); - } - g_object_unref (cache_dir_file); -} - +/** + * soup_cache_load: + * @cache: a #SoupCache + * + * Loads the contents of @cache's index into memory. + * + * Since: 2.34 + */ void soup_cache_load (SoupCache *cache) { @@ -1741,7 +1573,7 @@ soup_cache_load (SoupCache *cache) entry->status_code = status_code; if (!soup_cache_entry_insert (cache, entry, FALSE)) - soup_cache_entry_free (entry, get_file_from_entry (cache, entry)); + soup_cache_entry_free (entry); } cache->priv->lru_start = g_list_reverse (cache->priv->lru_start); @@ -1751,6 +1583,15 @@ soup_cache_load (SoupCache *cache) g_variant_unref (cache_variant); } +/** + * soup_cache_set_max_size: + * @cache: a #SoupCache + * @max_size: the maximum size of the cache, in bytes + * + * Sets the maximum size of the cache. + * + * Since: 2.34 + */ void soup_cache_set_max_size (SoupCache *cache, guint max_size) @@ -1759,6 +1600,16 @@ soup_cache_set_max_size (SoupCache *cache, cache->priv->max_entry_data_size = cache->priv->max_size / MAX_ENTRY_DATA_PERCENTAGE; } +/** + * soup_cache_get_max_size: + * @cache: a #SoupCache + * + * Gets the maximum size of the cache. + * + * Return value: the maximum size of the cache, in bytes. + * + * Since: 2.34 + */ guint soup_cache_get_max_size (SoupCache *cache) { |