/src/libsoup/libsoup/cache/soup-cache.c
Line | Count | Source |
1 | | /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ |
2 | | /* |
3 | | * soup-cache.c |
4 | | * |
5 | | * Copyright (C) 2009, 2010 Igalia S.L. |
6 | | * |
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. |
11 | | * |
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. |
16 | | * |
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. |
21 | | */ |
22 | | |
23 | | /* TODO: |
24 | | * - Need to hook the feature in the sync SoupSession. |
25 | | * - Need more tests. |
26 | | */ |
27 | | |
28 | | #ifdef HAVE_CONFIG_H |
29 | | #include <config.h> |
30 | | #endif |
31 | | |
32 | | #include <string.h> |
33 | | #include <glib/gstdio.h> |
34 | | |
35 | | #include "soup-cache.h" |
36 | | #include "soup-body-input-stream.h" |
37 | | #include "soup-cache-client-input-stream.h" |
38 | | #include "soup-cache-input-stream.h" |
39 | | #include "soup-cache-private.h" |
40 | | #include "soup-content-processor.h" |
41 | | #include "soup-message-private.h" |
42 | | #include "soup-message-headers-private.h" |
43 | | #include "soup.h" |
44 | | #include "soup-message-metrics-private.h" |
45 | | #include "soup-misc.h" |
46 | | #include "soup-session-private.h" |
47 | | #include "soup-session-feature-private.h" |
48 | | |
49 | | /** |
50 | | * SoupCache: |
51 | | * |
52 | | * File-based cache for HTTP resources. |
53 | | */ |
54 | | |
55 | | /** |
56 | | * SoupCacheability: |
57 | | * @SOUP_CACHE_CACHEABLE: The message should be cached |
58 | | * @SOUP_CACHE_UNCACHEABLE: The message shouldn't be cached |
59 | | * @SOUP_CACHE_INVALIDATES: The messages cache should be invalidated |
60 | | * @SOUP_CACHE_VALIDATES: The messages cache should be updated |
61 | | * |
62 | | * Indicates if a message should or shouldn't be cached. |
63 | | */ |
64 | | |
65 | | static void soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data); |
66 | | |
67 | | static SoupContentProcessorInterface *soup_cache_default_content_processor_interface; |
68 | | static void soup_cache_content_processor_init (SoupContentProcessorInterface *interface, gpointer interface_data); |
69 | | |
70 | 0 | #define DEFAULT_MAX_SIZE (50 * 1024 * 1024) |
71 | 0 | #define MAX_ENTRY_DATA_PERCENTAGE 10 /* Percentage of the total size |
72 | | of the cache that can be |
73 | | filled by a single entry */ |
74 | | |
75 | | /* |
76 | | * Version 2: cache is now saved in soup.cache2. Added the version |
77 | | * number to the beginning of the file. |
78 | | * |
79 | | * Version 3: added HTTP status code to the cache entries. |
80 | | * |
81 | | * Version 4: replaced several types. |
82 | | * - freshness_lifetime,corrected_initial_age,response_time: time_t -> guint32 |
83 | | * - status_code: guint -> guint16 |
84 | | * - hits: guint -> guint32 |
85 | | * |
86 | | * Version 5: key is no longer stored on disk as it can be easily |
87 | | * built from the URI. Apart from that some fields in the |
88 | | * SoupCacheEntry have changed: |
89 | | * - entry key is now a uint32 instead of a (char *). |
90 | | * - added uri, used to check for collisions |
91 | | * - removed filename, it's built from the entry key. |
92 | | */ |
93 | 0 | #define SOUP_CACHE_CURRENT_VERSION 5 |
94 | | |
95 | | #define OLD_SOUP_CACHE_FILE "soup.cache" |
96 | 0 | #define SOUP_CACHE_FILE "soup.cache2" |
97 | | |
98 | 0 | #define SOUP_CACHE_HEADERS_FORMAT "{ss}" |
99 | 0 | #define SOUP_CACHE_PHEADERS_FORMAT "(sbuuuuuqa" SOUP_CACHE_HEADERS_FORMAT ")" |
100 | 0 | #define SOUP_CACHE_ENTRIES_FORMAT "(qa" SOUP_CACHE_PHEADERS_FORMAT ")" |
101 | | |
102 | | /* Basically the same format than above except that some strings are |
103 | | prepended with &. This way the GVariant returns a pointer to the |
104 | | data instead of duplicating the string */ |
105 | | #define SOUP_CACHE_DECODE_HEADERS_FORMAT "{&s&s}" |
106 | | |
107 | | |
108 | | typedef struct _SoupCacheEntry { |
109 | | guint32 key; |
110 | | char *uri; |
111 | | guint32 freshness_lifetime; |
112 | | gboolean must_revalidate; |
113 | | gsize length; |
114 | | guint32 corrected_initial_age; |
115 | | guint32 response_time; |
116 | | gboolean dirty; |
117 | | gboolean being_validated; |
118 | | SoupMessageHeaders *headers; |
119 | | guint32 hits; |
120 | | GCancellable *cancellable; |
121 | | guint16 status_code; |
122 | | } SoupCacheEntry; |
123 | | |
124 | | typedef struct { |
125 | | char *cache_dir; |
126 | | GMutex mutex; |
127 | | GHashTable *cache; |
128 | | guint n_pending; |
129 | | SoupSession *session; |
130 | | SoupCacheType cache_type; |
131 | | guint size; |
132 | | guint max_size; |
133 | | guint max_entry_data_size; /* Computed value. Here for performance reasons */ |
134 | | GList *lru_start; |
135 | | } SoupCachePrivate; |
136 | | |
137 | | enum { |
138 | | PROP_0, |
139 | | PROP_CACHE_DIR, |
140 | | PROP_CACHE_TYPE, |
141 | | |
142 | | LAST_PROPERTY |
143 | | }; |
144 | | |
145 | | static GParamSpec *properties[LAST_PROPERTY] = { NULL, }; |
146 | | |
147 | 0 | G_DEFINE_TYPE_WITH_CODE (SoupCache, soup_cache, G_TYPE_OBJECT, |
148 | 0 | G_ADD_PRIVATE (SoupCache) |
149 | 0 | G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE, |
150 | 0 | soup_cache_session_feature_init) |
151 | 0 | G_IMPLEMENT_INTERFACE (SOUP_TYPE_CONTENT_PROCESSOR, |
152 | 0 | soup_cache_content_processor_init)) |
153 | 0 |
|
154 | 0 | static gboolean soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry, gboolean purge); |
155 | 0 | static void make_room_for_new_entry (SoupCache *cache, guint length_to_add); |
156 | 0 | static gboolean cache_accepts_entries_of_size (SoupCache *cache, guint length_to_add); |
157 | 0 |
|
158 | 0 | static GFile * |
159 | 0 | get_file_from_entry (SoupCache *cache, SoupCacheEntry *entry) |
160 | 0 | { |
161 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
162 | 0 | char *filename = g_strdup_printf ("%s%s%u", priv->cache_dir, |
163 | 0 | G_DIR_SEPARATOR_S, (guint) entry->key); |
164 | 0 | GFile *file = g_file_new_for_path (filename); |
165 | 0 | g_free (filename); |
166 | |
|
167 | 0 | return file; |
168 | 0 | } |
169 | | |
170 | | static SoupCacheability |
171 | | get_cacheability (SoupCache *cache, SoupMessage *msg) |
172 | 0 | { |
173 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
174 | 0 | SoupCacheability cacheability; |
175 | 0 | const char *cache_control, *content_type; |
176 | 0 | gboolean has_max_age = FALSE; |
177 | | |
178 | | /* 1. The request method must be cacheable */ |
179 | 0 | if (soup_message_get_method (msg) == SOUP_METHOD_GET) |
180 | 0 | cacheability = SOUP_CACHE_CACHEABLE; |
181 | 0 | else if (soup_message_get_method (msg) == SOUP_METHOD_HEAD || |
182 | 0 | soup_message_get_method (msg) == SOUP_METHOD_TRACE || |
183 | 0 | soup_message_get_method (msg) == SOUP_METHOD_CONNECT) |
184 | 0 | return SOUP_CACHE_UNCACHEABLE; |
185 | 0 | else |
186 | 0 | return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES); |
187 | | |
188 | 0 | content_type = soup_message_headers_get_content_type (soup_message_get_response_headers (msg), NULL); |
189 | 0 | if (content_type && !g_ascii_strcasecmp (content_type, "multipart/x-mixed-replace")) |
190 | 0 | return SOUP_CACHE_UNCACHEABLE; |
191 | | |
192 | 0 | cache_control = soup_message_headers_get_list_common (soup_message_get_response_headers (msg), SOUP_HEADER_CACHE_CONTROL); |
193 | 0 | if (cache_control && *cache_control) { |
194 | 0 | GHashTable *hash; |
195 | |
|
196 | 0 | hash = soup_header_parse_param_list (cache_control); |
197 | | |
198 | | /* Shared caches MUST NOT store private resources */ |
199 | 0 | if (priv->cache_type == SOUP_CACHE_SHARED) { |
200 | 0 | if (g_hash_table_lookup_extended (hash, "private", NULL, NULL)) { |
201 | 0 | soup_header_free_param_list (hash); |
202 | 0 | return SOUP_CACHE_UNCACHEABLE; |
203 | 0 | } |
204 | 0 | } |
205 | | |
206 | | /* 2. The 'no-store' cache directive does not appear in the |
207 | | * headers |
208 | | */ |
209 | 0 | if (g_hash_table_lookup_extended (hash, "no-store", NULL, NULL)) { |
210 | 0 | soup_header_free_param_list (hash); |
211 | 0 | return SOUP_CACHE_UNCACHEABLE; |
212 | 0 | } |
213 | | |
214 | 0 | if (g_hash_table_lookup_extended (hash, "max-age", NULL, NULL)) |
215 | 0 | has_max_age = TRUE; |
216 | | |
217 | | /* This does not appear in section 2.1, but I think it makes |
218 | | * sense to check it too? |
219 | | */ |
220 | 0 | if (g_hash_table_lookup_extended (hash, "no-cache", NULL, NULL)) { |
221 | 0 | soup_header_free_param_list (hash); |
222 | 0 | return SOUP_CACHE_UNCACHEABLE; |
223 | 0 | } |
224 | | |
225 | 0 | soup_header_free_param_list (hash); |
226 | 0 | } |
227 | | |
228 | | /* Section 13.9 */ |
229 | 0 | if ((g_uri_get_query (soup_message_get_uri (msg))) && |
230 | 0 | !soup_message_headers_get_one_common (soup_message_get_response_headers (msg), SOUP_HEADER_EXPIRES) && |
231 | 0 | !has_max_age) |
232 | 0 | return SOUP_CACHE_UNCACHEABLE; |
233 | | |
234 | 0 | switch (soup_message_get_status (msg)) { |
235 | 0 | case SOUP_STATUS_PARTIAL_CONTENT: |
236 | | /* We don't cache partial responses, but they only |
237 | | * invalidate cached full responses if the headers |
238 | | * don't match. |
239 | | */ |
240 | 0 | cacheability = SOUP_CACHE_UNCACHEABLE; |
241 | 0 | break; |
242 | | |
243 | 0 | case SOUP_STATUS_NOT_MODIFIED: |
244 | | /* A 304 response validates an existing cache entry */ |
245 | 0 | cacheability = SOUP_CACHE_VALIDATES; |
246 | 0 | break; |
247 | | |
248 | 0 | case SOUP_STATUS_MULTIPLE_CHOICES: |
249 | 0 | case SOUP_STATUS_MOVED_PERMANENTLY: |
250 | 0 | case SOUP_STATUS_GONE: |
251 | | /* FIXME: cacheable unless indicated otherwise */ |
252 | 0 | cacheability = SOUP_CACHE_UNCACHEABLE; |
253 | 0 | break; |
254 | | |
255 | 0 | case SOUP_STATUS_FOUND: |
256 | 0 | case SOUP_STATUS_TEMPORARY_REDIRECT: |
257 | | /* FIXME: cacheable if explicitly indicated */ |
258 | 0 | cacheability = SOUP_CACHE_UNCACHEABLE; |
259 | 0 | break; |
260 | | |
261 | 0 | case SOUP_STATUS_SEE_OTHER: |
262 | 0 | case SOUP_STATUS_FORBIDDEN: |
263 | 0 | case SOUP_STATUS_NOT_FOUND: |
264 | 0 | case SOUP_STATUS_METHOD_NOT_ALLOWED: |
265 | 0 | return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES); |
266 | | |
267 | 0 | default: |
268 | | /* Any 5xx status or any 4xx status not handled above |
269 | | * is uncacheable but doesn't break the cache. |
270 | | */ |
271 | 0 | if ((soup_message_get_status (msg) >= SOUP_STATUS_BAD_REQUEST && |
272 | 0 | soup_message_get_status (msg) <= SOUP_STATUS_FAILED_DEPENDENCY) || |
273 | 0 | soup_message_get_status (msg) >= SOUP_STATUS_INTERNAL_SERVER_ERROR) |
274 | 0 | return SOUP_CACHE_UNCACHEABLE; |
275 | | |
276 | | /* An unrecognized 2xx, 3xx, or 4xx response breaks |
277 | | * the cache. |
278 | | */ |
279 | 0 | if ((soup_message_get_status (msg) > SOUP_STATUS_PARTIAL_CONTENT && |
280 | 0 | soup_message_get_status (msg) < SOUP_STATUS_MULTIPLE_CHOICES) || |
281 | 0 | (soup_message_get_status (msg) > SOUP_STATUS_TEMPORARY_REDIRECT && |
282 | 0 | soup_message_get_status (msg) < SOUP_STATUS_INTERNAL_SERVER_ERROR)) |
283 | 0 | return (SOUP_CACHE_UNCACHEABLE | SOUP_CACHE_INVALIDATES); |
284 | 0 | break; |
285 | 0 | } |
286 | | |
287 | 0 | return cacheability; |
288 | 0 | } |
289 | | |
290 | | /* NOTE: this function deletes the file pointed by the file argument |
291 | | * and also unref's the GFile object representing it. |
292 | | */ |
293 | | static void |
294 | | soup_cache_entry_free (SoupCacheEntry *entry) |
295 | 0 | { |
296 | 0 | g_free (entry->uri); |
297 | 0 | g_clear_pointer (&entry->headers, soup_message_headers_unref); |
298 | 0 | g_clear_object (&entry->cancellable); |
299 | |
|
300 | 0 | g_slice_free (SoupCacheEntry, entry); |
301 | 0 | } |
302 | | |
303 | | static void |
304 | | copy_headers (const char *name, const char *value, SoupMessageHeaders *headers) |
305 | 0 | { |
306 | 0 | soup_message_headers_append (headers, name, value); |
307 | 0 | } |
308 | | |
309 | | static void |
310 | | remove_headers (const char *name, const char *value, SoupMessageHeaders *headers) |
311 | 0 | { |
312 | 0 | soup_message_headers_remove (headers, name); |
313 | 0 | } |
314 | | |
315 | | static SoupHeaderName hop_by_hop_headers[] = { |
316 | | SOUP_HEADER_CONNECTION, |
317 | | SOUP_HEADER_KEEP_ALIVE, |
318 | | SOUP_HEADER_PROXY_AUTHENTICATE, |
319 | | SOUP_HEADER_PROXY_AUTHORIZATION, |
320 | | SOUP_HEADER_TE, |
321 | | SOUP_HEADER_TRAILER, |
322 | | SOUP_HEADER_TRANSFER_ENCODING, |
323 | | SOUP_HEADER_UPGRADE |
324 | | }; |
325 | | |
326 | | static void |
327 | | copy_end_to_end_headers (SoupMessageHeaders *source, SoupMessageHeaders *destination) |
328 | 0 | { |
329 | 0 | int i; |
330 | |
|
331 | 0 | soup_message_headers_foreach (source, (SoupMessageHeadersForeachFunc) copy_headers, destination); |
332 | 0 | for (i = 0; i < G_N_ELEMENTS (hop_by_hop_headers); i++) |
333 | 0 | soup_message_headers_remove_common (destination, hop_by_hop_headers[i]); |
334 | 0 | soup_message_headers_clean_connection_headers (destination); |
335 | 0 | } |
336 | | |
337 | | static guint |
338 | | soup_cache_entry_get_current_age (SoupCacheEntry *entry) |
339 | 0 | { |
340 | 0 | time_t now = time (NULL); |
341 | 0 | time_t resident_time; |
342 | |
|
343 | 0 | resident_time = now - entry->response_time; |
344 | 0 | return entry->corrected_initial_age + resident_time; |
345 | 0 | } |
346 | | |
347 | | static gboolean |
348 | | soup_cache_entry_is_fresh_enough (SoupCacheEntry *entry, gint min_fresh) |
349 | 0 | { |
350 | 0 | guint limit = (min_fresh == -1) ? soup_cache_entry_get_current_age (entry) : (guint) min_fresh; |
351 | 0 | return entry->freshness_lifetime > limit; |
352 | 0 | } |
353 | | |
354 | | static inline guint32 |
355 | | get_cache_key_from_uri (const char *uri) |
356 | 0 | { |
357 | 0 | return (guint32) g_str_hash (uri); |
358 | 0 | } |
359 | | |
360 | | static void |
361 | | soup_cache_entry_set_freshness (SoupCacheEntry *entry, SoupMessage *msg, SoupCache *cache) |
362 | 0 | { |
363 | 0 | const char *cache_control; |
364 | 0 | const char *expires, *date, *last_modified; |
365 | | |
366 | | /* Reset these values. We have to do this to ensure that |
367 | | * revalidations overwrite previous values for the headers. |
368 | | */ |
369 | 0 | entry->must_revalidate = FALSE; |
370 | 0 | entry->freshness_lifetime = 0; |
371 | |
|
372 | 0 | cache_control = soup_message_headers_get_list_common (entry->headers, SOUP_HEADER_CACHE_CONTROL); |
373 | 0 | if (cache_control && *cache_control) { |
374 | 0 | const char *max_age, *s_maxage; |
375 | 0 | gint64 freshness_lifetime = 0; |
376 | 0 | GHashTable *hash; |
377 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
378 | |
|
379 | 0 | hash = soup_header_parse_param_list (cache_control); |
380 | | |
381 | | /* Should we re-validate the entry when it goes stale */ |
382 | 0 | entry->must_revalidate = g_hash_table_lookup_extended (hash, "must-revalidate", NULL, NULL); |
383 | | |
384 | | /* Section 2.3.1 */ |
385 | 0 | if (priv->cache_type == SOUP_CACHE_SHARED) { |
386 | 0 | s_maxage = g_hash_table_lookup (hash, "s-maxage"); |
387 | 0 | if (s_maxage) { |
388 | 0 | freshness_lifetime = g_ascii_strtoll (s_maxage, NULL, 10); |
389 | 0 | if (freshness_lifetime) { |
390 | | /* Implies proxy-revalidate. TODO: is it true? */ |
391 | 0 | entry->must_revalidate = TRUE; |
392 | 0 | soup_header_free_param_list (hash); |
393 | 0 | return; |
394 | 0 | } |
395 | 0 | } |
396 | 0 | } |
397 | | |
398 | | /* If 'max-age' cache directive is present, use that */ |
399 | 0 | max_age = g_hash_table_lookup (hash, "max-age"); |
400 | 0 | if (max_age) |
401 | 0 | freshness_lifetime = g_ascii_strtoll (max_age, NULL, 10); |
402 | |
|
403 | 0 | if (freshness_lifetime) { |
404 | 0 | entry->freshness_lifetime = (guint32) MIN (freshness_lifetime, G_MAXUINT32); |
405 | 0 | soup_header_free_param_list (hash); |
406 | 0 | return; |
407 | 0 | } |
408 | | |
409 | 0 | soup_header_free_param_list (hash); |
410 | 0 | } |
411 | | |
412 | | /* If the 'Expires' response header is present, use its value |
413 | | * minus the value of the 'Date' response header |
414 | | */ |
415 | 0 | expires = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_EXPIRES); |
416 | 0 | date = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_DATE); |
417 | 0 | if (expires && date) { |
418 | 0 | GDateTime *expires_d, *date_d; |
419 | 0 | gint64 expires_t, date_t; |
420 | |
|
421 | 0 | expires_d = soup_date_time_new_from_http_string (expires); |
422 | 0 | date_d = soup_date_time_new_from_http_string (date); |
423 | |
|
424 | 0 | if (expires_d && date_d) { |
425 | 0 | expires_t = g_date_time_to_unix (expires_d); |
426 | 0 | date_t = g_date_time_to_unix (date_d); |
427 | |
|
428 | 0 | g_date_time_unref (expires_d); |
429 | 0 | g_date_time_unref (date_d); |
430 | |
|
431 | 0 | if (expires_t && date_t) { |
432 | 0 | entry->freshness_lifetime = (guint32) MAX (expires_t - date_t, 0); |
433 | 0 | return; |
434 | 0 | } |
435 | 0 | } else { |
436 | | /* If Expires is not a valid date we should |
437 | | treat it as already expired, see section |
438 | | 3.3 */ |
439 | 0 | entry->freshness_lifetime = 0; |
440 | 0 | return; |
441 | 0 | } |
442 | 0 | } |
443 | | |
444 | | /* Otherwise an heuristic may be used */ |
445 | | |
446 | | /* Heuristics MUST NOT be used with stored responses with |
447 | | these status codes (section 2.3.1.1) */ |
448 | 0 | if (entry->status_code != SOUP_STATUS_OK && |
449 | 0 | entry->status_code != SOUP_STATUS_NON_AUTHORITATIVE && |
450 | 0 | entry->status_code != SOUP_STATUS_PARTIAL_CONTENT && |
451 | 0 | entry->status_code != SOUP_STATUS_MULTIPLE_CHOICES && |
452 | 0 | entry->status_code != SOUP_STATUS_MOVED_PERMANENTLY && |
453 | 0 | entry->status_code != SOUP_STATUS_GONE) |
454 | 0 | goto expire; |
455 | | |
456 | | /* TODO: attach warning 113 if response's current_age is more |
457 | | than 24h (section 2.3.1.1) when using heuristics */ |
458 | | |
459 | | /* Last-Modified based heuristic */ |
460 | 0 | last_modified = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_LAST_MODIFIED); |
461 | 0 | if (last_modified) { |
462 | 0 | GDateTime *soup_date; |
463 | 0 | gint64 now, last_modified_t; |
464 | |
|
465 | 0 | soup_date = soup_date_time_new_from_http_string (last_modified); |
466 | 0 | if (soup_date) { |
467 | 0 | last_modified_t = g_date_time_to_unix (soup_date); |
468 | 0 | now = time (NULL); |
469 | |
|
470 | 0 | #define HEURISTIC_FACTOR 0.1 /* From Section 2.3.1.1 */ |
471 | |
|
472 | 0 | entry->freshness_lifetime = MAX (0, (now - last_modified_t) * HEURISTIC_FACTOR); |
473 | 0 | g_date_time_unref (soup_date); |
474 | 0 | } |
475 | 0 | } |
476 | |
|
477 | 0 | return; |
478 | | |
479 | 0 | expire: |
480 | | /* If all else fails, make the entry expire immediately */ |
481 | 0 | entry->freshness_lifetime = 0; |
482 | 0 | } |
483 | | |
484 | | static SoupCacheEntry * |
485 | | soup_cache_entry_new (SoupCache *cache, SoupMessage *msg, time_t request_time, time_t response_time) |
486 | 0 | { |
487 | 0 | SoupCacheEntry *entry; |
488 | 0 | const char *date; |
489 | 0 | GDateTime *soup_date = NULL; |
490 | |
|
491 | 0 | entry = g_slice_new0 (SoupCacheEntry); |
492 | 0 | entry->dirty = FALSE; |
493 | 0 | entry->being_validated = FALSE; |
494 | 0 | entry->status_code = soup_message_get_status (msg); |
495 | 0 | entry->response_time = response_time; |
496 | 0 | entry->uri = g_uri_to_string_partial (soup_message_get_uri (msg), G_URI_HIDE_PASSWORD); |
497 | | |
498 | | /* Headers */ |
499 | 0 | entry->headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE); |
500 | 0 | copy_end_to_end_headers (soup_message_get_response_headers (msg), entry->headers); |
501 | | |
502 | | /* LRU list */ |
503 | 0 | entry->hits = 0; |
504 | | |
505 | | /* Section 2.3.1, Freshness Lifetime */ |
506 | 0 | soup_cache_entry_set_freshness (entry, msg, cache); |
507 | | |
508 | | /* Section 2.3.2, Calculating Age */ |
509 | 0 | date = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_DATE); |
510 | 0 | if (date) |
511 | 0 | soup_date = soup_date_time_new_from_http_string (date); |
512 | |
|
513 | 0 | if (soup_date) { |
514 | 0 | const char *age; |
515 | 0 | gint64 date_value, apparent_age, corrected_received_age, response_delay, age_value = 0; |
516 | |
|
517 | 0 | date_value = g_date_time_to_unix (soup_date); |
518 | 0 | g_date_time_unref (soup_date); |
519 | |
|
520 | 0 | age = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_AGE); |
521 | 0 | if (age) |
522 | 0 | age_value = g_ascii_strtoll (age, NULL, 10); |
523 | |
|
524 | 0 | apparent_age = MAX (0, entry->response_time - date_value); |
525 | 0 | corrected_received_age = MAX (apparent_age, age_value); |
526 | 0 | response_delay = entry->response_time - request_time; |
527 | 0 | entry->corrected_initial_age = corrected_received_age + response_delay; |
528 | 0 | } else { |
529 | | /* Is this correct ? */ |
530 | 0 | entry->corrected_initial_age = time (NULL); |
531 | 0 | } |
532 | |
|
533 | 0 | return entry; |
534 | 0 | } |
535 | | |
536 | | static gboolean |
537 | | soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry, gboolean purge) |
538 | 0 | { |
539 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
540 | 0 | GList *lru_item; |
541 | |
|
542 | 0 | if (entry->dirty) { |
543 | 0 | g_cancellable_cancel (entry->cancellable); |
544 | 0 | return FALSE; |
545 | 0 | } |
546 | | |
547 | 0 | g_assert (!entry->dirty); |
548 | 0 | g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache)); |
549 | | |
550 | 0 | if (!g_hash_table_remove (priv->cache, GUINT_TO_POINTER (entry->key))) { |
551 | 0 | g_mutex_unlock (&priv->mutex); |
552 | 0 | return FALSE; |
553 | 0 | } |
554 | | |
555 | | /* Remove from LRU */ |
556 | 0 | lru_item = g_list_find (priv->lru_start, entry); |
557 | 0 | priv->lru_start = g_list_delete_link (priv->lru_start, lru_item); |
558 | | |
559 | | /* Adjust cache size */ |
560 | 0 | priv->size -= entry->length; |
561 | |
|
562 | 0 | g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache)); |
563 | | |
564 | | /* Free resources */ |
565 | 0 | if (purge) { |
566 | 0 | GFile *file = get_file_from_entry (cache, entry); |
567 | 0 | g_file_delete (file, NULL, NULL); |
568 | 0 | g_object_unref (file); |
569 | 0 | } |
570 | 0 | soup_cache_entry_free (entry); |
571 | |
|
572 | 0 | return TRUE; |
573 | 0 | } |
574 | | |
575 | | static gint |
576 | | lru_compare_func (gconstpointer a, gconstpointer b) |
577 | 0 | { |
578 | 0 | SoupCacheEntry *entry_a = (SoupCacheEntry *)a; |
579 | 0 | SoupCacheEntry *entry_b = (SoupCacheEntry *)b; |
580 | | |
581 | | /* The rationale of this sorting func is |
582 | | * |
583 | | * 1. sort by hits -> LRU algorithm, then |
584 | | * |
585 | | * 2. sort by freshness lifetime, we better discard first |
586 | | * entries that are close to expire |
587 | | * |
588 | | * 3. sort by size, replace first small size resources as they |
589 | | * are cheaper to download |
590 | | */ |
591 | | |
592 | | /* Sort by hits */ |
593 | 0 | if (entry_a->hits != entry_b->hits) |
594 | 0 | return entry_a->hits - entry_b->hits; |
595 | | |
596 | | /* Sort by freshness_lifetime */ |
597 | 0 | if (entry_a->freshness_lifetime != entry_b->freshness_lifetime) |
598 | 0 | return entry_a->freshness_lifetime - entry_b->freshness_lifetime; |
599 | | |
600 | | /* Sort by size */ |
601 | 0 | return entry_a->length - entry_b->length; |
602 | 0 | } |
603 | | |
604 | | static gboolean |
605 | | cache_accepts_entries_of_size (SoupCache *cache, guint length_to_add) |
606 | 0 | { |
607 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
608 | | /* We could add here some more heuristics. TODO: review how |
609 | | this is done by other HTTP caches */ |
610 | |
|
611 | 0 | return length_to_add <= priv->max_entry_data_size; |
612 | 0 | } |
613 | | |
614 | | static void |
615 | | make_room_for_new_entry (SoupCache *cache, guint length_to_add) |
616 | 0 | { |
617 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
618 | 0 | GList *lru_entry = priv->lru_start; |
619 | | |
620 | | /* Check that there is enough room for the new entry. This is |
621 | | an approximation as we're not working out the size of the |
622 | | cache file or the size of the headers for performance |
623 | | reasons. TODO: check if that would be really that expensive */ |
624 | |
|
625 | 0 | while (lru_entry && |
626 | 0 | (length_to_add + priv->size > priv->max_size)) { |
627 | 0 | SoupCacheEntry *old_entry = (SoupCacheEntry *)lru_entry->data; |
628 | | |
629 | | /* Discard entries. Once cancelled resources will be |
630 | | * freed in close_ready_cb |
631 | | */ |
632 | 0 | if (soup_cache_entry_remove (cache, old_entry, TRUE)) |
633 | 0 | lru_entry = priv->lru_start; |
634 | 0 | else |
635 | 0 | lru_entry = g_list_next (lru_entry); |
636 | 0 | } |
637 | 0 | } |
638 | | |
639 | | static gboolean |
640 | | soup_cache_entry_insert (SoupCache *cache, |
641 | | SoupCacheEntry *entry, |
642 | | gboolean sort) |
643 | 0 | { |
644 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
645 | 0 | guint length_to_add = 0; |
646 | 0 | SoupCacheEntry *old_entry; |
647 | | |
648 | | /* Fill the key */ |
649 | 0 | entry->key = get_cache_key_from_uri ((const char *) entry->uri); |
650 | |
|
651 | 0 | if (soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CONTENT_LENGTH) |
652 | 0 | length_to_add = soup_message_headers_get_content_length (entry->headers); |
653 | | |
654 | | /* Check if we are going to store the resource depending on its size */ |
655 | 0 | if (length_to_add) { |
656 | 0 | if (!cache_accepts_entries_of_size (cache, length_to_add)) |
657 | 0 | return FALSE; |
658 | | |
659 | | /* Make room for new entry if needed */ |
660 | 0 | make_room_for_new_entry (cache, length_to_add); |
661 | 0 | } |
662 | | |
663 | | /* Remove any previous entry */ |
664 | 0 | if ((old_entry = g_hash_table_lookup (priv->cache, GUINT_TO_POINTER (entry->key))) != NULL) { |
665 | 0 | if (!soup_cache_entry_remove (cache, old_entry, TRUE)) |
666 | 0 | return FALSE; |
667 | 0 | } |
668 | | |
669 | | /* Add to hash table */ |
670 | 0 | g_hash_table_insert (priv->cache, GUINT_TO_POINTER (entry->key), entry); |
671 | | |
672 | | /* Compute new cache size */ |
673 | 0 | priv->size += length_to_add; |
674 | | |
675 | | /* Update LRU */ |
676 | 0 | if (sort) |
677 | 0 | priv->lru_start = g_list_insert_sorted (priv->lru_start, entry, lru_compare_func); |
678 | 0 | else |
679 | 0 | priv->lru_start = g_list_prepend (priv->lru_start, entry); |
680 | |
|
681 | 0 | g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache)); |
682 | | |
683 | 0 | return TRUE; |
684 | 0 | } |
685 | | |
686 | | static SoupCacheEntry* |
687 | | soup_cache_entry_lookup (SoupCache *cache, |
688 | | SoupMessage *msg) |
689 | 0 | { |
690 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
691 | 0 | SoupCacheEntry *entry; |
692 | 0 | guint32 key; |
693 | 0 | char *uri = NULL; |
694 | |
|
695 | 0 | uri = g_uri_to_string_partial (soup_message_get_uri (msg), G_URI_HIDE_PASSWORD); |
696 | 0 | key = get_cache_key_from_uri ((const char *) uri); |
697 | |
|
698 | 0 | entry = g_hash_table_lookup (priv->cache, GUINT_TO_POINTER (key)); |
699 | |
|
700 | 0 | if (entry != NULL && (strcmp (entry->uri, uri) != 0)) |
701 | 0 | entry = NULL; |
702 | |
|
703 | 0 | g_free (uri); |
704 | 0 | return entry; |
705 | 0 | } |
706 | | |
707 | | GInputStream * |
708 | | soup_cache_send_response (SoupCache *cache, SoupMessage *msg) |
709 | 0 | { |
710 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
711 | 0 | SoupCacheEntry *entry; |
712 | 0 | GInputStream *file_stream, *body_stream, *cache_stream, *client_stream; |
713 | 0 | GFile *file; |
714 | 0 | SoupMessageMetrics *metrics; |
715 | |
|
716 | 0 | g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL); |
717 | 0 | g_return_val_if_fail (SOUP_IS_MESSAGE (msg), NULL); |
718 | | |
719 | 0 | soup_message_set_metrics_timestamp (msg, SOUP_MESSAGE_METRICS_REQUEST_START); |
720 | |
|
721 | 0 | g_mutex_lock (&priv->mutex); |
722 | 0 | entry = soup_cache_entry_lookup (cache, msg); |
723 | 0 | g_mutex_unlock (&priv->mutex); |
724 | 0 | g_return_val_if_fail (entry, NULL); |
725 | | |
726 | 0 | file = get_file_from_entry (cache, entry); |
727 | 0 | file_stream = G_INPUT_STREAM (g_file_read (file, NULL, NULL)); |
728 | 0 | g_object_unref (file); |
729 | | |
730 | | /* Do not change the original message if there is no resource */ |
731 | 0 | if (!file_stream) |
732 | 0 | return NULL; |
733 | | |
734 | 0 | body_stream = soup_body_input_stream_new (file_stream, SOUP_ENCODING_CONTENT_LENGTH, entry->length); |
735 | 0 | g_object_unref (file_stream); |
736 | |
|
737 | 0 | if (!body_stream) |
738 | 0 | return NULL; |
739 | | |
740 | 0 | metrics = soup_message_get_metrics (msg); |
741 | 0 | if (metrics) |
742 | 0 | metrics->response_body_size = entry->length; |
743 | | |
744 | | /* If we are told to send a response from cache any validation |
745 | | in course is over by now */ |
746 | 0 | entry->being_validated = FALSE; |
747 | | |
748 | | /* Message starting */ |
749 | 0 | soup_message_starting (msg); |
750 | |
|
751 | 0 | soup_message_set_metrics_timestamp (msg, SOUP_MESSAGE_METRICS_RESPONSE_START); |
752 | | |
753 | | /* Status */ |
754 | 0 | soup_message_set_status (msg, entry->status_code, NULL); |
755 | | |
756 | | /* Headers */ |
757 | 0 | copy_end_to_end_headers (entry->headers, soup_message_get_response_headers (msg)); |
758 | | |
759 | | /* Create the cache stream. */ |
760 | 0 | soup_message_disable_feature (msg, SOUP_TYPE_CACHE); |
761 | 0 | cache_stream = soup_session_setup_message_body_input_stream (priv->session, |
762 | 0 | msg, body_stream, |
763 | 0 | SOUP_STAGE_ENTITY_BODY); |
764 | 0 | g_object_unref (body_stream); |
765 | |
|
766 | 0 | client_stream = soup_cache_client_input_stream_new (cache_stream); |
767 | 0 | g_object_unref (cache_stream); |
768 | |
|
769 | 0 | return client_stream; |
770 | 0 | } |
771 | | |
772 | | static void |
773 | | msg_got_headers_cb (SoupMessage *msg, gpointer user_data) |
774 | 0 | { |
775 | 0 | g_object_set_data (G_OBJECT (msg), "response-time", GINT_TO_POINTER (time (NULL))); |
776 | 0 | g_signal_handlers_disconnect_by_func (msg, msg_got_headers_cb, user_data); |
777 | 0 | } |
778 | | |
779 | | static void |
780 | | msg_starting_cb (SoupMessage *msg, gpointer user_data) |
781 | 0 | { |
782 | 0 | g_object_set_data (G_OBJECT (msg), "request-time", GINT_TO_POINTER (time (NULL))); |
783 | 0 | g_signal_connect (msg, "got-headers", G_CALLBACK (msg_got_headers_cb), user_data); |
784 | 0 | g_signal_handlers_disconnect_by_func (msg, msg_starting_cb, user_data); |
785 | 0 | } |
786 | | |
787 | | static void |
788 | | request_queued (SoupSessionFeature *feature, |
789 | | SoupMessage *msg) |
790 | 0 | { |
791 | 0 | g_signal_connect (msg, "starting", G_CALLBACK (msg_starting_cb), feature); |
792 | 0 | } |
793 | | |
794 | | static void |
795 | | attach (SoupSessionFeature *feature, SoupSession *session) |
796 | 0 | { |
797 | 0 | SoupCache *cache = SOUP_CACHE (feature); |
798 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
799 | 0 | priv->session = session; |
800 | 0 | } |
801 | | |
802 | | static void |
803 | | soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface, |
804 | | gpointer interface_data) |
805 | 0 | { |
806 | 0 | feature_interface->attach = attach; |
807 | 0 | feature_interface->request_queued = request_queued; |
808 | 0 | } |
809 | | |
810 | | typedef struct { |
811 | | SoupCache *cache; |
812 | | SoupCacheEntry *entry; |
813 | | } StreamHelper; |
814 | | |
815 | | static void |
816 | | istream_caching_finished (SoupCacheInputStream *istream, |
817 | | gsize bytes_written, |
818 | | GError *error, |
819 | | gpointer user_data) |
820 | 0 | { |
821 | 0 | StreamHelper *helper = (StreamHelper *) user_data; |
822 | 0 | SoupCache *cache = helper->cache; |
823 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
824 | 0 | SoupCacheEntry *entry = helper->entry; |
825 | |
|
826 | 0 | g_mutex_lock (&priv->mutex); |
827 | |
|
828 | 0 | --priv->n_pending; |
829 | |
|
830 | 0 | entry->dirty = FALSE; |
831 | 0 | entry->length = bytes_written; |
832 | 0 | g_clear_object (&entry->cancellable); |
833 | |
|
834 | 0 | if (error) { |
835 | | /* Update cache size */ |
836 | 0 | if (soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CONTENT_LENGTH) |
837 | 0 | priv->size -= soup_message_headers_get_content_length (entry->headers); |
838 | |
|
839 | 0 | soup_cache_entry_remove (cache, entry, TRUE); |
840 | 0 | helper->entry = entry = NULL; |
841 | 0 | goto cleanup; |
842 | 0 | } |
843 | | |
844 | 0 | if (soup_message_headers_get_encoding (entry->headers) != SOUP_ENCODING_CONTENT_LENGTH) { |
845 | |
|
846 | 0 | if (cache_accepts_entries_of_size (cache, entry->length)) { |
847 | 0 | make_room_for_new_entry (cache, entry->length); |
848 | 0 | priv->size += entry->length; |
849 | 0 | } else { |
850 | 0 | soup_cache_entry_remove (cache, entry, TRUE); |
851 | 0 | helper->entry = entry = NULL; |
852 | 0 | } |
853 | 0 | } |
854 | |
|
855 | 0 | cleanup: |
856 | 0 | g_mutex_unlock (&priv->mutex); |
857 | 0 | g_object_unref (helper->cache); |
858 | 0 | g_slice_free (StreamHelper, helper); |
859 | 0 | } |
860 | | |
861 | | static GInputStream* |
862 | | soup_cache_content_processor_wrap_input (SoupContentProcessor *processor, |
863 | | GInputStream *base_stream, |
864 | | SoupMessage *msg, |
865 | | GError **error) |
866 | 0 | { |
867 | 0 | SoupCache *cache = (SoupCache*) processor; |
868 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
869 | 0 | SoupCacheEntry *entry; |
870 | 0 | SoupCacheability cacheability; |
871 | 0 | GInputStream *istream; |
872 | 0 | GFile *file; |
873 | 0 | StreamHelper *helper; |
874 | 0 | time_t request_time, response_time; |
875 | |
|
876 | 0 | g_mutex_lock (&priv->mutex); |
877 | | |
878 | | /* First of all, check if we should cache the resource. */ |
879 | 0 | cacheability = soup_cache_get_cacheability (cache, msg); |
880 | 0 | entry = soup_cache_entry_lookup (cache, msg); |
881 | |
|
882 | 0 | if (cacheability & SOUP_CACHE_INVALIDATES) { |
883 | 0 | if (entry) |
884 | 0 | soup_cache_entry_remove (cache, entry, TRUE); |
885 | 0 | g_mutex_unlock (&priv->mutex); |
886 | 0 | return NULL; |
887 | 0 | } |
888 | | |
889 | 0 | if (cacheability & SOUP_CACHE_VALIDATES) { |
890 | | /* It's possible to get a CACHE_VALIDATES with no |
891 | | * entry in the hash table. This could happen if for |
892 | | * example the soup client is the one creating the |
893 | | * conditional request. |
894 | | */ |
895 | 0 | if (entry) |
896 | 0 | soup_cache_update_from_conditional_request (cache, msg); |
897 | 0 | g_mutex_unlock (&priv->mutex); |
898 | 0 | return NULL; |
899 | 0 | } |
900 | | |
901 | 0 | if (!(cacheability & SOUP_CACHE_CACHEABLE)) { |
902 | 0 | g_mutex_unlock (&priv->mutex); |
903 | 0 | return NULL; |
904 | 0 | } |
905 | | |
906 | | /* Check if we are already caching this resource */ |
907 | 0 | if (entry && (entry->dirty || entry->being_validated)) { |
908 | 0 | g_mutex_unlock (&priv->mutex); |
909 | 0 | return NULL; |
910 | 0 | } |
911 | | |
912 | | /* Create a new entry, deleting any old one if present */ |
913 | 0 | if (entry) |
914 | 0 | soup_cache_entry_remove (cache, entry, TRUE); |
915 | |
|
916 | 0 | request_time = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "request-time")); |
917 | 0 | response_time = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "response-time")); |
918 | 0 | entry = soup_cache_entry_new (cache, msg, request_time, response_time); |
919 | 0 | entry->hits = 1; |
920 | 0 | entry->dirty = TRUE; |
921 | | |
922 | | /* Do not continue if it can not be stored */ |
923 | 0 | if (!soup_cache_entry_insert (cache, entry, TRUE)) { |
924 | 0 | soup_cache_entry_free (entry); |
925 | 0 | g_mutex_unlock (&priv->mutex); |
926 | 0 | return NULL; |
927 | 0 | } |
928 | | |
929 | 0 | entry->cancellable = g_cancellable_new (); |
930 | 0 | ++priv->n_pending; |
931 | |
|
932 | 0 | g_mutex_unlock (&priv->mutex); |
933 | |
|
934 | 0 | helper = g_slice_new (StreamHelper); |
935 | 0 | helper->cache = g_object_ref (cache); |
936 | 0 | helper->entry = entry; |
937 | |
|
938 | 0 | file = get_file_from_entry (cache, entry); |
939 | 0 | istream = soup_cache_input_stream_new (base_stream, file); |
940 | 0 | g_object_unref (file); |
941 | |
|
942 | 0 | g_signal_connect (istream, "caching-finished", G_CALLBACK (istream_caching_finished), helper); |
943 | |
|
944 | 0 | return istream; |
945 | 0 | } |
946 | | |
947 | | static void |
948 | | soup_cache_content_processor_init (SoupContentProcessorInterface *processor_interface, |
949 | | gpointer interface_data) |
950 | 0 | { |
951 | 0 | soup_cache_default_content_processor_interface = |
952 | 0 | g_type_default_interface_peek (SOUP_TYPE_CONTENT_PROCESSOR); |
953 | |
|
954 | 0 | processor_interface->processing_stage = SOUP_STAGE_ENTITY_BODY; |
955 | 0 | processor_interface->wrap_input = soup_cache_content_processor_wrap_input; |
956 | 0 | } |
957 | | |
958 | | static void |
959 | | soup_cache_init (SoupCache *cache) |
960 | 0 | { |
961 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
962 | |
|
963 | 0 | priv->cache = g_hash_table_new (g_direct_hash, g_direct_equal); |
964 | | /* LRU */ |
965 | 0 | priv->lru_start = NULL; |
966 | | |
967 | | /* */ |
968 | 0 | priv->n_pending = 0; |
969 | | |
970 | | /* Cache size */ |
971 | 0 | priv->max_size = DEFAULT_MAX_SIZE; |
972 | 0 | priv->max_entry_data_size = priv->max_size / MAX_ENTRY_DATA_PERCENTAGE; |
973 | 0 | priv->size = 0; |
974 | |
|
975 | 0 | g_mutex_init (&priv->mutex); |
976 | 0 | } |
977 | | |
978 | | static void |
979 | | remove_cache_item (gpointer data, |
980 | | gpointer user_data) |
981 | 0 | { |
982 | 0 | soup_cache_entry_remove ((SoupCache *) user_data, (SoupCacheEntry *) data, FALSE); |
983 | 0 | } |
984 | | |
985 | | static void |
986 | | soup_cache_finalize (GObject *object) |
987 | 0 | { |
988 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private ((SoupCache*)object); |
989 | 0 | GList *entries; |
990 | | |
991 | | /* Cannot use g_hash_table_foreach as callbacks must not modify the hash table */ |
992 | 0 | entries = g_hash_table_get_values (priv->cache); |
993 | 0 | g_list_foreach (entries, remove_cache_item, object); |
994 | 0 | g_list_free (entries); |
995 | |
|
996 | 0 | g_hash_table_destroy (priv->cache); |
997 | 0 | g_free (priv->cache_dir); |
998 | |
|
999 | 0 | g_list_free (priv->lru_start); |
1000 | |
|
1001 | 0 | g_mutex_clear (&priv->mutex); |
1002 | |
|
1003 | 0 | G_OBJECT_CLASS (soup_cache_parent_class)->finalize (object); |
1004 | 0 | } |
1005 | | |
1006 | | static void |
1007 | | soup_cache_set_property (GObject *object, guint prop_id, |
1008 | | const GValue *value, GParamSpec *pspec) |
1009 | 0 | { |
1010 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private ((SoupCache*)object); |
1011 | |
|
1012 | 0 | switch (prop_id) { |
1013 | 0 | case PROP_CACHE_DIR: |
1014 | 0 | g_assert (!priv->cache_dir); |
1015 | | |
1016 | 0 | priv->cache_dir = g_value_dup_string (value); |
1017 | |
|
1018 | 0 | if (!priv->cache_dir) |
1019 | | /* Set a default cache dir, different for each user */ |
1020 | 0 | priv->cache_dir = g_build_filename (g_get_user_cache_dir (), |
1021 | 0 | "httpcache", |
1022 | 0 | NULL); |
1023 | | |
1024 | | /* Create directory if it does not exist */ |
1025 | 0 | if (!g_file_test (priv->cache_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) |
1026 | 0 | g_mkdir_with_parents (priv->cache_dir, 0700); |
1027 | 0 | break; |
1028 | 0 | case PROP_CACHE_TYPE: |
1029 | 0 | priv->cache_type = g_value_get_enum (value); |
1030 | | /* TODO: clear private entries and issue a warning if moving to shared? */ |
1031 | 0 | break; |
1032 | 0 | default: |
1033 | 0 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
1034 | 0 | break; |
1035 | 0 | } |
1036 | 0 | } |
1037 | | |
1038 | | static void |
1039 | | soup_cache_get_property (GObject *object, guint prop_id, |
1040 | | GValue *value, GParamSpec *pspec) |
1041 | 0 | { |
1042 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private ((SoupCache*)object); |
1043 | |
|
1044 | 0 | switch (prop_id) { |
1045 | 0 | case PROP_CACHE_DIR: |
1046 | 0 | g_value_set_string (value, priv->cache_dir); |
1047 | 0 | break; |
1048 | 0 | case PROP_CACHE_TYPE: |
1049 | 0 | g_value_set_enum (value, priv->cache_type); |
1050 | 0 | break; |
1051 | 0 | default: |
1052 | 0 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
1053 | 0 | break; |
1054 | 0 | } |
1055 | 0 | } |
1056 | | |
1057 | | static void |
1058 | | soup_cache_class_init (SoupCacheClass *cache_class) |
1059 | 0 | { |
1060 | 0 | GObjectClass *gobject_class = (GObjectClass *)cache_class; |
1061 | |
|
1062 | 0 | gobject_class->finalize = soup_cache_finalize; |
1063 | 0 | gobject_class->set_property = soup_cache_set_property; |
1064 | 0 | gobject_class->get_property = soup_cache_get_property; |
1065 | |
|
1066 | 0 | cache_class->get_cacheability = get_cacheability; |
1067 | | |
1068 | | /** |
1069 | | * SoupCache:cache-dir: |
1070 | | * The directory to store the cache files. |
1071 | | */ |
1072 | 0 | properties[PROP_CACHE_DIR] = |
1073 | 0 | g_param_spec_string ("cache-dir", |
1074 | 0 | "Cache directory", |
1075 | 0 | "The directory to store the cache files", |
1076 | 0 | NULL, |
1077 | 0 | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | |
1078 | 0 | G_PARAM_STATIC_STRINGS); |
1079 | | |
1080 | | /** |
1081 | | * SoupCache:cache-type: |
1082 | | * Whether the cache is private or shared. |
1083 | | */ |
1084 | 0 | properties[PROP_CACHE_TYPE] = |
1085 | 0 | g_param_spec_enum ("cache-type", |
1086 | 0 | "Cache type", |
1087 | 0 | "Whether the cache is private or shared", |
1088 | 0 | SOUP_TYPE_CACHE_TYPE, |
1089 | 0 | SOUP_CACHE_SINGLE_USER, |
1090 | 0 | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | |
1091 | 0 | G_PARAM_STATIC_STRINGS); |
1092 | |
|
1093 | 0 | g_object_class_install_properties (gobject_class, LAST_PROPERTY, properties); |
1094 | 0 | } |
1095 | | |
1096 | | /** |
1097 | | * SoupCacheType: |
1098 | | * @SOUP_CACHE_SINGLE_USER: a single-user cache |
1099 | | * @SOUP_CACHE_SHARED: a shared cache |
1100 | | * |
1101 | | * The type of cache; this affects what kinds of responses will be |
1102 | | * saved. |
1103 | | * |
1104 | | */ |
1105 | | |
1106 | | /** |
1107 | | * soup_cache_new: |
1108 | | * @cache_dir: (nullable): the directory to store the cached data, or %NULL |
1109 | | * to use the default one. Note that since the cache isn't safe to access for |
1110 | | * multiple processes at once, and the default directory isn't namespaced by |
1111 | | * process, clients are strongly discouraged from passing %NULL. |
1112 | | * @cache_type: the #SoupCacheType of the cache |
1113 | | * |
1114 | | * Creates a new #SoupCache. |
1115 | | * |
1116 | | * Returns: a new #SoupCache |
1117 | | * |
1118 | | */ |
1119 | | SoupCache * |
1120 | | soup_cache_new (const char *cache_dir, SoupCacheType cache_type) |
1121 | 0 | { |
1122 | 0 | return g_object_new (SOUP_TYPE_CACHE, |
1123 | 0 | "cache-dir", cache_dir, |
1124 | 0 | "cache-type", cache_type, |
1125 | 0 | NULL); |
1126 | 0 | } |
1127 | | |
1128 | | /** |
1129 | | * soup_cache_has_response: |
1130 | | * @cache: a #SoupCache |
1131 | | * @msg: a #SoupMessage |
1132 | | * |
1133 | | * This function calculates whether the @cache object has a proper |
1134 | | * response for the request @msg given the flags both in the request |
1135 | | * and the cached reply and the time ellapsed since it was cached. |
1136 | | * |
1137 | | * Returns: whether or not the @cache has a valid response for @msg |
1138 | | * |
1139 | | */ |
1140 | | SoupCacheResponse |
1141 | | soup_cache_has_response (SoupCache *cache, SoupMessage *msg) |
1142 | 0 | { |
1143 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1144 | 0 | SoupCacheEntry *entry; |
1145 | 0 | const char *cache_control; |
1146 | 0 | gpointer value; |
1147 | 0 | int max_age, max_stale, min_fresh; |
1148 | 0 | GList *lru_item, *item; |
1149 | |
|
1150 | 0 | g_mutex_lock (&priv->mutex); |
1151 | |
|
1152 | 0 | entry = soup_cache_entry_lookup (cache, msg); |
1153 | | |
1154 | | /* 1. The presented Request-URI and that of stored response |
1155 | | * match |
1156 | | */ |
1157 | 0 | if (!entry) { |
1158 | 0 | g_mutex_unlock (&priv->mutex); |
1159 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1160 | 0 | } |
1161 | | |
1162 | | /* Increase hit count. Take sorting into account */ |
1163 | 0 | entry->hits++; |
1164 | 0 | lru_item = g_list_find (priv->lru_start, entry); |
1165 | 0 | item = lru_item; |
1166 | 0 | while (item->next && lru_compare_func (item->data, item->next->data) > 0) |
1167 | 0 | item = g_list_next (item); |
1168 | |
|
1169 | 0 | if (item != lru_item) { |
1170 | 0 | priv->lru_start = g_list_remove_link (priv->lru_start, lru_item); |
1171 | 0 | item = g_list_insert_sorted (item, lru_item->data, lru_compare_func); |
1172 | 0 | (void)item; // Silence scan-build warning |
1173 | 0 | g_list_free (lru_item); |
1174 | 0 | } |
1175 | |
|
1176 | 0 | g_mutex_unlock (&priv->mutex); |
1177 | |
|
1178 | 0 | if (entry->dirty || entry->being_validated) |
1179 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1180 | | |
1181 | | /* 2. The request method associated with the stored response |
1182 | | * allows it to be used for the presented request |
1183 | | */ |
1184 | | |
1185 | | /* In practice this means we only return our resource for GET, |
1186 | | * cacheability for other methods is a TODO in the RFC |
1187 | | * (TODO: although we could return the headers for HEAD |
1188 | | * probably). |
1189 | | */ |
1190 | 0 | if (soup_message_get_method (msg) != SOUP_METHOD_GET) |
1191 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1192 | | |
1193 | | /* 3. Selecting request-headers nominated by the stored |
1194 | | * response (if any) match those presented. |
1195 | | */ |
1196 | | |
1197 | | /* TODO */ |
1198 | | |
1199 | | /* 4. The request is a conditional request issued by the client. |
1200 | | */ |
1201 | 0 | if (soup_message_headers_get_one_common (soup_message_get_request_headers (msg), SOUP_HEADER_IF_MODIFIED_SINCE) || |
1202 | 0 | soup_message_headers_get_list_common (soup_message_get_request_headers (msg), SOUP_HEADER_IF_NONE_MATCH)) |
1203 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1204 | | |
1205 | | /* 5. The presented request and stored response are free from |
1206 | | * directives that would prevent its use. |
1207 | | */ |
1208 | | |
1209 | 0 | max_age = max_stale = min_fresh = -1; |
1210 | | |
1211 | | /* For HTTP 1.0 compatibility. RFC2616 section 14.9.4 |
1212 | | */ |
1213 | 0 | if (soup_message_headers_header_contains_common (soup_message_get_request_headers (msg), SOUP_HEADER_PRAGMA, "no-cache")) |
1214 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1215 | | |
1216 | 0 | cache_control = soup_message_headers_get_list_common (soup_message_get_request_headers (msg), SOUP_HEADER_CACHE_CONTROL); |
1217 | 0 | if (cache_control && *cache_control) { |
1218 | 0 | GHashTable *hash = soup_header_parse_param_list (cache_control); |
1219 | |
|
1220 | 0 | if (g_hash_table_lookup_extended (hash, "no-store", NULL, NULL)) { |
1221 | 0 | soup_header_free_param_list (hash); |
1222 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1223 | 0 | } |
1224 | | |
1225 | 0 | if (g_hash_table_lookup_extended (hash, "no-cache", NULL, NULL)) { |
1226 | 0 | soup_header_free_param_list (hash); |
1227 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1228 | 0 | } |
1229 | | |
1230 | 0 | if (g_hash_table_lookup_extended (hash, "max-age", NULL, &value) && value) { |
1231 | 0 | max_age = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32); |
1232 | | /* Forcing cache revalidaton |
1233 | | */ |
1234 | 0 | if (!max_age) { |
1235 | 0 | soup_header_free_param_list (hash); |
1236 | 0 | return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION; |
1237 | 0 | } |
1238 | 0 | } |
1239 | | |
1240 | | /* max-stale can have no value set, we need to use _extended */ |
1241 | 0 | if (g_hash_table_lookup_extended (hash, "max-stale", NULL, &value)) { |
1242 | 0 | if (value) |
1243 | 0 | max_stale = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32); |
1244 | 0 | else |
1245 | 0 | max_stale = G_MAXINT32; |
1246 | 0 | } |
1247 | |
|
1248 | 0 | value = g_hash_table_lookup (hash, "min-fresh"); |
1249 | 0 | if (value) |
1250 | 0 | min_fresh = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32); |
1251 | |
|
1252 | 0 | soup_header_free_param_list (hash); |
1253 | |
|
1254 | 0 | if (max_age > 0) { |
1255 | 0 | guint current_age = soup_cache_entry_get_current_age (entry); |
1256 | | |
1257 | | /* If we are over max-age and max-stale is not |
1258 | | set, do not use the value from the cache |
1259 | | without validation */ |
1260 | 0 | if ((guint) max_age <= current_age && max_stale == -1) |
1261 | 0 | return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION; |
1262 | 0 | } |
1263 | 0 | } |
1264 | | |
1265 | | /* 6. The stored response is either: fresh, allowed to be |
1266 | | * served stale or succesfully validated |
1267 | | */ |
1268 | 0 | if (!soup_cache_entry_is_fresh_enough (entry, min_fresh)) { |
1269 | | /* Not fresh, can it be served stale? */ |
1270 | | |
1271 | | /* When the must-revalidate directive is present in a |
1272 | | * response received by a cache, that cache MUST NOT |
1273 | | * use the entry after it becomes stale |
1274 | | */ |
1275 | | /* TODO consider also proxy-revalidate & s-maxage */ |
1276 | 0 | if (entry->must_revalidate) |
1277 | 0 | return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION; |
1278 | | |
1279 | 0 | if (max_stale != -1) { |
1280 | | /* G_MAXINT32 means we accept any staleness */ |
1281 | 0 | if (max_stale == G_MAXINT32) |
1282 | 0 | return SOUP_CACHE_RESPONSE_FRESH; |
1283 | | |
1284 | 0 | if ((soup_cache_entry_get_current_age (entry) - entry->freshness_lifetime) <= (guint) max_stale) |
1285 | 0 | return SOUP_CACHE_RESPONSE_FRESH; |
1286 | 0 | } |
1287 | | |
1288 | 0 | return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION; |
1289 | 0 | } |
1290 | | |
1291 | 0 | return SOUP_CACHE_RESPONSE_FRESH; |
1292 | 0 | } |
1293 | | |
1294 | | /** |
1295 | | * soup_cache_get_cacheability: |
1296 | | * @cache: a #SoupCache |
1297 | | * @msg: a #SoupMessage |
1298 | | * |
1299 | | * Calculates whether the @msg can be cached or not. |
1300 | | * |
1301 | | * Returns: a [flags@Cacheability] value indicating whether the @msg can be cached |
1302 | | * or not. |
1303 | | */ |
1304 | | SoupCacheability |
1305 | | soup_cache_get_cacheability (SoupCache *cache, SoupMessage *msg) |
1306 | 0 | { |
1307 | 0 | g_return_val_if_fail (SOUP_IS_CACHE (cache), SOUP_CACHE_UNCACHEABLE); |
1308 | 0 | g_return_val_if_fail (SOUP_IS_MESSAGE (msg), SOUP_CACHE_UNCACHEABLE); |
1309 | | |
1310 | 0 | return SOUP_CACHE_GET_CLASS (cache)->get_cacheability (cache, msg); |
1311 | 0 | } |
1312 | | |
1313 | | static gboolean |
1314 | | force_flush_timeout (gpointer data) |
1315 | 0 | { |
1316 | 0 | gboolean *forced = (gboolean *)data; |
1317 | 0 | *forced = TRUE; |
1318 | |
|
1319 | 0 | return FALSE; |
1320 | 0 | } |
1321 | | |
1322 | | /** |
1323 | | * soup_cache_flush: |
1324 | | * @cache: a #SoupCache |
1325 | | * |
1326 | | * Forces all pending writes in the @cache to be |
1327 | | * committed to disk. |
1328 | | * |
1329 | | * For doing so it will iterate the [struct@GLib.MainContext] associated with |
1330 | | * @cache's session as long as needed. |
1331 | | * |
1332 | | * Contrast with [method@Cache.dump], which writes out the cache index file. |
1333 | | */ |
1334 | | void |
1335 | | soup_cache_flush (SoupCache *cache) |
1336 | 0 | { |
1337 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1338 | 0 | GMainContext *async_context; |
1339 | 0 | SoupSession *session; |
1340 | 0 | GSource *timeout; |
1341 | 0 | gboolean forced = FALSE; |
1342 | |
|
1343 | 0 | g_return_if_fail (SOUP_IS_CACHE (cache)); |
1344 | | |
1345 | 0 | session = priv->session; |
1346 | 0 | g_return_if_fail (SOUP_IS_SESSION (session)); |
1347 | 0 | async_context = g_main_context_get_thread_default (); |
1348 | | |
1349 | | /* We give cache 10 secs to finish */ |
1350 | 0 | timeout = soup_add_timeout (async_context, 10000, force_flush_timeout, &forced); |
1351 | |
|
1352 | 0 | while (!forced && priv->n_pending > 0) |
1353 | 0 | g_main_context_iteration (async_context, FALSE); |
1354 | |
|
1355 | 0 | if (!forced) |
1356 | 0 | g_source_destroy (timeout); |
1357 | 0 | else |
1358 | 0 | g_warning ("Cache flush finished despite %d pending requests", priv->n_pending); |
1359 | |
|
1360 | 0 | g_source_unref (timeout); |
1361 | 0 | } |
1362 | | |
1363 | | typedef void (* SoupCacheForeachFileFunc) (SoupCache *cache, const char *name, gpointer user_data); |
1364 | | |
1365 | | static void |
1366 | | soup_cache_foreach_file (SoupCache *cache, SoupCacheForeachFileFunc func, gpointer user_data) |
1367 | 0 | { |
1368 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1369 | 0 | GDir *dir; |
1370 | 0 | const char *name; |
1371 | |
|
1372 | 0 | dir = g_dir_open (priv->cache_dir, 0, NULL); |
1373 | 0 | while ((name = g_dir_read_name (dir))) { |
1374 | 0 | if (g_str_has_prefix (name, "soup.")) |
1375 | 0 | continue; |
1376 | | |
1377 | 0 | func (cache, name, user_data); |
1378 | 0 | } |
1379 | 0 | g_dir_close (dir); |
1380 | 0 | } |
1381 | | |
1382 | | static void |
1383 | | clear_cache_item (gpointer data, |
1384 | | gpointer user_data) |
1385 | 0 | { |
1386 | 0 | soup_cache_entry_remove ((SoupCache *) user_data, (SoupCacheEntry *) data, TRUE); |
1387 | 0 | } |
1388 | | |
1389 | | static void |
1390 | | delete_cache_file (SoupCache *cache, const char *name, gpointer user_data) |
1391 | 0 | { |
1392 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1393 | 0 | gchar *path; |
1394 | |
|
1395 | 0 | path = g_build_filename (priv->cache_dir, name, NULL); |
1396 | 0 | g_unlink (path); |
1397 | 0 | g_free (path); |
1398 | 0 | } |
1399 | | |
1400 | | static void |
1401 | | clear_cache_files (SoupCache *cache) |
1402 | 0 | { |
1403 | 0 | soup_cache_foreach_file (cache, delete_cache_file, NULL); |
1404 | 0 | } |
1405 | | |
1406 | | /** |
1407 | | * soup_cache_clear: |
1408 | | * @cache: a #SoupCache |
1409 | | * |
1410 | | * Will remove all entries in the @cache plus all the cache files. |
1411 | | * |
1412 | | * This is not thread safe and must be called only from the thread that created the [class@Cache] |
1413 | | */ |
1414 | | void |
1415 | | soup_cache_clear (SoupCache *cache) |
1416 | 0 | { |
1417 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1418 | 0 | GList *entries; |
1419 | |
|
1420 | 0 | g_return_if_fail (SOUP_IS_CACHE (cache)); |
1421 | 0 | g_return_if_fail (priv->cache); |
1422 | | |
1423 | | /* Cannot use g_hash_table_foreach as callbacks must not modify the hash table */ |
1424 | 0 | entries = g_hash_table_get_values (priv->cache); |
1425 | 0 | g_list_foreach (entries, clear_cache_item, cache); |
1426 | 0 | g_list_free (entries); |
1427 | | |
1428 | | /* Remove also any file not associated with a cache entry. */ |
1429 | 0 | clear_cache_files (cache); |
1430 | 0 | } |
1431 | | |
1432 | | SoupMessage * |
1433 | | soup_cache_generate_conditional_request (SoupCache *cache, SoupMessage *original) |
1434 | 0 | { |
1435 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1436 | 0 | SoupMessage *msg; |
1437 | 0 | GUri *uri; |
1438 | 0 | SoupCacheEntry *entry; |
1439 | 0 | const char *last_modified, *etag; |
1440 | 0 | GList *disabled_features, *f; |
1441 | |
|
1442 | 0 | g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL); |
1443 | 0 | g_return_val_if_fail (SOUP_IS_MESSAGE (original), NULL); |
1444 | | |
1445 | | /* Add the validator entries in the header from the cached data */ |
1446 | 0 | g_mutex_lock (&priv->mutex); |
1447 | 0 | entry = soup_cache_entry_lookup (cache, original); |
1448 | 0 | g_mutex_unlock (&priv->mutex); |
1449 | 0 | g_return_val_if_fail (entry, NULL); |
1450 | | |
1451 | 0 | last_modified = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_LAST_MODIFIED); |
1452 | 0 | etag = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_ETAG); |
1453 | |
|
1454 | 0 | if (!last_modified && !etag) |
1455 | 0 | return NULL; |
1456 | | |
1457 | 0 | entry->being_validated = TRUE; |
1458 | | |
1459 | | /* Copy the data we need from the original message */ |
1460 | 0 | uri = soup_message_get_uri (original); |
1461 | 0 | msg = soup_message_new_from_uri (soup_message_get_method (original), uri); |
1462 | 0 | soup_message_set_flags (msg, soup_message_get_flags (original)); |
1463 | 0 | soup_message_disable_feature (msg, SOUP_TYPE_CACHE); |
1464 | |
|
1465 | 0 | soup_message_headers_foreach (soup_message_get_request_headers (original), |
1466 | 0 | (SoupMessageHeadersForeachFunc)copy_headers, |
1467 | 0 | soup_message_get_request_headers (msg)); |
1468 | |
|
1469 | 0 | disabled_features = soup_message_get_disabled_features (original); |
1470 | 0 | for (f = disabled_features; f; f = f->next) |
1471 | 0 | soup_message_disable_feature (msg, (GType) GPOINTER_TO_SIZE (f->data)); |
1472 | 0 | g_list_free (disabled_features); |
1473 | |
|
1474 | 0 | if (last_modified) |
1475 | 0 | soup_message_headers_append_common (soup_message_get_request_headers (msg), |
1476 | 0 | SOUP_HEADER_IF_MODIFIED_SINCE, |
1477 | 0 | last_modified, SOUP_HEADER_VALUE_TRUSTED); |
1478 | 0 | if (etag) |
1479 | 0 | soup_message_headers_append_common (soup_message_get_request_headers (msg), |
1480 | 0 | SOUP_HEADER_IF_NONE_MATCH, |
1481 | 0 | etag, SOUP_HEADER_VALUE_TRUSTED); |
1482 | |
|
1483 | 0 | return msg; |
1484 | 0 | } |
1485 | | |
1486 | | void |
1487 | | soup_cache_cancel_conditional_request (SoupCache *cache, |
1488 | | SoupMessage *msg) |
1489 | 0 | { |
1490 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1491 | 0 | SoupCacheEntry *entry; |
1492 | |
|
1493 | 0 | g_mutex_lock (&priv->mutex); |
1494 | 0 | entry = soup_cache_entry_lookup (cache, msg); |
1495 | 0 | g_mutex_unlock (&priv->mutex); |
1496 | 0 | if (entry) |
1497 | 0 | entry->being_validated = FALSE; |
1498 | |
|
1499 | 0 | soup_session_cancel_message (priv->session, msg); |
1500 | 0 | } |
1501 | | |
1502 | | void |
1503 | | soup_cache_update_from_conditional_request (SoupCache *cache, |
1504 | | SoupMessage *msg) |
1505 | 0 | { |
1506 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1507 | 0 | SoupCacheEntry *entry; |
1508 | |
|
1509 | 0 | g_mutex_lock (&priv->mutex); |
1510 | 0 | entry = soup_cache_entry_lookup (cache, msg); |
1511 | 0 | g_mutex_unlock (&priv->mutex); |
1512 | 0 | if (!entry) |
1513 | 0 | return; |
1514 | | |
1515 | 0 | entry->being_validated = FALSE; |
1516 | |
|
1517 | 0 | if (soup_message_get_status (msg) == SOUP_STATUS_NOT_MODIFIED) { |
1518 | 0 | soup_message_headers_foreach (soup_message_get_response_headers (msg), |
1519 | 0 | (SoupMessageHeadersForeachFunc) remove_headers, |
1520 | 0 | entry->headers); |
1521 | 0 | copy_end_to_end_headers (soup_message_get_response_headers (msg), entry->headers); |
1522 | |
|
1523 | 0 | soup_cache_entry_set_freshness (entry, msg, cache); |
1524 | 0 | } |
1525 | 0 | } |
1526 | | |
1527 | | static void |
1528 | | pack_entry (gpointer data, |
1529 | | gpointer user_data) |
1530 | 0 | { |
1531 | 0 | SoupCacheEntry *entry = (SoupCacheEntry *) data; |
1532 | 0 | SoupMessageHeadersIter iter; |
1533 | 0 | const char *header_key, *header_value; |
1534 | 0 | GVariantBuilder *entries_builder = (GVariantBuilder *)user_data; |
1535 | | |
1536 | | /* Do not store non-consolidated entries */ |
1537 | 0 | if (entry->dirty || !entry->key) |
1538 | 0 | return; |
1539 | | |
1540 | 0 | g_variant_builder_open (entries_builder, G_VARIANT_TYPE (SOUP_CACHE_PHEADERS_FORMAT)); |
1541 | 0 | g_variant_builder_add (entries_builder, "s", entry->uri); |
1542 | 0 | g_variant_builder_add (entries_builder, "b", entry->must_revalidate); |
1543 | 0 | g_variant_builder_add (entries_builder, "u", entry->freshness_lifetime); |
1544 | 0 | g_variant_builder_add (entries_builder, "u", entry->corrected_initial_age); |
1545 | 0 | g_variant_builder_add (entries_builder, "u", entry->response_time); |
1546 | 0 | g_variant_builder_add (entries_builder, "u", entry->hits); |
1547 | 0 | g_variant_builder_add (entries_builder, "u", entry->length); |
1548 | 0 | g_variant_builder_add (entries_builder, "q", entry->status_code); |
1549 | | |
1550 | | /* Pack headers */ |
1551 | 0 | g_variant_builder_open (entries_builder, G_VARIANT_TYPE ("a" SOUP_CACHE_HEADERS_FORMAT)); |
1552 | 0 | soup_message_headers_iter_init (&iter, entry->headers); |
1553 | 0 | while (soup_message_headers_iter_next (&iter, &header_key, &header_value)) { |
1554 | 0 | if (g_utf8_validate (header_value, -1, NULL)) |
1555 | 0 | g_variant_builder_add (entries_builder, SOUP_CACHE_HEADERS_FORMAT, |
1556 | 0 | header_key, header_value); |
1557 | 0 | } |
1558 | 0 | g_variant_builder_close (entries_builder); /* "a" SOUP_CACHE_HEADERS_FORMAT */ |
1559 | 0 | g_variant_builder_close (entries_builder); /* SOUP_CACHE_PHEADERS_FORMAT */ |
1560 | 0 | } |
1561 | | |
1562 | | /** |
1563 | | * soup_cache_dump: |
1564 | | * @cache: a #SoupCache |
1565 | | * |
1566 | | * Synchronously writes the cache index out to disk. |
1567 | | * |
1568 | | * Contrast with [method@Cache.flush], which writes pending cache *entries* to |
1569 | | * disk. |
1570 | | * |
1571 | | * You must call this before exiting if you want your cache data to |
1572 | | * persist between sessions. |
1573 | | * |
1574 | | * This is not thread safe and must be called only from the thread that created the [class@Cache] |
1575 | | */ |
1576 | | void |
1577 | | soup_cache_dump (SoupCache *cache) |
1578 | 0 | { |
1579 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1580 | 0 | char *filename; |
1581 | 0 | GVariantBuilder entries_builder; |
1582 | 0 | GVariant *cache_variant; |
1583 | |
|
1584 | 0 | if (!g_list_length (priv->lru_start)) |
1585 | 0 | return; |
1586 | | |
1587 | | /* Create the builder and iterate over all entries */ |
1588 | 0 | g_variant_builder_init (&entries_builder, G_VARIANT_TYPE (SOUP_CACHE_ENTRIES_FORMAT)); |
1589 | 0 | g_variant_builder_add (&entries_builder, "q", SOUP_CACHE_CURRENT_VERSION); |
1590 | 0 | g_variant_builder_open (&entries_builder, G_VARIANT_TYPE ("a" SOUP_CACHE_PHEADERS_FORMAT)); |
1591 | 0 | g_list_foreach (priv->lru_start, pack_entry, &entries_builder); |
1592 | 0 | g_variant_builder_close (&entries_builder); |
1593 | | |
1594 | | /* Serialize and dump */ |
1595 | 0 | cache_variant = g_variant_builder_end (&entries_builder); |
1596 | 0 | g_variant_ref_sink (cache_variant); |
1597 | 0 | filename = g_build_filename (priv->cache_dir, SOUP_CACHE_FILE, NULL); |
1598 | 0 | g_file_set_contents (filename, (const char *) g_variant_get_data (cache_variant), |
1599 | 0 | g_variant_get_size (cache_variant), NULL); |
1600 | 0 | g_free (filename); |
1601 | 0 | g_variant_unref (cache_variant); |
1602 | 0 | } |
1603 | | |
1604 | | static inline guint32 |
1605 | | get_key_from_cache_filename (const char *name) |
1606 | 0 | { |
1607 | 0 | guint64 key; |
1608 | |
|
1609 | 0 | key = g_ascii_strtoull (name, NULL, 10); |
1610 | 0 | return key ? (guint32)key : 0; |
1611 | 0 | } |
1612 | | |
1613 | | static void |
1614 | | insert_cache_file (SoupCache *cache, const char *name, GHashTable *leaked_entries) |
1615 | 0 | { |
1616 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1617 | 0 | gchar *path; |
1618 | |
|
1619 | 0 | path = g_build_filename (priv->cache_dir, name, NULL); |
1620 | 0 | if (g_file_test (path, G_FILE_TEST_IS_REGULAR)) { |
1621 | 0 | guint32 key = get_key_from_cache_filename (name); |
1622 | |
|
1623 | 0 | if (key) { |
1624 | 0 | g_hash_table_insert (leaked_entries, GUINT_TO_POINTER (key), path); |
1625 | 0 | return; |
1626 | 0 | } |
1627 | 0 | } |
1628 | 0 | g_free (path); |
1629 | 0 | } |
1630 | | |
1631 | | /** |
1632 | | * soup_cache_load: |
1633 | | * @cache: a #SoupCache |
1634 | | * |
1635 | | * Loads the contents of @cache's index into memory. |
1636 | | * |
1637 | | * This is not thread safe and must be called only from the thread that created the [class@Cache] |
1638 | | */ |
1639 | | void |
1640 | | soup_cache_load (SoupCache *cache) |
1641 | 0 | { |
1642 | 0 | gboolean must_revalidate; |
1643 | 0 | guint32 freshness_lifetime, hits; |
1644 | 0 | guint32 corrected_initial_age, response_time; |
1645 | 0 | char *url, *filename = NULL, *contents = NULL; |
1646 | 0 | GVariant *cache_variant; |
1647 | 0 | GVariantIter *entries_iter = NULL, *headers_iter = NULL; |
1648 | 0 | gsize length; |
1649 | 0 | SoupCacheEntry *entry; |
1650 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1651 | 0 | guint16 version, status_code; |
1652 | 0 | GHashTable *leaked_entries = NULL; |
1653 | 0 | GHashTableIter iter; |
1654 | 0 | gpointer value; |
1655 | |
|
1656 | 0 | filename = g_build_filename (priv->cache_dir, SOUP_CACHE_FILE, NULL); |
1657 | 0 | if (!g_file_get_contents (filename, &contents, &length, NULL)) { |
1658 | 0 | g_free (filename); |
1659 | 0 | g_free (contents); |
1660 | 0 | clear_cache_files (cache); |
1661 | 0 | return; |
1662 | 0 | } |
1663 | 0 | g_free (filename); |
1664 | |
|
1665 | 0 | cache_variant = g_variant_new_from_data (G_VARIANT_TYPE (SOUP_CACHE_ENTRIES_FORMAT), |
1666 | 0 | (const char *) contents, length, FALSE, g_free, contents); |
1667 | 0 | g_variant_get (cache_variant, SOUP_CACHE_ENTRIES_FORMAT, &version, &entries_iter); |
1668 | 0 | if (version != SOUP_CACHE_CURRENT_VERSION) { |
1669 | 0 | g_variant_iter_free (entries_iter); |
1670 | 0 | g_variant_unref (cache_variant); |
1671 | 0 | clear_cache_files (cache); |
1672 | 0 | return; |
1673 | 0 | } |
1674 | | |
1675 | 0 | leaked_entries = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free); |
1676 | 0 | soup_cache_foreach_file (cache, (SoupCacheForeachFileFunc)insert_cache_file, leaked_entries); |
1677 | |
|
1678 | 0 | while (g_variant_iter_loop (entries_iter, SOUP_CACHE_PHEADERS_FORMAT, |
1679 | 0 | &url, &must_revalidate, &freshness_lifetime, &corrected_initial_age, |
1680 | 0 | &response_time, &hits, &length, &status_code, |
1681 | 0 | &headers_iter)) { |
1682 | 0 | const char *header_key, *header_value; |
1683 | 0 | SoupMessageHeaders *headers; |
1684 | 0 | SoupMessageHeadersIter soup_headers_iter; |
1685 | | |
1686 | | /* SoupMessage Headers */ |
1687 | 0 | headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE); |
1688 | 0 | while (g_variant_iter_loop (headers_iter, SOUP_CACHE_HEADERS_FORMAT, &header_key, &header_value)) |
1689 | 0 | if (*header_key && *header_value) |
1690 | 0 | soup_message_headers_append (headers, header_key, header_value); |
1691 | | |
1692 | | /* Check that we have headers */ |
1693 | 0 | soup_message_headers_iter_init (&soup_headers_iter, headers); |
1694 | 0 | if (!soup_message_headers_iter_next (&soup_headers_iter, &header_key, &header_value)) { |
1695 | 0 | soup_message_headers_unref (headers); |
1696 | 0 | continue; |
1697 | 0 | } |
1698 | | |
1699 | | /* Insert in cache */ |
1700 | 0 | entry = g_slice_new0 (SoupCacheEntry); |
1701 | 0 | entry->uri = g_strdup (url); |
1702 | 0 | entry->must_revalidate = must_revalidate; |
1703 | 0 | entry->freshness_lifetime = freshness_lifetime; |
1704 | 0 | entry->corrected_initial_age = corrected_initial_age; |
1705 | 0 | entry->response_time = response_time; |
1706 | 0 | entry->hits = hits; |
1707 | 0 | entry->length = length; |
1708 | 0 | entry->headers = headers; |
1709 | 0 | entry->status_code = status_code; |
1710 | |
|
1711 | 0 | if (!soup_cache_entry_insert (cache, entry, FALSE)) |
1712 | 0 | soup_cache_entry_free (entry); |
1713 | 0 | else |
1714 | 0 | g_hash_table_remove (leaked_entries, GUINT_TO_POINTER (entry->key)); |
1715 | 0 | } |
1716 | | |
1717 | | /* Remove the leaked files */ |
1718 | 0 | g_hash_table_iter_init (&iter, leaked_entries); |
1719 | 0 | while (g_hash_table_iter_next (&iter, NULL, &value)) |
1720 | 0 | g_unlink ((char *)value); |
1721 | 0 | g_hash_table_destroy (leaked_entries); |
1722 | |
|
1723 | 0 | priv->lru_start = g_list_reverse (priv->lru_start); |
1724 | | |
1725 | | /* frees */ |
1726 | 0 | g_variant_iter_free (entries_iter); |
1727 | 0 | g_variant_unref (cache_variant); |
1728 | 0 | } |
1729 | | |
1730 | | /** |
1731 | | * soup_cache_set_max_size: |
1732 | | * @cache: a #SoupCache |
1733 | | * @max_size: the maximum size of the cache, in bytes |
1734 | | * |
1735 | | * Sets the maximum size of the cache. |
1736 | | */ |
1737 | | void |
1738 | | soup_cache_set_max_size (SoupCache *cache, |
1739 | | guint max_size) |
1740 | 0 | { |
1741 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1742 | 0 | priv->max_size = max_size; |
1743 | 0 | priv->max_entry_data_size = priv->max_size / MAX_ENTRY_DATA_PERCENTAGE; |
1744 | 0 | } |
1745 | | |
1746 | | /** |
1747 | | * soup_cache_get_max_size: |
1748 | | * @cache: a #SoupCache |
1749 | | * |
1750 | | * Gets the maximum size of the cache. |
1751 | | * |
1752 | | * Returns: the maximum size of the cache, in bytes. |
1753 | | */ |
1754 | | guint |
1755 | | soup_cache_get_max_size (SoupCache *cache) |
1756 | 0 | { |
1757 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1758 | 0 | return priv->max_size; |
1759 | 0 | } |