* soup-cache.c
*
* Copyright (C) 2009, 2010 Igalia S.L.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
* - Need to hook the feature in the sync SoupSession.
* - Need more tests.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
#include <glib/gstdio.h>
#include "soup-cache.h"
#include "soup-body-input-stream.h"
#include "soup-cache-client-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-message-headers-private.h"
#include "soup.h"
#include "soup-message-metrics-private.h"
#include "soup-misc.h"
#include "soup-session-private.h"
#include "soup-session-feature-private.h"
* SoupCache:
*
* File-based cache for HTTP resources.
*/
* SoupCacheability:
* @SOUP_CACHE_CACHEABLE: The message should be cached
* @SOUP_CACHE_UNCACHEABLE: The message shouldn't be cached
* @SOUP_CACHE_INVALIDATES: The messages cache should be invalidated
* @SOUP_CACHE_VALIDATES: The messages cache should be updated
*
* Indicates if a message should or shouldn't be cached.
*/
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
of the cache that can be
filled by a single entry */
* Version 2: cache is now saved in soup.cache2. Added the version
* number to the beginning of the file.
*
* Version 3: added HTTP status code to the cache entries.
*
* Version 4: replaced several types.
* - freshness_lifetime,corrected_initial_age,response_time: time_t -> guint32
* - status_code: guint -> guint16
* - hits: guint -> guint32
*
* Version 5: key is no longer stored on disk as it can be easily
* built from the URI. Apart from that some fields in the
* SoupCacheEntry have changed:
* - entry key is now a uint32 instead of a (char *).
* - added uri, used to check for collisions
* - removed filename, it's built from the entry key.
*/
#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 ")"
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;
guint32 freshness_lifetime;
gboolean must_revalidate;
gsize length;
guint32 corrected_initial_age;
guint32 response_time;
gboolean dirty;
gboolean being_validated;
SoupMessageHeaders *headers;
guint32 hits;
GCancellable *cancellable;
guint16 status_code;
} SoupCacheEntry;
typedef struct {
char *cache_dir;
GMutex mutex;
GHashTable *cache;
guint n_pending;
SoupSession *session;
SoupCacheType cache_type;
guint size;
guint max_size;
guint max_entry_data_size;
GList *lru_start;
} SoupCachePrivate;
enum {
PROP_0,
PROP_CACHE_DIR,
PROP_CACHE_TYPE,
LAST_PROPERTY
};
static GParamSpec *properties[LAST_PROPERTY] = { NULL, };
G_DEFINE_TYPE_WITH_CODE (SoupCache, soup_cache, G_TYPE_OBJECT,
G_ADD_PRIVATE (SoupCache)
G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
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, 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 GFile *
get_file_from_entry (SoupCache *cache, SoupCacheEntry *entry)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
char *filename = g_strdup_printf ("%s%s%u", priv->cache_dir,
G_DIR_SEPARATOR_S, (guint) entry->key);
GFile *file = g_file_new_for_path (filename);
g_free (filename);
return file;
}
static SoupCacheability
get_cacheability (SoupCache *cache, SoupMessage *msg)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
SoupCacheability cacheability;
const char *cache_control, *content_type;
gboolean has_max_age = FALSE;
if (soup_message_get_method (msg) == SOUP_METHOD_GET)
cacheability = SOUP_CACHE_CACHEABLE;
else if (soup_message_get_method (msg) == SOUP_METHOD_HEAD ||
soup_message_get_method (msg) == SOUP_METHOD_TRACE ||
soup_message_get_method (msg) == SOUP_METHOD_CONNECT)
return SOUP_CACHE_UNCACHEABLE;
else
return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES);
content_type = soup_message_headers_get_content_type (soup_message_get_response_headers (msg), NULL);
if (content_type && !g_ascii_strcasecmp (content_type, "multipart/x-mixed-replace"))
return SOUP_CACHE_UNCACHEABLE;
cache_control = soup_message_headers_get_list_common (soup_message_get_response_headers (msg), SOUP_HEADER_CACHE_CONTROL);
if (cache_control && *cache_control) {
GHashTable *hash;
hash = soup_header_parse_param_list (cache_control);
if (priv->cache_type == SOUP_CACHE_SHARED) {
if (g_hash_table_lookup_extended (hash, "private", NULL, NULL)) {
soup_header_free_param_list (hash);
return SOUP_CACHE_UNCACHEABLE;
}
}
* headers
*/
if (g_hash_table_lookup_extended (hash, "no-store", NULL, NULL)) {
soup_header_free_param_list (hash);
return SOUP_CACHE_UNCACHEABLE;
}
if (g_hash_table_lookup_extended (hash, "max-age", NULL, NULL))
has_max_age = TRUE;
* sense to check it too?
*/
if (g_hash_table_lookup_extended (hash, "no-cache", NULL, NULL)) {
soup_header_free_param_list (hash);
return SOUP_CACHE_UNCACHEABLE;
}
soup_header_free_param_list (hash);
}
if ((g_uri_get_query (soup_message_get_uri (msg))) &&
!soup_message_headers_get_one_common (soup_message_get_response_headers (msg), SOUP_HEADER_EXPIRES) &&
!has_max_age)
return SOUP_CACHE_UNCACHEABLE;
switch (soup_message_get_status (msg)) {
case SOUP_STATUS_PARTIAL_CONTENT:
* invalidate cached full responses if the headers
* don't match.
*/
cacheability = SOUP_CACHE_UNCACHEABLE;
break;
case SOUP_STATUS_NOT_MODIFIED:
cacheability = SOUP_CACHE_VALIDATES;
break;
case SOUP_STATUS_MULTIPLE_CHOICES:
case SOUP_STATUS_MOVED_PERMANENTLY:
case SOUP_STATUS_GONE:
cacheability = SOUP_CACHE_UNCACHEABLE;
break;
case SOUP_STATUS_FOUND:
case SOUP_STATUS_TEMPORARY_REDIRECT:
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:
* is uncacheable but doesn't break the cache.
*/
if ((soup_message_get_status (msg) >= SOUP_STATUS_BAD_REQUEST &&
soup_message_get_status (msg) <= SOUP_STATUS_FAILED_DEPENDENCY) ||
soup_message_get_status (msg) >= SOUP_STATUS_INTERNAL_SERVER_ERROR)
return SOUP_CACHE_UNCACHEABLE;
* the cache.
*/
if ((soup_message_get_status (msg) > SOUP_STATUS_PARTIAL_CONTENT &&
soup_message_get_status (msg) < SOUP_STATUS_MULTIPLE_CHOICES) ||
(soup_message_get_status (msg) > SOUP_STATUS_TEMPORARY_REDIRECT &&
soup_message_get_status (msg) < SOUP_STATUS_INTERNAL_SERVER_ERROR))
return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES);
break;
}
return cacheability;
}
* and also unref's the GFile object representing it.
*/
static void
soup_cache_entry_free (SoupCacheEntry *entry)
{
g_free (entry->uri);
g_clear_pointer (&entry->headers, soup_message_headers_unref);
g_clear_object (&entry->cancellable);
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 void
remove_headers (const char *name, const char *value, SoupMessageHeaders *headers)
{
soup_message_headers_remove (headers, name);
}
static SoupHeaderName hop_by_hop_headers[] = {
SOUP_HEADER_CONNECTION,
SOUP_HEADER_KEEP_ALIVE,
SOUP_HEADER_PROXY_AUTHENTICATE,
SOUP_HEADER_PROXY_AUTHORIZATION,
SOUP_HEADER_TE,
SOUP_HEADER_TRAILER,
SOUP_HEADER_TRANSFER_ENCODING,
SOUP_HEADER_UPGRADE
};
static void
copy_end_to_end_headers (SoupMessageHeaders *source, SoupMessageHeaders *destination)
{
int i;
soup_message_headers_foreach (source, (SoupMessageHeadersForeachFunc) copy_headers, destination);
for (i = 0; i < G_N_ELEMENTS (hop_by_hop_headers); i++)
soup_message_headers_remove_common (destination, hop_by_hop_headers[i]);
soup_message_headers_clean_connection_headers (destination);
}
static guint
soup_cache_entry_get_current_age (SoupCacheEntry *entry)
{
time_t now = time (NULL);
time_t resident_time;
resident_time = now - entry->response_time;
return entry->corrected_initial_age + resident_time;
}
static gboolean
soup_cache_entry_is_fresh_enough (SoupCacheEntry *entry, gint min_fresh)
{
guint limit = (min_fresh == -1) ? soup_cache_entry_get_current_age (entry) : (guint) min_fresh;
return entry->freshness_lifetime > limit;
}
static inline guint32
get_cache_key_from_uri (const char *uri)
{
return (guint32) g_str_hash (uri);
}
static void
soup_cache_entry_set_freshness (SoupCacheEntry *entry, SoupMessage *msg, SoupCache *cache)
{
const char *cache_control;
const char *expires, *date, *last_modified;
* revalidations overwrite previous values for the headers.
*/
entry->must_revalidate = FALSE;
entry->freshness_lifetime = 0;
cache_control = soup_message_headers_get_list_common (entry->headers, SOUP_HEADER_CACHE_CONTROL);
if (cache_control && *cache_control) {
const char *max_age, *s_maxage;
gint64 freshness_lifetime = 0;
GHashTable *hash;
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
hash = soup_header_parse_param_list (cache_control);
entry->must_revalidate = g_hash_table_lookup_extended (hash, "must-revalidate", NULL, NULL);
if (priv->cache_type == SOUP_CACHE_SHARED) {
s_maxage = g_hash_table_lookup (hash, "s-maxage");
if (s_maxage) {
freshness_lifetime = g_ascii_strtoll (s_maxage, NULL, 10);
if (freshness_lifetime) {
entry->must_revalidate = TRUE;
soup_header_free_param_list (hash);
return;
}
}
}
max_age = g_hash_table_lookup (hash, "max-age");
if (max_age)
freshness_lifetime = g_ascii_strtoll (max_age, NULL, 10);
if (freshness_lifetime) {
entry->freshness_lifetime = (guint32) MIN (freshness_lifetime, G_MAXUINT32);
soup_header_free_param_list (hash);
return;
}
soup_header_free_param_list (hash);
}
* minus the value of the 'Date' response header
*/
expires = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_EXPIRES);
date = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_DATE);
if (expires && date) {
GDateTime *expires_d, *date_d;
gint64 expires_t, date_t;
expires_d = soup_date_time_new_from_http_string (expires);
if (expires_d) {
date_d = soup_date_time_new_from_http_string (date);
expires_t = g_date_time_to_unix (expires_d);
date_t = g_date_time_to_unix (date_d);
g_date_time_unref (expires_d);
g_date_time_unref (date_d);
if (expires_t && date_t) {
entry->freshness_lifetime = (guint32) MAX (expires_t - date_t, 0);
return;
}
} else {
treat it as already expired, see section
3.3 */
entry->freshness_lifetime = 0;
return;
}
}
these status codes (section 2.3.1.1) */
if (entry->status_code != SOUP_STATUS_OK &&
entry->status_code != SOUP_STATUS_NON_AUTHORITATIVE &&
entry->status_code != SOUP_STATUS_PARTIAL_CONTENT &&
entry->status_code != SOUP_STATUS_MULTIPLE_CHOICES &&
entry->status_code != SOUP_STATUS_MOVED_PERMANENTLY &&
entry->status_code != SOUP_STATUS_GONE)
goto expire;
than 24h (section 2.3.1.1) when using heuristics */
last_modified = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_LAST_MODIFIED);
if (last_modified) {
GDateTime *soup_date;
gint64 now, last_modified_t;
soup_date = soup_date_time_new_from_http_string (last_modified);
last_modified_t = g_date_time_to_unix (soup_date);
now = time (NULL);
#define HEURISTIC_FACTOR 0.1
entry->freshness_lifetime = MAX (0, (now - last_modified_t) * HEURISTIC_FACTOR);
g_date_time_unref (soup_date);
}
return;
expire:
entry->freshness_lifetime = 0;
}
static SoupCacheEntry *
soup_cache_entry_new (SoupCache *cache, SoupMessage *msg, time_t request_time, time_t response_time)
{
SoupCacheEntry *entry;
const char *date;
entry = g_slice_new0 (SoupCacheEntry);
entry->dirty = FALSE;
entry->being_validated = FALSE;
entry->status_code = soup_message_get_status (msg);
entry->response_time = response_time;
entry->uri = g_uri_to_string_partial (soup_message_get_uri (msg), G_URI_HIDE_PASSWORD);
entry->headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
copy_end_to_end_headers (soup_message_get_response_headers (msg), entry->headers);
entry->hits = 0;
soup_cache_entry_set_freshness (entry, msg, cache);
date = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_DATE);
if (date) {
GDateTime *soup_date;
const char *age;
gint64 date_value, apparent_age, corrected_received_age, response_delay, age_value = 0;
soup_date = soup_date_time_new_from_http_string (date);
date_value = g_date_time_to_unix (soup_date);
g_date_time_unref (soup_date);
age = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_AGE);
if (age)
age_value = g_ascii_strtoll (age, NULL, 10);
apparent_age = MAX (0, entry->response_time - date_value);
corrected_received_age = MAX (apparent_age, age_value);
response_delay = entry->response_time - request_time;
entry->corrected_initial_age = corrected_received_age + response_delay;
} else {
entry->corrected_initial_age = time (NULL);
}
return entry;
}
static gboolean
soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry, gboolean purge)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
GList *lru_item;
if (entry->dirty) {
g_cancellable_cancel (entry->cancellable);
return FALSE;
}
g_assert (!entry->dirty);
g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache));
if (!g_hash_table_remove (priv->cache, GUINT_TO_POINTER (entry->key))) {
g_mutex_unlock (&priv->mutex);
return FALSE;
}
lru_item = g_list_find (priv->lru_start, entry);
priv->lru_start = g_list_delete_link (priv->lru_start, lru_item);
priv->size -= entry->length;
g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache));
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;
}
static gint
lru_compare_func (gconstpointer a, gconstpointer b)
{
SoupCacheEntry *entry_a = (SoupCacheEntry *)a;
SoupCacheEntry *entry_b = (SoupCacheEntry *)b;
*
* 1. sort by hits -> LRU algorithm, then
*
* 2. sort by freshness lifetime, we better discard first
* entries that are close to expire
*
* 3. sort by size, replace first small size resources as they
* are cheaper to download
*/
if (entry_a->hits != entry_b->hits)
return entry_a->hits - entry_b->hits;
if (entry_a->freshness_lifetime != entry_b->freshness_lifetime)
return entry_a->freshness_lifetime - entry_b->freshness_lifetime;
return entry_a->length - entry_b->length;
}
static gboolean
cache_accepts_entries_of_size (SoupCache *cache, guint length_to_add)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
this is done by other HTTP caches */
return length_to_add <= priv->max_entry_data_size;
}
static void
make_room_for_new_entry (SoupCache *cache, guint length_to_add)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
GList *lru_entry = priv->lru_start;
an approximation as we're not working out the size of the
cache file or the size of the headers for performance
reasons. TODO: check if that would be really that expensive */
while (lru_entry &&
(length_to_add + priv->size > priv->max_size)) {
SoupCacheEntry *old_entry = (SoupCacheEntry *)lru_entry->data;
* freed in close_ready_cb
*/
if (soup_cache_entry_remove (cache, old_entry, TRUE))
lru_entry = priv->lru_start;
else
lru_entry = g_list_next (lru_entry);
}
}
static gboolean
soup_cache_entry_insert (SoupCache *cache,
SoupCacheEntry *entry,
gboolean sort)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
guint length_to_add = 0;
SoupCacheEntry *old_entry;
entry->key = get_cache_key_from_uri ((const char *) entry->uri);
if (soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CONTENT_LENGTH)
length_to_add = soup_message_headers_get_content_length (entry->headers);
if (length_to_add) {
if (!cache_accepts_entries_of_size (cache, length_to_add))
return FALSE;
make_room_for_new_entry (cache, length_to_add);
}
if ((old_entry = g_hash_table_lookup (priv->cache, GUINT_TO_POINTER (entry->key))) != NULL) {
if (!soup_cache_entry_remove (cache, old_entry, TRUE))
return FALSE;
}
g_hash_table_insert (priv->cache, GUINT_TO_POINTER (entry->key), entry);
priv->size += length_to_add;
if (sort)
priv->lru_start = g_list_insert_sorted (priv->lru_start, entry, lru_compare_func);
else
priv->lru_start = g_list_prepend (priv->lru_start, entry);
g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache));
return TRUE;
}
static SoupCacheEntry*
soup_cache_entry_lookup (SoupCache *cache,
SoupMessage *msg)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
SoupCacheEntry *entry;
guint32 key;
char *uri = NULL;
uri = g_uri_to_string_partial (soup_message_get_uri (msg), G_URI_HIDE_PASSWORD);
key = get_cache_key_from_uri ((const char *) uri);
entry = g_hash_table_lookup (priv->cache, GUINT_TO_POINTER (key));
if (entry != NULL && (strcmp (entry->uri, uri) != 0))
entry = NULL;
g_free (uri);
return entry;
}
GInputStream *
soup_cache_send_response (SoupCache *cache, SoupMessage *msg)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
SoupCacheEntry *entry;
GInputStream *file_stream, *body_stream, *cache_stream, *client_stream;
GFile *file;
SoupMessageMetrics *metrics;
g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL);
g_return_val_if_fail (SOUP_IS_MESSAGE (msg), NULL);
soup_message_set_metrics_timestamp (msg, SOUP_MESSAGE_METRICS_REQUEST_START);
g_mutex_lock (&priv->mutex);
entry = soup_cache_entry_lookup (cache, msg);
g_mutex_unlock (&priv->mutex);
g_return_val_if_fail (entry, NULL);
file = get_file_from_entry (cache, entry);
file_stream = G_INPUT_STREAM (g_file_read (file, NULL, NULL));
g_object_unref (file);
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;
metrics = soup_message_get_metrics (msg);
if (metrics)
metrics->response_body_size = entry->length;
in course is over by now */
entry->being_validated = FALSE;
soup_message_starting (msg);
soup_message_set_metrics_timestamp (msg, SOUP_MESSAGE_METRICS_RESPONSE_START);
soup_message_set_status (msg, entry->status_code, NULL);
copy_end_to_end_headers (entry->headers, soup_message_get_response_headers (msg));
soup_message_disable_feature (msg, SOUP_TYPE_CACHE);
cache_stream = soup_session_setup_message_body_input_stream (priv->session,
msg, body_stream,
SOUP_STAGE_ENTITY_BODY);
g_object_unref (body_stream);
client_stream = soup_cache_client_input_stream_new (cache_stream);
g_object_unref (cache_stream);
return client_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
msg_starting_cb (SoupMessage *msg, gpointer user_data)
{
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), user_data);
g_signal_handlers_disconnect_by_func (msg, msg_starting_cb, user_data);
}
static void
request_queued (SoupSessionFeature *feature,
SoupMessage *msg)
{
g_signal_connect (msg, "starting", G_CALLBACK (msg_starting_cb), feature);
}
static void
attach (SoupSessionFeature *feature, SoupSession *session)
{
SoupCache *cache = SOUP_CACHE (feature);
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
priv->session = session;
}
static void
soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface,
gpointer interface_data)
{
feature_interface->attach = attach;
feature_interface->request_queued = request_queued;
}
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;
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
SoupCacheEntry *entry = helper->entry;
g_mutex_lock (&priv->mutex);
--priv->n_pending;
entry->dirty = FALSE;
entry->length = bytes_written;
g_clear_object (&entry->cancellable);
if (error) {
if (soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CONTENT_LENGTH)
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);
priv->size += entry->length;
} else {
soup_cache_entry_remove (cache, entry, TRUE);
helper->entry = entry = NULL;
}
}
cleanup:
g_mutex_unlock (&priv->mutex);
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;
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
SoupCacheEntry *entry;
SoupCacheability cacheability;
GInputStream *istream;
GFile *file;
StreamHelper *helper;
time_t request_time, response_time;
g_mutex_lock (&priv->mutex);
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);
g_mutex_unlock (&priv->mutex);
return NULL;
}
if (cacheability & SOUP_CACHE_VALIDATES) {
* 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);
g_mutex_unlock (&priv->mutex);
return NULL;
}
if (!(cacheability & SOUP_CACHE_CACHEABLE)) {
g_mutex_unlock (&priv->mutex);
return NULL;
}
if (entry && (entry->dirty || entry->being_validated)) {
g_mutex_unlock (&priv->mutex);
return NULL;
}
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), "response-time"));
entry = soup_cache_entry_new (cache, msg, request_time, response_time);
entry->hits = 1;
entry->dirty = TRUE;
if (!soup_cache_entry_insert (cache, entry, TRUE)) {
soup_cache_entry_free (entry);
g_mutex_unlock (&priv->mutex);
return NULL;
}
entry->cancellable = g_cancellable_new ();
++priv->n_pending;
g_mutex_unlock (&priv->mutex);
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)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
priv->cache = g_hash_table_new (g_direct_hash, g_direct_equal);
priv->lru_start = NULL;
priv->n_pending = 0;
priv->max_size = DEFAULT_MAX_SIZE;
priv->max_entry_data_size = priv->max_size / MAX_ENTRY_DATA_PERCENTAGE;
priv->size = 0;
g_mutex_init (&priv->mutex);
}
static void
remove_cache_item (gpointer data,
gpointer user_data)
{
soup_cache_entry_remove ((SoupCache *) user_data, (SoupCacheEntry *) data, FALSE);
}
static void
soup_cache_finalize (GObject *object)
{
SoupCachePrivate *priv = soup_cache_get_instance_private ((SoupCache*)object);
GList *entries;
entries = g_hash_table_get_values (priv->cache);
g_list_foreach (entries, remove_cache_item, object);
g_list_free (entries);
g_hash_table_destroy (priv->cache);
g_free (priv->cache_dir);
g_list_free (priv->lru_start);
g_mutex_clear (&priv->mutex);
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_get_instance_private ((SoupCache*)object);
switch (prop_id) {
case PROP_CACHE_DIR:
g_assert (!priv->cache_dir);
priv->cache_dir = g_value_dup_string (value);
if (!priv->cache_dir)
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);
break;
case PROP_CACHE_TYPE:
priv->cache_type = g_value_get_enum (value);
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_get_instance_private ((SoupCache*)object);
switch (prop_id) {
case PROP_CACHE_DIR:
g_value_set_string (value, priv->cache_dir);
break;
case PROP_CACHE_TYPE:
g_value_set_enum (value, priv->cache_type);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
soup_cache_class_init (SoupCacheClass *cache_class)
{
GObjectClass *gobject_class = (GObjectClass *)cache_class;
gobject_class->finalize = soup_cache_finalize;
gobject_class->set_property = soup_cache_set_property;
gobject_class->get_property = soup_cache_get_property;
cache_class->get_cacheability = get_cacheability;
* SoupCache:cache-dir:
* The directory to store the cache files.
*/
properties[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_PARAM_STATIC_STRINGS);
* SoupCache:cache-type:
* Whether the cache is private or shared.
*/
properties[PROP_CACHE_TYPE] =
g_param_spec_enum ("cache-type",
"Cache type",
"Whether the cache is private or shared",
SOUP_TYPE_CACHE_TYPE,
SOUP_CACHE_SINGLE_USER,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (gobject_class, LAST_PROPERTY, properties);
}
* SoupCacheType:
* @SOUP_CACHE_SINGLE_USER: a single-user cache
* @SOUP_CACHE_SHARED: a shared cache
*
* The type of cache; this affects what kinds of responses will be
* saved.
*
*/
* soup_cache_new:
* @cache_dir: (nullable): the directory to store the cached data, or %NULL
* to use the default one. Note that since the cache isn't safe to access for
* multiple processes at once, and the default directory isn't namespaced by
* process, clients are strongly discouraged from passing %NULL.
* @cache_type: the #SoupCacheType of the cache
*
* Creates a new #SoupCache.
*
* Returns: a new #SoupCache
*
*/
SoupCache *
soup_cache_new (const char *cache_dir, SoupCacheType cache_type)
{
return g_object_new (SOUP_TYPE_CACHE,
"cache-dir", cache_dir,
"cache-type", cache_type,
NULL);
}
* soup_cache_has_response:
* @cache: a #SoupCache
* @msg: a #SoupMessage
*
* This function calculates whether the @cache object has a proper
* response for the request @msg given the flags both in the request
* and the cached reply and the time ellapsed since it was cached.
*
* Returns: whether or not the @cache has a valid response for @msg
*
*/
SoupCacheResponse
soup_cache_has_response (SoupCache *cache, SoupMessage *msg)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
SoupCacheEntry *entry;
const char *cache_control;
gpointer value;
int max_age, max_stale, min_fresh;
GList *lru_item, *item;
g_mutex_lock (&priv->mutex);
entry = soup_cache_entry_lookup (cache, msg);
* match
*/
if (!entry) {
g_mutex_unlock (&priv->mutex);
return SOUP_CACHE_RESPONSE_STALE;
}
entry->hits++;
lru_item = g_list_find (priv->lru_start, entry);
item = lru_item;
while (item->next && lru_compare_func (item->data, item->next->data) > 0)
item = g_list_next (item);
if (item != lru_item) {
priv->lru_start = g_list_remove_link (priv->lru_start, lru_item);
item = g_list_insert_sorted (item, lru_item->data, lru_compare_func);
(void)item;
g_list_free (lru_item);
}
g_mutex_unlock (&priv->mutex);
if (entry->dirty || entry->being_validated)
return SOUP_CACHE_RESPONSE_STALE;
* allows it to be used for the presented request
*/
* cacheability for other methods is a TODO in the RFC
* (TODO: although we could return the headers for HEAD
* probably).
*/
if (soup_message_get_method (msg) != SOUP_METHOD_GET)
return SOUP_CACHE_RESPONSE_STALE;
* response (if any) match those presented.
*/
*/
if (soup_message_headers_get_one_common (soup_message_get_request_headers (msg), SOUP_HEADER_IF_MODIFIED_SINCE) ||
soup_message_headers_get_list_common (soup_message_get_request_headers (msg), SOUP_HEADER_IF_NONE_MATCH))
return SOUP_CACHE_RESPONSE_STALE;
* directives that would prevent its use.
*/
max_age = max_stale = min_fresh = -1;
*/
if (soup_message_headers_header_contains_common (soup_message_get_request_headers (msg), SOUP_HEADER_PRAGMA, "no-cache"))
return SOUP_CACHE_RESPONSE_STALE;
cache_control = soup_message_headers_get_list_common (soup_message_get_request_headers (msg), SOUP_HEADER_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)) {
soup_header_free_param_list (hash);
return SOUP_CACHE_RESPONSE_STALE;
}
if (g_hash_table_lookup_extended (hash, "no-cache", NULL, NULL)) {
soup_header_free_param_list (hash);
return SOUP_CACHE_RESPONSE_STALE;
}
if (g_hash_table_lookup_extended (hash, "max-age", NULL, &value) && value) {
max_age = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32);
*/
if (!max_age) {
soup_header_free_param_list (hash);
return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
}
}
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);
soup_header_free_param_list (hash);
if (max_age > 0) {
guint current_age = soup_cache_entry_get_current_age (entry);
set, do not use the value from the cache
without validation */
if ((guint) max_age <= current_age && max_stale == -1)
return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
}
}
* served stale or succesfully validated
*/
if (!soup_cache_entry_is_fresh_enough (entry, min_fresh)) {
* response received by a cache, that cache MUST NOT
* use the entry after it becomes stale
*/
if (entry->must_revalidate)
return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
if (max_stale != -1) {
if (max_stale == G_MAXINT32)
return SOUP_CACHE_RESPONSE_FRESH;
if ((soup_cache_entry_get_current_age (entry) - entry->freshness_lifetime) <= (guint) max_stale)
return SOUP_CACHE_RESPONSE_FRESH;
}
return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION;
}
return SOUP_CACHE_RESPONSE_FRESH;
}
* soup_cache_get_cacheability:
* @cache: a #SoupCache
* @msg: a #SoupMessage
*
* Calculates whether the @msg can be cached or not.
*
* Returns: a #SoupCacheability value indicating whether the @msg can be cached
* or not.
*/
SoupCacheability
soup_cache_get_cacheability (SoupCache *cache, SoupMessage *msg)
{
g_return_val_if_fail (SOUP_IS_CACHE (cache), SOUP_CACHE_UNCACHEABLE);
g_return_val_if_fail (SOUP_IS_MESSAGE (msg), SOUP_CACHE_UNCACHEABLE);
return SOUP_CACHE_GET_CLASS (cache)->get_cacheability (cache, msg);
}
static gboolean
force_flush_timeout (gpointer data)
{
gboolean *forced = (gboolean *)data;
*forced = TRUE;
return FALSE;
}
* soup_cache_flush:
* @cache: a #SoupCache
*
* Forces all pending writes in the @cache to be
* committed to disk.
*
* For doing so it will iterate the [struct@GLib.MainContext] associated with
* @cache's session as long as needed.
*
* Contrast with [method@Cache.dump], which writes out the cache index file.
*/
void
soup_cache_flush (SoupCache *cache)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
GMainContext *async_context;
SoupSession *session;
GSource *timeout;
gboolean forced = FALSE;
g_return_if_fail (SOUP_IS_CACHE (cache));
session = priv->session;
g_return_if_fail (SOUP_IS_SESSION (session));
async_context = g_main_context_get_thread_default ();
timeout = soup_add_timeout (async_context, 10000, force_flush_timeout, &forced);
while (!forced && priv->n_pending > 0)
g_main_context_iteration (async_context, FALSE);
if (!forced)
g_source_destroy (timeout);
else
g_warning ("Cache flush finished despite %d pending requests", priv->n_pending);
g_source_unref (timeout);
}
typedef void (* SoupCacheForeachFileFunc) (SoupCache *cache, const char *name, gpointer user_data);
static void
soup_cache_foreach_file (SoupCache *cache, SoupCacheForeachFileFunc func, gpointer user_data)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
GDir *dir;
const char *name;
dir = g_dir_open (priv->cache_dir, 0, NULL);
while ((name = g_dir_read_name (dir))) {
if (g_str_has_prefix (name, "soup."))
continue;
func (cache, name, user_data);
}
g_dir_close (dir);
}
static void
clear_cache_item (gpointer data,
gpointer user_data)
{
soup_cache_entry_remove ((SoupCache *) user_data, (SoupCacheEntry *) data, TRUE);
}
static void
delete_cache_file (SoupCache *cache, const char *name, gpointer user_data)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
gchar *path;
path = g_build_filename (priv->cache_dir, name, NULL);
g_unlink (path);
g_free (path);
}
static void
clear_cache_files (SoupCache *cache)
{
soup_cache_foreach_file (cache, delete_cache_file, NULL);
}
* soup_cache_clear:
* @cache: a #SoupCache
*
* Will remove all entries in the @cache plus all the cache files.
*
* This is not thread safe and must be called only from the thread that created the #SoupCache
*/
void
soup_cache_clear (SoupCache *cache)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
GList *entries;
g_return_if_fail (SOUP_IS_CACHE (cache));
g_return_if_fail (priv->cache);
entries = g_hash_table_get_values (priv->cache);
g_list_foreach (entries, clear_cache_item, cache);
g_list_free (entries);
clear_cache_files (cache);
}
SoupMessage *
soup_cache_generate_conditional_request (SoupCache *cache, SoupMessage *original)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
SoupMessage *msg;
GUri *uri;
SoupCacheEntry *entry;
const char *last_modified, *etag;
GList *disabled_features, *f;
g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL);
g_return_val_if_fail (SOUP_IS_MESSAGE (original), NULL);
g_mutex_lock (&priv->mutex);
entry = soup_cache_entry_lookup (cache, original);
g_mutex_unlock (&priv->mutex);
g_return_val_if_fail (entry, NULL);
last_modified = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_LAST_MODIFIED);
etag = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_ETAG);
if (!last_modified && !etag)
return NULL;
entry->being_validated = TRUE;
uri = soup_message_get_uri (original);
msg = soup_message_new_from_uri (soup_message_get_method (original), uri);
soup_message_set_flags (msg, soup_message_get_flags (original));
soup_message_disable_feature (msg, SOUP_TYPE_CACHE);
soup_message_headers_foreach (soup_message_get_request_headers (original),
(SoupMessageHeadersForeachFunc)copy_headers,
soup_message_get_request_headers (msg));
disabled_features = soup_message_get_disabled_features (original);
for (f = disabled_features; f; f = f->next)
soup_message_disable_feature (msg, (GType) GPOINTER_TO_SIZE (f->data));
g_list_free (disabled_features);
if (last_modified)
soup_message_headers_append_common (soup_message_get_request_headers (msg),
SOUP_HEADER_IF_MODIFIED_SINCE,
last_modified);
if (etag)
soup_message_headers_append_common (soup_message_get_request_headers (msg),
SOUP_HEADER_IF_NONE_MATCH,
etag);
return msg;
}
void
soup_cache_cancel_conditional_request (SoupCache *cache,
SoupMessage *msg)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
SoupCacheEntry *entry;
g_mutex_lock (&priv->mutex);
entry = soup_cache_entry_lookup (cache, msg);
g_mutex_unlock (&priv->mutex);
if (entry)
entry->being_validated = FALSE;
soup_session_cancel_message (priv->session, msg);
}
void
soup_cache_update_from_conditional_request (SoupCache *cache,
SoupMessage *msg)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
SoupCacheEntry *entry;
g_mutex_lock (&priv->mutex);
entry = soup_cache_entry_lookup (cache, msg);
g_mutex_unlock (&priv->mutex);
if (!entry)
return;
entry->being_validated = FALSE;
if (soup_message_get_status (msg) == SOUP_STATUS_NOT_MODIFIED) {
soup_message_headers_foreach (soup_message_get_response_headers (msg),
(SoupMessageHeadersForeachFunc) remove_headers,
entry->headers);
copy_end_to_end_headers (soup_message_get_response_headers (msg), entry->headers);
soup_cache_entry_set_freshness (entry, msg, cache);
}
}
static void
pack_entry (gpointer data,
gpointer user_data)
{
SoupCacheEntry *entry = (SoupCacheEntry *) data;
SoupMessageHeadersIter iter;
const char *header_key, *header_value;
GVariantBuilder *entries_builder = (GVariantBuilder *)user_data;
if (entry->dirty || !entry->key)
return;
g_variant_builder_open (entries_builder, G_VARIANT_TYPE (SOUP_CACHE_PHEADERS_FORMAT));
g_variant_builder_add (entries_builder, "s", entry->uri);
g_variant_builder_add (entries_builder, "b", entry->must_revalidate);
g_variant_builder_add (entries_builder, "u", entry->freshness_lifetime);
g_variant_builder_add (entries_builder, "u", entry->corrected_initial_age);
g_variant_builder_add (entries_builder, "u", entry->response_time);
g_variant_builder_add (entries_builder, "u", entry->hits);
g_variant_builder_add (entries_builder, "u", entry->length);
g_variant_builder_add (entries_builder, "q", entry->status_code);
g_variant_builder_open (entries_builder, G_VARIANT_TYPE ("a" SOUP_CACHE_HEADERS_FORMAT));
soup_message_headers_iter_init (&iter, entry->headers);
while (soup_message_headers_iter_next (&iter, &header_key, &header_value)) {
if (g_utf8_validate (header_value, -1, NULL))
g_variant_builder_add (entries_builder, SOUP_CACHE_HEADERS_FORMAT,
header_key, header_value);
}
g_variant_builder_close (entries_builder);
g_variant_builder_close (entries_builder);
}
* soup_cache_dump:
* @cache: a #SoupCache
*
* Synchronously writes the cache index out to disk.
*
* Contrast with [method@Cache.flush], which writes pending cache *entries* to
* disk.
*
* You must call this before exiting if you want your cache data to
* persist between sessions.
*
* This is not thread safe and must be called only from the thread that created the #SoupCache
*/
void
soup_cache_dump (SoupCache *cache)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
char *filename;
GVariantBuilder entries_builder;
GVariant *cache_variant;
if (!g_list_length (priv->lru_start))
return;
g_variant_builder_init (&entries_builder, G_VARIANT_TYPE (SOUP_CACHE_ENTRIES_FORMAT));
g_variant_builder_add (&entries_builder, "q", SOUP_CACHE_CURRENT_VERSION);
g_variant_builder_open (&entries_builder, G_VARIANT_TYPE ("a" SOUP_CACHE_PHEADERS_FORMAT));
g_list_foreach (priv->lru_start, pack_entry, &entries_builder);
g_variant_builder_close (&entries_builder);
cache_variant = g_variant_builder_end (&entries_builder);
g_variant_ref_sink (cache_variant);
filename = g_build_filename (priv->cache_dir, SOUP_CACHE_FILE, NULL);
g_file_set_contents (filename, (const char *) g_variant_get_data (cache_variant),
g_variant_get_size (cache_variant), NULL);
g_free (filename);
g_variant_unref (cache_variant);
}
static inline guint32
get_key_from_cache_filename (const char *name)
{
guint64 key;
key = g_ascii_strtoull (name, NULL, 10);
return key ? (guint32)key : 0;
}
static void
insert_cache_file (SoupCache *cache, const char *name, GHashTable *leaked_entries)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
gchar *path;
path = g_build_filename (priv->cache_dir, name, NULL);
if (g_file_test (path, G_FILE_TEST_IS_REGULAR)) {
guint32 key = get_key_from_cache_filename (name);
if (key) {
g_hash_table_insert (leaked_entries, GUINT_TO_POINTER (key), path);
return;
}
}
g_free (path);
}
* soup_cache_load:
* @cache: a #SoupCache
*
* Loads the contents of @cache's index into memory.
*
* This is not thread safe and must be called only from the thread that created the #SoupCache
*/
void
soup_cache_load (SoupCache *cache)
{
gboolean must_revalidate;
guint32 freshness_lifetime, hits;
guint32 corrected_initial_age, response_time;
char *url, *filename = NULL, *contents = NULL;
GVariant *cache_variant;
GVariantIter *entries_iter = NULL, *headers_iter = NULL;
gsize length;
SoupCacheEntry *entry;
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
guint16 version, status_code;
GHashTable *leaked_entries = NULL;
GHashTableIter iter;
gpointer value;
filename = g_build_filename (priv->cache_dir, SOUP_CACHE_FILE, NULL);
if (!g_file_get_contents (filename, &contents, &length, NULL)) {
g_free (filename);
g_free (contents);
clear_cache_files (cache);
return;
}
g_free (filename);
cache_variant = g_variant_new_from_data (G_VARIANT_TYPE (SOUP_CACHE_ENTRIES_FORMAT),
(const char *) contents, length, FALSE, g_free, contents);
g_variant_get (cache_variant, SOUP_CACHE_ENTRIES_FORMAT, &version, &entries_iter);
if (version != SOUP_CACHE_CURRENT_VERSION) {
g_variant_iter_free (entries_iter);
g_variant_unref (cache_variant);
clear_cache_files (cache);
return;
}
leaked_entries = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free);
soup_cache_foreach_file (cache, (SoupCacheForeachFileFunc)insert_cache_file, leaked_entries);
while (g_variant_iter_loop (entries_iter, SOUP_CACHE_PHEADERS_FORMAT,
&url, &must_revalidate, &freshness_lifetime, &corrected_initial_age,
&response_time, &hits, &length, &status_code,
&headers_iter)) {
const char *header_key, *header_value;
SoupMessageHeaders *headers;
SoupMessageHeadersIter soup_headers_iter;
headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
while (g_variant_iter_loop (headers_iter, SOUP_CACHE_HEADERS_FORMAT, &header_key, &header_value))
if (*header_key && *header_value)
soup_message_headers_append (headers, header_key, header_value);
soup_message_headers_iter_init (&soup_headers_iter, headers);
if (!soup_message_headers_iter_next (&soup_headers_iter, &header_key, &header_value)) {
soup_message_headers_unref (headers);
continue;
}
entry = g_slice_new0 (SoupCacheEntry);
entry->uri = g_strdup (url);
entry->must_revalidate = must_revalidate;
entry->freshness_lifetime = freshness_lifetime;
entry->corrected_initial_age = corrected_initial_age;
entry->response_time = response_time;
entry->hits = hits;
entry->length = length;
entry->headers = headers;
entry->status_code = status_code;
if (!soup_cache_entry_insert (cache, entry, FALSE))
soup_cache_entry_free (entry);
else
g_hash_table_remove (leaked_entries, GUINT_TO_POINTER (entry->key));
}
g_hash_table_iter_init (&iter, leaked_entries);
while (g_hash_table_iter_next (&iter, NULL, &value))
g_unlink ((char *)value);
g_hash_table_destroy (leaked_entries);
priv->lru_start = g_list_reverse (priv->lru_start);
g_variant_iter_free (entries_iter);
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.
*/
void
soup_cache_set_max_size (SoupCache *cache,
guint max_size)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
priv->max_size = max_size;
priv->max_entry_data_size = priv->max_size / MAX_ENTRY_DATA_PERCENTAGE;
}
* soup_cache_get_max_size:
* @cache: a #SoupCache
*
* Gets the maximum size of the cache.
*
* Returns: the maximum size of the cache, in bytes.
*/
guint
soup_cache_get_max_size (SoupCache *cache)
{
SoupCachePrivate *priv = soup_cache_get_instance_private (cache);
return priv->max_size;
}