1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
5 * Copyright (C) 2009, 2010 Igalia S.L.
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Library General Public
9 * License as published by the Free Software Foundation; either
10 * version 2 of the License, or (at your option) any later version.
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Library General Public License for more details.
17 * You should have received a copy of the GNU Library General Public License
18 * along with this library; see the file COPYING.LIB. If not, write to
19 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20 * Boston, MA 02110-1301, USA.
24 * - Need to hook the feature in the sync SoupSession.
36 #define LIBSOUP_USE_UNSTABLE_REQUEST_API
38 #include "soup-cache.h"
39 #include "soup-cache-private.h"
40 #include "soup-date.h"
41 #include "soup-enum-types.h"
42 #include "soup-headers.h"
43 #include "soup-session.h"
44 #include "soup-session-feature.h"
47 static SoupSessionFeatureInterface *soup_cache_default_feature_interface;
48 static void soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
50 #define DEFAULT_MAX_SIZE 50 * 1024 * 1024
51 #define MAX_ENTRY_DATA_PERCENTAGE 10 /* Percentage of the total size
52 of the cache that can be
53 filled by a single entry */
56 * Version 2: cache is now saved in soup.cache2. Added the version
57 * number to the beginning of the file.
59 * Version 3: added HTTP status code to the cache entries.
61 * Version 4: replaced several types.
62 * - freshness_lifetime,corrected_initial_age,response_time: time_t -> guint32
63 * - status_code: guint -> guint16
64 * - hits: guint -> guint32
66 #define SOUP_CACHE_CURRENT_VERSION 4
68 typedef struct _SoupCacheEntry {
71 guint32 freshness_lifetime;
72 gboolean must_revalidate;
74 guint32 corrected_initial_age;
75 guint32 response_time;
76 SoupBuffer *current_writing_buffer;
79 gboolean being_validated;
80 SoupMessageHeaders *headers;
81 GOutputStream *stream;
84 GCancellable *cancellable;
88 struct _SoupCachePrivate {
93 SoupCacheType cache_type;
96 guint max_entry_data_size; /* Computed value. Here for performance reasons */
102 SoupCacheEntry *entry;
104 gulong got_chunk_handler;
105 gulong got_body_handler;
106 gulong restarted_handler;
107 GQueue *buffer_queue;
108 } SoupCacheWritingFixture;
116 #define SOUP_CACHE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_CACHE, SoupCachePrivate))
118 G_DEFINE_TYPE_WITH_CODE (SoupCache, soup_cache, G_TYPE_OBJECT,
119 G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
120 soup_cache_session_feature_init))
122 static gboolean soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry);
123 static void make_room_for_new_entry (SoupCache *cache, guint length_to_add);
124 static gboolean cache_accepts_entries_of_size (SoupCache *cache, guint length_to_add);
125 static gboolean write_next_buffer (SoupCacheEntry *entry, SoupCacheWritingFixture *fixture);
127 static SoupCacheability
128 get_cacheability (SoupCache *cache, SoupMessage *msg)
130 SoupCacheability cacheability;
131 const char *cache_control, *content_type;
133 /* 1. The request method must be cacheable */
134 if (msg->method == SOUP_METHOD_GET)
135 cacheability = SOUP_CACHE_CACHEABLE;
136 else if (msg->method == SOUP_METHOD_HEAD ||
137 msg->method == SOUP_METHOD_TRACE ||
138 msg->method == SOUP_METHOD_CONNECT)
139 return SOUP_CACHE_UNCACHEABLE;
141 return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES);
143 content_type = soup_message_headers_get_content_type (msg->response_headers, NULL);
144 if (content_type && !g_ascii_strcasecmp (content_type, "multipart/x-mixed-replace"))
145 return SOUP_CACHE_UNCACHEABLE;
147 cache_control = soup_message_headers_get (msg->response_headers, "Cache-Control");
150 SoupCachePrivate *priv = SOUP_CACHE_GET_PRIVATE (cache);
152 hash = soup_header_parse_param_list (cache_control);
154 /* Shared caches MUST NOT store private resources */
155 if (priv->cache_type == SOUP_CACHE_SHARED) {
156 if (g_hash_table_lookup_extended (hash, "private", NULL, NULL)) {
157 soup_header_free_param_list (hash);
158 return SOUP_CACHE_UNCACHEABLE;
162 /* 2. The 'no-store' cache directive does not appear in the
165 if (g_hash_table_lookup_extended (hash, "no-store", NULL, NULL)) {
166 soup_header_free_param_list (hash);
167 return SOUP_CACHE_UNCACHEABLE;
170 /* This does not appear in section 2.1, but I think it makes
171 * sense to check it too?
173 if (g_hash_table_lookup_extended (hash, "no-cache", NULL, NULL)) {
174 soup_header_free_param_list (hash);
175 return SOUP_CACHE_UNCACHEABLE;
178 soup_header_free_param_list (hash);
181 switch (msg->status_code) {
182 case SOUP_STATUS_PARTIAL_CONTENT:
183 /* We don't cache partial responses, but they only
184 * invalidate cached full responses if the headers
187 cacheability = SOUP_CACHE_UNCACHEABLE;
190 case SOUP_STATUS_NOT_MODIFIED:
191 /* A 304 response validates an existing cache entry */
192 cacheability = SOUP_CACHE_VALIDATES;
195 case SOUP_STATUS_MULTIPLE_CHOICES:
196 case SOUP_STATUS_MOVED_PERMANENTLY:
197 case SOUP_STATUS_GONE:
198 /* FIXME: cacheable unless indicated otherwise */
199 cacheability = SOUP_CACHE_UNCACHEABLE;
202 case SOUP_STATUS_FOUND:
203 case SOUP_STATUS_TEMPORARY_REDIRECT:
204 /* FIXME: cacheable if explicitly indicated */
205 cacheability = SOUP_CACHE_UNCACHEABLE;
208 case SOUP_STATUS_SEE_OTHER:
209 case SOUP_STATUS_FORBIDDEN:
210 case SOUP_STATUS_NOT_FOUND:
211 case SOUP_STATUS_METHOD_NOT_ALLOWED:
212 return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES);
215 /* Any 5xx status or any 4xx status not handled above
216 * is uncacheable but doesn't break the cache.
218 if ((msg->status_code >= SOUP_STATUS_BAD_REQUEST &&
219 msg->status_code <= SOUP_STATUS_FAILED_DEPENDENCY) ||
220 msg->status_code >= SOUP_STATUS_INTERNAL_SERVER_ERROR)
221 return SOUP_CACHE_UNCACHEABLE;
223 /* An unrecognized 2xx, 3xx, or 4xx response breaks
226 if ((msg->status_code > SOUP_STATUS_PARTIAL_CONTENT &&
227 msg->status_code < SOUP_STATUS_MULTIPLE_CHOICES) ||
228 (msg->status_code > SOUP_STATUS_TEMPORARY_REDIRECT &&
229 msg->status_code < SOUP_STATUS_INTERNAL_SERVER_ERROR))
230 return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES);
238 soup_cache_entry_free (SoupCacheEntry *entry, gboolean purge)
241 GFile *file = g_file_new_for_path (entry->filename);
242 g_file_delete (file, NULL, NULL);
243 g_object_unref (file);
246 g_free (entry->filename);
247 entry->filename = NULL;
251 if (entry->current_writing_buffer) {
252 soup_buffer_free (entry->current_writing_buffer);
253 entry->current_writing_buffer = NULL;
256 if (entry->headers) {
257 soup_message_headers_free (entry->headers);
258 entry->headers = NULL;
261 g_error_free (entry->error);
264 if (entry->cancellable) {
265 g_object_unref (entry->cancellable);
266 entry->cancellable = NULL;
269 g_slice_free (SoupCacheEntry, entry);
273 copy_headers (const char *name, const char *value, SoupMessageHeaders *headers)
275 soup_message_headers_append (headers, name, value);
278 static char *hop_by_hop_headers[] = {"Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailer", "Transfer-Encoding", "Upgrade"};
281 copy_end_to_end_headers (SoupMessageHeaders *source, SoupMessageHeaders *destination)
285 soup_message_headers_foreach (source, (SoupMessageHeadersForeachFunc) copy_headers, destination);
286 for (i = 0; i < G_N_ELEMENTS (hop_by_hop_headers); i++)
287 soup_message_headers_remove (destination, hop_by_hop_headers[i]);
288 soup_message_headers_clean_connection_headers (destination);
292 soup_cache_entry_get_current_age (SoupCacheEntry *entry)
294 time_t now = time (NULL);
295 time_t resident_time;
297 resident_time = now - entry->response_time;
298 return entry->corrected_initial_age + resident_time;
302 soup_cache_entry_is_fresh_enough (SoupCacheEntry *entry, gint min_fresh)
304 guint limit = (min_fresh == -1) ? soup_cache_entry_get_current_age (entry) : (guint) min_fresh;
305 return entry->freshness_lifetime > limit;
309 soup_message_get_cache_key (SoupMessage *msg)
311 SoupURI *uri = soup_message_get_uri (msg);
312 return soup_uri_to_string (uri, FALSE);
316 soup_cache_entry_set_freshness (SoupCacheEntry *entry, SoupMessage *msg, SoupCache *cache)
318 const char *cache_control;
319 const char *expires, *date, *last_modified;
321 cache_control = soup_message_headers_get (entry->headers, "Cache-Control");
323 const char *max_age, *s_maxage;
324 gint64 freshness_lifetime = 0;
326 SoupCachePrivate *priv = SOUP_CACHE_GET_PRIVATE (cache);
328 hash = soup_header_parse_param_list (cache_control);
330 /* Should we re-validate the entry when it goes stale */
331 entry->must_revalidate = g_hash_table_lookup_extended (hash, "must-revalidate", NULL, NULL);
334 if (priv->cache_type == SOUP_CACHE_SHARED) {
335 s_maxage = g_hash_table_lookup (hash, "s-maxage");
337 freshness_lifetime = g_ascii_strtoll (s_maxage, NULL, 10);
338 if (freshness_lifetime) {
339 /* Implies proxy-revalidate. TODO: is it true? */
340 entry->must_revalidate = TRUE;
341 soup_header_free_param_list (hash);
347 /* If 'max-age' cache directive is present, use that */
348 max_age = g_hash_table_lookup (hash, "max-age");
350 freshness_lifetime = g_ascii_strtoll (max_age, NULL, 10);
352 if (freshness_lifetime) {
353 entry->freshness_lifetime = (guint32) MIN (freshness_lifetime, G_MAXUINT32);
354 soup_header_free_param_list (hash);
358 soup_header_free_param_list (hash);
361 /* If the 'Expires' response header is present, use its value
362 * minus the value of the 'Date' response header
364 expires = soup_message_headers_get (entry->headers, "Expires");
365 date = soup_message_headers_get (entry->headers, "Date");
366 if (expires && date) {
367 SoupDate *expires_d, *date_d;
368 time_t expires_t, date_t;
370 expires_d = soup_date_new_from_string (expires);
372 date_d = soup_date_new_from_string (date);
374 expires_t = soup_date_to_time_t (expires_d);
375 date_t = soup_date_to_time_t (date_d);
377 soup_date_free (expires_d);
378 soup_date_free (date_d);
380 if (expires_t && date_t) {
381 entry->freshness_lifetime = (guint32) MAX (expires_t - date_t, 0);
385 /* If Expires is not a valid date we should
386 treat it as already expired, see section
388 entry->freshness_lifetime = 0;
393 /* Otherwise an heuristic may be used */
395 /* Heuristics MUST NOT be used with these status codes
397 if (msg->status_code != SOUP_STATUS_OK &&
398 msg->status_code != SOUP_STATUS_NON_AUTHORITATIVE &&
399 msg->status_code != SOUP_STATUS_PARTIAL_CONTENT &&
400 msg->status_code != SOUP_STATUS_MULTIPLE_CHOICES &&
401 msg->status_code != SOUP_STATUS_MOVED_PERMANENTLY &&
402 msg->status_code != SOUP_STATUS_GONE)
405 /* TODO: attach warning 113 if response's current_age is more
406 than 24h (section 2.3.1.1) when using heuristics */
408 /* Last-Modified based heuristic */
409 last_modified = soup_message_headers_get (entry->headers, "Last-Modified");
412 time_t now, last_modified_t;
414 soup_date = soup_date_new_from_string (last_modified);
415 last_modified_t = soup_date_to_time_t (soup_date);
418 #define HEURISTIC_FACTOR 0.1 /* From Section 2.3.1.1 */
420 entry->freshness_lifetime = MAX (0, (now - last_modified_t) * HEURISTIC_FACTOR);
421 soup_date_free (soup_date);
427 /* If all else fails, make the entry expire immediately */
428 entry->freshness_lifetime = 0;
431 static SoupCacheEntry *
432 soup_cache_entry_new (SoupCache *cache, SoupMessage *msg, time_t request_time, time_t response_time)
434 SoupCacheEntry *entry;
438 entry = g_slice_new0 (SoupCacheEntry);
439 entry->dirty = FALSE;
440 entry->current_writing_buffer = NULL;
441 entry->got_body = FALSE;
442 entry->being_validated = FALSE;
444 entry->status_code = msg->status_code;
447 entry->key = soup_message_get_cache_key (msg);
448 md5 = g_compute_checksum_for_string (G_CHECKSUM_MD5, entry->key, -1);
449 entry->filename = g_build_filename (cache->priv->cache_dir, md5, NULL);
453 entry->headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
454 copy_end_to_end_headers (msg->response_headers, entry->headers);
459 /* Section 2.3.1, Freshness Lifetime */
460 soup_cache_entry_set_freshness (entry, msg, cache);
462 /* Section 2.3.2, Calculating Age */
463 date = soup_message_headers_get (entry->headers, "Date");
468 time_t date_value, apparent_age, corrected_received_age, response_delay, age_value = 0;
470 soup_date = soup_date_new_from_string (date);
471 date_value = soup_date_to_time_t (soup_date);
472 soup_date_free (soup_date);
474 age = soup_message_headers_get (entry->headers, "Age");
476 age_value = g_ascii_strtoll (age, NULL, 10);
478 entry->response_time = response_time;
479 apparent_age = MAX (0, entry->response_time - date_value);
480 corrected_received_age = MAX (apparent_age, age_value);
481 response_delay = entry->response_time - request_time;
482 entry->corrected_initial_age = corrected_received_age + response_delay;
484 /* Is this correct ? */
485 entry->corrected_initial_age = time (NULL);
492 soup_cache_writing_fixture_free (SoupCacheWritingFixture *fixture)
494 /* Free fixture. And disconnect signals, we don't want to
495 listen to more SoupMessage events as we're finished with
497 if (g_signal_handler_is_connected (fixture->msg, fixture->got_chunk_handler))
498 g_signal_handler_disconnect (fixture->msg, fixture->got_chunk_handler);
499 if (g_signal_handler_is_connected (fixture->msg, fixture->got_body_handler))
500 g_signal_handler_disconnect (fixture->msg, fixture->got_body_handler);
501 if (g_signal_handler_is_connected (fixture->msg, fixture->restarted_handler))
502 g_signal_handler_disconnect (fixture->msg, fixture->restarted_handler);
503 g_queue_foreach (fixture->buffer_queue, (GFunc) soup_buffer_free, NULL);
504 g_queue_free (fixture->buffer_queue);
505 g_object_unref (fixture->msg);
506 g_object_unref (fixture->cache);
507 g_slice_free (SoupCacheWritingFixture, fixture);
511 close_ready_cb (GObject *source, GAsyncResult *result, SoupCacheWritingFixture *fixture)
513 SoupCacheEntry *entry = fixture->entry;
514 SoupCache *cache = fixture->cache;
515 GOutputStream *stream = G_OUTPUT_STREAM (source);
516 goffset content_length;
518 g_warn_if_fail (entry->error == NULL);
520 /* FIXME: what do we do on error ? */
523 g_output_stream_close_finish (stream, result, NULL);
524 g_object_unref (stream);
526 entry->stream = NULL;
528 content_length = soup_message_headers_get_content_length (entry->headers);
530 /* If the process was cancelled, then delete the entry from
531 the cache. Do it also if the size of a chunked resource is
532 too much for the cache */
533 if (g_cancellable_is_cancelled (entry->cancellable)) {
534 entry->dirty = FALSE;
535 soup_cache_entry_remove (cache, entry);
536 soup_cache_entry_free (entry, TRUE);
538 } else if ((soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CHUNKED) ||
539 entry->length != (gsize) content_length) {
540 /** Two options here:
542 * 1. "chunked" data, entry was temporarily added to
543 * cache (as content-length is 0) and now that we have
544 * the actual size we have to evaluate if we want it
545 * in the cache or not
547 * 2. Content-Length has a different value than actual
548 * length, means that the content was encoded for
549 * transmission (typically compressed) and thus we
550 * have to substract the content-length value that was
551 * added to the cache and add the unencoded length
553 gint length_to_add = entry->length - content_length;
555 /* Make room in cache if needed */
556 if (cache_accepts_entries_of_size (cache, length_to_add)) {
557 make_room_for_new_entry (cache, length_to_add);
559 cache->priv->size += length_to_add;
561 entry->dirty = FALSE;
562 soup_cache_entry_remove (cache, entry);
563 soup_cache_entry_free (entry, TRUE);
569 entry->dirty = FALSE;
570 entry->got_body = FALSE;
572 if (entry->current_writing_buffer) {
573 soup_buffer_free (entry->current_writing_buffer);
574 entry->current_writing_buffer = NULL;
577 g_object_unref (entry->cancellable);
578 entry->cancellable = NULL;
581 cache->priv->n_pending--;
584 soup_cache_writing_fixture_free (fixture);
588 write_ready_cb (GObject *source, GAsyncResult *result, SoupCacheWritingFixture *fixture)
590 GOutputStream *stream = G_OUTPUT_STREAM (source);
591 GError *error = NULL;
593 SoupCacheEntry *entry = fixture->entry;
595 if (g_cancellable_is_cancelled (entry->cancellable)) {
596 g_output_stream_close_async (stream,
599 (GAsyncReadyCallback)close_ready_cb,
604 write_size = g_output_stream_write_finish (stream, result, &error);
605 if (write_size <= 0 || error) {
607 entry->error = error;
608 g_output_stream_close_async (stream,
611 (GAsyncReadyCallback)close_ready_cb,
613 /* FIXME: We should completely stop caching the
614 resource at this point */
616 /* Are we still writing and is there new data to write
618 if (fixture->buffer_queue->length > 0)
619 write_next_buffer (entry, fixture);
621 soup_buffer_free (entry->current_writing_buffer);
622 entry->current_writing_buffer = NULL;
624 if (entry->got_body) {
625 /* If we already received 'got-body'
626 and we have written all the data,
627 we can close the stream */
628 g_output_stream_close_async (entry->stream,
631 (GAsyncReadyCallback)close_ready_cb,
639 write_next_buffer (SoupCacheEntry *entry, SoupCacheWritingFixture *fixture)
641 SoupBuffer *buffer = g_queue_pop_head (fixture->buffer_queue);
646 /* Free the old buffer */
647 if (entry->current_writing_buffer) {
648 soup_buffer_free (entry->current_writing_buffer);
649 entry->current_writing_buffer = NULL;
651 entry->current_writing_buffer = buffer;
653 g_output_stream_write_async (entry->stream, buffer->data, buffer->length,
654 G_PRIORITY_LOW, entry->cancellable,
655 (GAsyncReadyCallback) write_ready_cb,
661 msg_got_chunk_cb (SoupMessage *msg, SoupBuffer *chunk, SoupCacheWritingFixture *fixture)
663 SoupCacheEntry *entry = fixture->entry;
665 /* Ignore this if the writing or appending was cancelled */
666 if (!g_cancellable_is_cancelled (entry->cancellable)) {
667 g_queue_push_tail (fixture->buffer_queue, soup_buffer_copy (chunk));
668 entry->length += chunk->length;
670 if (!cache_accepts_entries_of_size (fixture->cache, entry->length)) {
671 /* Quickly cancel the caching of the resource */
672 g_cancellable_cancel (entry->cancellable);
676 /* FIXME: remove the error check when we cancel the caching at
677 the first write error */
678 /* Only write if the entry stream is ready */
679 if (entry->current_writing_buffer == NULL && entry->error == NULL && entry->stream)
680 write_next_buffer (entry, fixture);
684 msg_got_body_cb (SoupMessage *msg, SoupCacheWritingFixture *fixture)
686 SoupCacheEntry *entry = fixture->entry;
687 g_return_if_fail (entry);
689 entry->got_body = TRUE;
691 if (!entry->stream && fixture->buffer_queue->length > 0)
692 /* The stream is not ready to be written but we still
693 have data to write, we'll write it when the stream
694 is opened for writing */
698 if (fixture->buffer_queue->length > 0) {
699 /* If we still have data to write, write it,
700 write_ready_cb will close the stream */
701 if (entry->current_writing_buffer == NULL && entry->error == NULL && entry->stream)
702 write_next_buffer (entry, fixture);
706 if (entry->stream && entry->current_writing_buffer == NULL)
707 g_output_stream_close_async (entry->stream,
710 (GAsyncReadyCallback)close_ready_cb,
715 soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry)
719 /* if (entry->dirty && !g_cancellable_is_cancelled (entry->cancellable)) { */
721 g_cancellable_cancel (entry->cancellable);
725 g_assert (!entry->dirty);
726 g_assert (g_list_length (cache->priv->lru_start) == g_hash_table_size (cache->priv->cache));
728 /* Remove from cache */
729 if (!g_hash_table_remove (cache->priv->cache, entry->key))
732 /* Remove from LRU */
733 lru_item = g_list_find (cache->priv->lru_start, entry);
734 cache->priv->lru_start = g_list_delete_link (cache->priv->lru_start, lru_item);
736 /* Adjust cache size */
737 cache->priv->size -= entry->length;
739 g_assert (g_list_length (cache->priv->lru_start) == g_hash_table_size (cache->priv->cache));
745 lru_compare_func (gconstpointer a, gconstpointer b)
747 SoupCacheEntry *entry_a = (SoupCacheEntry *)a;
748 SoupCacheEntry *entry_b = (SoupCacheEntry *)b;
750 /** The rationale of this sorting func is
752 * 1. sort by hits -> LRU algorithm, then
754 * 2. sort by freshness lifetime, we better discard first
755 * entries that are close to expire
757 * 3. sort by size, replace first small size resources as they
758 * are cheaper to download
762 if (entry_a->hits != entry_b->hits)
763 return entry_a->hits - entry_b->hits;
765 /* Sort by freshness_lifetime */
766 if (entry_a->freshness_lifetime != entry_b->freshness_lifetime)
767 return entry_a->freshness_lifetime - entry_b->freshness_lifetime;
770 return entry_a->length - entry_b->length;
774 cache_accepts_entries_of_size (SoupCache *cache, guint length_to_add)
776 /* We could add here some more heuristics. TODO: review how
777 this is done by other HTTP caches */
779 return length_to_add <= cache->priv->max_entry_data_size;
783 make_room_for_new_entry (SoupCache *cache, guint length_to_add)
785 GList *lru_entry = cache->priv->lru_start;
787 /* Check that there is enough room for the new entry. This is
788 an approximation as we're not working out the size of the
789 cache file or the size of the headers for performance
790 reasons. TODO: check if that would be really that expensive */
793 (length_to_add + cache->priv->size > cache->priv->max_size)) {
794 SoupCacheEntry *old_entry = (SoupCacheEntry *)lru_entry->data;
796 /* Discard entries. Once cancelled resources will be
797 * freed in close_ready_cb
799 if (soup_cache_entry_remove (cache, old_entry)) {
800 soup_cache_entry_free (old_entry, TRUE);
801 lru_entry = cache->priv->lru_start;
803 lru_entry = g_list_next (lru_entry);
808 soup_cache_entry_insert_by_key (SoupCache *cache,
810 SoupCacheEntry *entry,
813 guint length_to_add = 0;
815 if (soup_message_headers_get_encoding (entry->headers) != SOUP_ENCODING_CHUNKED)
816 length_to_add = soup_message_headers_get_content_length (entry->headers);
818 /* Check if we are going to store the resource depending on its size */
820 if (!cache_accepts_entries_of_size (cache, length_to_add))
823 /* Make room for new entry if needed */
824 make_room_for_new_entry (cache, length_to_add);
827 g_hash_table_insert (cache->priv->cache, g_strdup (key), entry);
829 /* Compute new cache size */
830 cache->priv->size += length_to_add;
834 cache->priv->lru_start = g_list_insert_sorted (cache->priv->lru_start, entry, lru_compare_func);
836 cache->priv->lru_start = g_list_prepend (cache->priv->lru_start, entry);
838 g_assert (g_list_length (cache->priv->lru_start) == g_hash_table_size (cache->priv->cache));
844 msg_restarted_cb (SoupMessage *msg, SoupCacheEntry *entry)
846 /* FIXME: What should we do here exactly? */
850 replace_cb (GObject *source, GAsyncResult *result, SoupCacheWritingFixture *fixture)
852 SoupCacheEntry *entry = fixture->entry;
853 GOutputStream *stream = (GOutputStream *) g_file_replace_finish (G_FILE (source),
854 result, &entry->error);
856 if (g_cancellable_is_cancelled (entry->cancellable) || entry->error) {
858 g_object_unref (stream);
859 fixture->cache->priv->n_pending--;
860 entry->dirty = FALSE;
861 soup_cache_entry_remove (fixture->cache, entry);
862 soup_cache_entry_free (entry, TRUE);
863 soup_cache_writing_fixture_free (fixture);
867 entry->stream = stream;
869 /* If we already got all the data we have to initiate the
870 * writing here, since we won't get more 'got-chunk'
873 if (!entry->got_body)
876 /* It could happen that reading the data from server
877 * was completed before this happens. In that case
880 if (!write_next_buffer (entry, fixture))
881 /* Could happen if the resource is empty */
882 g_output_stream_close_async (stream, G_PRIORITY_LOW, entry->cancellable,
883 (GAsyncReadyCallback) close_ready_cb,
889 SoupSessionFeature *feature;
890 gulong got_headers_handler;
894 msg_got_headers_cb (SoupMessage *msg, gpointer user_data)
897 SoupCacheability cacheable;
898 RequestHelper *helper;
899 time_t request_time, response_time;
901 response_time = time (NULL);
903 helper = (RequestHelper *)user_data;
904 cache = SOUP_CACHE (helper->feature);
905 request_time = helper->request_time;
906 g_signal_handlers_disconnect_by_func (msg, msg_got_headers_cb, user_data);
907 g_slice_free (RequestHelper, helper);
909 cacheable = soup_cache_get_cacheability (cache, msg);
911 if (cacheable & SOUP_CACHE_CACHEABLE) {
912 SoupCacheEntry *entry;
915 SoupCacheWritingFixture *fixture;
917 /* Check if we are already caching this resource */
918 key = soup_message_get_cache_key (msg);
919 entry = g_hash_table_lookup (cache->priv->cache, key);
922 if (entry && entry->dirty)
925 /* Create a new entry, deleting any old one if present */
927 soup_cache_entry_remove (cache, entry);
928 soup_cache_entry_free (entry, TRUE);
931 entry = soup_cache_entry_new (cache, msg, request_time, response_time);
934 /* Do not continue if it can not be stored */
935 if (!soup_cache_entry_insert_by_key (cache, (const gchar *)entry->key, entry, TRUE)) {
936 soup_cache_entry_free (entry, TRUE);
940 fixture = g_slice_new0 (SoupCacheWritingFixture);
941 fixture->cache = g_object_ref (cache);
942 fixture->entry = entry;
943 fixture->msg = g_object_ref (msg);
944 fixture->buffer_queue = g_queue_new ();
946 /* We connect now to these signals and buffer the data
947 if it comes before the file is ready for writing */
948 fixture->got_chunk_handler =
949 g_signal_connect (msg, "got-chunk", G_CALLBACK (msg_got_chunk_cb), fixture);
950 fixture->got_body_handler =
951 g_signal_connect (msg, "got-body", G_CALLBACK (msg_got_body_cb), fixture);
952 fixture->restarted_handler =
953 g_signal_connect (msg, "restarted", G_CALLBACK (msg_restarted_cb), entry);
956 file = g_file_new_for_path (entry->filename);
957 cache->priv->n_pending++;
960 entry->cancellable = g_cancellable_new ();
961 g_file_replace_async (file, NULL, FALSE,
962 G_FILE_CREATE_PRIVATE | G_FILE_CREATE_REPLACE_DESTINATION,
963 G_PRIORITY_LOW, entry->cancellable,
964 (GAsyncReadyCallback) replace_cb, fixture);
965 g_object_unref (file);
966 } else if (cacheable & SOUP_CACHE_INVALIDATES) {
968 SoupCacheEntry *entry;
970 key = soup_message_get_cache_key (msg);
971 entry = g_hash_table_lookup (cache->priv->cache, key);
975 if (soup_cache_entry_remove (cache, entry))
976 soup_cache_entry_free (entry, TRUE);
978 } else if (cacheable & SOUP_CACHE_VALIDATES) {
980 SoupCacheEntry *entry;
982 key = soup_message_get_cache_key (msg);
983 entry = g_hash_table_lookup (cache->priv->cache, key);
986 /* It's possible to get a CACHE_VALIDATES with no
987 * entry in the hash table. This could happen if for
988 * example the soup client is the one creating the
989 * conditional request.
992 entry->being_validated = FALSE;
993 copy_end_to_end_headers (msg->response_headers, entry->headers);
994 soup_cache_entry_set_freshness (entry, msg, cache);
1000 soup_cache_send_response (SoupCache *cache, SoupMessage *msg)
1003 SoupCacheEntry *entry;
1005 GInputStream *stream = NULL;
1008 g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL);
1009 g_return_val_if_fail (SOUP_IS_MESSAGE (msg), NULL);
1011 key = soup_message_get_cache_key (msg);
1012 entry = g_hash_table_lookup (cache->priv->cache, key);
1014 g_return_val_if_fail (entry, NULL);
1016 /* TODO: the original idea was to save reads, but current code
1017 assumes that a stream is always returned. Need to reach
1018 some agreement here. Also we have to handle the situation
1019 were the file was no longer there (for example files
1020 removed without notifying the cache */
1021 file = g_file_new_for_path (entry->filename);
1022 stream = G_INPUT_STREAM (g_file_read (file, NULL, NULL));
1023 g_object_unref (file);
1025 /* Do not change the original message if there is no resource */
1029 /* If we are told to send a response from cache any validation
1030 in course is over by now */
1031 entry->being_validated = FALSE;
1034 soup_message_set_status (msg, entry->status_code);
1037 copy_end_to_end_headers (entry->headers, msg->response_headers);
1039 /* Add 'Age' header with the current age */
1040 current_age = g_strdup_printf ("%d", soup_cache_entry_get_current_age (entry));
1041 soup_message_headers_replace (msg->response_headers,
1044 g_free (current_age);
1050 request_started (SoupSessionFeature *feature, SoupSession *session,
1051 SoupMessage *msg, SoupSocket *socket)
1053 RequestHelper *helper = g_slice_new0 (RequestHelper);
1054 helper->request_time = time (NULL);
1055 helper->feature = feature;
1056 helper->got_headers_handler = g_signal_connect (msg, "got-headers",
1057 G_CALLBACK (msg_got_headers_cb),
1062 attach (SoupSessionFeature *feature, SoupSession *session)
1064 SoupCache *cache = SOUP_CACHE (feature);
1065 cache->priv->session = session;
1067 soup_cache_default_feature_interface->attach (feature, session);
1071 soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface,
1072 gpointer interface_data)
1074 soup_cache_default_feature_interface =
1075 g_type_default_interface_peek (SOUP_TYPE_SESSION_FEATURE);
1077 feature_interface->attach = attach;
1078 feature_interface->request_started = request_started;
1082 soup_cache_init (SoupCache *cache)
1084 SoupCachePrivate *priv;
1086 priv = cache->priv = SOUP_CACHE_GET_PRIVATE (cache);
1088 priv->cache = g_hash_table_new_full (g_str_hash,
1090 (GDestroyNotify)g_free,
1094 priv->lru_start = NULL;
1097 priv->n_pending = 0;
1100 priv->max_size = DEFAULT_MAX_SIZE;
1101 priv->max_entry_data_size = priv->max_size / MAX_ENTRY_DATA_PERCENTAGE;
1106 remove_cache_item (gpointer data,
1109 SoupCache *cache = (SoupCache *) user_data;
1110 SoupCacheEntry *entry = (SoupCacheEntry *) data;
1112 if (soup_cache_entry_remove (cache, entry))
1113 soup_cache_entry_free (entry, FALSE);
1117 soup_cache_finalize (GObject *object)
1119 SoupCachePrivate *priv;
1122 priv = SOUP_CACHE (object)->priv;
1124 // Cannot use g_hash_table_foreach as callbacks must not modify the hash table
1125 entries = g_hash_table_get_values (priv->cache);
1126 g_list_foreach (entries, remove_cache_item, object);
1127 g_list_free (entries);
1129 g_hash_table_destroy (priv->cache);
1130 g_free (priv->cache_dir);
1132 g_list_free (priv->lru_start);
1133 priv->lru_start = NULL;
1135 G_OBJECT_CLASS (soup_cache_parent_class)->finalize (object);
1139 soup_cache_set_property (GObject *object, guint prop_id,
1140 const GValue *value, GParamSpec *pspec)
1142 SoupCachePrivate *priv = SOUP_CACHE (object)->priv;
1145 case PROP_CACHE_DIR:
1146 priv->cache_dir = g_value_dup_string (value);
1147 /* Create directory if it does not exist (FIXME: should we?) */
1148 if (!g_file_test (priv->cache_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))
1149 g_mkdir_with_parents (priv->cache_dir, 0700);
1151 case PROP_CACHE_TYPE:
1152 priv->cache_type = g_value_get_enum (value);
1153 /* TODO: clear private entries and issue a warning if moving to shared? */
1156 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1162 soup_cache_get_property (GObject *object, guint prop_id,
1163 GValue *value, GParamSpec *pspec)
1165 SoupCachePrivate *priv = SOUP_CACHE (object)->priv;
1168 case PROP_CACHE_DIR:
1169 g_value_set_string (value, priv->cache_dir);
1171 case PROP_CACHE_TYPE:
1172 g_value_set_enum (value, priv->cache_type);
1175 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1181 soup_cache_constructed (GObject *object)
1183 SoupCachePrivate *priv;
1185 priv = SOUP_CACHE (object)->priv;
1187 if (!priv->cache_dir) {
1188 /* Set a default cache dir, different for each user */
1189 priv->cache_dir = g_build_filename (g_get_user_cache_dir (),
1192 if (!g_file_test (priv->cache_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))
1193 g_mkdir_with_parents (priv->cache_dir, 0700);
1196 if (G_OBJECT_CLASS (soup_cache_parent_class)->constructed)
1197 G_OBJECT_CLASS (soup_cache_parent_class)->constructed (object);
1201 soup_cache_class_init (SoupCacheClass *cache_class)
1203 GObjectClass *gobject_class = (GObjectClass *)cache_class;
1205 gobject_class->finalize = soup_cache_finalize;
1206 gobject_class->constructed = soup_cache_constructed;
1207 gobject_class->set_property = soup_cache_set_property;
1208 gobject_class->get_property = soup_cache_get_property;
1210 cache_class->get_cacheability = get_cacheability;
1212 g_object_class_install_property (gobject_class, PROP_CACHE_DIR,
1213 g_param_spec_string ("cache-dir",
1215 "The directory to store the cache files",
1217 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
1219 g_object_class_install_property (gobject_class, PROP_CACHE_TYPE,
1220 g_param_spec_enum ("cache-type",
1222 "Whether the cache is private or shared",
1223 SOUP_TYPE_CACHE_TYPE,
1224 SOUP_CACHE_SINGLE_USER,
1225 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
1227 g_type_class_add_private (cache_class, sizeof (SoupCachePrivate));
1232 * @cache_dir: the directory to store the cached data, or %NULL to use the default one
1233 * @cache_type: the #SoupCacheType of the cache
1235 * Creates a new #SoupCache.
1237 * Returns: a new #SoupCache
1242 soup_cache_new (const char *cache_dir, SoupCacheType cache_type)
1244 return g_object_new (SOUP_TYPE_CACHE,
1245 "cache-dir", cache_dir,
1246 "cache-type", cache_type,
1251 * soup_cache_has_response:
1252 * @cache: a #SoupCache
1253 * @msg: a #SoupMessage
1255 * This function calculates whether the @cache object has a proper
1256 * response for the request @msg given the flags both in the request
1257 * and the cached reply and the time ellapsed since it was cached.
1259 * Returns: whether or not the @cache has a valid response for @msg
1262 soup_cache_has_response (SoupCache *cache, SoupMessage *msg)
1265 SoupCacheEntry *entry;
1266 const char *cache_control, *pragma;
1268 int max_age, max_stale, min_fresh;
1269 GList *lru_item, *item;
1271 key = soup_message_get_cache_key (msg);
1272 entry = g_hash_table_lookup (cache->priv->cache, key);
1275 /* 1. The presented Request-URI and that of stored response
1279 return SOUP_CACHE_RESPONSE_STALE;
1281 /* Increase hit count. Take sorting into account */
1283 lru_item = g_list_find (cache->priv->lru_start, entry);
1285 while (item->next && lru_compare_func (item->data, item->next->data) > 0)
1286 item = g_list_next (item);
1288 if (item != lru_item) {
1289 cache->priv->lru_start = g_list_remove_link (cache->priv->lru_start, lru_item);
1290 item = g_list_insert_sorted (item, lru_item->data, lru_compare_func);
1291 g_list_free (lru_item);
1294 if (entry->dirty || entry->being_validated)
1295 return SOUP_CACHE_RESPONSE_STALE;
1297 /* 2. The request method associated with the stored response
1298 * allows it to be used for the presented request
1301 /* In practice this means we only return our resource for GET,
1302 * cacheability for other methods is a TODO in the RFC
1303 * (TODO: although we could return the headers for HEAD
1306 if (msg->method != SOUP_METHOD_GET)
1307 return SOUP_CACHE_RESPONSE_STALE;
1309 /* 3. Selecting request-headers nominated by the stored
1310 * response (if any) match those presented.
1315 /* 4. The request is a conditional request issued by the client.
1317 if (soup_message_headers_get (msg->request_headers, "If-Modified-Since") ||
1318 soup_message_headers_get (msg->request_headers, "If-None-Match"))
1319 return SOUP_CACHE_RESPONSE_STALE;
1321 /* 5. The presented request and stored response are free from
1322 * directives that would prevent its use.
1325 max_age = max_stale = min_fresh = -1;
1327 /* For HTTP 1.0 compatibility. RFC2616 section 14.9.4
1329 pragma = soup_message_headers_get (msg->request_headers, "Pragma");
1330 if (pragma && soup_header_contains (pragma, "no-cache"))
1331 return SOUP_CACHE_RESPONSE_STALE;
1333 cache_control = soup_message_headers_get (msg->request_headers, "Cache-Control");
1334 if (cache_control) {
1335 GHashTable *hash = soup_header_parse_param_list (cache_control);
1337 if (g_hash_table_lookup_extended (hash, "no-store", NULL, NULL)) {
1338 soup_header_free_param_list (hash);
1339 return SOUP_CACHE_RESPONSE_STALE;
1342 if (g_hash_table_lookup_extended (hash, "no-cache", NULL, NULL)) {
1343 soup_header_free_param_list (hash);
1344 return SOUP_CACHE_RESPONSE_STALE;
1347 if (g_hash_table_lookup_extended (hash, "max-age", NULL, &value)) {
1348 max_age = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32);
1349 /* Forcing cache revalidaton
1352 soup_header_free_param_list (hash);
1353 return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
1357 /* max-stale can have no value set, we need to use _extended */
1358 if (g_hash_table_lookup_extended (hash, "max-stale", NULL, &value)) {
1360 max_stale = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32);
1362 max_stale = G_MAXINT32;
1365 value = g_hash_table_lookup (hash, "min-fresh");
1367 min_fresh = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32);
1369 soup_header_free_param_list (hash);
1372 guint current_age = soup_cache_entry_get_current_age (entry);
1374 /* If we are over max-age and max-stale is not
1375 set, do not use the value from the cache
1376 without validation */
1377 if ((guint) max_age <= current_age && max_stale == -1)
1378 return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
1382 /* 6. The stored response is either: fresh, allowed to be
1383 * served stale or succesfully validated
1385 /* TODO consider also proxy-revalidate & s-maxage */
1386 if (entry->must_revalidate)
1387 return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
1389 if (!soup_cache_entry_is_fresh_enough (entry, min_fresh)) {
1390 /* Not fresh, can it be served stale? */
1391 if (max_stale != -1) {
1392 /* G_MAXINT32 means we accept any staleness */
1393 if (max_stale == G_MAXINT32)
1394 return SOUP_CACHE_RESPONSE_FRESH;
1396 if ((soup_cache_entry_get_current_age (entry) - entry->freshness_lifetime) <= (guint) max_stale)
1397 return SOUP_CACHE_RESPONSE_FRESH;
1400 return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
1403 return SOUP_CACHE_RESPONSE_FRESH;
1407 * soup_cache_get_cacheability:
1408 * @cache: a #SoupCache
1409 * @msg: a #SoupMessage
1411 * Calculates whether the @msg can be cached or not.
1413 * Returns: a #SoupCacheability value indicating whether the @msg can be cached or not.
1416 soup_cache_get_cacheability (SoupCache *cache, SoupMessage *msg)
1418 g_return_val_if_fail (SOUP_IS_CACHE (cache), SOUP_CACHE_UNCACHEABLE);
1419 g_return_val_if_fail (SOUP_IS_MESSAGE (msg), SOUP_CACHE_UNCACHEABLE);
1421 return SOUP_CACHE_GET_CLASS (cache)->get_cacheability (cache, msg);
1425 force_flush_timeout (gpointer data)
1427 gboolean *forced = (gboolean *)data;
1435 * @cache: a #SoupCache
1436 * @session: the #SoupSession associated with the @cache
1438 * This function will force all pending writes in the @cache to be
1439 * committed to disk. For doing so it will iterate the #GMainContext
1440 * associated with the @session (which can be the default one) as long
1444 soup_cache_flush (SoupCache *cache)
1446 GMainContext *async_context;
1447 SoupSession *session;
1449 gboolean forced = FALSE;
1451 g_return_if_fail (SOUP_IS_CACHE (cache));
1453 session = cache->priv->session;
1454 g_return_if_fail (SOUP_IS_SESSION (session));
1455 async_context = soup_session_get_async_context (session);
1457 /* We give cache 10 secs to finish */
1458 timeout_id = g_timeout_add (10000, force_flush_timeout, &forced);
1460 while (!forced && cache->priv->n_pending > 0)
1461 g_main_context_iteration (async_context, FALSE);
1464 g_source_remove (timeout_id);
1466 g_warning ("Cache flush finished despite %d pending requests", cache->priv->n_pending);
1470 clear_cache_item (gpointer data,
1473 SoupCache *cache = (SoupCache *) user_data;
1474 SoupCacheEntry *entry = (SoupCacheEntry *) data;
1476 if (soup_cache_entry_remove (cache, entry))
1477 soup_cache_entry_free (entry, TRUE);
1482 * @cache: a #SoupCache
1484 * Will remove all entries in the @cache plus all the cache files
1485 * associated with them.
1488 soup_cache_clear (SoupCache *cache)
1493 g_return_if_fail (SOUP_IS_CACHE (cache));
1495 hash = cache->priv->cache;
1496 g_return_if_fail (hash);
1498 // Cannot use g_hash_table_foreach as callbacks must not modify the hash table
1499 entries = g_hash_table_get_values (hash);
1500 g_list_foreach (entries, clear_cache_item, cache);
1501 g_list_free (entries);
1505 soup_cache_generate_conditional_request (SoupCache *cache, SoupMessage *original)
1509 SoupCacheEntry *entry;
1513 g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL);
1514 g_return_val_if_fail (SOUP_IS_MESSAGE (original), NULL);
1516 /* First copy the data we need from the original message */
1517 uri = soup_message_get_uri (original);
1518 msg = soup_message_new_from_uri (original->method, uri);
1520 soup_message_headers_foreach (original->request_headers,
1521 (SoupMessageHeadersForeachFunc)copy_headers,
1522 msg->request_headers);
1524 /* Now add the validator entries in the header from the cached
1526 key = soup_message_get_cache_key (original);
1527 entry = g_hash_table_lookup (cache->priv->cache, key);
1530 g_return_val_if_fail (entry, NULL);
1532 entry->being_validated = TRUE;
1534 value = soup_message_headers_get (entry->headers, "Last-Modified");
1536 soup_message_headers_append (msg->request_headers,
1537 "If-Modified-Since",
1539 value = soup_message_headers_get (entry->headers, "ETag");
1541 soup_message_headers_append (msg->request_headers,
1547 #define OLD_SOUP_CACHE_FILE "soup.cache"
1548 #define SOUP_CACHE_FILE "soup.cache2"
1550 #define SOUP_CACHE_HEADERS_FORMAT "{ss}"
1551 #define SOUP_CACHE_PHEADERS_FORMAT "(ssbuuuuuqa" SOUP_CACHE_HEADERS_FORMAT ")"
1552 #define SOUP_CACHE_ENTRIES_FORMAT "(qa" SOUP_CACHE_PHEADERS_FORMAT ")"
1554 /* Basically the same format than above except that some strings are
1555 prepended with &. This way the GVariant returns a pointer to the
1556 data instead of duplicating the string */
1557 #define SOUP_CACHE_DECODE_HEADERS_FORMAT "{&s&s}"
1560 pack_entry (gpointer data,
1563 SoupCacheEntry *entry = (SoupCacheEntry *) data;
1564 SoupMessageHeadersIter iter;
1565 const gchar *header_key, *header_value;
1566 GVariantBuilder *entries_builder = (GVariantBuilder *)user_data;
1568 /* Do not store non-consolidated entries */
1569 if (entry->dirty || entry->current_writing_buffer != NULL || !entry->key)
1572 g_variant_builder_open (entries_builder, G_VARIANT_TYPE (SOUP_CACHE_PHEADERS_FORMAT));
1573 g_variant_builder_add (entries_builder, "s", entry->key);
1574 g_variant_builder_add (entries_builder, "s", entry->filename);
1575 g_variant_builder_add (entries_builder, "b", entry->must_revalidate);
1576 g_variant_builder_add (entries_builder, "u", entry->freshness_lifetime);
1577 g_variant_builder_add (entries_builder, "u", entry->corrected_initial_age);
1578 g_variant_builder_add (entries_builder, "u", entry->response_time);
1579 g_variant_builder_add (entries_builder, "u", entry->hits);
1580 g_variant_builder_add (entries_builder, "u", entry->length);
1581 g_variant_builder_add (entries_builder, "q", entry->status_code);
1584 g_variant_builder_open (entries_builder, G_VARIANT_TYPE ("a" SOUP_CACHE_HEADERS_FORMAT));
1585 soup_message_headers_iter_init (&iter, entry->headers);
1586 while (soup_message_headers_iter_next (&iter, &header_key, &header_value)) {
1587 if (g_utf8_validate (header_value, -1, NULL))
1588 g_variant_builder_add (entries_builder, SOUP_CACHE_HEADERS_FORMAT,
1589 header_key, header_value);
1591 g_variant_builder_close (entries_builder); /* "a" SOUP_CACHE_HEADERS_FORMAT */
1592 g_variant_builder_close (entries_builder); /* SOUP_CACHE_PHEADERS_FORMAT */
1596 soup_cache_dump (SoupCache *cache)
1598 SoupCachePrivate *priv = SOUP_CACHE_GET_PRIVATE (cache);
1600 GVariantBuilder entries_builder;
1601 GVariant *cache_variant;
1603 if (!g_list_length (cache->priv->lru_start))
1606 /* Create the builder and iterate over all entries */
1607 g_variant_builder_init (&entries_builder, G_VARIANT_TYPE (SOUP_CACHE_ENTRIES_FORMAT));
1608 g_variant_builder_add (&entries_builder, "q", SOUP_CACHE_CURRENT_VERSION);
1609 g_variant_builder_open (&entries_builder, G_VARIANT_TYPE ("a" SOUP_CACHE_PHEADERS_FORMAT));
1610 g_list_foreach (cache->priv->lru_start, pack_entry, &entries_builder);
1611 g_variant_builder_close (&entries_builder);
1613 /* Serialize and dump */
1614 cache_variant = g_variant_builder_end (&entries_builder);
1615 g_variant_ref_sink (cache_variant);
1616 filename = g_build_filename (priv->cache_dir, SOUP_CACHE_FILE, NULL);
1617 g_file_set_contents (filename, (const gchar *)g_variant_get_data (cache_variant),
1618 g_variant_get_size (cache_variant), NULL);
1620 g_variant_unref (cache_variant);
1624 clear_cache_files (SoupCache *cache)
1626 GFileInfo *file_info;
1627 GFileEnumerator *file_enumerator;
1628 GFile *cache_dir_file = g_file_new_for_path (cache->priv->cache_dir);
1630 file_enumerator = g_file_enumerate_children (cache_dir_file, G_FILE_ATTRIBUTE_STANDARD_NAME,
1631 G_FILE_QUERY_INFO_NONE, NULL, NULL);
1632 if (file_enumerator) {
1633 while ((file_info = g_file_enumerator_next_file (file_enumerator, NULL, NULL)) != NULL) {
1634 const gchar *filename = g_file_info_get_name (file_info);
1636 if (strcmp (filename, SOUP_CACHE_FILE) != 0) {
1637 GFile *cache_file = g_file_get_child (cache_dir_file, filename);
1638 g_file_delete (cache_file, NULL, NULL);
1639 g_object_unref (cache_file);
1642 g_object_unref (file_enumerator);
1644 g_object_unref (cache_dir_file);
1648 soup_cache_load (SoupCache *cache)
1650 gboolean must_revalidate;
1651 guint32 freshness_lifetime, hits;
1652 guint32 corrected_initial_age, response_time;
1653 char *key, *filename = NULL, *contents = NULL;
1654 GVariant *cache_variant;
1655 GVariantIter *entries_iter = NULL, *headers_iter = NULL;
1656 gsize length, items;
1657 SoupCacheEntry *entry;
1658 SoupCachePrivate *priv = cache->priv;
1659 guint16 version, status_code;
1661 filename = g_build_filename (priv->cache_dir, SOUP_CACHE_FILE, NULL);
1662 if (!g_file_get_contents (filename, &contents, &length, NULL)) {
1665 clear_cache_files (cache);
1670 cache_variant = g_variant_new_from_data (G_VARIANT_TYPE (SOUP_CACHE_ENTRIES_FORMAT),
1671 (const gchar *) contents, length, FALSE, g_free, contents);
1672 g_variant_get (cache_variant, SOUP_CACHE_ENTRIES_FORMAT, &version, &entries_iter);
1673 if (version != SOUP_CACHE_CURRENT_VERSION) {
1674 g_variant_iter_free (entries_iter);
1675 g_variant_unref (cache_variant);
1676 clear_cache_files (cache);
1680 items = g_variant_iter_n_children (entries_iter);
1682 while (g_variant_iter_loop (entries_iter, SOUP_CACHE_PHEADERS_FORMAT,
1683 &key, &filename, &must_revalidate,
1684 &freshness_lifetime, &corrected_initial_age,
1685 &response_time, &hits, &length, &status_code,
1687 const gchar *header_key, *header_value;
1688 SoupMessageHeaders *headers;
1689 SoupMessageHeadersIter soup_headers_iter;
1691 /* SoupMessage Headers */
1692 headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
1693 while (g_variant_iter_loop (headers_iter, SOUP_CACHE_HEADERS_FORMAT, &header_key, &header_value))
1694 if (*header_key && *header_value)
1695 soup_message_headers_append (headers, header_key, header_value);
1697 /* Check that we have headers */
1698 soup_message_headers_iter_init (&soup_headers_iter, headers);
1699 if (!soup_message_headers_iter_next (&soup_headers_iter, &header_key, &header_value)) {
1700 soup_message_headers_free (headers);
1704 /* Insert in cache */
1705 entry = g_slice_new0 (SoupCacheEntry);
1706 entry->key = g_strdup (key);
1707 entry->filename = g_strdup (filename);
1708 entry->must_revalidate = must_revalidate;
1709 entry->freshness_lifetime = freshness_lifetime;
1710 entry->corrected_initial_age = corrected_initial_age;
1711 entry->response_time = response_time;
1713 entry->length = length;
1714 entry->headers = headers;
1715 entry->status_code = status_code;
1717 if (!soup_cache_entry_insert_by_key (cache, entry->key, entry, FALSE))
1718 soup_cache_entry_free (entry, TRUE);
1721 cache->priv->lru_start = g_list_reverse (cache->priv->lru_start);
1724 g_variant_iter_free (entries_iter);
1725 g_variant_unref (cache_variant);
1729 soup_cache_set_max_size (SoupCache *cache,
1732 cache->priv->max_size = max_size;
1733 cache->priv->max_entry_data_size = cache->priv->max_size / MAX_ENTRY_DATA_PERCENTAGE;
1737 soup_cache_get_max_size (SoupCache *cache)
1739 return cache->priv->max_size;