Branch data Line data Source code
1 : : /*
2 : : * Copyright (C) 2009 Pierre-Luc Beaudoin <pierre-luc@pierlux.com>
3 : : * Copyright (C) 2010-2013 Jiri Techet <techet@gmail.com>
4 : : * Copyright (C) 2019 Marcus Lundblad <ml@update.uu.se>
5 : : *
6 : : * This library is free software; you can redistribute it and/or
7 : : * modify it under the terms of the GNU Lesser General Public
8 : : * License as published by the Free Software Foundation; either
9 : : * version 2.1 of the License, or (at your option) any later version.
10 : : *
11 : : * This library is distributed in the hope that it will be useful,
12 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 : : * Lesser General Public License for more details.
15 : : *
16 : : * You should have received a copy of the GNU Lesser General Public
17 : : * License along with this library; if not, write to the Free Software
18 : : * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 : : */
20 : :
21 : : /**
22 : : * ShumateFileCache:
23 : : *
24 : : * A cache that stores and retrieves tiles from the file system. It is mainly
25 : : * used by [class@TileDownloader], but can also be used by custom data
26 : : * sources.
27 : : *
28 : : * The cache will be filled up to a certain size limit. When this limit is
29 : : * reached, the cache can be purged, and the tiles that are accessed least are
30 : : * deleted.
31 : : *
32 : : * ## ETags
33 : : *
34 : : * The cache can optionally store an ETag string with each tile. This is
35 : : * useful to avoid redownloading old tiles that haven't changed (for example,
36 : : * using the HTTP If-None-Match header).
37 : : */
38 : :
39 : : #include "shumate-file-cache.h"
40 : :
41 : : #include <sqlite3.h>
42 : : #include <errno.h>
43 : : #include <glib.h>
44 : : #include <gio/gio.h>
45 : : #include <string.h>
46 : : #include <stdlib.h>
47 : :
48 : : enum
49 : : {
50 : : PROP_0,
51 : : PROP_SIZE_LIMIT,
52 : : PROP_CACHE_DIR,
53 : : PROP_CACHE_KEY,
54 : : N_PROPS
55 : : };
56 : :
57 : : static GParamSpec *properties[N_PROPS];
58 : :
59 : : struct _ShumateFileCache
60 : : {
61 : : GObject parent_instance;
62 : :
63 : : guint size_limit;
64 : : char *cache_dir;
65 : : char *cache_key;
66 : :
67 : : sqlite3 *db;
68 : : sqlite3_stmt *stmt_select;
69 : : sqlite3_stmt *stmt_update;
70 : :
71 : : int size_estimate;
72 : : gboolean have_size_estimate;
73 : : gboolean purge_in_progress;
74 : : };
75 : :
76 [ + + + - ]: 133 : G_DEFINE_TYPE (ShumateFileCache, shumate_file_cache, G_TYPE_OBJECT);
77 : :
78 : :
79 : : typedef char sqlite_str;
80 [ - - - + ]: 4 : G_DEFINE_AUTOPTR_CLEANUP_FUNC (sqlite_str, sqlite3_free);
81 : 2 : G_DEFINE_AUTOPTR_CLEANUP_FUNC (sqlite3_stmt, sqlite3_finalize);
82 : :
83 : :
84 : : static void finalize_sql (ShumateFileCache *file_cache);
85 : : static void init_cache (ShumateFileCache *file_cache);
86 : : static char *get_filename (ShumateFileCache *file_cache,
87 : : int x,
88 : : int y,
89 : : int zoom_level);
90 : : static void delete_tile (ShumateFileCache *file_cache,
91 : : const char *filename);
92 : : static gboolean create_cache_dir (const char *dir_name);
93 : :
94 : : static void on_tile_filled (ShumateFileCache *self,
95 : : int x,
96 : : int y,
97 : : int zoom_level);
98 : :
99 : : static void
100 : 0 : shumate_file_cache_get_property (GObject *object,
101 : : guint property_id,
102 : : GValue *value,
103 : : GParamSpec *pspec)
104 : : {
105 : 0 : ShumateFileCache *self = SHUMATE_FILE_CACHE (object);
106 : :
107 [ # # # # ]: 0 : switch (property_id)
108 : : {
109 : 0 : case PROP_SIZE_LIMIT:
110 : 0 : g_value_set_uint (value, shumate_file_cache_get_size_limit (self));
111 : 0 : break;
112 : :
113 : 0 : case PROP_CACHE_DIR:
114 : 0 : g_value_set_string (value, shumate_file_cache_get_cache_dir (self));
115 : 0 : break;
116 : :
117 : 0 : case PROP_CACHE_KEY:
118 : 0 : g_value_set_string (value, shumate_file_cache_get_cache_key (self));
119 : 0 : break;
120 : :
121 : 0 : default:
122 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
123 : : }
124 : 0 : }
125 : :
126 : :
127 : : static void
128 : 111 : shumate_file_cache_set_property (GObject *object,
129 : : guint property_id,
130 : : const GValue *value,
131 : : GParamSpec *pspec)
132 : : {
133 : 111 : ShumateFileCache *self = SHUMATE_FILE_CACHE (object);
134 : :
135 [ + + + - ]: 111 : switch (property_id)
136 : : {
137 : 37 : case PROP_SIZE_LIMIT:
138 : 37 : shumate_file_cache_set_size_limit (self, g_value_get_uint (value));
139 : 37 : break;
140 : :
141 : 37 : case PROP_CACHE_DIR:
142 : 37 : g_free (self->cache_dir);
143 [ - + ]: 37 : self->cache_dir = g_strdup (g_value_get_string (value));
144 : 37 : break;
145 : :
146 : 37 : case PROP_CACHE_KEY:
147 : 37 : g_free (self->cache_key);
148 [ - + ]: 37 : self->cache_key = g_strdup (g_value_get_string (value));
149 : 37 : break;
150 : :
151 : 0 : default:
152 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
153 : : }
154 : 111 : }
155 : :
156 : : static void
157 : 37 : finalize_sql (ShumateFileCache *self)
158 : : {
159 [ + + ]: 37 : g_clear_pointer (&self->stmt_select, sqlite3_finalize);
160 [ + + ]: 37 : g_clear_pointer (&self->stmt_update, sqlite3_finalize);
161 : :
162 [ + - ]: 37 : if (self->db)
163 : : {
164 : 37 : int error = sqlite3_close (self->db);
165 [ - + ]: 37 : if (error != SQLITE_OK)
166 : 0 : g_debug ("Sqlite returned error %d when closing cache.db", error);
167 : 37 : self->db = NULL;
168 : : }
169 : 37 : }
170 : :
171 : :
172 : : static void
173 : 37 : shumate_file_cache_finalize (GObject *object)
174 : : {
175 : 37 : ShumateFileCache *self = SHUMATE_FILE_CACHE (object);
176 : :
177 : 37 : finalize_sql (self);
178 : :
179 [ + - ]: 37 : g_clear_pointer (&self->cache_dir, g_free);
180 [ + - ]: 37 : g_clear_pointer (&self->cache_key, g_free);
181 : :
182 : 37 : G_OBJECT_CLASS (shumate_file_cache_parent_class)->finalize (object);
183 : 37 : }
184 : :
185 : :
186 : : static gboolean
187 : 37 : create_cache_dir (const char *dir_name)
188 : : {
189 : : /* If needed, create the cache's dirs */
190 [ + - ]: 37 : if (dir_name)
191 : : {
192 [ + - - - ]: 37 : if (g_mkdir_with_parents (dir_name, 0700) == -1 && errno != EEXIST)
193 : : {
194 : 0 : g_warning ("Unable to create the image cache path '%s': %s",
195 : : dir_name, g_strerror (errno));
196 : 0 : return FALSE;
197 : : }
198 : : }
199 : : return TRUE;
200 : : }
201 : :
202 : :
203 : : static void
204 : 37 : init_cache (ShumateFileCache *self)
205 : : {
206 : 37 : char *filename = NULL;
207 : 37 : char *error_msg = NULL;
208 : 37 : gint error;
209 : :
210 [ + - ]: 45 : g_return_if_fail (create_cache_dir (self->cache_dir));
211 : :
212 : 37 : filename = g_build_filename (self->cache_dir,
213 : : "cache.db", NULL);
214 : :
215 : : /* Make sure the database is opened in serialized mode (OPEN_FULLMUTEX)
216 : : * because shumate_file_cache_purge_cache_async() runs in a separate thread.
217 : : * See <https://sqlite.org/threadsafe.html> */
218 : 37 : error = sqlite3_open_v2 (filename, &self->db,
219 : : SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
220 : 37 : g_free (filename);
221 : :
222 [ - + ]: 37 : if (error == SQLITE_ERROR)
223 : : {
224 : 0 : g_debug ("Sqlite returned error %d when opening cache.db", error);
225 : 0 : return;
226 : : }
227 : :
228 : 37 : sqlite3_exec (self->db,
229 : : "PRAGMA synchronous=OFF;"
230 : : "PRAGMA auto_vacuum=INCREMENTAL;",
231 : : NULL, NULL, &error_msg);
232 [ + + ]: 37 : if (error_msg != NULL)
233 : : {
234 : 8 : g_debug ("Set PRAGMA: %s", error_msg);
235 : 8 : sqlite3_free (error_msg);
236 : 8 : return;
237 : : }
238 : :
239 : 29 : sqlite3_exec (self->db,
240 : : "CREATE TABLE IF NOT EXISTS tiles ("
241 : : "filename TEXT PRIMARY KEY, "
242 : : "etag TEXT, "
243 : : "popularity INT DEFAULT 1, "
244 : : "size INT DEFAULT 0)",
245 : : NULL, NULL, &error_msg);
246 [ - + ]: 29 : if (error_msg != NULL)
247 : : {
248 : 0 : g_debug ("Creating table 'tiles' failed: %s", error_msg);
249 : 0 : sqlite3_free (error_msg);
250 : 0 : return;
251 : : }
252 : :
253 : 29 : error = sqlite3_prepare_v2 (self->db,
254 : : "SELECT etag FROM tiles WHERE filename = ?", -1,
255 : : &self->stmt_select, NULL);
256 [ - + ]: 29 : if (error != SQLITE_OK)
257 : : {
258 : 0 : self->stmt_select = NULL;
259 : 0 : g_debug ("Failed to prepare the select Etag statement, error:%d: %s",
260 : : error, sqlite3_errmsg (self->db));
261 : 0 : return;
262 : : }
263 : :
264 : 29 : error = sqlite3_prepare_v2 (self->db,
265 : : "UPDATE tiles SET popularity = popularity + 1 WHERE filename = ?", -1,
266 : : &self->stmt_update, NULL);
267 [ - + ]: 29 : if (error != SQLITE_OK)
268 : : {
269 : 0 : self->stmt_update = NULL;
270 : 0 : g_debug ("Failed to prepare the update popularity statement, error: %s",
271 : : sqlite3_errmsg (self->db));
272 : 0 : return;
273 : : }
274 : :
275 : 29 : g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CACHE_DIR]);
276 : : }
277 : :
278 : :
279 : : static void
280 : 37 : shumate_file_cache_constructed (GObject *object)
281 : : {
282 : 37 : ShumateFileCache *self = SHUMATE_FILE_CACHE (object);
283 : :
284 [ + - ]: 37 : if (!self->cache_dir)
285 : : {
286 : 37 : self->cache_dir = g_build_path (G_DIR_SEPARATOR_S,
287 : : g_get_user_cache_dir (),
288 : : "shumate", NULL);
289 : : }
290 : :
291 : 37 : init_cache (self);
292 : :
293 : 37 : G_OBJECT_CLASS (shumate_file_cache_parent_class)->constructed (object);
294 : 37 : }
295 : :
296 : :
297 : : static void
298 : 12 : shumate_file_cache_class_init (ShumateFileCacheClass *klass)
299 : : {
300 : 12 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
301 : :
302 : 12 : object_class->finalize = shumate_file_cache_finalize;
303 : 12 : object_class->get_property = shumate_file_cache_get_property;
304 : 12 : object_class->set_property = shumate_file_cache_set_property;
305 : 12 : object_class->constructed = shumate_file_cache_constructed;
306 : :
307 : : /**
308 : : * ShumateFileCache:size-limit:
309 : : *
310 : : * The cache size limit in bytes.
311 : : *
312 : : * Note: this new value will not be applied until you call shumate_file_cache_purge()
313 : : */
314 : 24 : properties[PROP_SIZE_LIMIT] =
315 : 12 : g_param_spec_uint ("size-limit",
316 : : "Size Limit",
317 : : "The cache's size limit (Mb)",
318 : : 1,
319 : : G_MAXINT,
320 : : 100000000,
321 : : G_PARAM_CONSTRUCT | G_PARAM_READWRITE);
322 : :
323 : : /**
324 : : * ShumateFileCache:cache-dir:
325 : : *
326 : : * The directory where the tile database is stored.
327 : : */
328 : 24 : properties[PROP_CACHE_DIR] =
329 : 12 : g_param_spec_string ("cache-dir",
330 : : "Cache Directory",
331 : : "The directory of the cache",
332 : : NULL,
333 : : G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);
334 : :
335 : : /**
336 : : * ShumateFileCache:cache-key:
337 : : *
338 : : * The key used to store and retrieve tiles from the cache. Different keys
339 : : * can be used to store multiple tilesets in the same cache directory.
340 : : */
341 : 24 : properties[PROP_CACHE_KEY] =
342 : 12 : g_param_spec_string ("cache-key",
343 : : "Cache Key",
344 : : "The key used when storing and retrieving tiles",
345 : : NULL,
346 : : G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
347 : :
348 : 12 : g_object_class_install_properties (object_class, N_PROPS, properties);
349 : 12 : }
350 : :
351 : :
352 : : static void
353 : 37 : shumate_file_cache_init (ShumateFileCache *self)
354 : : {
355 : 37 : self->cache_dir = NULL;
356 : 37 : self->size_limit = 100000000;
357 : 37 : self->size_estimate = 0;
358 : 37 : self->have_size_estimate = FALSE;
359 : 37 : self->cache_dir = NULL;
360 : 37 : self->db = NULL;
361 : 37 : self->stmt_select = NULL;
362 : 37 : self->stmt_update = NULL;
363 : 37 : }
364 : :
365 : :
366 : : /**
367 : : * shumate_file_cache_new_full:
368 : : * @size_limit: maximum size of the cache in bytes
369 : : * @cache_key: an ID for the tileset to store/retrieve
370 : : * @cache_dir: (allow-none): the directory where the cache is created. When cache_dir == NULL,
371 : : * a cache in ~/.cache/shumate is used.
372 : : *
373 : : * Constructor of #ShumateFileCache.
374 : : *
375 : : * Returns: a constructed #ShumateFileCache
376 : : */
377 : : ShumateFileCache *
378 : 37 : shumate_file_cache_new_full (guint size_limit,
379 : : const char *cache_key,
380 : : const char *cache_dir)
381 : : {
382 : 37 : ShumateFileCache *cache;
383 : :
384 [ + - ]: 37 : g_return_val_if_fail (cache_key != NULL, NULL);
385 : :
386 : 37 : cache = g_object_new (SHUMATE_TYPE_FILE_CACHE,
387 : : "size-limit", size_limit,
388 : : "cache-key", cache_key,
389 : : "cache-dir", cache_dir,
390 : : NULL);
391 : 37 : return cache;
392 : : }
393 : :
394 : :
395 : : /**
396 : : * shumate_file_cache_get_size_limit:
397 : : * @self: a #ShumateFileCache
398 : : *
399 : : * Gets the cache size limit in bytes.
400 : : *
401 : : * Returns: size limit
402 : : */
403 : : guint
404 : 0 : shumate_file_cache_get_size_limit (ShumateFileCache *self)
405 : : {
406 [ # # ]: 0 : g_return_val_if_fail (SHUMATE_IS_FILE_CACHE (self), 0);
407 : :
408 : 0 : return self->size_limit;
409 : : }
410 : :
411 : :
412 : : /**
413 : : * shumate_file_cache_get_cache_dir:
414 : : * @self: a #ShumateFileCache
415 : : *
416 : : * Gets the directory where the cache database is stored.
417 : : *
418 : : * Returns: the directory
419 : : */
420 : : const char *
421 : 0 : shumate_file_cache_get_cache_dir (ShumateFileCache *self)
422 : : {
423 [ # # ]: 0 : g_return_val_if_fail (SHUMATE_IS_FILE_CACHE (self), NULL);
424 : :
425 : 0 : return self->cache_dir;
426 : : }
427 : :
428 : :
429 : : /**
430 : : * shumate_file_cache_get_cache_key:
431 : : * @self: a #ShumateFileCache
432 : : *
433 : : * Gets the key used to store and retrieve tiles from the cache. Different keys
434 : : * can be used to store multiple tilesets in the same cache directory.
435 : : *
436 : : * Returns: the cache key
437 : : */
438 : : const char *
439 : 10 : shumate_file_cache_get_cache_key (ShumateFileCache *self)
440 : : {
441 [ + - ]: 10 : g_return_val_if_fail (SHUMATE_IS_FILE_CACHE (self), NULL);
442 : :
443 : 10 : return self->cache_key;
444 : : }
445 : :
446 : :
447 : : /**
448 : : * shumate_file_cache_set_size_limit:
449 : : * @self: a #ShumateFileCache
450 : : * @size_limit: the cache limit in bytes
451 : : *
452 : : * Sets the cache size limit in bytes.
453 : : */
454 : : void
455 : 37 : shumate_file_cache_set_size_limit (ShumateFileCache *self,
456 : : guint size_limit)
457 : : {
458 [ + - ]: 37 : g_return_if_fail (SHUMATE_IS_FILE_CACHE (self));
459 : :
460 : 37 : self->size_limit = size_limit;
461 : 37 : g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SIZE_LIMIT]);
462 : : }
463 : :
464 : :
465 : : static char *
466 : 10 : get_filename (ShumateFileCache *self,
467 : : int x,
468 : : int y,
469 : : int zoom_level)
470 : : {
471 : 10 : const char *cache_key;
472 : :
473 [ + - ]: 10 : g_return_val_if_fail (SHUMATE_IS_FILE_CACHE (self), NULL);
474 [ - + ]: 10 : g_return_val_if_fail (self->cache_dir, NULL);
475 : :
476 : 10 : cache_key = shumate_file_cache_get_cache_key (self);
477 : :
478 : 10 : char *filename = g_strdup_printf ("%s" G_DIR_SEPARATOR_S
479 : : "%s" G_DIR_SEPARATOR_S
480 : : "%d" G_DIR_SEPARATOR_S
481 : : "%d" G_DIR_SEPARATOR_S "%d.png",
482 : : self->cache_dir,
483 : : cache_key,
484 : : zoom_level,
485 : : x,
486 : : y);
487 : 10 : return filename;
488 : : }
489 : :
490 : :
491 : : static char *
492 : 2 : db_get_etag (ShumateFileCache *self,
493 : : int x,
494 : : int y,
495 : : int zoom_level)
496 : : {
497 : 2 : int sql_rc = SQLITE_OK;
498 : 2 : g_autofree char *filename = get_filename (self, x, y, zoom_level);
499 : :
500 : 2 : sqlite3_reset (self->stmt_select);
501 : 2 : sql_rc = sqlite3_bind_text (self->stmt_select, 1, filename, -1, SQLITE_STATIC);
502 [ - + ]: 2 : if (sql_rc == SQLITE_ERROR)
503 : : {
504 : 0 : g_debug ("Failed to prepare the SQL query for finding the Etag of '%s', error: %s",
505 : : filename, sqlite3_errmsg (self->db));
506 : 0 : return NULL;
507 : : }
508 : :
509 : 2 : sql_rc = sqlite3_step (self->stmt_select);
510 [ + - ]: 2 : if (sql_rc == SQLITE_ROW)
511 : : {
512 : 2 : const char *etag = (const char *) sqlite3_column_text (self->stmt_select, 0);
513 [ - + ]: 4 : return g_strdup (etag);
514 : : }
515 [ # # ]: 0 : else if (sql_rc == SQLITE_DONE)
516 : : {
517 : 0 : g_debug ("'%s' doesn't have an etag",
518 : : filename);
519 : : }
520 [ # # ]: 0 : else if (sql_rc == SQLITE_ERROR)
521 : : {
522 : 0 : g_debug ("Failed to finding the Etag of '%s', %d error: %s",
523 : : filename, sql_rc, sqlite3_errmsg (self->db));
524 : : }
525 : :
526 : : return NULL;
527 : : }
528 : :
529 : :
530 : : /**
531 : : * shumate_file_cache_mark_up_to_date:
532 : : * @self: a #ShumateFileCache
533 : : * @x: the X coordinate of the tile
534 : : * @y: the Y coordinate of the tile
535 : : * @zoom_level: the zoom level of the tile
536 : : *
537 : : * Marks a tile in the cache as being up to date, without changing its data.
538 : : *
539 : : * For example, a network source might call this function when it gets an HTTP
540 : : * 304 Not Modified response.
541 : : */
542 : : void
543 : 0 : shumate_file_cache_mark_up_to_date (ShumateFileCache *self,
544 : : int x,
545 : : int y,
546 : : int zoom_level)
547 : : {
548 : 0 : g_autofree char *filename = NULL;
549 : 0 : g_autoptr(GFile) file = NULL;
550 [ # # ]: 0 : g_autoptr(GFileInfo) info = NULL;
551 : :
552 [ # # ]: 0 : g_return_if_fail (SHUMATE_IS_FILE_CACHE (self));
553 : :
554 : 0 : filename = get_filename (self, x, y, zoom_level);
555 : 0 : file = g_file_new_for_path (filename);
556 : :
557 : 0 : info = g_file_query_info (file, G_FILE_ATTRIBUTE_TIME_MODIFIED,
558 : : G_FILE_QUERY_INFO_NONE, NULL, NULL);
559 : :
560 [ # # ]: 0 : if (info)
561 : : {
562 : 0 : g_autoptr(GDateTime) now = g_date_time_new_now_utc ();
563 : :
564 : 0 : g_file_info_set_modification_date_time (info, now);
565 [ # # ]: 0 : g_file_set_attributes_from_info (file, info, G_FILE_QUERY_INFO_NONE, NULL, NULL);
566 : : }
567 : : }
568 : :
569 : :
570 : : static void
571 : 2 : on_tile_filled (ShumateFileCache *self,
572 : : int x,
573 : : int y,
574 : : int zoom_level)
575 : : {
576 : 2 : int sql_rc = SQLITE_OK;
577 : 2 : g_autofree char *filename = NULL;
578 : :
579 : 2 : filename = get_filename (self, x, y, zoom_level);
580 : :
581 : 2 : g_debug ("popularity of %s", filename);
582 : :
583 : 2 : sqlite3_reset (self->stmt_update);
584 : 2 : sql_rc = sqlite3_bind_text (self->stmt_update, 1, filename, -1, SQLITE_STATIC);
585 [ - + ]: 2 : if (sql_rc != SQLITE_OK)
586 : : {
587 : 0 : g_debug ("Failed to set values to the popularity query of '%s', error: %s",
588 : : filename, sqlite3_errmsg (self->db));
589 : 0 : return;
590 : : }
591 : :
592 : 2 : sql_rc = sqlite3_step (self->stmt_update);
593 [ + - ]: 2 : if (sql_rc != SQLITE_DONE)
594 : : {
595 : : /* may not be present in this cache */
596 : : return;
597 : : }
598 : : }
599 : :
600 : :
601 : : static void
602 : 0 : delete_tile (ShumateFileCache *self,
603 : : const char *filename)
604 : : {
605 [ # # ]: 0 : g_return_if_fail (SHUMATE_IS_FILE_CACHE (self));
606 : 0 : g_autoptr(sqlite_str) query = NULL;
607 [ # # ]: 0 : g_autoptr(sqlite_str) sql_error = NULL;
608 [ # # ]: 0 : g_autoptr(GError) gerror = NULL;
609 [ # # ]: 0 : g_autoptr(GFile) file = NULL;
610 : :
611 : 0 : file = g_file_new_for_path (filename);
612 [ # # ]: 0 : if (!g_file_delete (file, NULL, &gerror))
613 : : {
614 : 0 : g_debug ("Deleting tile from disk failed: %s", gerror->message);
615 : : }
616 : :
617 : 0 : query = sqlite3_mprintf ("DELETE FROM tiles WHERE filename = %Q", filename);
618 : 0 : sqlite3_exec (self->db, query, NULL, NULL, &sql_error);
619 [ # # ]: 0 : if (sql_error != NULL)
620 : : {
621 : 0 : g_debug ("Deleting tile from db failed: %s", sql_error);
622 : : }
623 : : }
624 : :
625 : :
626 : : static void
627 : 2 : purge_cache (GTask *task,
628 : : gpointer source_object,
629 : : gpointer _task_data,
630 : : GCancellable *cancellable)
631 : : {
632 : 2 : ShumateFileCache *self = (ShumateFileCache *) source_object;
633 : :
634 : 2 : char *query;
635 : 2 : g_autoptr(sqlite3_stmt) stmt = NULL;
636 : 2 : int rc = 0;
637 : 2 : guint original_size, current_size = 0;
638 : 2 : guint highest_popularity = 0;
639 [ - - + - ]: 2 : g_autoptr(sqlite_str) error = NULL;
640 : :
641 : 2 : query = "SELECT SUM (size) FROM tiles";
642 : 2 : rc = sqlite3_prepare (self->db, query, strlen (query), &stmt, NULL);
643 [ - + ]: 2 : if (rc != SQLITE_OK)
644 : : {
645 : 0 : g_warning ("Can't compute cache size %s", sqlite3_errmsg (self->db));
646 : 0 : g_task_return_boolean (task, FALSE);
647 : 0 : return;
648 : : }
649 : :
650 : 2 : rc = sqlite3_step (stmt);
651 [ - + ]: 2 : if (rc != SQLITE_ROW)
652 : : {
653 : 0 : g_warning ("Failed to count the total cache consumption %s",
654 : : sqlite3_errmsg (self->db));
655 : 0 : g_task_return_boolean (task, FALSE);
656 : 0 : return;
657 : : }
658 : :
659 : 2 : current_size = sqlite3_column_int (stmt, 0);
660 : 2 : original_size = current_size;
661 [ + - ]: 2 : if (current_size < self->size_limit)
662 : : {
663 : 2 : g_debug ("Cache doesn't need to be purged at %d bytes", current_size);
664 : 2 : self->size_estimate = current_size;
665 : 2 : g_task_return_boolean (task, FALSE);
666 : 2 : return;
667 : : }
668 : :
669 : 0 : sqlite3_finalize (stmt);
670 : :
671 : : /* Ok, delete the less popular tiles until size_limit reached */
672 : 0 : query = "SELECT filename, size, popularity FROM tiles ORDER BY popularity";
673 : 0 : rc = sqlite3_prepare (self->db, query, strlen (query), &stmt, NULL);
674 [ # # ]: 0 : if (rc != SQLITE_OK)
675 : : {
676 : 0 : g_warning ("Can't fetch tiles to delete: %s", sqlite3_errmsg (self->db));
677 : : }
678 : :
679 : 0 : rc = sqlite3_step (stmt);
680 [ # # # # ]: 0 : while (rc == SQLITE_ROW && current_size > self->size_limit)
681 : : {
682 : 0 : const char *filename;
683 : 0 : guint size;
684 : :
685 : 0 : filename = (const char *) sqlite3_column_text (stmt, 0);
686 : 0 : size = sqlite3_column_int (stmt, 1);
687 : 0 : highest_popularity = sqlite3_column_int (stmt, 2);
688 : 0 : g_debug ("Deleting %s of size %d", filename, size);
689 : :
690 : 0 : delete_tile (self, filename);
691 : :
692 : 0 : current_size -= size;
693 : :
694 : 0 : rc = sqlite3_step (stmt);
695 : : }
696 : :
697 : 0 : g_debug ("Cache size is now %d bytes (reduced by %d bytes)", current_size, original_size - current_size);
698 : 0 : self->size_estimate = current_size;
699 : 0 : self->have_size_estimate = TRUE;
700 : :
701 : 0 : query = sqlite3_mprintf ("UPDATE tiles SET popularity = popularity - %d",
702 : : highest_popularity);
703 : 0 : sqlite3_exec (self->db, query, NULL, NULL, &error);
704 [ # # ]: 0 : if (error != NULL)
705 : : {
706 : 0 : g_warning ("Updating popularity failed: %s", error);
707 : 0 : sqlite3_free (error);
708 : : }
709 : 0 : sqlite3_free (query);
710 : :
711 : 0 : sqlite3_exec (self->db, "PRAGMA incremental_vacuum;", NULL, NULL, &error);
712 : :
713 : 0 : self->purge_in_progress = FALSE;
714 [ # # ]: 0 : g_task_return_boolean (task, TRUE);
715 : : }
716 : :
717 : : /**
718 : : * shumate_file_cache_purge_cache_async:
719 : : * @self: a #ShumateFileCache
720 : : * @cancellable: (nullable): a #GCancellable
721 : : * @callback: a #GAsyncReadyCallback to execute upon completion
722 : : * @user_data: closure data for @callback
723 : : *
724 : : * Removes less used tiles from the cache, if necessary, until it fits in
725 : : * the size limit.
726 : : */
727 : : void
728 : 2 : shumate_file_cache_purge_cache_async (ShumateFileCache *self,
729 : : GCancellable *cancellable,
730 : : GAsyncReadyCallback callback,
731 : : gpointer user_data)
732 : : {
733 : 4 : g_autoptr(GTask) task = NULL;
734 : :
735 [ + - ]: 2 : g_return_if_fail (SHUMATE_IS_FILE_CACHE (self));
736 [ - + - - : 2 : g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
- - - - ]
737 : :
738 : 2 : task = g_task_new (self, cancellable, callback, user_data);
739 [ + - ]: 2 : g_task_set_source_tag (task, shumate_file_cache_purge_cache_async);
740 : :
741 [ - + ]: 2 : if (self->purge_in_progress)
742 : : {
743 : 0 : g_task_return_boolean (task, FALSE);
744 [ # # ]: 0 : return;
745 : : }
746 : :
747 : 2 : self->purge_in_progress = TRUE;
748 [ + - ]: 2 : g_task_run_in_thread (task, purge_cache);
749 : : }
750 : :
751 : : /**
752 : : * shumate_file_cache_purge_cache_finish:
753 : : * @self: a #ShumateFileCache
754 : : * @result: a #GAsyncResult provided to callback
755 : : * @error: a location for a #GError, or %NULL
756 : : *
757 : : * Gets the result of an async operation started using
758 : : * shumate_file_cache_purge_cache_async().
759 : : *
760 : : * Returns: %TRUE if any tiles were removed, otherwise %FALSE
761 : : */
762 : : gboolean
763 : 0 : shumate_file_cache_purge_cache_finish (ShumateFileCache *self,
764 : : GAsyncResult *result,
765 : : GError **error)
766 : : {
767 [ # # ]: 0 : g_return_val_if_fail (SHUMATE_IS_FILE_CACHE (self), FALSE);
768 [ # # ]: 0 : g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
769 : :
770 : 0 : return g_task_propagate_boolean (G_TASK (result), error);
771 : : }
772 : :
773 : :
774 : : typedef struct {
775 : : char *etag;
776 : : GDateTime *modtime;
777 : : } GetTileData;
778 : :
779 : : static void
780 : 4 : get_tile_data_free (GetTileData *data)
781 : : {
782 [ - + ]: 4 : g_clear_pointer (&data->etag, g_free);
783 [ - + ]: 4 : g_clear_pointer (&data->modtime, g_date_time_unref);
784 : 4 : g_free (data);
785 : 4 : }
786 : :
787 : : static void on_get_tile_file_loaded (GObject *source_object, GAsyncResult *res, gpointer user_data);
788 : :
789 : :
790 : : /**
791 : : * shumate_file_cache_get_tile_async:
792 : : * @self: a #ShumateFileCache
793 : : * @x: the X coordinate of the tile
794 : : * @y: the Y coordinate of the tile
795 : : * @zoom_level: the zoom level of the tile
796 : : * @cancellable: (nullable): a #GCancellable
797 : : * @callback: a #GAsyncReadyCallback to execute upon completion
798 : : * @user_data: closure data for @callback
799 : : *
800 : : * Gets tile data from the cache, if it is available.
801 : : */
802 : : void
803 : 4 : shumate_file_cache_get_tile_async (ShumateFileCache *self,
804 : : int x,
805 : : int y,
806 : : int zoom_level,
807 : : GCancellable *cancellable,
808 : : GAsyncReadyCallback callback,
809 : : gpointer user_data)
810 : : {
811 : 4 : g_autoptr(GTask) task = NULL;
812 [ + - + - ]: 4 : g_autoptr(GFile) file = NULL;
813 [ + - + - ]: 4 : g_autofree char *filename = NULL;
814 : 4 : g_autoptr(GFileInfo) info = NULL;
815 [ + - - + ]: 4 : g_autoptr(GError) error = NULL;
816 : 4 : GetTileData *task_data = NULL;
817 : :
818 [ + - ]: 4 : g_return_if_fail (SHUMATE_IS_FILE_CACHE (self));
819 [ - + - - : 4 : g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
- - - - ]
820 : :
821 : 4 : task = g_task_new (self, cancellable, callback, user_data);
822 [ + - ]: 4 : g_task_set_source_tag (task, shumate_file_cache_get_tile_async);
823 : :
824 : 4 : task_data = g_new0 (GetTileData, 1);
825 : 4 : g_task_set_task_data (task, task_data, (GDestroyNotify) get_tile_data_free);
826 : :
827 : 4 : filename = get_filename (self, x, y, zoom_level);
828 : 4 : file = g_file_new_for_path (filename);
829 : :
830 : : /* Retrieve modification time */
831 : 4 : info = g_file_query_info (file,
832 : : G_FILE_ATTRIBUTE_TIME_MODIFIED,
833 : : G_FILE_QUERY_INFO_NONE, cancellable, &error);
834 [ + + ]: 4 : if (error)
835 : : {
836 [ + - ]: 2 : if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
837 : 2 : g_task_return_pointer (task, NULL, NULL);
838 : : else
839 : 0 : g_task_return_error (task, g_error_copy (error));
840 : :
841 : 2 : return;
842 : : }
843 : :
844 : 2 : task_data->modtime = g_file_info_get_modification_date_time (info);
845 : 2 : task_data->etag = db_get_etag (self, x, y, zoom_level);
846 : :
847 : : /* update tile popularity */
848 : 2 : on_tile_filled (self, x, y, zoom_level);
849 : :
850 [ - + ]: 2 : g_file_load_bytes_async (file, cancellable, on_get_tile_file_loaded, g_object_ref (task));
851 : : }
852 : :
853 : :
854 : : static void
855 : 2 : on_get_tile_file_loaded (GObject *source_object,
856 : : GAsyncResult *res,
857 : : gpointer user_data)
858 : : {
859 : 2 : g_autoptr(GTask) task = user_data;
860 : 2 : GFile *file = G_FILE (source_object);
861 [ - - + - ]: 2 : g_autoptr(GError) error = NULL;
862 : 2 : GBytes *bytes;
863 : :
864 : 2 : bytes = g_file_load_bytes_finish (file, res, NULL, &error);
865 : :
866 [ - + ]: 2 : if (error != NULL)
867 : : {
868 : : /* Return NULL but not an error if the file doesn't exist */
869 [ # # ]: 0 : if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
870 : 0 : g_task_return_pointer (task, NULL, NULL);
871 : : else
872 : 0 : g_task_return_error (task, g_error_copy (error));
873 : :
874 [ # # ]: 0 : return;
875 : : }
876 : :
877 [ - + ]: 2 : g_task_return_pointer (task, bytes, (GDestroyNotify) g_bytes_unref);
878 : : }
879 : :
880 : :
881 : : /**
882 : : * shumate_file_cache_get_tile_finish:
883 : : * @self: a #ShumateFileCache
884 : : * @etag: (nullable) (out) (optional): a location for the data's ETag, or %NULL
885 : : * @modtime: (nullable) (out) (optional): a location to return the tile's last modification time, or %NULL
886 : : * @result: a #GAsyncResult provided to callback
887 : : * @error: a location for a #GError, or %NULL
888 : : *
889 : : * Gets the tile data from a completed shumate_file_cache_get_tile_async()
890 : : * operation.
891 : : *
892 : : * @modtime will be set to the time the tile was added to the cache, or the
893 : : * latest time it was confirmed to be up to date.
894 : : *
895 : : * @etag will be set to the data's ETag, if present.
896 : : *
897 : : * Returns: a #GBytes containing the tile data, or %NULL if the tile was not in
898 : : * the cache or an error occurred
899 : : */
900 : : GBytes *
901 : 4 : shumate_file_cache_get_tile_finish (ShumateFileCache *self,
902 : : char **etag,
903 : : GDateTime **modtime,
904 : : GAsyncResult *result,
905 : : GError **error)
906 : : {
907 : 4 : GetTileData *data = g_task_get_task_data (G_TASK (result));
908 : :
909 [ + - ]: 4 : g_return_val_if_fail (SHUMATE_IS_FILE_CACHE (self), NULL);
910 [ - + ]: 4 : g_return_val_if_fail (g_task_is_valid (result, self), NULL);
911 : :
912 [ + - ]: 4 : if (etag)
913 : 4 : *etag = g_steal_pointer (&data->etag);
914 [ + - ]: 4 : if (modtime)
915 : 4 : *modtime = g_steal_pointer (&data->modtime);
916 : :
917 : 4 : return g_task_propagate_pointer (G_TASK (result), error);
918 : : }
919 : :
920 : :
921 : : typedef struct {
922 : : ShumateFileCache *self;
923 : : char *etag;
924 : : GBytes *bytes;
925 : : char *filename;
926 : : } StoreTileData;
927 : :
928 : : static void
929 : 2 : store_tile_data_free (StoreTileData *data)
930 : : {
931 [ + - ]: 2 : g_clear_object (&data->self);
932 [ + - ]: 2 : g_clear_pointer (&data->etag, g_free);
933 [ + - ]: 2 : g_clear_pointer (&data->bytes, g_bytes_unref);
934 [ + - ]: 2 : g_clear_pointer (&data->filename, g_free);
935 : 2 : g_free (data);
936 : 2 : }
937 : : G_DEFINE_AUTOPTR_CLEANUP_FUNC (StoreTileData, store_tile_data_free);
938 : :
939 : : static void on_file_created (GObject *object, GAsyncResult *result, gpointer user_data);
940 : : static void on_file_written (GObject *object, GAsyncResult *result, gpointer user_data);
941 : :
942 : : /**
943 : : * shumate_file_cache_store_tile_async:
944 : : * @self: a #ShumateFileCache
945 : : * @x: the X coordinate of the tile
946 : : * @y: the Y coordinate of the tile
947 : : * @zoom_level: the zoom level of the tile
948 : : * @bytes: a #GBytes
949 : : * @etag: (nullable): an ETag string, or %NULL
950 : : * @cancellable: (nullable): a #GCancellable
951 : : * @callback: a #GAsyncReadyCallback to execute upon completion
952 : : * @user_data: closure data for @callback
953 : : *
954 : : * Stores a tile in the cache.
955 : : */
956 : : void
957 : 2 : shumate_file_cache_store_tile_async (ShumateFileCache *self,
958 : : int x,
959 : : int y,
960 : : int zoom_level,
961 : : GBytes *bytes,
962 : : const char *etag,
963 : : GCancellable *cancellable,
964 : : GAsyncReadyCallback callback,
965 : : gpointer user_data)
966 : : {
967 : 4 : g_autoptr(GTask) task = NULL;
968 [ + - - - ]: 2 : g_autofree char *filename = NULL;
969 : 2 : g_autoptr(GFile) file = NULL;
970 [ + - - - ]: 2 : g_autofree char *path = NULL;
971 : 2 : StoreTileData *data;
972 : :
973 [ + - ]: 2 : g_return_if_fail (SHUMATE_IS_FILE_CACHE (self));
974 [ - + ]: 2 : g_return_if_fail (bytes != NULL);
975 [ - + - - : 2 : g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
- - - - ]
976 : :
977 : 2 : task = g_task_new (self, cancellable, callback, user_data);
978 [ + - ]: 2 : g_task_set_source_tag (task, shumate_file_cache_store_tile_async);
979 : :
980 : 2 : filename = get_filename (self, x, y, zoom_level);
981 : 2 : file = g_file_new_for_path (filename);
982 : :
983 : 2 : g_debug ("Update of tile (%d %d zoom %d)", x, y, zoom_level);
984 : :
985 : : /* If needed, create the cache's dirs */
986 : 2 : path = g_path_get_dirname (filename);
987 [ - + ]: 2 : if (g_mkdir_with_parents (path, 0700) == -1)
988 : : {
989 [ # # ]: 0 : if (errno != EEXIST)
990 : : {
991 : 0 : const char *error_string = g_strerror (errno);
992 : 0 : g_task_return_new_error (task, SHUMATE_FILE_CACHE_ERROR,
993 : : SHUMATE_FILE_CACHE_ERROR_FAILED,
994 : : "Failed to create cache directory %s: %s", path, error_string);
995 : 0 : return;
996 : : }
997 : : }
998 : :
999 : 2 : data = g_new0 (StoreTileData, 1);
1000 : 2 : data->self = g_object_ref (self);
1001 [ - + ]: 2 : data->etag = g_strdup (etag);
1002 : 2 : data->bytes = g_bytes_ref (bytes);
1003 : 2 : data->filename = g_steal_pointer (&filename);
1004 : 2 : g_task_set_task_data (task, data, (GDestroyNotify) store_tile_data_free);
1005 : :
1006 : 2 : g_file_replace_async (file, NULL, FALSE, G_FILE_CREATE_PRIVATE, G_PRIORITY_DEFAULT, cancellable, on_file_created, g_object_ref (task));
1007 : : }
1008 : :
1009 : : static void
1010 : 2 : on_file_created (GObject *object,
1011 : : GAsyncResult *res,
1012 : : gpointer user_data)
1013 : : {
1014 : 2 : g_autoptr(GTask) task = user_data;
1015 : 2 : StoreTileData *data = g_task_get_task_data (task);
1016 : 2 : GCancellable *cancellable = g_task_get_cancellable (task);
1017 : 2 : GError *error = NULL;
1018 [ - - + - ]: 2 : g_autoptr(GFileOutputStream) ostream = NULL;
1019 : 2 : gconstpointer contents;
1020 : 2 : gsize size;
1021 : :
1022 : 2 : ostream = g_file_create_finish (G_FILE (object), res, &error);
1023 [ - + ]: 2 : if (error != NULL)
1024 : : {
1025 : 0 : g_task_return_error (task, error);
1026 [ # # ]: 0 : return;
1027 : : }
1028 : :
1029 : 2 : contents = g_bytes_get_data (data->bytes, &size);
1030 : :
1031 [ + - ]: 2 : g_output_stream_write_all_async (G_OUTPUT_STREAM (ostream),
1032 : : contents, size, G_PRIORITY_DEFAULT,
1033 : : cancellable, on_file_written, g_object_ref (task));
1034 : : }
1035 : :
1036 : : static void
1037 : 2 : on_file_written (GObject *object,
1038 : : GAsyncResult *res,
1039 : : gpointer user_data)
1040 : : {
1041 : 2 : g_autoptr(GTask) task = user_data;
1042 : 2 : StoreTileData *data = g_task_get_task_data (task);
1043 [ + - - - ]: 2 : g_autoptr(sqlite_str) query = NULL;
1044 [ + - - - ]: 2 : g_autoptr(sqlite_str) sql_error = NULL;
1045 : 2 : GError *error = NULL;
1046 : 2 : guint tile_size = g_bytes_get_size (data->bytes);
1047 : :
1048 : 2 : g_output_stream_write_all_finish (G_OUTPUT_STREAM (object), res, NULL, &error);
1049 [ - + ]: 2 : if (error != NULL)
1050 : : {
1051 : 0 : g_task_return_error (task, error);
1052 : 0 : return;
1053 : : }
1054 : :
1055 : 2 : query = sqlite3_mprintf ("REPLACE INTO tiles (filename, etag, size) VALUES (%Q, %Q, %d)",
1056 : : data->filename, data->etag, tile_size);
1057 : 2 : sqlite3_exec (data->self->db, query, NULL, NULL, &sql_error);
1058 [ - + ]: 2 : if (sql_error != NULL)
1059 : : {
1060 : 0 : g_task_return_new_error (task, SHUMATE_FILE_CACHE_ERROR, SHUMATE_FILE_CACHE_ERROR_FAILED,
1061 : : "Failed to insert tile into SQLite database: %s", sql_error);
1062 : 0 : return;
1063 : : }
1064 : :
1065 : 2 : data->self->size_estimate += tile_size;
1066 [ - + - - ]: 2 : if (!data->self->have_size_estimate || data->self->size_estimate > data->self->size_limit + 5000000)
1067 : : {
1068 : : /* automatically purge the cache if the size estimate is 5MB over
1069 : : * the limit, or if there is no estimate of the cache size yet */
1070 : :
1071 : 2 : shumate_file_cache_purge_cache_async (data->self, NULL, NULL, NULL);
1072 : : }
1073 : :
1074 : :
1075 [ - + ]: 2 : g_task_return_boolean (task, TRUE);
1076 : : }
1077 : :
1078 : :
1079 : : /**
1080 : : * shumate_file_cache_store_tile_finish:
1081 : : * @self: an #ShumateFileCache
1082 : : * @result: a #GAsyncResult provided to callback
1083 : : * @error: a location for a #GError, or %NULL
1084 : : *
1085 : : * Gets the success value of a completed shumate_file_cache_store_tile_async()
1086 : : * operation.
1087 : : *
1088 : : * Returns: %TRUE if the operation was successful, otherwise %FALSE
1089 : : */
1090 : : gboolean
1091 : 2 : shumate_file_cache_store_tile_finish (ShumateFileCache *self,
1092 : : GAsyncResult *result,
1093 : : GError **error)
1094 : : {
1095 [ + - ]: 2 : g_return_val_if_fail (SHUMATE_IS_FILE_CACHE (self), FALSE);
1096 [ - + ]: 2 : g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
1097 : :
1098 : 2 : return g_task_propagate_boolean (G_TASK (result), error);
1099 : : }
1100 : :
1101 : : /**
1102 : : * shumate_file_cache_error_quark:
1103 : : *
1104 : : * Gets the #ShumateFileCache error quark.
1105 : : *
1106 : : * Returns: a #GQuark
1107 : : */
1108 [ + - ]: 1 : G_DEFINE_QUARK (shumate-file-cache-error-quark, shumate_file_cache_error);
|