1 /*
2 * Copyright (C) 2004 John Ellis
3 * Copyright (C) 2008 - 2016 The Geeqie Team
4 *
5 * Author: John Ellis
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program 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
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 */
21
22 #include "main.h"
23 #include "cache.h"
24
25 #include "md5-util.h"
26 #include "secure_save.h"
27 #include "thumb_standard.h"
28 #include "ui_fileops.h"
29
30 #include <utime.h>
31 #include <errno.h>
32
33
34 /*
35 *-------------------------------------------------------------------
36 * Cache data file format:
37 *-------------------------------------------------------------------
38 *
39 * SIMcache
40 * #comment
41 * Dimensions=[<width> x <height>]
42 * Date=[<value in time_t format, or -1 if no embedded date>]
43 * MD5sum=[<32 character ascii text digest>]
44 * SimilarityGrid[32 x 32]=<3072 bytes of data (1024 pixels in RGB format, 1 pixel is 24bits)>
45 *
46 *
47 * The first line (9 bytes) indicates it is a SIMcache format file. (new line char must exist)
48 * Comment lines starting with a # are ignored up to a new line.
49 * All data lines should end with a new line char.
50 * Format is very strict, data must begin with the char immediately following '='.
51 * Currently SimilarityGrid is always assumed to be 32 x 32 RGB.
52 */
53
54
55 /*
56 *-------------------------------------------------------------------
57 * sim cache data
58 *-------------------------------------------------------------------
59 */
60
cache_sim_data_new(void)61 CacheData *cache_sim_data_new(void)
62 {
63 CacheData *cd;
64
65 cd = g_new0(CacheData, 1);
66 cd->date = -1;
67
68 return cd;
69 }
70
cache_sim_data_free(CacheData * cd)71 void cache_sim_data_free(CacheData *cd)
72 {
73 if (!cd) return;
74
75 g_free(cd->path);
76 image_sim_free(cd->sim);
77 g_free(cd);
78 }
79
80 /*
81 *-------------------------------------------------------------------
82 * sim cache write
83 *-------------------------------------------------------------------
84 */
85
cache_sim_write_dimensions(SecureSaveInfo * ssi,CacheData * cd)86 static gboolean cache_sim_write_dimensions(SecureSaveInfo *ssi, CacheData *cd)
87 {
88 if (!cd || !cd->dimensions) return FALSE;
89
90 secure_fprintf(ssi, "Dimensions=[%d x %d]\n", cd->width, cd->height);
91
92 return TRUE;
93 }
94
cache_sim_write_date(SecureSaveInfo * ssi,CacheData * cd)95 static gboolean cache_sim_write_date(SecureSaveInfo *ssi, CacheData *cd)
96 {
97 if (!cd || !cd->have_date) return FALSE;
98
99 secure_fprintf(ssi, "Date=[%ld]\n", cd->date);
100
101 return TRUE;
102 }
103
cache_sim_write_md5sum(SecureSaveInfo * ssi,CacheData * cd)104 static gboolean cache_sim_write_md5sum(SecureSaveInfo *ssi, CacheData *cd)
105 {
106 gchar *text;
107
108 if (!cd || !cd->have_md5sum) return FALSE;
109
110 text = md5_digest_to_text(cd->md5sum);
111 secure_fprintf(ssi, "MD5sum=[%s]\n", text);
112 g_free(text);
113
114 return TRUE;
115 }
116
cache_sim_write_similarity(SecureSaveInfo * ssi,CacheData * cd)117 static gboolean cache_sim_write_similarity(SecureSaveInfo *ssi, CacheData *cd)
118 {
119 guint x, y;
120 guint8 buf[3 * 32];
121
122 if (!cd || !cd->similarity || !cd->sim || !cd->sim->filled) return FALSE;
123
124 secure_fprintf(ssi, "SimilarityGrid[32 x 32]=");
125 for (y = 0; y < 32; y++)
126 {
127 guint s = y * 32;
128 guint8 *avg_r = &cd->sim->avg_r[s];
129 guint8 *avg_g = &cd->sim->avg_g[s];
130 guint8 *avg_b = &cd->sim->avg_b[s];
131 guint n = 0;
132
133 for (x = 0; x < 32; x++)
134 {
135 buf[n++] = avg_r[x];
136 buf[n++] = avg_g[x];
137 buf[n++] = avg_b[x];
138 }
139
140 secure_fwrite(buf, sizeof(buf), 1, ssi);
141 }
142
143 secure_fputc(ssi, '\n');
144
145 return TRUE;
146 }
147
cache_sim_data_save(CacheData * cd)148 gboolean cache_sim_data_save(CacheData *cd)
149 {
150 SecureSaveInfo *ssi;
151 gchar *pathl;
152
153 if (!cd || !cd->path) return FALSE;
154
155 pathl = path_from_utf8(cd->path);
156 ssi = secure_open(pathl);
157 g_free(pathl);
158
159 if (!ssi)
160 {
161 log_printf("Unable to save sim cache data: %s\n", cd->path);
162 return FALSE;
163 }
164
165 secure_fprintf(ssi, "SIMcache\n#%s %s\n", PACKAGE, VERSION);
166 cache_sim_write_dimensions(ssi, cd);
167 cache_sim_write_date(ssi, cd);
168 cache_sim_write_md5sum(ssi, cd);
169 cache_sim_write_similarity(ssi, cd);
170
171 if (secure_close(ssi))
172 {
173 log_printf(_("error saving sim cache data: %s\nerror: %s\n"), cd->path,
174 secsave_strerror(secsave_errno));
175 return FALSE;
176 }
177
178 return TRUE;
179 }
180
181 /*
182 *-------------------------------------------------------------------
183 * sim cache read
184 *-------------------------------------------------------------------
185 */
186
cache_sim_read_skipline(FILE * f,gint s)187 static gboolean cache_sim_read_skipline(FILE *f, gint s)
188 {
189 if (!f) return FALSE;
190
191 if (fseek(f, 0 - s, SEEK_CUR) == 0)
192 {
193 gchar b;
194 while (fread(&b, sizeof(b), 1, f) == 1)
195 {
196 if (b == '\n') return TRUE;
197 }
198 return TRUE;
199 }
200
201 return FALSE;
202 }
203
cache_sim_read_comment(FILE * f,gchar * buf,gint s,CacheData * cd)204 static gboolean cache_sim_read_comment(FILE *f, gchar *buf, gint s, CacheData *cd)
205 {
206 if (!f || !buf || !cd) return FALSE;
207
208 if (s < 1 || buf[0] != '#') return FALSE;
209
210 return cache_sim_read_skipline(f, s - 1);
211 }
212
cache_sim_read_dimensions(FILE * f,gchar * buf,gint s,CacheData * cd)213 static gboolean cache_sim_read_dimensions(FILE *f, gchar *buf, gint s, CacheData *cd)
214 {
215 if (!f || !buf || !cd) return FALSE;
216
217 if (s < 10 || strncmp("Dimensions", buf, 10) != 0) return FALSE;
218
219 if (fseek(f, - s, SEEK_CUR) == 0)
220 {
221 gchar b;
222 gchar buf[1024];
223 gsize p = 0;
224 gint w, h;
225
226 b = 'X';
227 while (b != '[')
228 {
229 if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
230 }
231 while (b != ']' && p < sizeof(buf) - 1)
232 {
233 if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
234 buf[p] = b;
235 p++;
236 }
237
238 while (b != '\n')
239 {
240 if (fread(&b, sizeof(b), 1, f) != 1) break;
241 }
242
243 buf[p] = '\0';
244 if (sscanf(buf, "%d x %d", &w, &h) != 2) return FALSE;
245
246 cd->width = w;
247 cd->height = h;
248 cd->dimensions = TRUE;
249
250 return TRUE;
251 }
252
253 return FALSE;
254 }
255
cache_sim_read_date(FILE * f,gchar * buf,gint s,CacheData * cd)256 static gboolean cache_sim_read_date(FILE *f, gchar *buf, gint s, CacheData *cd)
257 {
258 if (!f || !buf || !cd) return FALSE;
259
260 if (s < 4 || strncmp("Date", buf, 4) != 0) return FALSE;
261
262 if (fseek(f, - s, SEEK_CUR) == 0)
263 {
264 gchar b;
265 gchar buf[1024];
266 gsize p = 0;
267
268 b = 'X';
269 while (b != '[')
270 {
271 if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
272 }
273 while (b != ']' && p < sizeof(buf) - 1)
274 {
275 if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
276 buf[p] = b;
277 p++;
278 }
279
280 while (b != '\n')
281 {
282 if (fread(&b, sizeof(b), 1, f) != 1) break;
283 }
284
285 buf[p] = '\0';
286 cd->date = strtol(buf, NULL, 10);
287
288 cd->have_date = TRUE;
289
290 return TRUE;
291 }
292
293 return FALSE;
294 }
295
cache_sim_read_md5sum(FILE * f,gchar * buf,gint s,CacheData * cd)296 static gboolean cache_sim_read_md5sum(FILE *f, gchar *buf, gint s, CacheData *cd)
297 {
298 if (!f || !buf || !cd) return FALSE;
299
300 if (s < 8 || strncmp("MD5sum", buf, 6) != 0) return FALSE;
301
302 if (fseek(f, - s, SEEK_CUR) == 0)
303 {
304 gchar b;
305 gchar buf[64];
306 gsize p = 0;
307
308 b = 'X';
309 while (b != '[')
310 {
311 if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
312 }
313 while (b != ']' && p < sizeof(buf) - 1)
314 {
315 if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
316 buf[p] = b;
317 p++;
318 }
319 while (b != '\n')
320 {
321 if (fread(&b, sizeof(b), 1, f) != 1) break;
322 }
323
324 buf[p] = '\0';
325 cd->have_md5sum = md5_digest_from_text(buf, cd->md5sum);
326
327 return TRUE;
328 }
329
330 return FALSE;
331 }
332
cache_sim_read_similarity(FILE * f,gchar * buf,gint s,CacheData * cd)333 static gboolean cache_sim_read_similarity(FILE *f, gchar *buf, gint s, CacheData *cd)
334 {
335 if (!f || !buf || !cd) return FALSE;
336
337 if (s < 11 || strncmp("Similarity", buf, 10) != 0) return FALSE;
338
339 if (strncmp("Grid[32 x 32]", buf + 10, 13) != 0) return FALSE;
340
341 if (fseek(f, - s, SEEK_CUR) == 0)
342 {
343 gchar b;
344 guint8 pixel_buf[3];
345 ImageSimilarityData *sd;
346 gint x, y;
347
348 b = 'X';
349 while (b != '=')
350 {
351 if (fread(&b, sizeof(b), 1, f) != 1) return FALSE;
352 }
353
354 if (cd->sim)
355 {
356 /* use current sim that may already contain data we will not touch here */
357 sd = cd->sim;
358 cd->sim = NULL;
359 cd->similarity = FALSE;
360 }
361 else
362 {
363 sd = image_sim_new();
364 }
365
366 for (y = 0; y < 32; y++)
367 {
368 gint s = y * 32;
369 for (x = 0; x < 32; x++)
370 {
371 if (fread(&pixel_buf, sizeof(pixel_buf), 1, f) != 1)
372 {
373 image_sim_free(sd);
374 return FALSE;
375 }
376 sd->avg_r[s + x] = pixel_buf[0];
377 sd->avg_g[s + x] = pixel_buf[1];
378 sd->avg_b[s + x] = pixel_buf[2];
379 }
380 }
381
382 if (fread(&b, sizeof(b), 1, f) == 1)
383 {
384 if (b != '\n') fseek(f, -1, SEEK_CUR);
385 }
386
387 cd->sim = sd;
388 cd->sim->filled = TRUE;
389 cd->similarity = TRUE;
390
391 return TRUE;
392 }
393
394 return FALSE;
395 }
396
397 #define CACHE_LOAD_LINE_NOISE 8
398
cache_sim_data_load(const gchar * path)399 CacheData *cache_sim_data_load(const gchar *path)
400 {
401 FILE *f;
402 CacheData *cd = NULL;
403 gchar buf[32];
404 gint success = CACHE_LOAD_LINE_NOISE;
405 gchar *pathl;
406
407 if (!path) return NULL;
408
409 pathl = path_from_utf8(path);
410 f = fopen(pathl, "r");
411 g_free(pathl);
412
413 if (!f) return NULL;
414
415 cd = cache_sim_data_new();
416 cd->path = g_strdup(path);
417
418 if (fread(&buf, sizeof(gchar), 9, f) != 9 ||
419 strncmp(buf, "SIMcache", 8) != 0)
420 {
421 DEBUG_1("%s is not a cache file", cd->path);
422 success = 0;
423 }
424
425 while (success > 0)
426 {
427 gint s;
428 s = fread(&buf, sizeof(gchar), sizeof(buf), f);
429
430 if (s < 1)
431 {
432 success = 0;
433 }
434 else
435 {
436 if (!cache_sim_read_comment(f, buf, s, cd) &&
437 !cache_sim_read_dimensions(f, buf, s, cd) &&
438 !cache_sim_read_date(f, buf, s, cd) &&
439 !cache_sim_read_md5sum(f, buf, s, cd) &&
440 !cache_sim_read_similarity(f, buf, s, cd))
441 {
442 if (!cache_sim_read_skipline(f, s))
443 {
444 success = 0;
445 }
446 else
447 {
448 success--;
449 }
450 }
451 else
452 {
453 success = CACHE_LOAD_LINE_NOISE;
454 }
455 }
456 }
457
458 fclose(f);
459
460 if (!cd->dimensions &&
461 !cd->have_date &&
462 !cd->have_md5sum &&
463 !cd->similarity)
464 {
465 cache_sim_data_free(cd);
466 cd = NULL;
467 }
468
469 return cd;
470 }
471
472 /*
473 *-------------------------------------------------------------------
474 * sim cache setting
475 *-------------------------------------------------------------------
476 */
477
cache_sim_data_set_dimensions(CacheData * cd,gint w,gint h)478 void cache_sim_data_set_dimensions(CacheData *cd, gint w, gint h)
479 {
480 if (!cd) return;
481
482 cd->width = w;
483 cd->height = h;
484 cd->dimensions = TRUE;
485 }
486
cache_sim_data_set_date(CacheData * cd,time_t date)487 void cache_sim_data_set_date(CacheData *cd, time_t date)
488 {
489 if (!cd) return;
490
491 cd->date = date;
492 cd->have_date = TRUE;
493 }
494
cache_sim_data_set_md5sum(CacheData * cd,guchar digest[16])495 void cache_sim_data_set_md5sum(CacheData *cd, guchar digest[16])
496 {
497 gint i;
498
499 if (!cd) return;
500
501 for (i = 0; i < 16; i++)
502 {
503 cd->md5sum[i] = digest[i];
504 }
505 cd->have_md5sum = TRUE;
506 }
507
cache_sim_data_set_similarity(CacheData * cd,ImageSimilarityData * sd)508 void cache_sim_data_set_similarity(CacheData *cd, ImageSimilarityData *sd)
509 {
510 if (!cd || !sd || !sd->filled) return;
511
512 if (!cd->sim) cd->sim = image_sim_new();
513
514 memcpy(cd->sim->avg_r, sd->avg_r, 1024);
515 memcpy(cd->sim->avg_g, sd->avg_g, 1024);
516 memcpy(cd->sim->avg_b, sd->avg_b, 1024);
517 cd->sim->filled = TRUE;
518
519 cd->similarity = TRUE;
520 }
521
cache_sim_data_filled(ImageSimilarityData * sd)522 gboolean cache_sim_data_filled(ImageSimilarityData *sd)
523 {
524 if (!sd) return FALSE;
525 return sd->filled;
526 }
527
528 /*
529 *-------------------------------------------------------------------
530 * cache path location utils
531 *-------------------------------------------------------------------
532 */
533
534
cache_path_parts(CacheType type,const gchar ** cache_rc,const gchar ** cache_local,const gchar ** cache_ext)535 static void cache_path_parts(CacheType type,
536 const gchar **cache_rc, const gchar **cache_local, const gchar **cache_ext)
537 {
538 switch (type)
539 {
540 case CACHE_TYPE_THUMB:
541 *cache_rc = get_thumbnails_cache_dir();
542 *cache_local = GQ_CACHE_LOCAL_THUMB;
543 *cache_ext = GQ_CACHE_EXT_THUMB;
544 break;
545 case CACHE_TYPE_SIM:
546 *cache_rc = get_thumbnails_cache_dir();
547 *cache_local = GQ_CACHE_LOCAL_THUMB;
548 *cache_ext = GQ_CACHE_EXT_SIM;
549 break;
550 case CACHE_TYPE_METADATA:
551 *cache_rc = get_metadata_cache_dir();
552 *cache_local = GQ_CACHE_LOCAL_METADATA;
553 *cache_ext = GQ_CACHE_EXT_METADATA;
554 break;
555 case CACHE_TYPE_XMP_METADATA:
556 *cache_rc = get_metadata_cache_dir();
557 *cache_local = GQ_CACHE_LOCAL_METADATA;
558 *cache_ext = GQ_CACHE_EXT_XMP_METADATA;
559 break;
560 }
561 }
562
cache_get_location(CacheType type,const gchar * source,gint include_name,mode_t * mode)563 gchar *cache_get_location(CacheType type, const gchar *source, gint include_name, mode_t *mode)
564 {
565 gchar *path = NULL;
566 gchar *base;
567 gchar *name = NULL;
568 const gchar *cache_rc;
569 const gchar *cache_local;
570 const gchar *cache_ext;
571
572 if (!source) return NULL;
573
574 cache_path_parts(type, &cache_rc, &cache_local, &cache_ext);
575
576 base = remove_level_from_path(source);
577 if (include_name)
578 {
579 name = g_strconcat(filename_from_path(source), cache_ext, NULL);
580 }
581
582 if (((type != CACHE_TYPE_METADATA && type != CACHE_TYPE_XMP_METADATA && options->thumbnails.cache_into_dirs) ||
583 ((type == CACHE_TYPE_METADATA || type == CACHE_TYPE_XMP_METADATA) && options->metadata.enable_metadata_dirs)) &&
584 access_file(base, W_OK))
585 {
586 path = g_build_filename(base, cache_local, name, NULL);
587 if (mode) *mode = 0775;
588 }
589
590 if (!path)
591 {
592 path = g_build_filename(cache_rc, base, name, NULL);
593 if (mode) *mode = 0755;
594 }
595
596 g_free(base);
597 if (name) g_free(name);
598
599 return path;
600 }
601
cache_build_path_local(const gchar * source,const gchar * cache_local,const gchar * cache_ext)602 static gchar *cache_build_path_local(const gchar *source, const gchar *cache_local, const gchar *cache_ext)
603 {
604 gchar *path;
605 gchar *base = remove_level_from_path(source);
606 gchar *name = g_strconcat(filename_from_path(source), cache_ext, NULL);
607 path = g_build_filename(base, cache_local, name, NULL);
608 g_free(name);
609 g_free(base);
610
611 return path;
612 }
613
cache_build_path_rc(const gchar * source,const gchar * cache_rc,const gchar * cache_ext)614 static gchar *cache_build_path_rc(const gchar *source, const gchar *cache_rc, const gchar *cache_ext)
615 {
616 gchar *path;
617 gchar *name = g_strconcat(source, cache_ext, NULL);
618 path = g_build_filename(cache_rc, name, NULL);
619 g_free(name);
620
621 return path;
622 }
623
cache_find_location(CacheType type,const gchar * source)624 gchar *cache_find_location(CacheType type, const gchar *source)
625 {
626 gchar *path;
627 const gchar *cache_rc;
628 const gchar *cache_local;
629 const gchar *cache_ext;
630 gboolean prefer_local;
631
632 if (!source) return NULL;
633
634 cache_path_parts(type, &cache_rc, &cache_local, &cache_ext);
635
636 if (type == CACHE_TYPE_METADATA || type == CACHE_TYPE_XMP_METADATA)
637 {
638 prefer_local = options->metadata.enable_metadata_dirs;
639 }
640 else
641 {
642 prefer_local = options->thumbnails.cache_into_dirs;
643 }
644
645 if (prefer_local)
646 {
647 path = cache_build_path_local(source, cache_local, cache_ext);
648 }
649 else
650 {
651 path = cache_build_path_rc(source, cache_rc, cache_ext);
652 }
653
654 if (!isfile(path))
655 {
656 g_free(path);
657
658 /* try the opposite method if not found */
659 if (!prefer_local)
660 {
661 path = cache_build_path_local(source, cache_local, cache_ext);
662 }
663 else
664 {
665 path = cache_build_path_rc(source, cache_rc, cache_ext);
666 }
667
668 if (!isfile(path))
669 {
670 g_free(path);
671 path = NULL;
672 }
673 }
674
675 return path;
676 }
677
cache_time_valid(const gchar * cache,const gchar * path)678 gboolean cache_time_valid(const gchar *cache, const gchar *path)
679 {
680 struct stat cache_st;
681 struct stat path_st;
682 gchar *cachel;
683 gchar *pathl;
684 gboolean ret = FALSE;
685
686 if (!cache || !path) return FALSE;
687
688 cachel = path_from_utf8(cache);
689 pathl = path_from_utf8(path);
690
691 if (stat(cachel, &cache_st) == 0 &&
692 stat(pathl, &path_st) == 0)
693 {
694 if (cache_st.st_mtime == path_st.st_mtime)
695 {
696 ret = TRUE;
697 }
698 else if (cache_st.st_mtime > path_st.st_mtime)
699 {
700 struct utimbuf ut;
701
702 ut.actime = ut.modtime = cache_st.st_mtime;
703 if (utime(cachel, &ut) < 0 &&
704 errno == EPERM)
705 {
706 DEBUG_1("cache permission workaround: %s", cachel);
707 ret = TRUE;
708 }
709 }
710 }
711
712 g_free(pathl);
713 g_free(cachel);
714
715 return ret;
716 }
717
get_thumbnails_cache_dir(void)718 const gchar *get_thumbnails_cache_dir(void)
719 {
720 static gchar *thumbnails_cache_dir = NULL;
721
722 if (thumbnails_cache_dir) return thumbnails_cache_dir;
723
724 if (USE_XDG)
725 {
726 thumbnails_cache_dir = g_build_filename(xdg_cache_home_get(),
727 GQ_APPNAME_LC, GQ_CACHE_THUMB, NULL);
728 }
729 else
730 {
731 thumbnails_cache_dir = g_build_filename(get_rc_dir(), GQ_CACHE_THUMB, NULL);
732 }
733
734 return thumbnails_cache_dir;
735 }
736
get_thumbnails_standard_cache_dir(void)737 const gchar *get_thumbnails_standard_cache_dir(void)
738 {
739 static gchar *thumbnails_standard_cache_dir = NULL;
740
741 if (thumbnails_standard_cache_dir) return thumbnails_standard_cache_dir;
742
743 thumbnails_standard_cache_dir = g_build_filename(xdg_cache_home_get(),
744 THUMB_FOLDER_GLOBAL, NULL);
745
746 return thumbnails_standard_cache_dir;
747 }
748
get_metadata_cache_dir(void)749 const gchar *get_metadata_cache_dir(void)
750 {
751 static gchar *metadata_cache_dir = NULL;
752
753 if (metadata_cache_dir) return metadata_cache_dir;
754
755 if (USE_XDG)
756 {
757 /* Metadata go to $XDG_DATA_HOME.
758 * "Keywords and comments, among other things, are irreplaceable and cannot be auto-generated,
759 * so I don't think they'd be appropriate for the cache directory." -- Omari Stephens on geeqie-devel ml
760 */
761 metadata_cache_dir = g_build_filename(xdg_data_home_get(), GQ_APPNAME_LC, GQ_CACHE_METADATA, NULL);
762 }
763 else
764 {
765 metadata_cache_dir = g_build_filename(get_rc_dir(), GQ_CACHE_METADATA, NULL);
766 }
767
768 return metadata_cache_dir;
769 }
770
771 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */
772