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