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