/src/libsoup/libsoup/cache/soup-cache.c
Line | Count | Source (jump to first uncovered line) |
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 | | G_DEFINE_TYPE_WITH_CODE (SoupCache, soup_cache, G_TYPE_OBJECT, |
148 | | G_ADD_PRIVATE (SoupCache) |
149 | | G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE, |
150 | | soup_cache_session_feature_init) |
151 | | G_IMPLEMENT_INTERFACE (SOUP_TYPE_CONTENT_PROCESSOR, |
152 | | soup_cache_content_processor_init)) |
153 | | |
154 | | static gboolean soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry, gboolean purge); |
155 | | static void make_room_for_new_entry (SoupCache *cache, guint length_to_add); |
156 | | static gboolean cache_accepts_entries_of_size (SoupCache *cache, guint length_to_add); |
157 | | |
158 | | static GFile * |
159 | | 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 | if (expires_d) { |
423 | 0 | date_d = soup_date_time_new_from_http_string (date); |
424 | |
|
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 | last_modified_t = g_date_time_to_unix (soup_date); |
467 | 0 | now = time (NULL); |
468 | |
|
469 | 0 | #define HEURISTIC_FACTOR 0.1 /* From Section 2.3.1.1 */ |
470 | |
|
471 | 0 | entry->freshness_lifetime = MAX (0, (now - last_modified_t) * HEURISTIC_FACTOR); |
472 | 0 | g_date_time_unref (soup_date); |
473 | 0 | } |
474 | |
|
475 | 0 | return; |
476 | | |
477 | 0 | expire: |
478 | | /* If all else fails, make the entry expire immediately */ |
479 | 0 | entry->freshness_lifetime = 0; |
480 | 0 | } |
481 | | |
482 | | static SoupCacheEntry * |
483 | | soup_cache_entry_new (SoupCache *cache, SoupMessage *msg, time_t request_time, time_t response_time) |
484 | 0 | { |
485 | 0 | SoupCacheEntry *entry; |
486 | 0 | const char *date; |
487 | |
|
488 | 0 | entry = g_slice_new0 (SoupCacheEntry); |
489 | 0 | entry->dirty = FALSE; |
490 | 0 | entry->being_validated = FALSE; |
491 | 0 | entry->status_code = soup_message_get_status (msg); |
492 | 0 | entry->response_time = response_time; |
493 | 0 | entry->uri = g_uri_to_string_partial (soup_message_get_uri (msg), G_URI_HIDE_PASSWORD); |
494 | | |
495 | | /* Headers */ |
496 | 0 | entry->headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE); |
497 | 0 | copy_end_to_end_headers (soup_message_get_response_headers (msg), entry->headers); |
498 | | |
499 | | /* LRU list */ |
500 | 0 | entry->hits = 0; |
501 | | |
502 | | /* Section 2.3.1, Freshness Lifetime */ |
503 | 0 | soup_cache_entry_set_freshness (entry, msg, cache); |
504 | | |
505 | | /* Section 2.3.2, Calculating Age */ |
506 | 0 | date = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_DATE); |
507 | |
|
508 | 0 | if (date) { |
509 | 0 | GDateTime *soup_date; |
510 | 0 | const char *age; |
511 | 0 | gint64 date_value, apparent_age, corrected_received_age, response_delay, age_value = 0; |
512 | |
|
513 | 0 | soup_date = soup_date_time_new_from_http_string (date); |
514 | 0 | date_value = g_date_time_to_unix (soup_date); |
515 | 0 | g_date_time_unref (soup_date); |
516 | |
|
517 | 0 | age = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_AGE); |
518 | 0 | if (age) |
519 | 0 | age_value = g_ascii_strtoll (age, NULL, 10); |
520 | |
|
521 | 0 | apparent_age = MAX (0, entry->response_time - date_value); |
522 | 0 | corrected_received_age = MAX (apparent_age, age_value); |
523 | 0 | response_delay = entry->response_time - request_time; |
524 | 0 | entry->corrected_initial_age = corrected_received_age + response_delay; |
525 | 0 | } else { |
526 | | /* Is this correct ? */ |
527 | 0 | entry->corrected_initial_age = time (NULL); |
528 | 0 | } |
529 | |
|
530 | 0 | return entry; |
531 | 0 | } |
532 | | |
533 | | static gboolean |
534 | | soup_cache_entry_remove (SoupCache *cache, SoupCacheEntry *entry, gboolean purge) |
535 | 0 | { |
536 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
537 | 0 | GList *lru_item; |
538 | |
|
539 | 0 | if (entry->dirty) { |
540 | 0 | g_cancellable_cancel (entry->cancellable); |
541 | 0 | return FALSE; |
542 | 0 | } |
543 | | |
544 | 0 | g_assert (!entry->dirty); |
545 | 0 | g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache)); |
546 | | |
547 | 0 | if (!g_hash_table_remove (priv->cache, GUINT_TO_POINTER (entry->key))) { |
548 | 0 | g_mutex_unlock (&priv->mutex); |
549 | 0 | return FALSE; |
550 | 0 | } |
551 | | |
552 | | /* Remove from LRU */ |
553 | 0 | lru_item = g_list_find (priv->lru_start, entry); |
554 | 0 | priv->lru_start = g_list_delete_link (priv->lru_start, lru_item); |
555 | | |
556 | | /* Adjust cache size */ |
557 | 0 | priv->size -= entry->length; |
558 | |
|
559 | 0 | g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache)); |
560 | | |
561 | | /* Free resources */ |
562 | 0 | if (purge) { |
563 | 0 | GFile *file = get_file_from_entry (cache, entry); |
564 | 0 | g_file_delete (file, NULL, NULL); |
565 | 0 | g_object_unref (file); |
566 | 0 | } |
567 | 0 | soup_cache_entry_free (entry); |
568 | |
|
569 | 0 | return TRUE; |
570 | 0 | } |
571 | | |
572 | | static gint |
573 | | lru_compare_func (gconstpointer a, gconstpointer b) |
574 | 0 | { |
575 | 0 | SoupCacheEntry *entry_a = (SoupCacheEntry *)a; |
576 | 0 | SoupCacheEntry *entry_b = (SoupCacheEntry *)b; |
577 | | |
578 | | /* The rationale of this sorting func is |
579 | | * |
580 | | * 1. sort by hits -> LRU algorithm, then |
581 | | * |
582 | | * 2. sort by freshness lifetime, we better discard first |
583 | | * entries that are close to expire |
584 | | * |
585 | | * 3. sort by size, replace first small size resources as they |
586 | | * are cheaper to download |
587 | | */ |
588 | | |
589 | | /* Sort by hits */ |
590 | 0 | if (entry_a->hits != entry_b->hits) |
591 | 0 | return entry_a->hits - entry_b->hits; |
592 | | |
593 | | /* Sort by freshness_lifetime */ |
594 | 0 | if (entry_a->freshness_lifetime != entry_b->freshness_lifetime) |
595 | 0 | return entry_a->freshness_lifetime - entry_b->freshness_lifetime; |
596 | | |
597 | | /* Sort by size */ |
598 | 0 | return entry_a->length - entry_b->length; |
599 | 0 | } |
600 | | |
601 | | static gboolean |
602 | | cache_accepts_entries_of_size (SoupCache *cache, guint length_to_add) |
603 | 0 | { |
604 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
605 | | /* We could add here some more heuristics. TODO: review how |
606 | | this is done by other HTTP caches */ |
607 | |
|
608 | 0 | return length_to_add <= priv->max_entry_data_size; |
609 | 0 | } |
610 | | |
611 | | static void |
612 | | make_room_for_new_entry (SoupCache *cache, guint length_to_add) |
613 | 0 | { |
614 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
615 | 0 | GList *lru_entry = priv->lru_start; |
616 | | |
617 | | /* Check that there is enough room for the new entry. This is |
618 | | an approximation as we're not working out the size of the |
619 | | cache file or the size of the headers for performance |
620 | | reasons. TODO: check if that would be really that expensive */ |
621 | |
|
622 | 0 | while (lru_entry && |
623 | 0 | (length_to_add + priv->size > priv->max_size)) { |
624 | 0 | SoupCacheEntry *old_entry = (SoupCacheEntry *)lru_entry->data; |
625 | | |
626 | | /* Discard entries. Once cancelled resources will be |
627 | | * freed in close_ready_cb |
628 | | */ |
629 | 0 | if (soup_cache_entry_remove (cache, old_entry, TRUE)) |
630 | 0 | lru_entry = priv->lru_start; |
631 | 0 | else |
632 | 0 | lru_entry = g_list_next (lru_entry); |
633 | 0 | } |
634 | 0 | } |
635 | | |
636 | | static gboolean |
637 | | soup_cache_entry_insert (SoupCache *cache, |
638 | | SoupCacheEntry *entry, |
639 | | gboolean sort) |
640 | 0 | { |
641 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
642 | 0 | guint length_to_add = 0; |
643 | 0 | SoupCacheEntry *old_entry; |
644 | | |
645 | | /* Fill the key */ |
646 | 0 | entry->key = get_cache_key_from_uri ((const char *) entry->uri); |
647 | |
|
648 | 0 | if (soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CONTENT_LENGTH) |
649 | 0 | length_to_add = soup_message_headers_get_content_length (entry->headers); |
650 | | |
651 | | /* Check if we are going to store the resource depending on its size */ |
652 | 0 | if (length_to_add) { |
653 | 0 | if (!cache_accepts_entries_of_size (cache, length_to_add)) |
654 | 0 | return FALSE; |
655 | | |
656 | | /* Make room for new entry if needed */ |
657 | 0 | make_room_for_new_entry (cache, length_to_add); |
658 | 0 | } |
659 | | |
660 | | /* Remove any previous entry */ |
661 | 0 | if ((old_entry = g_hash_table_lookup (priv->cache, GUINT_TO_POINTER (entry->key))) != NULL) { |
662 | 0 | if (!soup_cache_entry_remove (cache, old_entry, TRUE)) |
663 | 0 | return FALSE; |
664 | 0 | } |
665 | | |
666 | | /* Add to hash table */ |
667 | 0 | g_hash_table_insert (priv->cache, GUINT_TO_POINTER (entry->key), entry); |
668 | | |
669 | | /* Compute new cache size */ |
670 | 0 | priv->size += length_to_add; |
671 | | |
672 | | /* Update LRU */ |
673 | 0 | if (sort) |
674 | 0 | priv->lru_start = g_list_insert_sorted (priv->lru_start, entry, lru_compare_func); |
675 | 0 | else |
676 | 0 | priv->lru_start = g_list_prepend (priv->lru_start, entry); |
677 | |
|
678 | 0 | g_assert (g_list_length (priv->lru_start) == g_hash_table_size (priv->cache)); |
679 | | |
680 | 0 | return TRUE; |
681 | 0 | } |
682 | | |
683 | | static SoupCacheEntry* |
684 | | soup_cache_entry_lookup (SoupCache *cache, |
685 | | SoupMessage *msg) |
686 | 0 | { |
687 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
688 | 0 | SoupCacheEntry *entry; |
689 | 0 | guint32 key; |
690 | 0 | char *uri = NULL; |
691 | |
|
692 | 0 | uri = g_uri_to_string_partial (soup_message_get_uri (msg), G_URI_HIDE_PASSWORD); |
693 | 0 | key = get_cache_key_from_uri ((const char *) uri); |
694 | |
|
695 | 0 | entry = g_hash_table_lookup (priv->cache, GUINT_TO_POINTER (key)); |
696 | |
|
697 | 0 | if (entry != NULL && (strcmp (entry->uri, uri) != 0)) |
698 | 0 | entry = NULL; |
699 | |
|
700 | 0 | g_free (uri); |
701 | 0 | return entry; |
702 | 0 | } |
703 | | |
704 | | GInputStream * |
705 | | soup_cache_send_response (SoupCache *cache, SoupMessage *msg) |
706 | 0 | { |
707 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
708 | 0 | SoupCacheEntry *entry; |
709 | 0 | GInputStream *file_stream, *body_stream, *cache_stream, *client_stream; |
710 | 0 | GFile *file; |
711 | 0 | SoupMessageMetrics *metrics; |
712 | |
|
713 | 0 | g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL); |
714 | 0 | g_return_val_if_fail (SOUP_IS_MESSAGE (msg), NULL); |
715 | | |
716 | 0 | soup_message_set_metrics_timestamp (msg, SOUP_MESSAGE_METRICS_REQUEST_START); |
717 | |
|
718 | 0 | g_mutex_lock (&priv->mutex); |
719 | 0 | entry = soup_cache_entry_lookup (cache, msg); |
720 | 0 | g_mutex_unlock (&priv->mutex); |
721 | 0 | g_return_val_if_fail (entry, NULL); |
722 | | |
723 | 0 | file = get_file_from_entry (cache, entry); |
724 | 0 | file_stream = G_INPUT_STREAM (g_file_read (file, NULL, NULL)); |
725 | 0 | g_object_unref (file); |
726 | | |
727 | | /* Do not change the original message if there is no resource */ |
728 | 0 | if (!file_stream) |
729 | 0 | return NULL; |
730 | | |
731 | 0 | body_stream = soup_body_input_stream_new (file_stream, SOUP_ENCODING_CONTENT_LENGTH, entry->length); |
732 | 0 | g_object_unref (file_stream); |
733 | |
|
734 | 0 | if (!body_stream) |
735 | 0 | return NULL; |
736 | | |
737 | 0 | metrics = soup_message_get_metrics (msg); |
738 | 0 | if (metrics) |
739 | 0 | metrics->response_body_size = entry->length; |
740 | | |
741 | | /* If we are told to send a response from cache any validation |
742 | | in course is over by now */ |
743 | 0 | entry->being_validated = FALSE; |
744 | | |
745 | | /* Message starting */ |
746 | 0 | soup_message_starting (msg); |
747 | |
|
748 | 0 | soup_message_set_metrics_timestamp (msg, SOUP_MESSAGE_METRICS_RESPONSE_START); |
749 | | |
750 | | /* Status */ |
751 | 0 | soup_message_set_status (msg, entry->status_code, NULL); |
752 | | |
753 | | /* Headers */ |
754 | 0 | copy_end_to_end_headers (entry->headers, soup_message_get_response_headers (msg)); |
755 | | |
756 | | /* Create the cache stream. */ |
757 | 0 | soup_message_disable_feature (msg, SOUP_TYPE_CACHE); |
758 | 0 | cache_stream = soup_session_setup_message_body_input_stream (priv->session, |
759 | 0 | msg, body_stream, |
760 | 0 | SOUP_STAGE_ENTITY_BODY); |
761 | 0 | g_object_unref (body_stream); |
762 | |
|
763 | 0 | client_stream = soup_cache_client_input_stream_new (cache_stream); |
764 | 0 | g_object_unref (cache_stream); |
765 | |
|
766 | 0 | return client_stream; |
767 | 0 | } |
768 | | |
769 | | static void |
770 | | msg_got_headers_cb (SoupMessage *msg, gpointer user_data) |
771 | 0 | { |
772 | 0 | g_object_set_data (G_OBJECT (msg), "response-time", GINT_TO_POINTER (time (NULL))); |
773 | 0 | g_signal_handlers_disconnect_by_func (msg, msg_got_headers_cb, user_data); |
774 | 0 | } |
775 | | |
776 | | static void |
777 | | msg_starting_cb (SoupMessage *msg, gpointer user_data) |
778 | 0 | { |
779 | 0 | g_object_set_data (G_OBJECT (msg), "request-time", GINT_TO_POINTER (time (NULL))); |
780 | 0 | g_signal_connect (msg, "got-headers", G_CALLBACK (msg_got_headers_cb), user_data); |
781 | 0 | g_signal_handlers_disconnect_by_func (msg, msg_starting_cb, user_data); |
782 | 0 | } |
783 | | |
784 | | static void |
785 | | request_queued (SoupSessionFeature *feature, |
786 | | SoupMessage *msg) |
787 | 0 | { |
788 | 0 | g_signal_connect (msg, "starting", G_CALLBACK (msg_starting_cb), feature); |
789 | 0 | } |
790 | | |
791 | | static void |
792 | | attach (SoupSessionFeature *feature, SoupSession *session) |
793 | 0 | { |
794 | 0 | SoupCache *cache = SOUP_CACHE (feature); |
795 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
796 | 0 | priv->session = session; |
797 | 0 | } |
798 | | |
799 | | static void |
800 | | soup_cache_session_feature_init (SoupSessionFeatureInterface *feature_interface, |
801 | | gpointer interface_data) |
802 | 0 | { |
803 | 0 | feature_interface->attach = attach; |
804 | 0 | feature_interface->request_queued = request_queued; |
805 | 0 | } |
806 | | |
807 | | typedef struct { |
808 | | SoupCache *cache; |
809 | | SoupCacheEntry *entry; |
810 | | } StreamHelper; |
811 | | |
812 | | static void |
813 | | istream_caching_finished (SoupCacheInputStream *istream, |
814 | | gsize bytes_written, |
815 | | GError *error, |
816 | | gpointer user_data) |
817 | 0 | { |
818 | 0 | StreamHelper *helper = (StreamHelper *) user_data; |
819 | 0 | SoupCache *cache = helper->cache; |
820 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
821 | 0 | SoupCacheEntry *entry = helper->entry; |
822 | |
|
823 | 0 | g_mutex_lock (&priv->mutex); |
824 | |
|
825 | 0 | --priv->n_pending; |
826 | |
|
827 | 0 | entry->dirty = FALSE; |
828 | 0 | entry->length = bytes_written; |
829 | 0 | g_clear_object (&entry->cancellable); |
830 | |
|
831 | 0 | if (error) { |
832 | | /* Update cache size */ |
833 | 0 | if (soup_message_headers_get_encoding (entry->headers) == SOUP_ENCODING_CONTENT_LENGTH) |
834 | 0 | priv->size -= soup_message_headers_get_content_length (entry->headers); |
835 | |
|
836 | 0 | soup_cache_entry_remove (cache, entry, TRUE); |
837 | 0 | helper->entry = entry = NULL; |
838 | 0 | goto cleanup; |
839 | 0 | } |
840 | | |
841 | 0 | if (soup_message_headers_get_encoding (entry->headers) != SOUP_ENCODING_CONTENT_LENGTH) { |
842 | |
|
843 | 0 | if (cache_accepts_entries_of_size (cache, entry->length)) { |
844 | 0 | make_room_for_new_entry (cache, entry->length); |
845 | 0 | priv->size += entry->length; |
846 | 0 | } else { |
847 | 0 | soup_cache_entry_remove (cache, entry, TRUE); |
848 | 0 | helper->entry = entry = NULL; |
849 | 0 | } |
850 | 0 | } |
851 | |
|
852 | 0 | cleanup: |
853 | 0 | g_mutex_unlock (&priv->mutex); |
854 | 0 | g_object_unref (helper->cache); |
855 | 0 | g_slice_free (StreamHelper, helper); |
856 | 0 | } |
857 | | |
858 | | static GInputStream* |
859 | | soup_cache_content_processor_wrap_input (SoupContentProcessor *processor, |
860 | | GInputStream *base_stream, |
861 | | SoupMessage *msg, |
862 | | GError **error) |
863 | 0 | { |
864 | 0 | SoupCache *cache = (SoupCache*) processor; |
865 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
866 | 0 | SoupCacheEntry *entry; |
867 | 0 | SoupCacheability cacheability; |
868 | 0 | GInputStream *istream; |
869 | 0 | GFile *file; |
870 | 0 | StreamHelper *helper; |
871 | 0 | time_t request_time, response_time; |
872 | |
|
873 | 0 | g_mutex_lock (&priv->mutex); |
874 | | |
875 | | /* First of all, check if we should cache the resource. */ |
876 | 0 | cacheability = soup_cache_get_cacheability (cache, msg); |
877 | 0 | entry = soup_cache_entry_lookup (cache, msg); |
878 | |
|
879 | 0 | if (cacheability & SOUP_CACHE_INVALIDATES) { |
880 | 0 | if (entry) |
881 | 0 | soup_cache_entry_remove (cache, entry, TRUE); |
882 | 0 | g_mutex_unlock (&priv->mutex); |
883 | 0 | return NULL; |
884 | 0 | } |
885 | | |
886 | 0 | if (cacheability & SOUP_CACHE_VALIDATES) { |
887 | | /* It's possible to get a CACHE_VALIDATES with no |
888 | | * entry in the hash table. This could happen if for |
889 | | * example the soup client is the one creating the |
890 | | * conditional request. |
891 | | */ |
892 | 0 | if (entry) |
893 | 0 | soup_cache_update_from_conditional_request (cache, msg); |
894 | 0 | g_mutex_unlock (&priv->mutex); |
895 | 0 | return NULL; |
896 | 0 | } |
897 | | |
898 | 0 | if (!(cacheability & SOUP_CACHE_CACHEABLE)) { |
899 | 0 | g_mutex_unlock (&priv->mutex); |
900 | 0 | return NULL; |
901 | 0 | } |
902 | | |
903 | | /* Check if we are already caching this resource */ |
904 | 0 | if (entry && (entry->dirty || entry->being_validated)) { |
905 | 0 | g_mutex_unlock (&priv->mutex); |
906 | 0 | return NULL; |
907 | 0 | } |
908 | | |
909 | | /* Create a new entry, deleting any old one if present */ |
910 | 0 | if (entry) |
911 | 0 | soup_cache_entry_remove (cache, entry, TRUE); |
912 | |
|
913 | 0 | request_time = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "request-time")); |
914 | 0 | response_time = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (msg), "response-time")); |
915 | 0 | entry = soup_cache_entry_new (cache, msg, request_time, response_time); |
916 | 0 | entry->hits = 1; |
917 | 0 | entry->dirty = TRUE; |
918 | | |
919 | | /* Do not continue if it can not be stored */ |
920 | 0 | if (!soup_cache_entry_insert (cache, entry, TRUE)) { |
921 | 0 | soup_cache_entry_free (entry); |
922 | 0 | g_mutex_unlock (&priv->mutex); |
923 | 0 | return NULL; |
924 | 0 | } |
925 | | |
926 | 0 | entry->cancellable = g_cancellable_new (); |
927 | 0 | ++priv->n_pending; |
928 | |
|
929 | 0 | g_mutex_unlock (&priv->mutex); |
930 | |
|
931 | 0 | helper = g_slice_new (StreamHelper); |
932 | 0 | helper->cache = g_object_ref (cache); |
933 | 0 | helper->entry = entry; |
934 | |
|
935 | 0 | file = get_file_from_entry (cache, entry); |
936 | 0 | istream = soup_cache_input_stream_new (base_stream, file); |
937 | 0 | g_object_unref (file); |
938 | |
|
939 | 0 | g_signal_connect (istream, "caching-finished", G_CALLBACK (istream_caching_finished), helper); |
940 | |
|
941 | 0 | return istream; |
942 | 0 | } |
943 | | |
944 | | static void |
945 | | soup_cache_content_processor_init (SoupContentProcessorInterface *processor_interface, |
946 | | gpointer interface_data) |
947 | 0 | { |
948 | 0 | soup_cache_default_content_processor_interface = |
949 | 0 | g_type_default_interface_peek (SOUP_TYPE_CONTENT_PROCESSOR); |
950 | |
|
951 | 0 | processor_interface->processing_stage = SOUP_STAGE_ENTITY_BODY; |
952 | 0 | processor_interface->wrap_input = soup_cache_content_processor_wrap_input; |
953 | 0 | } |
954 | | |
955 | | static void |
956 | | soup_cache_init (SoupCache *cache) |
957 | 0 | { |
958 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
959 | |
|
960 | 0 | priv->cache = g_hash_table_new (g_direct_hash, g_direct_equal); |
961 | | /* LRU */ |
962 | 0 | priv->lru_start = NULL; |
963 | | |
964 | | /* */ |
965 | 0 | priv->n_pending = 0; |
966 | | |
967 | | /* Cache size */ |
968 | 0 | priv->max_size = DEFAULT_MAX_SIZE; |
969 | 0 | priv->max_entry_data_size = priv->max_size / MAX_ENTRY_DATA_PERCENTAGE; |
970 | 0 | priv->size = 0; |
971 | |
|
972 | 0 | g_mutex_init (&priv->mutex); |
973 | 0 | } |
974 | | |
975 | | static void |
976 | | remove_cache_item (gpointer data, |
977 | | gpointer user_data) |
978 | 0 | { |
979 | 0 | soup_cache_entry_remove ((SoupCache *) user_data, (SoupCacheEntry *) data, FALSE); |
980 | 0 | } |
981 | | |
982 | | static void |
983 | | soup_cache_finalize (GObject *object) |
984 | 0 | { |
985 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private ((SoupCache*)object); |
986 | 0 | GList *entries; |
987 | | |
988 | | /* Cannot use g_hash_table_foreach as callbacks must not modify the hash table */ |
989 | 0 | entries = g_hash_table_get_values (priv->cache); |
990 | 0 | g_list_foreach (entries, remove_cache_item, object); |
991 | 0 | g_list_free (entries); |
992 | |
|
993 | 0 | g_hash_table_destroy (priv->cache); |
994 | 0 | g_free (priv->cache_dir); |
995 | |
|
996 | 0 | g_list_free (priv->lru_start); |
997 | |
|
998 | 0 | g_mutex_clear (&priv->mutex); |
999 | |
|
1000 | 0 | G_OBJECT_CLASS (soup_cache_parent_class)->finalize (object); |
1001 | 0 | } |
1002 | | |
1003 | | static void |
1004 | | soup_cache_set_property (GObject *object, guint prop_id, |
1005 | | const GValue *value, GParamSpec *pspec) |
1006 | 0 | { |
1007 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private ((SoupCache*)object); |
1008 | |
|
1009 | 0 | switch (prop_id) { |
1010 | 0 | case PROP_CACHE_DIR: |
1011 | 0 | g_assert (!priv->cache_dir); |
1012 | | |
1013 | 0 | priv->cache_dir = g_value_dup_string (value); |
1014 | |
|
1015 | 0 | if (!priv->cache_dir) |
1016 | | /* Set a default cache dir, different for each user */ |
1017 | 0 | priv->cache_dir = g_build_filename (g_get_user_cache_dir (), |
1018 | 0 | "httpcache", |
1019 | 0 | NULL); |
1020 | | |
1021 | | /* Create directory if it does not exist */ |
1022 | 0 | if (!g_file_test (priv->cache_dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) |
1023 | 0 | g_mkdir_with_parents (priv->cache_dir, 0700); |
1024 | 0 | break; |
1025 | 0 | case PROP_CACHE_TYPE: |
1026 | 0 | priv->cache_type = g_value_get_enum (value); |
1027 | | /* TODO: clear private entries and issue a warning if moving to shared? */ |
1028 | 0 | break; |
1029 | 0 | default: |
1030 | 0 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
1031 | 0 | break; |
1032 | 0 | } |
1033 | 0 | } |
1034 | | |
1035 | | static void |
1036 | | soup_cache_get_property (GObject *object, guint prop_id, |
1037 | | GValue *value, GParamSpec *pspec) |
1038 | 0 | { |
1039 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private ((SoupCache*)object); |
1040 | |
|
1041 | 0 | switch (prop_id) { |
1042 | 0 | case PROP_CACHE_DIR: |
1043 | 0 | g_value_set_string (value, priv->cache_dir); |
1044 | 0 | break; |
1045 | 0 | case PROP_CACHE_TYPE: |
1046 | 0 | g_value_set_enum (value, priv->cache_type); |
1047 | 0 | break; |
1048 | 0 | default: |
1049 | 0 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
1050 | 0 | break; |
1051 | 0 | } |
1052 | 0 | } |
1053 | | |
1054 | | static void |
1055 | | soup_cache_class_init (SoupCacheClass *cache_class) |
1056 | 0 | { |
1057 | 0 | GObjectClass *gobject_class = (GObjectClass *)cache_class; |
1058 | |
|
1059 | 0 | gobject_class->finalize = soup_cache_finalize; |
1060 | 0 | gobject_class->set_property = soup_cache_set_property; |
1061 | 0 | gobject_class->get_property = soup_cache_get_property; |
1062 | |
|
1063 | 0 | cache_class->get_cacheability = get_cacheability; |
1064 | | |
1065 | | /** |
1066 | | * SoupCache:cache-dir: |
1067 | | * The directory to store the cache files. |
1068 | | */ |
1069 | 0 | properties[PROP_CACHE_DIR] = |
1070 | 0 | g_param_spec_string ("cache-dir", |
1071 | 0 | "Cache directory", |
1072 | 0 | "The directory to store the cache files", |
1073 | 0 | NULL, |
1074 | 0 | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | |
1075 | 0 | G_PARAM_STATIC_STRINGS); |
1076 | | |
1077 | | /** |
1078 | | * SoupCache:cache-type: |
1079 | | * Whether the cache is private or shared. |
1080 | | */ |
1081 | 0 | properties[PROP_CACHE_TYPE] = |
1082 | 0 | g_param_spec_enum ("cache-type", |
1083 | 0 | "Cache type", |
1084 | 0 | "Whether the cache is private or shared", |
1085 | 0 | SOUP_TYPE_CACHE_TYPE, |
1086 | 0 | SOUP_CACHE_SINGLE_USER, |
1087 | 0 | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | |
1088 | 0 | G_PARAM_STATIC_STRINGS); |
1089 | |
|
1090 | 0 | g_object_class_install_properties (gobject_class, LAST_PROPERTY, properties); |
1091 | 0 | } |
1092 | | |
1093 | | /** |
1094 | | * SoupCacheType: |
1095 | | * @SOUP_CACHE_SINGLE_USER: a single-user cache |
1096 | | * @SOUP_CACHE_SHARED: a shared cache |
1097 | | * |
1098 | | * The type of cache; this affects what kinds of responses will be |
1099 | | * saved. |
1100 | | * |
1101 | | */ |
1102 | | |
1103 | | /** |
1104 | | * soup_cache_new: |
1105 | | * @cache_dir: (nullable): the directory to store the cached data, or %NULL |
1106 | | * to use the default one. Note that since the cache isn't safe to access for |
1107 | | * multiple processes at once, and the default directory isn't namespaced by |
1108 | | * process, clients are strongly discouraged from passing %NULL. |
1109 | | * @cache_type: the #SoupCacheType of the cache |
1110 | | * |
1111 | | * Creates a new #SoupCache. |
1112 | | * |
1113 | | * Returns: a new #SoupCache |
1114 | | * |
1115 | | */ |
1116 | | SoupCache * |
1117 | | soup_cache_new (const char *cache_dir, SoupCacheType cache_type) |
1118 | 0 | { |
1119 | 0 | return g_object_new (SOUP_TYPE_CACHE, |
1120 | 0 | "cache-dir", cache_dir, |
1121 | 0 | "cache-type", cache_type, |
1122 | 0 | NULL); |
1123 | 0 | } |
1124 | | |
1125 | | /** |
1126 | | * soup_cache_has_response: |
1127 | | * @cache: a #SoupCache |
1128 | | * @msg: a #SoupMessage |
1129 | | * |
1130 | | * This function calculates whether the @cache object has a proper |
1131 | | * response for the request @msg given the flags both in the request |
1132 | | * and the cached reply and the time ellapsed since it was cached. |
1133 | | * |
1134 | | * Returns: whether or not the @cache has a valid response for @msg |
1135 | | * |
1136 | | */ |
1137 | | SoupCacheResponse |
1138 | | soup_cache_has_response (SoupCache *cache, SoupMessage *msg) |
1139 | 0 | { |
1140 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1141 | 0 | SoupCacheEntry *entry; |
1142 | 0 | const char *cache_control; |
1143 | 0 | gpointer value; |
1144 | 0 | int max_age, max_stale, min_fresh; |
1145 | 0 | GList *lru_item, *item; |
1146 | |
|
1147 | 0 | g_mutex_lock (&priv->mutex); |
1148 | |
|
1149 | 0 | entry = soup_cache_entry_lookup (cache, msg); |
1150 | | |
1151 | | /* 1. The presented Request-URI and that of stored response |
1152 | | * match |
1153 | | */ |
1154 | 0 | if (!entry) { |
1155 | 0 | g_mutex_unlock (&priv->mutex); |
1156 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1157 | 0 | } |
1158 | | |
1159 | | /* Increase hit count. Take sorting into account */ |
1160 | 0 | entry->hits++; |
1161 | 0 | lru_item = g_list_find (priv->lru_start, entry); |
1162 | 0 | item = lru_item; |
1163 | 0 | while (item->next && lru_compare_func (item->data, item->next->data) > 0) |
1164 | 0 | item = g_list_next (item); |
1165 | |
|
1166 | 0 | if (item != lru_item) { |
1167 | 0 | priv->lru_start = g_list_remove_link (priv->lru_start, lru_item); |
1168 | 0 | item = g_list_insert_sorted (item, lru_item->data, lru_compare_func); |
1169 | 0 | (void)item; // Silence scan-build warning |
1170 | 0 | g_list_free (lru_item); |
1171 | 0 | } |
1172 | |
|
1173 | 0 | g_mutex_unlock (&priv->mutex); |
1174 | |
|
1175 | 0 | if (entry->dirty || entry->being_validated) |
1176 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1177 | | |
1178 | | /* 2. The request method associated with the stored response |
1179 | | * allows it to be used for the presented request |
1180 | | */ |
1181 | | |
1182 | | /* In practice this means we only return our resource for GET, |
1183 | | * cacheability for other methods is a TODO in the RFC |
1184 | | * (TODO: although we could return the headers for HEAD |
1185 | | * probably). |
1186 | | */ |
1187 | 0 | if (soup_message_get_method (msg) != SOUP_METHOD_GET) |
1188 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1189 | | |
1190 | | /* 3. Selecting request-headers nominated by the stored |
1191 | | * response (if any) match those presented. |
1192 | | */ |
1193 | | |
1194 | | /* TODO */ |
1195 | | |
1196 | | /* 4. The request is a conditional request issued by the client. |
1197 | | */ |
1198 | 0 | if (soup_message_headers_get_one_common (soup_message_get_request_headers (msg), SOUP_HEADER_IF_MODIFIED_SINCE) || |
1199 | 0 | soup_message_headers_get_list_common (soup_message_get_request_headers (msg), SOUP_HEADER_IF_NONE_MATCH)) |
1200 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1201 | | |
1202 | | /* 5. The presented request and stored response are free from |
1203 | | * directives that would prevent its use. |
1204 | | */ |
1205 | | |
1206 | 0 | max_age = max_stale = min_fresh = -1; |
1207 | | |
1208 | | /* For HTTP 1.0 compatibility. RFC2616 section 14.9.4 |
1209 | | */ |
1210 | 0 | if (soup_message_headers_header_contains_common (soup_message_get_request_headers (msg), SOUP_HEADER_PRAGMA, "no-cache")) |
1211 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1212 | | |
1213 | 0 | cache_control = soup_message_headers_get_list_common (soup_message_get_request_headers (msg), SOUP_HEADER_CACHE_CONTROL); |
1214 | 0 | if (cache_control && *cache_control) { |
1215 | 0 | GHashTable *hash = soup_header_parse_param_list (cache_control); |
1216 | |
|
1217 | 0 | if (g_hash_table_lookup_extended (hash, "no-store", NULL, NULL)) { |
1218 | 0 | soup_header_free_param_list (hash); |
1219 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1220 | 0 | } |
1221 | | |
1222 | 0 | if (g_hash_table_lookup_extended (hash, "no-cache", NULL, NULL)) { |
1223 | 0 | soup_header_free_param_list (hash); |
1224 | 0 | return SOUP_CACHE_RESPONSE_STALE; |
1225 | 0 | } |
1226 | | |
1227 | 0 | if (g_hash_table_lookup_extended (hash, "max-age", NULL, &value) && value) { |
1228 | 0 | max_age = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32); |
1229 | | /* Forcing cache revalidaton |
1230 | | */ |
1231 | 0 | if (!max_age) { |
1232 | 0 | soup_header_free_param_list (hash); |
1233 | 0 | return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION; |
1234 | 0 | } |
1235 | 0 | } |
1236 | | |
1237 | | /* max-stale can have no value set, we need to use _extended */ |
1238 | 0 | if (g_hash_table_lookup_extended (hash, "max-stale", NULL, &value)) { |
1239 | 0 | if (value) |
1240 | 0 | max_stale = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32); |
1241 | 0 | else |
1242 | 0 | max_stale = G_MAXINT32; |
1243 | 0 | } |
1244 | |
|
1245 | 0 | value = g_hash_table_lookup (hash, "min-fresh"); |
1246 | 0 | if (value) |
1247 | 0 | min_fresh = (int)MIN (g_ascii_strtoll (value, NULL, 10), G_MAXINT32); |
1248 | |
|
1249 | 0 | soup_header_free_param_list (hash); |
1250 | |
|
1251 | 0 | if (max_age > 0) { |
1252 | 0 | guint current_age = soup_cache_entry_get_current_age (entry); |
1253 | | |
1254 | | /* If we are over max-age and max-stale is not |
1255 | | set, do not use the value from the cache |
1256 | | without validation */ |
1257 | 0 | if ((guint) max_age <= current_age && max_stale == -1) |
1258 | 0 | return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION; |
1259 | 0 | } |
1260 | 0 | } |
1261 | | |
1262 | | /* 6. The stored response is either: fresh, allowed to be |
1263 | | * served stale or succesfully validated |
1264 | | */ |
1265 | 0 | if (!soup_cache_entry_is_fresh_enough (entry, min_fresh)) { |
1266 | | /* Not fresh, can it be served stale? */ |
1267 | | |
1268 | | /* When the must-revalidate directive is present in a |
1269 | | * response received by a cache, that cache MUST NOT |
1270 | | * use the entry after it becomes stale |
1271 | | */ |
1272 | | /* TODO consider also proxy-revalidate & s-maxage */ |
1273 | 0 | if (entry->must_revalidate) |
1274 | 0 | return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION; |
1275 | | |
1276 | 0 | if (max_stale != -1) { |
1277 | | /* G_MAXINT32 means we accept any staleness */ |
1278 | 0 | if (max_stale == G_MAXINT32) |
1279 | 0 | return SOUP_CACHE_RESPONSE_FRESH; |
1280 | | |
1281 | 0 | if ((soup_cache_entry_get_current_age (entry) - entry->freshness_lifetime) <= (guint) max_stale) |
1282 | 0 | return SOUP_CACHE_RESPONSE_FRESH; |
1283 | 0 | } |
1284 | | |
1285 | 0 | return SOUP_CACHE_RESPONSE_NEEDS_VALIDATION; |
1286 | 0 | } |
1287 | | |
1288 | 0 | return SOUP_CACHE_RESPONSE_FRESH; |
1289 | 0 | } |
1290 | | |
1291 | | /** |
1292 | | * soup_cache_get_cacheability: |
1293 | | * @cache: a #SoupCache |
1294 | | * @msg: a #SoupMessage |
1295 | | * |
1296 | | * Calculates whether the @msg can be cached or not. |
1297 | | * |
1298 | | * Returns: a [flags@Cacheability] value indicating whether the @msg can be cached |
1299 | | * or not. |
1300 | | */ |
1301 | | SoupCacheability |
1302 | | soup_cache_get_cacheability (SoupCache *cache, SoupMessage *msg) |
1303 | 0 | { |
1304 | 0 | g_return_val_if_fail (SOUP_IS_CACHE (cache), SOUP_CACHE_UNCACHEABLE); |
1305 | 0 | g_return_val_if_fail (SOUP_IS_MESSAGE (msg), SOUP_CACHE_UNCACHEABLE); |
1306 | | |
1307 | 0 | return SOUP_CACHE_GET_CLASS (cache)->get_cacheability (cache, msg); |
1308 | 0 | } |
1309 | | |
1310 | | static gboolean |
1311 | | force_flush_timeout (gpointer data) |
1312 | 0 | { |
1313 | 0 | gboolean *forced = (gboolean *)data; |
1314 | 0 | *forced = TRUE; |
1315 | |
|
1316 | 0 | return FALSE; |
1317 | 0 | } |
1318 | | |
1319 | | /** |
1320 | | * soup_cache_flush: |
1321 | | * @cache: a #SoupCache |
1322 | | * |
1323 | | * Forces all pending writes in the @cache to be |
1324 | | * committed to disk. |
1325 | | * |
1326 | | * For doing so it will iterate the [struct@GLib.MainContext] associated with |
1327 | | * @cache's session as long as needed. |
1328 | | * |
1329 | | * Contrast with [method@Cache.dump], which writes out the cache index file. |
1330 | | */ |
1331 | | void |
1332 | | soup_cache_flush (SoupCache *cache) |
1333 | 0 | { |
1334 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1335 | 0 | GMainContext *async_context; |
1336 | 0 | SoupSession *session; |
1337 | 0 | GSource *timeout; |
1338 | 0 | gboolean forced = FALSE; |
1339 | |
|
1340 | 0 | g_return_if_fail (SOUP_IS_CACHE (cache)); |
1341 | | |
1342 | 0 | session = priv->session; |
1343 | 0 | g_return_if_fail (SOUP_IS_SESSION (session)); |
1344 | 0 | async_context = g_main_context_get_thread_default (); |
1345 | | |
1346 | | /* We give cache 10 secs to finish */ |
1347 | 0 | timeout = soup_add_timeout (async_context, 10000, force_flush_timeout, &forced); |
1348 | |
|
1349 | 0 | while (!forced && priv->n_pending > 0) |
1350 | 0 | g_main_context_iteration (async_context, FALSE); |
1351 | |
|
1352 | 0 | if (!forced) |
1353 | 0 | g_source_destroy (timeout); |
1354 | 0 | else |
1355 | 0 | g_warning ("Cache flush finished despite %d pending requests", priv->n_pending); |
1356 | |
|
1357 | 0 | g_source_unref (timeout); |
1358 | 0 | } |
1359 | | |
1360 | | typedef void (* SoupCacheForeachFileFunc) (SoupCache *cache, const char *name, gpointer user_data); |
1361 | | |
1362 | | static void |
1363 | | soup_cache_foreach_file (SoupCache *cache, SoupCacheForeachFileFunc func, gpointer user_data) |
1364 | 0 | { |
1365 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1366 | 0 | GDir *dir; |
1367 | 0 | const char *name; |
1368 | |
|
1369 | 0 | dir = g_dir_open (priv->cache_dir, 0, NULL); |
1370 | 0 | while ((name = g_dir_read_name (dir))) { |
1371 | 0 | if (g_str_has_prefix (name, "soup.")) |
1372 | 0 | continue; |
1373 | | |
1374 | 0 | func (cache, name, user_data); |
1375 | 0 | } |
1376 | 0 | g_dir_close (dir); |
1377 | 0 | } |
1378 | | |
1379 | | static void |
1380 | | clear_cache_item (gpointer data, |
1381 | | gpointer user_data) |
1382 | 0 | { |
1383 | 0 | soup_cache_entry_remove ((SoupCache *) user_data, (SoupCacheEntry *) data, TRUE); |
1384 | 0 | } |
1385 | | |
1386 | | static void |
1387 | | delete_cache_file (SoupCache *cache, const char *name, gpointer user_data) |
1388 | 0 | { |
1389 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1390 | 0 | gchar *path; |
1391 | |
|
1392 | 0 | path = g_build_filename (priv->cache_dir, name, NULL); |
1393 | 0 | g_unlink (path); |
1394 | 0 | g_free (path); |
1395 | 0 | } |
1396 | | |
1397 | | static void |
1398 | | clear_cache_files (SoupCache *cache) |
1399 | 0 | { |
1400 | 0 | soup_cache_foreach_file (cache, delete_cache_file, NULL); |
1401 | 0 | } |
1402 | | |
1403 | | /** |
1404 | | * soup_cache_clear: |
1405 | | * @cache: a #SoupCache |
1406 | | * |
1407 | | * Will remove all entries in the @cache plus all the cache files. |
1408 | | * |
1409 | | * This is not thread safe and must be called only from the thread that created the [class@Cache] |
1410 | | */ |
1411 | | void |
1412 | | soup_cache_clear (SoupCache *cache) |
1413 | 0 | { |
1414 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1415 | 0 | GList *entries; |
1416 | |
|
1417 | 0 | g_return_if_fail (SOUP_IS_CACHE (cache)); |
1418 | 0 | g_return_if_fail (priv->cache); |
1419 | | |
1420 | | /* Cannot use g_hash_table_foreach as callbacks must not modify the hash table */ |
1421 | 0 | entries = g_hash_table_get_values (priv->cache); |
1422 | 0 | g_list_foreach (entries, clear_cache_item, cache); |
1423 | 0 | g_list_free (entries); |
1424 | | |
1425 | | /* Remove also any file not associated with a cache entry. */ |
1426 | 0 | clear_cache_files (cache); |
1427 | 0 | } |
1428 | | |
1429 | | SoupMessage * |
1430 | | soup_cache_generate_conditional_request (SoupCache *cache, SoupMessage *original) |
1431 | 0 | { |
1432 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1433 | 0 | SoupMessage *msg; |
1434 | 0 | GUri *uri; |
1435 | 0 | SoupCacheEntry *entry; |
1436 | 0 | const char *last_modified, *etag; |
1437 | 0 | GList *disabled_features, *f; |
1438 | |
|
1439 | 0 | g_return_val_if_fail (SOUP_IS_CACHE (cache), NULL); |
1440 | 0 | g_return_val_if_fail (SOUP_IS_MESSAGE (original), NULL); |
1441 | | |
1442 | | /* Add the validator entries in the header from the cached data */ |
1443 | 0 | g_mutex_lock (&priv->mutex); |
1444 | 0 | entry = soup_cache_entry_lookup (cache, original); |
1445 | 0 | g_mutex_unlock (&priv->mutex); |
1446 | 0 | g_return_val_if_fail (entry, NULL); |
1447 | | |
1448 | 0 | last_modified = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_LAST_MODIFIED); |
1449 | 0 | etag = soup_message_headers_get_one_common (entry->headers, SOUP_HEADER_ETAG); |
1450 | |
|
1451 | 0 | if (!last_modified && !etag) |
1452 | 0 | return NULL; |
1453 | | |
1454 | 0 | entry->being_validated = TRUE; |
1455 | | |
1456 | | /* Copy the data we need from the original message */ |
1457 | 0 | uri = soup_message_get_uri (original); |
1458 | 0 | msg = soup_message_new_from_uri (soup_message_get_method (original), uri); |
1459 | 0 | soup_message_set_flags (msg, soup_message_get_flags (original)); |
1460 | 0 | soup_message_disable_feature (msg, SOUP_TYPE_CACHE); |
1461 | |
|
1462 | 0 | soup_message_headers_foreach (soup_message_get_request_headers (original), |
1463 | 0 | (SoupMessageHeadersForeachFunc)copy_headers, |
1464 | 0 | soup_message_get_request_headers (msg)); |
1465 | |
|
1466 | 0 | disabled_features = soup_message_get_disabled_features (original); |
1467 | 0 | for (f = disabled_features; f; f = f->next) |
1468 | 0 | soup_message_disable_feature (msg, (GType) GPOINTER_TO_SIZE (f->data)); |
1469 | 0 | g_list_free (disabled_features); |
1470 | |
|
1471 | 0 | if (last_modified) |
1472 | 0 | soup_message_headers_append_common (soup_message_get_request_headers (msg), |
1473 | 0 | SOUP_HEADER_IF_MODIFIED_SINCE, |
1474 | 0 | last_modified); |
1475 | 0 | if (etag) |
1476 | 0 | soup_message_headers_append_common (soup_message_get_request_headers (msg), |
1477 | 0 | SOUP_HEADER_IF_NONE_MATCH, |
1478 | 0 | etag); |
1479 | |
|
1480 | 0 | return msg; |
1481 | 0 | } |
1482 | | |
1483 | | void |
1484 | | soup_cache_cancel_conditional_request (SoupCache *cache, |
1485 | | SoupMessage *msg) |
1486 | 0 | { |
1487 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1488 | 0 | SoupCacheEntry *entry; |
1489 | |
|
1490 | 0 | g_mutex_lock (&priv->mutex); |
1491 | 0 | entry = soup_cache_entry_lookup (cache, msg); |
1492 | 0 | g_mutex_unlock (&priv->mutex); |
1493 | 0 | if (entry) |
1494 | 0 | entry->being_validated = FALSE; |
1495 | |
|
1496 | 0 | soup_session_cancel_message (priv->session, msg); |
1497 | 0 | } |
1498 | | |
1499 | | void |
1500 | | soup_cache_update_from_conditional_request (SoupCache *cache, |
1501 | | SoupMessage *msg) |
1502 | 0 | { |
1503 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1504 | 0 | SoupCacheEntry *entry; |
1505 | |
|
1506 | 0 | g_mutex_lock (&priv->mutex); |
1507 | 0 | entry = soup_cache_entry_lookup (cache, msg); |
1508 | 0 | g_mutex_unlock (&priv->mutex); |
1509 | 0 | if (!entry) |
1510 | 0 | return; |
1511 | | |
1512 | 0 | entry->being_validated = FALSE; |
1513 | |
|
1514 | 0 | if (soup_message_get_status (msg) == SOUP_STATUS_NOT_MODIFIED) { |
1515 | 0 | soup_message_headers_foreach (soup_message_get_response_headers (msg), |
1516 | 0 | (SoupMessageHeadersForeachFunc) remove_headers, |
1517 | 0 | entry->headers); |
1518 | 0 | copy_end_to_end_headers (soup_message_get_response_headers (msg), entry->headers); |
1519 | |
|
1520 | 0 | soup_cache_entry_set_freshness (entry, msg, cache); |
1521 | 0 | } |
1522 | 0 | } |
1523 | | |
1524 | | static void |
1525 | | pack_entry (gpointer data, |
1526 | | gpointer user_data) |
1527 | 0 | { |
1528 | 0 | SoupCacheEntry *entry = (SoupCacheEntry *) data; |
1529 | 0 | SoupMessageHeadersIter iter; |
1530 | 0 | const char *header_key, *header_value; |
1531 | 0 | GVariantBuilder *entries_builder = (GVariantBuilder *)user_data; |
1532 | | |
1533 | | /* Do not store non-consolidated entries */ |
1534 | 0 | if (entry->dirty || !entry->key) |
1535 | 0 | return; |
1536 | | |
1537 | 0 | g_variant_builder_open (entries_builder, G_VARIANT_TYPE (SOUP_CACHE_PHEADERS_FORMAT)); |
1538 | 0 | g_variant_builder_add (entries_builder, "s", entry->uri); |
1539 | 0 | g_variant_builder_add (entries_builder, "b", entry->must_revalidate); |
1540 | 0 | g_variant_builder_add (entries_builder, "u", entry->freshness_lifetime); |
1541 | 0 | g_variant_builder_add (entries_builder, "u", entry->corrected_initial_age); |
1542 | 0 | g_variant_builder_add (entries_builder, "u", entry->response_time); |
1543 | 0 | g_variant_builder_add (entries_builder, "u", entry->hits); |
1544 | 0 | g_variant_builder_add (entries_builder, "u", entry->length); |
1545 | 0 | g_variant_builder_add (entries_builder, "q", entry->status_code); |
1546 | | |
1547 | | /* Pack headers */ |
1548 | 0 | g_variant_builder_open (entries_builder, G_VARIANT_TYPE ("a" SOUP_CACHE_HEADERS_FORMAT)); |
1549 | 0 | soup_message_headers_iter_init (&iter, entry->headers); |
1550 | 0 | while (soup_message_headers_iter_next (&iter, &header_key, &header_value)) { |
1551 | 0 | if (g_utf8_validate (header_value, -1, NULL)) |
1552 | 0 | g_variant_builder_add (entries_builder, SOUP_CACHE_HEADERS_FORMAT, |
1553 | 0 | header_key, header_value); |
1554 | 0 | } |
1555 | 0 | g_variant_builder_close (entries_builder); /* "a" SOUP_CACHE_HEADERS_FORMAT */ |
1556 | 0 | g_variant_builder_close (entries_builder); /* SOUP_CACHE_PHEADERS_FORMAT */ |
1557 | 0 | } |
1558 | | |
1559 | | /** |
1560 | | * soup_cache_dump: |
1561 | | * @cache: a #SoupCache |
1562 | | * |
1563 | | * Synchronously writes the cache index out to disk. |
1564 | | * |
1565 | | * Contrast with [method@Cache.flush], which writes pending cache *entries* to |
1566 | | * disk. |
1567 | | * |
1568 | | * You must call this before exiting if you want your cache data to |
1569 | | * persist between sessions. |
1570 | | * |
1571 | | * This is not thread safe and must be called only from the thread that created the [class@Cache] |
1572 | | */ |
1573 | | void |
1574 | | soup_cache_dump (SoupCache *cache) |
1575 | 0 | { |
1576 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1577 | 0 | char *filename; |
1578 | 0 | GVariantBuilder entries_builder; |
1579 | 0 | GVariant *cache_variant; |
1580 | |
|
1581 | 0 | if (!g_list_length (priv->lru_start)) |
1582 | 0 | return; |
1583 | | |
1584 | | /* Create the builder and iterate over all entries */ |
1585 | 0 | g_variant_builder_init (&entries_builder, G_VARIANT_TYPE (SOUP_CACHE_ENTRIES_FORMAT)); |
1586 | 0 | g_variant_builder_add (&entries_builder, "q", SOUP_CACHE_CURRENT_VERSION); |
1587 | 0 | g_variant_builder_open (&entries_builder, G_VARIANT_TYPE ("a" SOUP_CACHE_PHEADERS_FORMAT)); |
1588 | 0 | g_list_foreach (priv->lru_start, pack_entry, &entries_builder); |
1589 | 0 | g_variant_builder_close (&entries_builder); |
1590 | | |
1591 | | /* Serialize and dump */ |
1592 | 0 | cache_variant = g_variant_builder_end (&entries_builder); |
1593 | 0 | g_variant_ref_sink (cache_variant); |
1594 | 0 | filename = g_build_filename (priv->cache_dir, SOUP_CACHE_FILE, NULL); |
1595 | 0 | g_file_set_contents (filename, (const char *) g_variant_get_data (cache_variant), |
1596 | 0 | g_variant_get_size (cache_variant), NULL); |
1597 | 0 | g_free (filename); |
1598 | 0 | g_variant_unref (cache_variant); |
1599 | 0 | } |
1600 | | |
1601 | | static inline guint32 |
1602 | | get_key_from_cache_filename (const char *name) |
1603 | 0 | { |
1604 | 0 | guint64 key; |
1605 | |
|
1606 | 0 | key = g_ascii_strtoull (name, NULL, 10); |
1607 | 0 | return key ? (guint32)key : 0; |
1608 | 0 | } |
1609 | | |
1610 | | static void |
1611 | | insert_cache_file (SoupCache *cache, const char *name, GHashTable *leaked_entries) |
1612 | 0 | { |
1613 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1614 | 0 | gchar *path; |
1615 | |
|
1616 | 0 | path = g_build_filename (priv->cache_dir, name, NULL); |
1617 | 0 | if (g_file_test (path, G_FILE_TEST_IS_REGULAR)) { |
1618 | 0 | guint32 key = get_key_from_cache_filename (name); |
1619 | |
|
1620 | 0 | if (key) { |
1621 | 0 | g_hash_table_insert (leaked_entries, GUINT_TO_POINTER (key), path); |
1622 | 0 | return; |
1623 | 0 | } |
1624 | 0 | } |
1625 | 0 | g_free (path); |
1626 | 0 | } |
1627 | | |
1628 | | /** |
1629 | | * soup_cache_load: |
1630 | | * @cache: a #SoupCache |
1631 | | * |
1632 | | * Loads the contents of @cache's index into memory. |
1633 | | * |
1634 | | * This is not thread safe and must be called only from the thread that created the [class@Cache] |
1635 | | */ |
1636 | | void |
1637 | | soup_cache_load (SoupCache *cache) |
1638 | 0 | { |
1639 | 0 | gboolean must_revalidate; |
1640 | 0 | guint32 freshness_lifetime, hits; |
1641 | 0 | guint32 corrected_initial_age, response_time; |
1642 | 0 | char *url, *filename = NULL, *contents = NULL; |
1643 | 0 | GVariant *cache_variant; |
1644 | 0 | GVariantIter *entries_iter = NULL, *headers_iter = NULL; |
1645 | 0 | gsize length; |
1646 | 0 | SoupCacheEntry *entry; |
1647 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1648 | 0 | guint16 version, status_code; |
1649 | 0 | GHashTable *leaked_entries = NULL; |
1650 | 0 | GHashTableIter iter; |
1651 | 0 | gpointer value; |
1652 | |
|
1653 | 0 | filename = g_build_filename (priv->cache_dir, SOUP_CACHE_FILE, NULL); |
1654 | 0 | if (!g_file_get_contents (filename, &contents, &length, NULL)) { |
1655 | 0 | g_free (filename); |
1656 | 0 | g_free (contents); |
1657 | 0 | clear_cache_files (cache); |
1658 | 0 | return; |
1659 | 0 | } |
1660 | 0 | g_free (filename); |
1661 | |
|
1662 | 0 | cache_variant = g_variant_new_from_data (G_VARIANT_TYPE (SOUP_CACHE_ENTRIES_FORMAT), |
1663 | 0 | (const char *) contents, length, FALSE, g_free, contents); |
1664 | 0 | g_variant_get (cache_variant, SOUP_CACHE_ENTRIES_FORMAT, &version, &entries_iter); |
1665 | 0 | if (version != SOUP_CACHE_CURRENT_VERSION) { |
1666 | 0 | g_variant_iter_free (entries_iter); |
1667 | 0 | g_variant_unref (cache_variant); |
1668 | 0 | clear_cache_files (cache); |
1669 | 0 | return; |
1670 | 0 | } |
1671 | | |
1672 | 0 | leaked_entries = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free); |
1673 | 0 | soup_cache_foreach_file (cache, (SoupCacheForeachFileFunc)insert_cache_file, leaked_entries); |
1674 | |
|
1675 | 0 | while (g_variant_iter_loop (entries_iter, SOUP_CACHE_PHEADERS_FORMAT, |
1676 | 0 | &url, &must_revalidate, &freshness_lifetime, &corrected_initial_age, |
1677 | 0 | &response_time, &hits, &length, &status_code, |
1678 | 0 | &headers_iter)) { |
1679 | 0 | const char *header_key, *header_value; |
1680 | 0 | SoupMessageHeaders *headers; |
1681 | 0 | SoupMessageHeadersIter soup_headers_iter; |
1682 | | |
1683 | | /* SoupMessage Headers */ |
1684 | 0 | headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE); |
1685 | 0 | while (g_variant_iter_loop (headers_iter, SOUP_CACHE_HEADERS_FORMAT, &header_key, &header_value)) |
1686 | 0 | if (*header_key && *header_value) |
1687 | 0 | soup_message_headers_append (headers, header_key, header_value); |
1688 | | |
1689 | | /* Check that we have headers */ |
1690 | 0 | soup_message_headers_iter_init (&soup_headers_iter, headers); |
1691 | 0 | if (!soup_message_headers_iter_next (&soup_headers_iter, &header_key, &header_value)) { |
1692 | 0 | soup_message_headers_unref (headers); |
1693 | 0 | continue; |
1694 | 0 | } |
1695 | | |
1696 | | /* Insert in cache */ |
1697 | 0 | entry = g_slice_new0 (SoupCacheEntry); |
1698 | 0 | entry->uri = g_strdup (url); |
1699 | 0 | entry->must_revalidate = must_revalidate; |
1700 | 0 | entry->freshness_lifetime = freshness_lifetime; |
1701 | 0 | entry->corrected_initial_age = corrected_initial_age; |
1702 | 0 | entry->response_time = response_time; |
1703 | 0 | entry->hits = hits; |
1704 | 0 | entry->length = length; |
1705 | 0 | entry->headers = headers; |
1706 | 0 | entry->status_code = status_code; |
1707 | |
|
1708 | 0 | if (!soup_cache_entry_insert (cache, entry, FALSE)) |
1709 | 0 | soup_cache_entry_free (entry); |
1710 | 0 | else |
1711 | 0 | g_hash_table_remove (leaked_entries, GUINT_TO_POINTER (entry->key)); |
1712 | 0 | } |
1713 | | |
1714 | | /* Remove the leaked files */ |
1715 | 0 | g_hash_table_iter_init (&iter, leaked_entries); |
1716 | 0 | while (g_hash_table_iter_next (&iter, NULL, &value)) |
1717 | 0 | g_unlink ((char *)value); |
1718 | 0 | g_hash_table_destroy (leaked_entries); |
1719 | |
|
1720 | 0 | priv->lru_start = g_list_reverse (priv->lru_start); |
1721 | | |
1722 | | /* frees */ |
1723 | 0 | g_variant_iter_free (entries_iter); |
1724 | 0 | g_variant_unref (cache_variant); |
1725 | 0 | } |
1726 | | |
1727 | | /** |
1728 | | * soup_cache_set_max_size: |
1729 | | * @cache: a #SoupCache |
1730 | | * @max_size: the maximum size of the cache, in bytes |
1731 | | * |
1732 | | * Sets the maximum size of the cache. |
1733 | | */ |
1734 | | void |
1735 | | soup_cache_set_max_size (SoupCache *cache, |
1736 | | guint max_size) |
1737 | 0 | { |
1738 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1739 | 0 | priv->max_size = max_size; |
1740 | 0 | priv->max_entry_data_size = priv->max_size / MAX_ENTRY_DATA_PERCENTAGE; |
1741 | 0 | } |
1742 | | |
1743 | | /** |
1744 | | * soup_cache_get_max_size: |
1745 | | * @cache: a #SoupCache |
1746 | | * |
1747 | | * Gets the maximum size of the cache. |
1748 | | * |
1749 | | * Returns: the maximum size of the cache, in bytes. |
1750 | | */ |
1751 | | guint |
1752 | | soup_cache_get_max_size (SoupCache *cache) |
1753 | 0 | { |
1754 | 0 | SoupCachePrivate *priv = soup_cache_get_instance_private (cache); |
1755 | 0 | return priv->max_size; |
1756 | 0 | } |