(Re)-implement natural and case sorting
[geeqie.git] / src / filedata.cc
1 /*
2  * Copyright (C) 2006 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 "filedata.h"
24
25 #include "filefilter.h"
26 #include "cache.h"
27 #include "thumb-standard.h"
28 #include "ui-fileops.h"
29 #include "metadata.h"
30 #include "trash.h"
31 #include "histogram.h"
32 #include "secure-save.h"
33
34 #include "exif.h"
35 #include "misc.h"
36
37 #include <grp.h>
38
39 #ifdef DEBUG_FILEDATA
40 gint global_file_data_count = 0;
41 #endif
42
43 static GHashTable *file_data_pool = nullptr;
44 static GHashTable *file_data_planned_change_hash = nullptr;
45
46 static gint sidecar_file_priority(const gchar *extension);
47 static void file_data_check_sidecars(const GList *basename_list);
48 static void file_data_disconnect_sidecar_file(FileData *target, FileData *sfd);
49
50
51 static SortType filelist_sort_method = SORT_NONE;
52 static gboolean filelist_sort_ascend = TRUE;
53 static gboolean filelist_sort_case = TRUE;
54
55 /*
56  *-----------------------------------------------------------------------------
57  * text conversion utils
58  *-----------------------------------------------------------------------------
59  */
60
61 gchar *text_from_size(gint64 size)
62 {
63         gchar *a, *b;
64         gchar *s, *d;
65         gint l, n, i;
66
67         /* what I would like to use is printf("%'d", size)
68          * BUT: not supported on every libc :(
69          */
70         if (size > G_MAXINT)
71                 {
72                 /* the %lld conversion is not valid in all libcs, so use a simple work-around */
73                 a = g_strdup_printf("%d%09d", static_cast<guint>(size / 1000000000), static_cast<guint>(size % 1000000000));
74                 }
75         else
76                 {
77                 a = g_strdup_printf("%d", static_cast<guint>(size));
78                 }
79         l = strlen(a);
80         n = (l - 1)/ 3;
81         if (n < 1) return a;
82
83         b = g_new(gchar, l + n + 1);
84
85         s = a;
86         d = b;
87         i = l - n * 3;
88         while (*s != '\0')
89                 {
90                 if (i < 1)
91                         {
92                         i = 3;
93                         *d = ',';
94                         d++;
95                         }
96
97                 *d = *s;
98                 s++;
99                 d++;
100                 i--;
101                 }
102         *d = '\0';
103
104         g_free(a);
105         return b;
106 }
107
108 gchar *text_from_size_abrev(gint64 size)
109 {
110         if (size < static_cast<gint64>(1024))
111                 {
112                 return g_strdup_printf(_("%d bytes"), static_cast<gint>(size));
113                 }
114         if (size < static_cast<gint64>(1048576))
115                 {
116                 return g_strdup_printf(_("%.1f KiB"), static_cast<gdouble>(size) / 1024.0);
117                 }
118         if (size < static_cast<gint64>(1073741824))
119                 {
120                 return g_strdup_printf(_("%.1f MiB"), static_cast<gdouble>(size) / 1048576.0);
121                 }
122
123         /* to avoid overflowing the gdouble, do division in two steps */
124         size /= 1048576;
125         return g_strdup_printf(_("%.1f GiB"), static_cast<gdouble>(size) / 1024.0);
126 }
127
128 /* note: returned string is valid until next call to text_from_time() */
129 const gchar *text_from_time(time_t t)
130 {
131         static gchar *ret = nullptr;
132         gchar buf[128];
133         gint buflen;
134         struct tm *btime;
135         GError *error = nullptr;
136
137         btime = localtime(&t);
138
139         /* the %x warning about 2 digit years is not an error */
140         buflen = strftime(buf, sizeof(buf), "%x %X", btime);
141         if (buflen < 1) return "";
142
143         g_free(ret);
144         ret = g_locale_to_utf8(buf, buflen, nullptr, nullptr, &error);
145         if (error)
146                 {
147                 log_printf("Error converting locale strftime to UTF-8: %s\n", error->message);
148                 g_error_free(error);
149                 return "";
150                 }
151
152         return ret;
153 }
154
155 /*
156  *-----------------------------------------------------------------------------
157  * changed files detection and notification
158  *-----------------------------------------------------------------------------
159  */
160
161 void file_data_increment_version(FileData *fd)
162 {
163         fd->version++;
164         fd->valid_marks = 0;
165         if (fd->parent)
166                 {
167                 fd->parent->version++;
168                 fd->parent->valid_marks = 0;
169                 }
170 }
171
172 static gboolean file_data_check_changed_single_file(FileData *fd, struct stat *st)
173 {
174         if (fd->size != st->st_size ||
175             fd->date != st->st_mtime)
176                 {
177                 fd->size = st->st_size;
178                 fd->date = st->st_mtime;
179                 fd->cdate = st->st_ctime;
180                 fd->mode = st->st_mode;
181                 if (fd->thumb_pixbuf) g_object_unref(fd->thumb_pixbuf);
182                 fd->thumb_pixbuf = nullptr;
183                 file_data_increment_version(fd);
184                 file_data_send_notification(fd, NOTIFY_REREAD);
185                 return TRUE;
186                 }
187         return FALSE;
188 }
189
190 static gboolean file_data_check_changed_files_recursive(FileData *fd, struct stat *st)
191 {
192         gboolean ret = FALSE;
193         GList *work;
194
195         ret = file_data_check_changed_single_file(fd, st);
196
197         work = fd->sidecar_files;
198         while (work)
199                 {
200                 auto sfd = static_cast<FileData *>(work->data);
201                 struct stat st;
202                 work = work->next;
203
204                 if (!stat_utf8(sfd->path, &st))
205                         {
206                         fd->size = 0;
207                         fd->date = 0;
208                         file_data_ref(sfd);
209                         file_data_disconnect_sidecar_file(fd, sfd);
210                         ret = TRUE;
211                         file_data_increment_version(sfd);
212                         file_data_send_notification(sfd, NOTIFY_REREAD);
213                         file_data_unref(sfd);
214                         continue;
215                         }
216
217                 ret |= file_data_check_changed_files_recursive(sfd, &st);
218                 }
219         return ret;
220 }
221
222
223 gboolean file_data_check_changed_files(FileData *fd)
224 {
225         gboolean ret = FALSE;
226         struct stat st;
227
228         if (fd->parent) fd = fd->parent;
229
230         if (!stat_utf8(fd->path, &st))
231                 {
232                 GList *sidecars;
233                 GList *work;
234                 FileData *sfd = nullptr;
235
236                 /* parent is missing, we have to rebuild whole group */
237                 ret = TRUE;
238                 fd->size = 0;
239                 fd->date = 0;
240
241                 /* file_data_disconnect_sidecar_file might delete the file,
242                    we have to keep the reference to prevent this */
243                 sidecars = filelist_copy(fd->sidecar_files);
244                 file_data_ref(fd);
245                 work = sidecars;
246                 while (work)
247                         {
248                         sfd = static_cast<FileData *>(work->data);
249                         work = work->next;
250
251                         file_data_disconnect_sidecar_file(fd, sfd);
252                         }
253                 file_data_check_sidecars(sidecars); /* this will group the sidecars back together */
254                 /* now we can release the sidecars */
255                 filelist_free(sidecars);
256                 file_data_increment_version(fd);
257                 file_data_send_notification(fd, NOTIFY_REREAD);
258                 file_data_unref(fd);
259                 }
260         else
261                 {
262                 ret |= file_data_check_changed_files_recursive(fd, &st);
263                 }
264
265         return ret;
266 }
267
268 /*
269  *-----------------------------------------------------------------------------
270  * file name, extension, sorting, ...
271  *-----------------------------------------------------------------------------
272  */
273
274 static void file_data_set_collate_keys(FileData *fd)
275 {
276         gchar *caseless_name;
277         gchar *valid_name;
278
279         valid_name = g_filename_display_name(fd->name);
280         caseless_name = g_utf8_casefold(valid_name, -1);
281
282         g_free(fd->collate_key_name);
283         g_free(fd->collate_key_name_nocase);
284
285         fd->collate_key_name_natural = g_utf8_collate_key_for_filename(fd->name, -1);
286         fd->collate_key_name_nocase_natural = g_utf8_collate_key_for_filename(caseless_name, -1);
287         fd->collate_key_name = g_utf8_collate_key(valid_name, -1);
288         fd->collate_key_name_nocase = g_utf8_collate_key(caseless_name, -1);
289
290         g_free(valid_name);
291         g_free(caseless_name);
292 }
293
294 static void file_data_set_path(FileData *fd, const gchar *path)
295 {
296         g_assert(path /* && *path*/); /* view_dir_tree uses FileData with zero length path */
297         g_assert(file_data_pool);
298
299         g_free(fd->path);
300
301         if (fd->original_path)
302                 {
303                 g_hash_table_remove(file_data_pool, fd->original_path);
304                 g_free(fd->original_path);
305                 }
306
307         g_assert(!g_hash_table_lookup(file_data_pool, path));
308
309         fd->original_path = g_strdup(path);
310         g_hash_table_insert(file_data_pool, fd->original_path, fd);
311
312         if (strcmp(path, G_DIR_SEPARATOR_S) == 0)
313                 {
314                 fd->path = g_strdup(path);
315                 fd->name = fd->path;
316                 fd->extension = fd->name + 1;
317                 file_data_set_collate_keys(fd);
318                 return;
319                 }
320
321         fd->path = g_strdup(path);
322         fd->name = filename_from_path(fd->path);
323
324         if (strcmp(fd->name, "..") == 0)
325                 {
326                 gchar *dir = remove_level_from_path(path);
327                 g_free(fd->path);
328                 fd->path = remove_level_from_path(dir);
329                 g_free(dir);
330                 fd->name = "..";
331                 fd->extension = fd->name + 2;
332                 file_data_set_collate_keys(fd);
333                 return;
334                 }
335         else if (strcmp(fd->name, ".") == 0)
336                 {
337                 g_free(fd->path);
338                 fd->path = remove_level_from_path(path);
339                 fd->name = ".";
340                 fd->extension = fd->name + 1;
341                 file_data_set_collate_keys(fd);
342                 return;
343                 }
344
345         fd->extension = registered_extension_from_path(fd->path);
346         if (fd->extension == nullptr)
347                 {
348                 fd->extension = fd->name + strlen(fd->name);
349                 }
350
351         fd->sidecar_priority = sidecar_file_priority(fd->extension);
352         file_data_set_collate_keys(fd);
353 }
354
355 /*
356  *-----------------------------------------------------------------------------
357  * create or reuse Filedata
358  *-----------------------------------------------------------------------------
359  */
360
361 static FileData *file_data_new(const gchar *path_utf8, struct stat *st, gboolean disable_sidecars)
362 {
363         FileData *fd;
364         struct passwd *user;
365         struct group *group;
366
367         DEBUG_2("file_data_new: '%s' %d", path_utf8, disable_sidecars);
368
369         if (S_ISDIR(st->st_mode)) disable_sidecars = TRUE;
370
371         if (!file_data_pool)
372                 file_data_pool = g_hash_table_new(g_str_hash, g_str_equal);
373
374         fd = static_cast<FileData *>(g_hash_table_lookup(file_data_pool, path_utf8));
375         if (fd)
376                 {
377                 file_data_ref(fd);
378                 }
379
380         if (!fd && file_data_planned_change_hash)
381                 {
382                 fd = static_cast<FileData *>(g_hash_table_lookup(file_data_planned_change_hash, path_utf8));
383                 if (fd)
384                         {
385                         DEBUG_1("planned change: using %s -> %s", path_utf8, fd->path);
386                         if (!isfile(fd->path))
387                                 {
388                                 file_data_ref(fd);
389                                 file_data_apply_ci(fd);
390                                 }
391                         else
392                                 {
393                                 fd = nullptr;
394                                 }
395                         }
396                 }
397
398         if (fd)
399                 {
400                 gboolean changed;
401
402                 if (disable_sidecars) file_data_disable_grouping(fd, TRUE);
403
404
405                 changed = file_data_check_changed_single_file(fd, st);
406
407                 DEBUG_2("file_data_pool hit: '%s' %s", fd->path, changed ? "(changed)" : "");
408
409                 return fd;
410                 }
411
412         fd = g_new0(FileData, 1);
413 #ifdef DEBUG_FILEDATA
414         global_file_data_count++;
415         DEBUG_2("file data count++: %d", global_file_data_count);
416 #endif
417
418         fd->size = st->st_size;
419         fd->date = st->st_mtime;
420         fd->cdate = st->st_ctime;
421         fd->mode = st->st_mode;
422         fd->ref = 1;
423         fd->magick = FD_MAGICK;
424         fd->exifdate = 0;
425         fd->rating = STAR_RATING_NOT_READ;
426         fd->format_class = filter_file_get_class(path_utf8);
427         fd->page_num = 0;
428         fd->page_total = 0;
429
430         user = getpwuid(st->st_uid);
431         if (!user)
432                 {
433                 fd->owner = g_strdup_printf("%u", st->st_uid);
434                 }
435         else
436                 {
437                 fd->owner = g_strdup(user->pw_name);
438                 }
439
440         group = getgrgid(st->st_gid);
441         if (!group)
442                 {
443                 fd->group = g_strdup_printf("%u", st->st_gid);
444                 }
445         else
446                 {
447                 fd->group = g_strdup(group->gr_name);
448                 }
449
450         fd->sym_link = get_symbolic_link(path_utf8);
451
452         if (disable_sidecars) fd->disable_grouping = TRUE;
453
454         file_data_set_path(fd, path_utf8); /* set path, name, collate_key_*, original_path */
455
456         return fd;
457 }
458
459 static FileData *file_data_new_local(const gchar *path, struct stat *st, gboolean disable_sidecars)
460 {
461         gchar *path_utf8 = path_to_utf8(path);
462         FileData *ret = file_data_new(path_utf8, st, disable_sidecars);
463
464         g_free(path_utf8);
465         return ret;
466 }
467
468 FileData *file_data_new_simple(const gchar *path_utf8)
469 {
470         struct stat st;
471         FileData *fd;
472
473         if (!stat_utf8(path_utf8, &st))
474                 {
475                 st.st_size = 0;
476                 st.st_mtime = 0;
477                 }
478
479         fd = static_cast<FileData *>(g_hash_table_lookup(file_data_pool, path_utf8));
480         if (!fd) fd = file_data_new(path_utf8, &st, TRUE);
481         if (fd)
482                 {
483                 file_data_ref(fd);
484                 }
485
486         return fd;
487 }
488
489 void read_exif_time_data(FileData *file)
490 {
491         if (file->exifdate > 0)
492                 {
493                 DEBUG_1("%s set_exif_time_data: Already exists for %s", get_exec_time(), file->path);
494                 return;
495                 }
496
497         if (!file->exif)
498                 {
499                 exif_read_fd(file);
500                 }
501
502         if (file->exif)
503                 {
504                 gchar *tmp = exif_get_data_as_text(file->exif, "Exif.Photo.DateTimeOriginal");
505                 DEBUG_2("%s set_exif_time_data: reading %p %s", get_exec_time(), (void *)file, file->path);
506
507                 if (tmp)
508                         {
509                         struct tm time_str;
510                         uint year, month, day, hour, min, sec;
511
512                         sscanf(tmp, "%4u:%2u:%2u %2u:%2u:%2u", &year, &month, &day, &hour, &min, &sec);
513                         time_str.tm_year  = year - 1900;
514                         time_str.tm_mon   = month - 1;
515                         time_str.tm_mday  = day;
516                         time_str.tm_hour  = hour;
517                         time_str.tm_min   = min;
518                         time_str.tm_sec   = sec;
519                         time_str.tm_isdst = 0;
520
521                         file->exifdate = mktime(&time_str);
522                         g_free(tmp);
523                         }
524                 }
525 }
526
527 void read_exif_time_digitized_data(FileData *file)
528 {
529         if (file->exifdate_digitized > 0)
530                 {
531                 DEBUG_1("%s set_exif_time_digitized_data: Already exists for %s", get_exec_time(), file->path);
532                 return;
533                 }
534
535         if (!file->exif)
536                 {
537                 exif_read_fd(file);
538                 }
539
540         if (file->exif)
541                 {
542                 gchar *tmp = exif_get_data_as_text(file->exif, "Exif.Photo.DateTimeDigitized");
543                 DEBUG_2("%s set_exif_time_digitized_data: reading %p %s", get_exec_time(), (void *)file, file->path);
544
545                 if (tmp)
546                         {
547                         struct tm time_str;
548                         uint year, month, day, hour, min, sec;
549
550                         sscanf(tmp, "%4u:%2u:%2u %2u:%2u:%2u", &year, &month, &day, &hour, &min, &sec);
551                         time_str.tm_year  = year - 1900;
552                         time_str.tm_mon   = month - 1;
553                         time_str.tm_mday  = day;
554                         time_str.tm_hour  = hour;
555                         time_str.tm_min   = min;
556                         time_str.tm_sec   = sec;
557                         time_str.tm_isdst = 0;
558
559                         file->exifdate_digitized = mktime(&time_str);
560                         g_free(tmp);
561                         }
562                 }
563 }
564
565 void read_rating_data(FileData *file)
566 {
567         gchar *rating_str;
568
569         rating_str = metadata_read_string(file, RATING_KEY, METADATA_PLAIN);
570         if (rating_str)
571                 {
572                 file->rating = atoi(rating_str);
573                 g_free(rating_str);
574                 }
575         else
576                 {
577                 file->rating = 0;
578                 }
579 }
580
581 #pragma GCC diagnostic push
582 #pragma GCC diagnostic ignored "-Wunused-function"
583 void set_exif_time_data_unused(GList *files)
584 {
585         DEBUG_1("%s set_exif_time_data: ...", get_exec_time());
586
587         while (files)
588                 {
589                 FileData *file = static_cast<FileData *>(files->data);
590
591                 read_exif_time_data(file);
592                 files = files->next;
593                 }
594 }
595
596 void set_exif_time_digitized_data_unused(GList *files)
597 {
598         DEBUG_1("%s set_exif_time_digitized_data: ...", get_exec_time());
599
600         while (files)
601                 {
602                 FileData *file = static_cast<FileData *>(files->data);
603
604                 read_exif_time_digitized_data(file);
605                 files = files->next;
606                 }
607 }
608
609 void set_rating_data_unused(GList *files)
610 {
611         gchar *rating_str;
612         DEBUG_1("%s set_rating_data: ...", get_exec_time());
613
614         while (files)
615                 {
616                 FileData *file = static_cast<FileData *>(files->data);
617                 rating_str = metadata_read_string(file, RATING_KEY, METADATA_PLAIN);
618                 if (rating_str )
619                         {
620                         file->rating = atoi(rating_str);
621                         g_free(rating_str);
622                         }
623                 files = files->next;
624                 }
625 }
626 #pragma GCC diagnostic pop
627
628 FileData *file_data_new_no_grouping(const gchar *path_utf8)
629 {
630         struct stat st;
631
632         if (!stat_utf8(path_utf8, &st))
633                 {
634                 st.st_size = 0;
635                 st.st_mtime = 0;
636                 }
637
638         return file_data_new(path_utf8, &st, TRUE);
639 }
640
641 FileData *file_data_new_dir(const gchar *path_utf8)
642 {
643         struct stat st;
644
645         if (!stat_utf8(path_utf8, &st))
646                 {
647                 st.st_size = 0;
648                 st.st_mtime = 0;
649                 }
650         else
651                 /* dir or non-existing yet */
652                 g_assert(S_ISDIR(st.st_mode));
653
654         return file_data_new(path_utf8, &st, TRUE);
655 }
656
657 /*
658  *-----------------------------------------------------------------------------
659  * reference counting
660  *-----------------------------------------------------------------------------
661  */
662
663 #ifdef DEBUG_FILEDATA
664 FileData *file_data_ref_debug(const gchar *file, gint line, FileData *fd)
665 #else
666 FileData *file_data_ref(FileData *fd)
667 #endif
668 {
669         if (fd == nullptr) return nullptr;
670         if (fd->magick != FD_MAGICK)
671 #ifdef DEBUG_FILEDATA
672                 log_printf("Error: fd magick mismatch @ %s:%d  fd=%p", file, line, (void *)fd);
673 #else
674                 log_printf("Error: fd magick mismatch fd=%p", fd);
675 #endif
676         g_assert(fd->magick == FD_MAGICK);
677         fd->ref++;
678
679 #ifdef DEBUG_FILEDATA
680         DEBUG_2("file_data_ref fd=%p (%d): '%s' @ %s:%d", (void *)fd, fd->ref, fd->path, file, line);
681 #else
682         DEBUG_2("file_data_ref fd=%p (%d): '%s'", fd, fd->ref, fd->path);
683 #endif
684         return fd;
685 }
686
687 /**
688  * @brief Print ref. count and image name
689  * @param
690  *
691  * Print image ref. count and full path name of all images in
692  * the file_data_pool.
693  *
694  * Used only by DEBUG_FD()
695  */
696 void file_data_dump()
697 {
698 #ifdef DEBUG_FILEDATA
699         FileData *fd;
700         GList *list;
701
702         if (file_data_pool)
703                 {
704                 list = g_hash_table_get_values(file_data_pool);
705
706                 log_printf("%d", global_file_data_count);
707                 log_printf("%d", g_list_length(list));
708
709                 GList *work = list;
710                 while (work)
711                         {
712                         fd = static_cast<FileData *>(work->data);
713                         log_printf("%-4d %s", fd->ref, fd->path);
714                         work = work->next;
715                         }
716
717                 g_list_free(list);
718                 }
719 #endif
720 }
721
722 static void file_data_free(FileData *fd)
723 {
724         g_assert(fd->magick == FD_MAGICK);
725         g_assert(fd->ref == 0);
726         g_assert(!fd->locked);
727
728 #ifdef DEBUG_FILEDATA
729         global_file_data_count--;
730         DEBUG_2("file data count--: %d", global_file_data_count);
731 #endif
732
733         metadata_cache_free(fd);
734         g_hash_table_remove(file_data_pool, fd->original_path);
735
736         g_free(fd->path);
737         g_free(fd->original_path);
738         g_free(fd->collate_key_name);
739         g_free(fd->collate_key_name_nocase);
740         g_free(fd->extended_extension);
741         if (fd->thumb_pixbuf) g_object_unref(fd->thumb_pixbuf);
742         histmap_free(fd->histmap);
743         g_free(fd->owner);
744         g_free(fd->group);
745         g_free(fd->sym_link);
746         g_free(fd->format_name);
747         g_assert(fd->sidecar_files == nullptr); /* sidecar files must be freed before calling this */
748
749         file_data_change_info_free(nullptr, fd);
750         g_free(fd);
751 }
752
753 /**
754  * @brief Checks if the FileData is referenced
755  *
756  * Checks the refcount and whether the FileData is locked.
757  */
758 static gboolean file_data_check_has_ref(FileData *fd)
759 {
760         return fd->ref > 0 || fd->locked;
761 }
762
763 /**
764  * @brief Consider freeing a FileData.
765  *
766  * This function will free a FileData and its children provided that neither its parent nor it has
767  * a positive refcount, and provided that neither is locked.
768  */
769 static void file_data_consider_free(FileData *fd)
770 {
771         GList *work;
772         FileData *parent = fd->parent ? fd->parent : fd;
773
774         g_assert(fd->magick == FD_MAGICK);
775         if (file_data_check_has_ref(fd)) return;
776         if (file_data_check_has_ref(parent)) return;
777
778         work = parent->sidecar_files;
779         while (work)
780                 {
781                 auto sfd = static_cast<FileData *>(work->data);
782                 if (file_data_check_has_ref(sfd)) return;
783                 work = work->next;
784                 }
785
786         /* Neither the parent nor the siblings are referenced, so we can free everything */
787         DEBUG_2("file_data_consider_free: deleting '%s', parent '%s'",
788                 fd->path, fd->parent ? parent->path : "-");
789
790         g_list_free_full(parent->sidecar_files, reinterpret_cast<GDestroyNotify>(file_data_free));
791         parent->sidecar_files = nullptr;
792
793         file_data_free(parent);
794 }
795
796 #ifdef DEBUG_FILEDATA
797 void file_data_unref_debug(const gchar *file, gint line, FileData *fd)
798 #else
799 void file_data_unref(FileData *fd)
800 #endif
801 {
802         if (fd == nullptr) return;
803         if (fd->magick != FD_MAGICK)
804 #ifdef DEBUG_FILEDATA
805                 log_printf("Error: fd magick mismatch @ %s:%d  fd=%p", file, line, (void *)fd);
806 #else
807                 log_printf("Error: fd magick mismatch fd=%p", fd);
808 #endif
809         g_assert(fd->magick == FD_MAGICK);
810
811         fd->ref--;
812 #ifdef DEBUG_FILEDATA
813         DEBUG_2("file_data_unref fd=%p (%d:%d): '%s' @ %s:%d", (void *)fd, fd->ref, fd->locked, fd->path,
814                 file, line);
815 #else
816         DEBUG_2("file_data_unref fd=%p (%d:%d): '%s'", fd, fd->ref, fd->locked, fd->path);
817 #endif
818
819         // Free FileData if it's no longer ref'd
820         file_data_consider_free(fd);
821 }
822
823 /**
824  * @brief Lock the FileData in memory.
825  *
826  * This allows the caller to prevent a FileData from being freed, even after its refcount is zero.
827  * This is intended to be used in cases where a FileData _should_ stay in memory as an optimization,
828  * even if the code would continue to function properly even if the FileData were freed.  Code that
829  * _requires_ the FileData to remain in memory should continue to use file_data_(un)ref.
830  * <p />
831  * Note: This differs from file_data_ref in that the behavior is reentrant -- after N calls to
832  * file_data_lock, a single call to file_data_unlock will unlock the FileData.
833  */
834 void file_data_lock(FileData *fd)
835 {
836         if (fd == nullptr) return;
837         if (fd->magick != FD_MAGICK) log_printf("Error: fd magick mismatch fd=%p", (void *)fd);
838
839         g_assert(fd->magick == FD_MAGICK);
840         fd->locked = TRUE;
841
842         DEBUG_2("file_data_ref fd=%p (%d): '%s'", (void *)fd, fd->ref, fd->path);
843 }
844
845 /**
846  * @brief Reset the maintain-FileData-in-memory lock
847  *
848  * This again allows the FileData to be freed when its refcount drops to zero.  Automatically frees
849  * the FileData if its refcount is already zero (which will happen if the lock is the only thing
850  * keeping it from being freed.
851  */
852 void file_data_unlock(FileData *fd)
853 {
854         if (fd == nullptr) return;
855         if (fd->magick != FD_MAGICK) log_printf("Error: fd magick mismatch fd=%p", (void *)fd);
856
857         g_assert(fd->magick == FD_MAGICK);
858         fd->locked = FALSE;
859
860         // Free FileData if it's no longer ref'd
861         file_data_consider_free(fd);
862 }
863
864 /**
865  * @brief Lock all of the FileDatas in the provided list
866  *
867  * @see file_data_lock(#FileData)
868  */
869 void file_data_lock_list(GList *list)
870 {
871         GList *work;
872
873         work = list;
874         while (work)
875                 {
876                 auto fd = static_cast<FileData *>(work->data);
877                 work = work->next;
878                 file_data_lock(fd);
879                 }
880 }
881
882 /**
883  * @brief Unlock all of the FileDatas in the provided list
884  *
885  * @see #file_data_unlock(#FileData)
886  */
887 void file_data_unlock_list(GList *list)
888 {
889         GList *work;
890
891         work = list;
892         while (work)
893                 {
894                 auto fd = static_cast<FileData *>(work->data);
895                 work = work->next;
896                 file_data_unlock(fd);
897                 }
898 }
899
900 /*
901  *-----------------------------------------------------------------------------
902  * sidecar file info struct
903  *-----------------------------------------------------------------------------
904  */
905
906 static gint file_data_sort_by_ext(gconstpointer a, gconstpointer b)
907 {
908         auto fda = static_cast<const FileData *>(a);
909         auto fdb = static_cast<const FileData *>(b);
910
911         if (fda->sidecar_priority < fdb->sidecar_priority) return -1;
912         if (fda->sidecar_priority > fdb->sidecar_priority) return 1;
913
914         return strcmp(fdb->extension, fda->extension);
915 }
916
917
918 static gint sidecar_file_priority(const gchar *extension)
919 {
920         gint i = 1;
921         GList *work;
922
923         if (extension == nullptr)
924                 return 0;
925
926         work = sidecar_ext_get_list();
927
928         while (work) {
929                 auto ext = static_cast<gchar *>(work->data);
930
931                 work = work->next;
932                 if (g_ascii_strcasecmp(extension, ext) == 0) return i;
933                 i++;
934         }
935         return 0;
936 }
937
938 static void file_data_check_sidecars(const GList *basename_list)
939 {
940         /* basename_list contains the new group - first is the parent, then sorted sidecars */
941         /* all files in the list have ref count > 0 */
942
943         const GList *work;
944         GList *s_work, *new_sidecars;
945         FileData *parent_fd;
946
947         if (!basename_list) return;
948
949
950         DEBUG_2("basename start");
951         work = basename_list;
952         while (work)
953                 {
954                 auto fd = static_cast<FileData *>(work->data);
955                 work = work->next;
956                 g_assert(fd->magick == FD_MAGICK);
957                 DEBUG_2("basename: %p %s", (void *)fd, fd->name);
958                 if (fd->parent)
959                         {
960                         g_assert(fd->parent->magick == FD_MAGICK);
961                         DEBUG_2("                  parent: %p", (void *)fd->parent);
962                         }
963                 s_work = fd->sidecar_files;
964                 while (s_work)
965                         {
966                         auto sfd = static_cast<FileData *>(s_work->data);
967                         s_work = s_work->next;
968                         g_assert(sfd->magick == FD_MAGICK);
969                         DEBUG_2("                  sidecar: %p %s", (void *)sfd, sfd->name);
970                         }
971
972                 g_assert(fd->parent == nullptr || fd->sidecar_files == nullptr);
973                 }
974
975         parent_fd = static_cast<FileData *>(basename_list->data);
976
977         /* check if the second and next entries of basename_list are already connected
978            as sidecars of the first entry (parent_fd) */
979         work = basename_list->next;
980         s_work = parent_fd->sidecar_files;
981
982         while (work && s_work)
983                 {
984                 if (work->data != s_work->data) break;
985                 work = work->next;
986                 s_work = s_work->next;
987                 }
988
989         if (!work && !s_work)
990                 {
991                 DEBUG_2("basename no change");
992                 return; /* no change in grouping */
993                 }
994
995         /* we have to regroup it */
996
997         /* first, disconnect everything and send notification*/
998
999         work = basename_list;
1000         while (work)
1001                 {
1002                 auto fd = static_cast<FileData *>(work->data);
1003                 work = work->next;
1004                 g_assert(fd->parent == nullptr || fd->sidecar_files == nullptr);
1005
1006                 if (fd->parent)
1007                         {
1008                         FileData *old_parent = fd->parent;
1009                         g_assert(old_parent->parent == nullptr || old_parent->sidecar_files == nullptr);
1010                         file_data_ref(old_parent);
1011                         file_data_disconnect_sidecar_file(old_parent, fd);
1012                         file_data_send_notification(old_parent, NOTIFY_REREAD);
1013                         file_data_unref(old_parent);
1014                         }
1015
1016                 while (fd->sidecar_files)
1017                         {
1018                         auto sfd = static_cast<FileData *>(fd->sidecar_files->data);
1019                         g_assert(sfd->parent == nullptr || sfd->sidecar_files == nullptr);
1020                         file_data_ref(sfd);
1021                         file_data_disconnect_sidecar_file(fd, sfd);
1022                         file_data_send_notification(sfd, NOTIFY_REREAD);
1023                         file_data_unref(sfd);
1024                         }
1025                 file_data_send_notification(fd, NOTIFY_GROUPING);
1026
1027                 g_assert(fd->parent == nullptr && fd->sidecar_files == nullptr);
1028                 }
1029
1030         /* now we can form the new group */
1031         work = basename_list->next;
1032         new_sidecars = nullptr;
1033         while (work)
1034                 {
1035                 auto sfd = static_cast<FileData *>(work->data);
1036                 g_assert(sfd->magick == FD_MAGICK);
1037                 g_assert(sfd->parent == nullptr && sfd->sidecar_files == nullptr);
1038                 sfd->parent = parent_fd;
1039                 new_sidecars = g_list_prepend(new_sidecars, sfd);
1040                 work = work->next;
1041                 }
1042         g_assert(parent_fd->sidecar_files == nullptr);
1043         parent_fd->sidecar_files = g_list_reverse(new_sidecars);
1044         DEBUG_1("basename group changed for %s", parent_fd->path);
1045 }
1046
1047
1048 static void file_data_disconnect_sidecar_file(FileData *target, FileData *sfd)
1049 {
1050         g_assert(target->magick == FD_MAGICK);
1051         g_assert(sfd->magick == FD_MAGICK);
1052         g_assert(g_list_find(target->sidecar_files, sfd));
1053
1054         file_data_ref(target);
1055         file_data_ref(sfd);
1056
1057         g_assert(sfd->parent == target);
1058
1059         file_data_increment_version(sfd); /* increments both sfd and target */
1060
1061         target->sidecar_files = g_list_remove(target->sidecar_files, sfd);
1062         sfd->parent = nullptr;
1063         g_free(sfd->extended_extension);
1064         sfd->extended_extension = nullptr;
1065
1066         file_data_unref(target);
1067         file_data_unref(sfd);
1068 }
1069
1070 /* disables / enables grouping for particular file, sends UPDATE notification */
1071 void file_data_disable_grouping(FileData *fd, gboolean disable)
1072 {
1073         if (!fd->disable_grouping == !disable) return;
1074
1075         fd->disable_grouping = !!disable;
1076
1077         if (disable)
1078                 {
1079                 if (fd->parent)
1080                         {
1081                         FileData *parent = file_data_ref(fd->parent);
1082                         file_data_disconnect_sidecar_file(parent, fd);
1083                         file_data_send_notification(parent, NOTIFY_GROUPING);
1084                         file_data_unref(parent);
1085                         }
1086                 else if (fd->sidecar_files)
1087                         {
1088                         GList *sidecar_files = filelist_copy(fd->sidecar_files);
1089                         GList *work = sidecar_files;
1090                         while (work)
1091                                 {
1092                                 auto sfd = static_cast<FileData *>(work->data);
1093                                 work = work->next;
1094                                 file_data_disconnect_sidecar_file(fd, sfd);
1095                                 file_data_send_notification(sfd, NOTIFY_GROUPING);
1096                                 }
1097                         file_data_check_sidecars(sidecar_files); /* this will group the sidecars back together */
1098                         filelist_free(sidecar_files);
1099                         }
1100                 else
1101                         {
1102                         file_data_increment_version(fd); /* the functions called in the cases above increments the version too */
1103                         }
1104                 }
1105         else
1106                 {
1107                 file_data_increment_version(fd);
1108                 /* file_data_check_sidecars call is not necessary - the file will be re-grouped on next dir read */
1109                 }
1110         file_data_send_notification(fd, NOTIFY_GROUPING);
1111 }
1112
1113 void file_data_disable_grouping_list(GList *fd_list, gboolean disable)
1114 {
1115         GList *work;
1116
1117         work = fd_list;
1118         while (work)
1119                 {
1120                 auto fd = static_cast<FileData *>(work->data);
1121
1122                 file_data_disable_grouping(fd, disable);
1123                 work = work->next;
1124                 }
1125 }
1126
1127
1128
1129 /*
1130  *-----------------------------------------------------------------------------
1131  * filelist sorting
1132  *-----------------------------------------------------------------------------
1133  */
1134
1135
1136 gint filelist_sort_compare_filedata(FileData *fa, FileData *fb)
1137 {
1138         gint ret;
1139         if (!filelist_sort_ascend)
1140                 {
1141                 FileData *tmp = fa;
1142                 fa = fb;
1143                 fb = tmp;
1144                 }
1145
1146         switch (filelist_sort_method)
1147                 {
1148                 case SORT_NAME:
1149                         break;
1150                 case SORT_SIZE:
1151                         if (fa->size < fb->size) return -1;
1152                         if (fa->size > fb->size) return 1;
1153                         /* fall back to name */
1154                         break;
1155                 case SORT_TIME:
1156                         if (fa->date < fb->date) return -1;
1157                         if (fa->date > fb->date) return 1;
1158                         /* fall back to name */
1159                         break;
1160                 case SORT_CTIME:
1161                         if (fa->cdate < fb->cdate) return -1;
1162                         if (fa->cdate > fb->cdate) return 1;
1163                         /* fall back to name */
1164                         break;
1165                 case SORT_EXIFTIME:
1166                         if (fa->exifdate < fb->exifdate) return -1;
1167                         if (fa->exifdate > fb->exifdate) return 1;
1168                         /* fall back to name */
1169                         break;
1170                 case SORT_EXIFTIMEDIGITIZED:
1171                         if (fa->exifdate_digitized < fb->exifdate_digitized) return -1;
1172                         if (fa->exifdate_digitized > fb->exifdate_digitized) return 1;
1173                         /* fall back to name */
1174                         break;
1175                 case SORT_RATING:
1176                         if (fa->rating < fb->rating) return -1;
1177                         if (fa->rating > fb->rating) return 1;
1178                         /* fall back to name */
1179                         break;
1180                 case SORT_CLASS:
1181                         if (fa->format_class < fb->format_class) return -1;
1182                         if (fa->format_class > fb->format_class) return 1;
1183                         /* fall back to name */
1184                         break;
1185                 case SORT_NUMBER:
1186                         ret = strcmp(fa->collate_key_name_natural, fb->collate_key_name_natural);
1187                         if (ret != 0) return ret;
1188                         /* fall back to name */
1189                         break;
1190                 default:
1191                         break;
1192                 }
1193
1194         if (filelist_sort_case)
1195                 ret = strcmp(fa->collate_key_name, fb->collate_key_name);
1196         else
1197                 ret = strcmp(fa->collate_key_name_nocase, fb->collate_key_name_nocase);
1198
1199         if (ret != 0) return ret;
1200
1201         /* do not return 0 unless the files are really the same
1202            file_data_pool ensures that original_path is unique
1203         */
1204         return strcmp(fa->original_path, fb->original_path);
1205 }
1206
1207 gint filelist_sort_compare_filedata_full(FileData *fa, FileData *fb, SortType method, gboolean ascend)
1208 {
1209         filelist_sort_method = method;
1210         filelist_sort_ascend = ascend;
1211         return filelist_sort_compare_filedata(fa, fb);
1212 }
1213
1214 static gint filelist_sort_file_cb(gpointer a, gpointer b)
1215 {
1216         return filelist_sort_compare_filedata(static_cast<FileData *>(a), static_cast<FileData *>(b));
1217 }
1218
1219 GList *filelist_sort_full(GList *list, SortType method, gboolean ascend, gboolean case_sensitive, GCompareFunc cb)
1220 {
1221         filelist_sort_method = method;
1222         filelist_sort_ascend = ascend;
1223         filelist_sort_case = case_sensitive;
1224         return g_list_sort(list, cb);
1225 }
1226
1227 GList *filelist_insert_sort_full(GList *list, gpointer data, SortType method, gboolean ascend, gboolean case_sensitive, GCompareFunc cb)
1228 {
1229         filelist_sort_method = method;
1230         filelist_sort_ascend = ascend;
1231         filelist_sort_case = case_sensitive;
1232         return g_list_insert_sorted(list, data, cb);
1233 }
1234
1235 GList *filelist_sort(GList *list, SortType method, gboolean ascend, gboolean case_sensitive)
1236 {
1237         return filelist_sort_full(list, method, ascend, case_sensitive, reinterpret_cast<GCompareFunc>(filelist_sort_file_cb));
1238 }
1239
1240 #pragma GCC diagnostic push
1241 #pragma GCC diagnostic ignored "-Wunused-function"
1242 GList *filelist_insert_sort_unused(GList *list, FileData *fd, SortType method, gboolean ascend)
1243 {
1244         return filelist_insert_sort_full(list, fd, method, ascend, ascend, (GCompareFunc) filelist_sort_file_cb);
1245 }
1246 #pragma GCC diagnostic pop
1247
1248 /*
1249  *-----------------------------------------------------------------------------
1250  * basename hash - grouping of sidecars in filelist
1251  *-----------------------------------------------------------------------------
1252  */
1253
1254
1255 static GHashTable *file_data_basename_hash_new()
1256 {
1257         return g_hash_table_new_full(g_str_hash, g_str_equal, g_free, nullptr);
1258 }
1259
1260 static GList * file_data_basename_hash_insert(GHashTable *basename_hash, FileData *fd)
1261 {
1262         GList *list;
1263         gchar *basename = g_strndup(fd->path, fd->extension - fd->path);
1264
1265         list = static_cast<GList *>(g_hash_table_lookup(basename_hash, basename));
1266
1267         if (!list)
1268                 {
1269                 DEBUG_1("TG: basename_hash not found for %s",fd->path);
1270                 const gchar *parent_extension = registered_extension_from_path(basename);
1271
1272                 if (parent_extension)
1273                         {
1274                         DEBUG_1("TG: parent extension %s",parent_extension);
1275                         gchar *parent_basename = g_strndup(basename, parent_extension - basename);
1276                         DEBUG_1("TG: parent basename %s",parent_basename);
1277                         auto parent_fd = static_cast<FileData *>(g_hash_table_lookup(file_data_pool, basename));
1278                         if (parent_fd)
1279                                 {
1280                                 DEBUG_1("TG: parent fd found");
1281                                 list = static_cast<GList *>(g_hash_table_lookup(basename_hash, parent_basename));
1282                                 if (!g_list_find(list, parent_fd))
1283                                         {
1284                                         DEBUG_1("TG: parent fd doesn't fit");
1285                                         g_free(parent_basename);
1286                                         list = nullptr;
1287                                         }
1288                                 else
1289                                         {
1290                                         g_free(basename);
1291                                         basename = parent_basename;
1292                                         fd->extended_extension = g_strconcat(parent_extension, fd->extension, NULL);
1293                                         }
1294                                 }
1295                         }
1296                 }
1297
1298         if (!g_list_find(list, fd))
1299                 {
1300                 list = g_list_insert_sorted(list, file_data_ref(fd), file_data_sort_by_ext);
1301                 g_hash_table_insert(basename_hash, basename, list);
1302                 }
1303         else
1304                 {
1305                 g_free(basename);
1306                 }
1307         return list;
1308 }
1309
1310 static void file_data_basename_hash_insert_cb(gpointer fd, gpointer basename_hash)
1311 {
1312         file_data_basename_hash_insert(static_cast<GHashTable *>(basename_hash), static_cast<FileData *>(fd));
1313 }
1314
1315 static void file_data_basename_hash_remove_list(gpointer, gpointer value, gpointer)
1316 {
1317         filelist_free(static_cast<GList *>(value));
1318 }
1319
1320 static void file_data_basename_hash_free(GHashTable *basename_hash)
1321 {
1322         g_hash_table_foreach(basename_hash, file_data_basename_hash_remove_list, nullptr);
1323         g_hash_table_destroy(basename_hash);
1324 }
1325
1326 /*
1327  *-----------------------------------------------------------------------------
1328  * handling sidecars in filelist
1329  *-----------------------------------------------------------------------------
1330  */
1331
1332 static GList *filelist_filter_out_sidecars(GList *flist)
1333 {
1334         GList *work = flist;
1335         GList *flist_filtered = nullptr;
1336
1337         while (work)
1338                 {
1339                 auto fd = static_cast<FileData *>(work->data);
1340
1341                 work = work->next;
1342                 if (fd->parent) /* remove fd's that are children */
1343                         file_data_unref(fd);
1344                 else
1345                         flist_filtered = g_list_prepend(flist_filtered, fd);
1346                 }
1347         g_list_free(flist);
1348
1349         return flist_filtered;
1350 }
1351
1352 static void file_data_basename_hash_to_sidecars(gpointer, gpointer value, gpointer)
1353 {
1354         auto basename_list = static_cast<GList *>(value);
1355         file_data_check_sidecars(basename_list);
1356 }
1357
1358
1359 static gboolean is_hidden_file(const gchar *name)
1360 {
1361         if (name[0] != '.') return FALSE;
1362         if (name[1] == '\0' || (name[1] == '.' && name[2] == '\0')) return FALSE;
1363         return TRUE;
1364 }
1365
1366 /*
1367  *-----------------------------------------------------------------------------
1368  * the main filelist function
1369  *-----------------------------------------------------------------------------
1370  */
1371
1372 static gboolean filelist_read_real(const gchar *dir_path, GList **files, GList **dirs, gboolean follow_symlinks)
1373 {
1374         DIR *dp;
1375         struct dirent *dir;
1376         gchar *pathl;
1377         GList *dlist = nullptr;
1378         GList *flist = nullptr;
1379         GList *xmp_files = nullptr;
1380         gint (*stat_func)(const gchar *path, struct stat *buf);
1381         GHashTable *basename_hash = nullptr;
1382
1383         g_assert(files || dirs);
1384
1385         if (files) *files = nullptr;
1386         if (dirs) *dirs = nullptr;
1387
1388         pathl = path_from_utf8(dir_path);
1389         if (!pathl) return FALSE;
1390
1391         dp = opendir(pathl);
1392         if (dp == nullptr)
1393                 {
1394                 g_free(pathl);
1395                 return FALSE;
1396                 }
1397
1398         if (files) basename_hash = file_data_basename_hash_new();
1399
1400         if (follow_symlinks)
1401                 stat_func = stat;
1402         else
1403                 stat_func = lstat;
1404
1405         while ((dir = readdir(dp)) != nullptr)
1406                 {
1407                 struct stat ent_sbuf;
1408                 const gchar *name = dir->d_name;
1409                 gchar *filepath;
1410
1411                 if (!options->file_filter.show_hidden_files && is_hidden_file(name))
1412                         continue;
1413
1414                 filepath = g_build_filename(pathl, name, NULL);
1415                 if (stat_func(filepath, &ent_sbuf) >= 0)
1416                         {
1417                         if (S_ISDIR(ent_sbuf.st_mode))
1418                                 {
1419                                 /* we ignore the .thumbnails dir for cleanliness */
1420                                 if (dirs &&
1421                                     (name[0] != '.' || (name[1] != '\0' && (name[1] != '.' || name[2] != '\0'))) &&
1422                                     strcmp(name, GQ_CACHE_LOCAL_THUMB) != 0 &&
1423                                     strcmp(name, GQ_CACHE_LOCAL_METADATA) != 0 &&
1424                                     strcmp(name, THUMB_FOLDER_LOCAL) != 0)
1425                                         {
1426                                         dlist = g_list_prepend(dlist, file_data_new_local(filepath, &ent_sbuf, TRUE));
1427                                         }
1428                                 }
1429                         else
1430                                 {
1431                                 if (files && filter_name_exists(name))
1432                                         {
1433                                         FileData *fd = file_data_new_local(filepath, &ent_sbuf, FALSE);
1434                                         flist = g_list_prepend(flist, fd);
1435                                         if (fd->sidecar_priority && !fd->disable_grouping)
1436                                                 {
1437                                                 if (strcmp(fd->extension, ".xmp") != 0)
1438                                                         file_data_basename_hash_insert(basename_hash, fd);
1439                                                 else
1440                                                         xmp_files = g_list_append(xmp_files, fd);
1441                                                 }
1442                                         }
1443                                 }
1444                         }
1445                 else
1446                         {
1447                         if (errno == EOVERFLOW)
1448                                 {
1449                                 log_printf("stat(): EOVERFLOW, skip '%s'", filepath);
1450                                 }
1451                         }
1452                 g_free(filepath);
1453                 }
1454
1455         closedir(dp);
1456
1457         g_free(pathl);
1458
1459         if (xmp_files)
1460                 {
1461                 g_list_foreach(xmp_files,file_data_basename_hash_insert_cb,basename_hash);
1462                 g_list_free(xmp_files);
1463                 }
1464
1465         if (dirs) *dirs = dlist;
1466
1467         if (files)
1468                 {
1469                 g_hash_table_foreach(basename_hash, file_data_basename_hash_to_sidecars, nullptr);
1470
1471                 *files = filelist_filter_out_sidecars(flist);
1472                 }
1473         if (basename_hash) file_data_basename_hash_free(basename_hash);
1474
1475         return TRUE;
1476 }
1477
1478 gboolean filelist_read(FileData *dir_fd, GList **files, GList **dirs)
1479 {
1480         return filelist_read_real(dir_fd->path, files, dirs, TRUE);
1481 }
1482
1483 gboolean filelist_read_lstat(FileData *dir_fd, GList **files, GList **dirs)
1484 {
1485         return filelist_read_real(dir_fd->path, files, dirs, FALSE);
1486 }
1487
1488 FileData *file_data_new_group(const gchar *path_utf8)
1489 {
1490         gchar *dir;
1491         struct stat st;
1492         FileData *fd;
1493         GList *files;
1494
1495         if (!file_data_pool)
1496                 {
1497                 file_data_pool = g_hash_table_new(g_str_hash, g_str_equal);
1498                 }
1499
1500         if (!stat_utf8(path_utf8, &st))
1501                 {
1502                 st.st_size = 0;
1503                 st.st_mtime = 0;
1504                 }
1505
1506         if (S_ISDIR(st.st_mode))
1507                 return file_data_new(path_utf8, &st, TRUE);
1508
1509         dir = remove_level_from_path(path_utf8);
1510
1511         filelist_read_real(dir, &files, nullptr, TRUE);
1512
1513         fd = static_cast<FileData *>(g_hash_table_lookup(file_data_pool, path_utf8));
1514         if (!fd) fd = file_data_new(path_utf8, &st, TRUE);
1515         if (fd)
1516                 {
1517                 file_data_ref(fd);
1518                 }
1519
1520         filelist_free(files);
1521         g_free(dir);
1522         return fd;
1523 }
1524
1525
1526 void filelist_free(GList *list)
1527 {
1528         GList *work;
1529
1530         work = list;
1531         while (work)
1532                 {
1533                 file_data_unref((FileData *)work->data);
1534                 work = work->next;
1535                 }
1536
1537         g_list_free(list);
1538 }
1539
1540
1541 GList *filelist_copy(GList *list)
1542 {
1543         GList *new_list = nullptr;
1544         GList *work;
1545
1546         work = list;
1547         while (work)
1548                 {
1549                 FileData *fd;
1550
1551                 fd = static_cast<FileData *>(work->data);
1552                 work = work->next;
1553
1554                 new_list = g_list_prepend(new_list, file_data_ref(fd));
1555                 }
1556
1557         return g_list_reverse(new_list);
1558 }
1559
1560 GList *filelist_from_path_list(GList *list)
1561 {
1562         GList *new_list = nullptr;
1563         GList *work;
1564
1565         work = list;
1566         while (work)
1567                 {
1568                 gchar *path;
1569
1570                 path = static_cast<gchar *>(work->data);
1571                 work = work->next;
1572
1573                 new_list = g_list_prepend(new_list, file_data_new_group(path));
1574                 }
1575
1576         return g_list_reverse(new_list);
1577 }
1578
1579 GList *filelist_to_path_list(GList *list)
1580 {
1581         GList *new_list = nullptr;
1582         GList *work;
1583
1584         work = list;
1585         while (work)
1586                 {
1587                 FileData *fd;
1588
1589                 fd = static_cast<FileData *>(work->data);
1590                 work = work->next;
1591
1592                 new_list = g_list_prepend(new_list, g_strdup(fd->path));
1593                 }
1594
1595         return g_list_reverse(new_list);
1596 }
1597
1598 GList *filelist_filter(GList *list, gboolean is_dir_list)
1599 {
1600         GList *work;
1601
1602         if (!is_dir_list && options->file_filter.disable && options->file_filter.show_hidden_files) return list;
1603
1604         work = list;
1605         while (work)
1606                 {
1607                 auto fd = static_cast<FileData *>(work->data);
1608                 const gchar *name = fd->name;
1609                 GList *link = work;
1610                 work = work->next;
1611
1612                 if ((!options->file_filter.show_hidden_files && is_hidden_file(name)) ||
1613                     (!is_dir_list && !filter_name_exists(name)) ||
1614                     (is_dir_list && name[0] == '.' && (strcmp(name, GQ_CACHE_LOCAL_THUMB) == 0 ||
1615                                                        strcmp(name, GQ_CACHE_LOCAL_METADATA) == 0)) )
1616                         {
1617                         list = g_list_remove_link(list, link);
1618                         file_data_unref(fd);
1619                         g_list_free(link);
1620                         }
1621                 }
1622
1623         return list;
1624 }
1625
1626 /*
1627  *-----------------------------------------------------------------------------
1628  * filelist recursive
1629  *-----------------------------------------------------------------------------
1630  */
1631
1632 static gint filelist_sort_path_cb(gconstpointer a, gconstpointer b)
1633 {
1634         return CASE_SORT(((FileData *)a)->path, ((FileData *)b)->path);
1635 }
1636
1637 GList *filelist_sort_path(GList *list)
1638 {
1639         return g_list_sort(list, filelist_sort_path_cb);
1640 }
1641
1642 static void filelist_recursive_append(GList **list, GList *dirs)
1643 {
1644         GList *work;
1645
1646         work = dirs;
1647         while (work)
1648                 {
1649                 auto fd = static_cast<FileData *>(work->data);
1650                 GList *f;
1651                 GList *d;
1652
1653                 if (filelist_read(fd, &f, &d))
1654                         {
1655                         f = filelist_filter(f, FALSE);
1656                         f = filelist_sort_path(f);
1657                         *list = g_list_concat(*list, f);
1658
1659                         d = filelist_filter(d, TRUE);
1660                         d = filelist_sort_path(d);
1661                         filelist_recursive_append(list, d);
1662                         filelist_free(d);
1663                         }
1664
1665                 work = work->next;
1666                 }
1667 }
1668
1669 static void filelist_recursive_append_full(GList **list, GList *dirs, SortType method, gboolean ascend, gboolean case_sensitive)
1670 {
1671         GList *work;
1672
1673         work = dirs;
1674         while (work)
1675                 {
1676                 auto fd = static_cast<FileData *>(work->data);
1677                 GList *f;
1678                 GList *d;
1679
1680                 if (filelist_read(fd, &f, &d))
1681                         {
1682                         f = filelist_filter(f, FALSE);
1683                         f = filelist_sort_full(f, method, ascend, case_sensitive, reinterpret_cast<GCompareFunc>(filelist_sort_file_cb));
1684                         *list = g_list_concat(*list, f);
1685
1686                         d = filelist_filter(d, TRUE);
1687                         d = filelist_sort_path(d);
1688                         filelist_recursive_append_full(list, d, method, ascend, case_sensitive);
1689                         filelist_free(d);
1690                         }
1691
1692                 work = work->next;
1693                 }
1694 }
1695
1696 GList *filelist_recursive(FileData *dir_fd)
1697 {
1698         GList *list;
1699         GList *d;
1700
1701         if (!filelist_read(dir_fd, &list, &d)) return nullptr;
1702         list = filelist_filter(list, FALSE);
1703         list = filelist_sort_path(list);
1704
1705         d = filelist_filter(d, TRUE);
1706         d = filelist_sort_path(d);
1707         filelist_recursive_append(&list, d);
1708         filelist_free(d);
1709
1710         return list;
1711 }
1712
1713 GList *filelist_recursive_full(FileData *dir_fd, SortType method, gboolean ascend, gboolean case_sensitive)
1714 {
1715         GList *list;
1716         GList *d;
1717
1718         if (!filelist_read(dir_fd, &list, &d)) return nullptr;
1719         list = filelist_filter(list, FALSE);
1720         list = filelist_sort_full(list, method, ascend, case_sensitive, reinterpret_cast<GCompareFunc>(filelist_sort_file_cb));
1721
1722         d = filelist_filter(d, TRUE);
1723         d = filelist_sort_path(d);
1724         filelist_recursive_append_full(&list, d, method, ascend, case_sensitive);
1725         filelist_free(d);
1726
1727         return list;
1728 }
1729
1730 /*
1731  *-----------------------------------------------------------------------------
1732  * file modification support
1733  *-----------------------------------------------------------------------------
1734  */
1735
1736
1737 void file_data_change_info_free(FileDataChangeInfo *fdci, FileData *fd)
1738 {
1739         if (!fdci && fd) fdci = fd->change;
1740
1741         if (!fdci) return;
1742
1743         g_free(fdci->source);
1744         g_free(fdci->dest);
1745
1746         g_free(fdci);
1747
1748         if (fd) fd->change = nullptr;
1749 }
1750
1751 static gboolean file_data_can_write_directly(FileData *fd)
1752 {
1753         return filter_name_is_writable(fd->extension);
1754 }
1755
1756 static gboolean file_data_can_write_sidecar(FileData *fd)
1757 {
1758         return filter_name_allow_sidecar(fd->extension) && !filter_name_is_writable(fd->extension);
1759 }
1760
1761 gchar *file_data_get_sidecar_path(FileData *fd, gboolean existing_only)
1762 {
1763         gchar *sidecar_path = nullptr;
1764         GList *work;
1765
1766         if (!file_data_can_write_sidecar(fd)) return nullptr;
1767
1768         work = fd->parent ? fd->parent->sidecar_files : fd->sidecar_files;
1769         gchar *extended_extension = g_strconcat(fd->parent ? fd->parent->extension : fd->extension, ".xmp", NULL);
1770         while (work)
1771                 {
1772                 auto sfd = static_cast<FileData *>(work->data);
1773                 work = work->next;
1774                 if (g_ascii_strcasecmp(sfd->extension, ".xmp") == 0 || g_ascii_strcasecmp(sfd->extension, extended_extension) == 0)
1775                         {
1776                         sidecar_path = g_strdup(sfd->path);
1777                         break;
1778                         }
1779                 }
1780         g_free(extended_extension);
1781
1782         if (!existing_only && !sidecar_path)
1783                 {
1784                 if (options->metadata.sidecar_extended_name)
1785                         sidecar_path = g_strconcat(fd->path, ".xmp", NULL);
1786                 else
1787                         {
1788                         gchar *base = g_strndup(fd->path, fd->extension - fd->path);
1789                         sidecar_path = g_strconcat(base, ".xmp", NULL);
1790                         g_free(base);
1791                         }
1792                 }
1793
1794         return sidecar_path;
1795 }
1796
1797 /*
1798  * marks and orientation
1799  */
1800
1801 static FileDataGetMarkFunc file_data_get_mark_func[FILEDATA_MARKS_SIZE];
1802 static FileDataSetMarkFunc file_data_set_mark_func[FILEDATA_MARKS_SIZE];
1803 static gpointer file_data_mark_func_data[FILEDATA_MARKS_SIZE];
1804 static GDestroyNotify file_data_destroy_mark_func[FILEDATA_MARKS_SIZE];
1805
1806 gboolean file_data_get_mark(FileData *fd, gint n)
1807 {
1808         gboolean valid = (fd->valid_marks & (1 << n));
1809
1810         if (file_data_get_mark_func[n] && !valid)
1811                 {
1812                 guint old = fd->marks;
1813                 gboolean value = (file_data_get_mark_func[n])(fd, n, file_data_mark_func_data[n]);
1814
1815                 if (!value != !(fd->marks & (1 << n)))
1816                         {
1817                         fd->marks = fd->marks ^ (1 << n);
1818                         }
1819
1820                 fd->valid_marks |= (1 << n);
1821                 if (old && !fd->marks) /* keep files with non-zero marks in memory */
1822                         {
1823                         file_data_unref(fd);
1824                         }
1825                 else if (!old && fd->marks)
1826                         {
1827                         file_data_ref(fd);
1828                         }
1829                 }
1830
1831         return !!(fd->marks & (1 << n));
1832 }
1833
1834 guint file_data_get_marks(FileData *fd)
1835 {
1836         gint i;
1837         for (i = 0; i < FILEDATA_MARKS_SIZE; i++) file_data_get_mark(fd, i);
1838         return fd->marks;
1839 }
1840
1841 void file_data_set_mark(FileData *fd, gint n, gboolean value)
1842 {
1843         guint old;
1844         if (!value == !file_data_get_mark(fd, n)) return;
1845
1846         if (file_data_set_mark_func[n])
1847                 {
1848                 (file_data_set_mark_func[n])(fd, n, value, file_data_mark_func_data[n]);
1849                 }
1850
1851         old = fd->marks;
1852
1853         fd->marks = fd->marks ^ (1 << n);
1854
1855         if (old && !fd->marks) /* keep files with non-zero marks in memory */
1856                 {
1857                 file_data_unref(fd);
1858                 }
1859         else if (!old && fd->marks)
1860                 {
1861                 file_data_ref(fd);
1862                 }
1863
1864         file_data_increment_version(fd);
1865         file_data_send_notification(fd, NOTIFY_MARKS);
1866 }
1867
1868 gboolean file_data_filter_marks(FileData *fd, guint filter)
1869 {
1870         gint i;
1871         for (i = 0; i < FILEDATA_MARKS_SIZE; i++) if (filter & (1 << i)) file_data_get_mark(fd, i);
1872         return ((fd->marks & filter) == filter);
1873 }
1874
1875 GList *file_data_filter_marks_list(GList *list, guint filter)
1876 {
1877         GList *work;
1878
1879         work = list;
1880         while (work)
1881                 {
1882                 auto fd = static_cast<FileData *>(work->data);
1883                 GList *link = work;
1884                 work = work->next;
1885
1886                 if (!file_data_filter_marks(fd, filter))
1887                         {
1888                         list = g_list_remove_link(list, link);
1889                         file_data_unref(fd);
1890                         g_list_free(link);
1891                         }
1892                 }
1893
1894         return list;
1895 }
1896
1897 gboolean file_data_filter_file_filter(FileData *fd, GRegex *filter)
1898 {
1899         return g_regex_match(filter, fd->name, static_cast<GRegexMatchFlags>(0), nullptr);
1900 }
1901
1902 GList *file_data_filter_file_filter_list(GList *list, GRegex *filter)
1903 {
1904         GList *work;
1905
1906         work = list;
1907         while (work)
1908                 {
1909                 auto fd = static_cast<FileData *>(work->data);
1910                 GList *link = work;
1911                 work = work->next;
1912
1913                 if (!file_data_filter_file_filter(fd, filter))
1914                         {
1915                         list = g_list_remove_link(list, link);
1916                         file_data_unref(fd);
1917                         g_list_free(link);
1918                         }
1919                 }
1920
1921         return list;
1922 }
1923
1924 static gboolean file_data_filter_class(FileData *fd, guint filter)
1925 {
1926         gint i;
1927
1928         for (i = 0; i < FILE_FORMAT_CLASSES; i++)
1929                 {
1930                 if (filter & (1 << i))
1931                         {
1932                         if (static_cast<FileFormatClass>(i) == filter_file_get_class(fd->path))
1933                                 {
1934                                 return TRUE;
1935                                 }
1936                         }
1937                 }
1938
1939         return FALSE;
1940 }
1941
1942 GList *file_data_filter_class_list(GList *list, guint filter)
1943 {
1944         GList *work;
1945
1946         work = list;
1947         while (work)
1948                 {
1949                 auto fd = static_cast<FileData *>(work->data);
1950                 GList *link = work;
1951                 work = work->next;
1952
1953                 if (!file_data_filter_class(fd, filter))
1954                         {
1955                         list = g_list_remove_link(list, link);
1956                         file_data_unref(fd);
1957                         g_list_free(link);
1958                         }
1959                 }
1960
1961         return list;
1962 }
1963
1964 static void file_data_notify_mark_func(gpointer, gpointer value, gpointer)
1965 {
1966         auto fd = static_cast<FileData *>(value);
1967         file_data_increment_version(fd);
1968         file_data_send_notification(fd, NOTIFY_MARKS);
1969 }
1970
1971 gboolean file_data_register_mark_func(gint n, FileDataGetMarkFunc get_mark_func, FileDataSetMarkFunc set_mark_func, gpointer data, GDestroyNotify notify)
1972 {
1973         if (n < 0 || n >= FILEDATA_MARKS_SIZE) return FALSE;
1974
1975         if (file_data_destroy_mark_func[n]) (file_data_destroy_mark_func[n])(file_data_mark_func_data[n]);
1976
1977         file_data_get_mark_func[n] = get_mark_func;
1978         file_data_set_mark_func[n] = set_mark_func;
1979         file_data_mark_func_data[n] = data;
1980         file_data_destroy_mark_func[n] = notify;
1981
1982         if (get_mark_func && file_data_pool)
1983                 {
1984                 /* this effectively changes all known files */
1985                 g_hash_table_foreach(file_data_pool, file_data_notify_mark_func, nullptr);
1986                 }
1987
1988         return TRUE;
1989 }
1990
1991 void file_data_get_registered_mark_func(gint n, FileDataGetMarkFunc *get_mark_func, FileDataSetMarkFunc *set_mark_func, gpointer *data)
1992 {
1993         if (get_mark_func) *get_mark_func = file_data_get_mark_func[n];
1994         if (set_mark_func) *set_mark_func = file_data_set_mark_func[n];
1995         if (data) *data = file_data_mark_func_data[n];
1996 }
1997
1998 #pragma GCC diagnostic push
1999 #pragma GCC diagnostic ignored "-Wunused-function"
2000 gint file_data_get_user_orientation_unused(FileData *fd)
2001 {
2002         return fd->user_orientation;
2003 }
2004
2005 void file_data_set_user_orientation_unused(FileData *fd, gint value)
2006 {
2007         if (fd->user_orientation == value) return;
2008
2009         fd->user_orientation = value;
2010         file_data_increment_version(fd);
2011         file_data_send_notification(fd, NOTIFY_ORIENTATION);
2012 }
2013 #pragma GCC diagnostic pop
2014
2015
2016 /*
2017  * file_data    - operates on the given fd
2018  * file_data_sc - operates on the given fd + sidecars - all fds linked via fd->sidecar_files or fd->parent
2019  */
2020
2021
2022 /* return list of sidecar file extensions in a string */
2023 gchar *file_data_sc_list_to_string(FileData *fd)
2024 {
2025         GList *work;
2026         GString *result = g_string_new("");
2027
2028         work = fd->sidecar_files;
2029         while (work)
2030                 {
2031                 auto sfd = static_cast<FileData *>(work->data);
2032
2033                 result = g_string_append(result, "+ ");
2034                 result = g_string_append(result, sfd->extension);
2035                 work = work->next;
2036                 if (work) result = g_string_append_c(result, ' ');
2037                 }
2038
2039         return g_string_free(result, FALSE);
2040 }
2041
2042
2043
2044 /*
2045  * add FileDataChangeInfo (see typedefs.h) for the given operation
2046  * uses file_data_add_change_info
2047  *
2048  * fails if the fd->change already exists - change operations can't run in parallel
2049  * fd->change_info works as a lock
2050  *
2051  * dest can be NULL - in this case the current name is used for now, it will
2052  * be changed later
2053  */
2054
2055 /*
2056    FileDataChangeInfo types:
2057    COPY
2058    MOVE   - path is changed, name may be changed too
2059    RENAME - path remains unchanged, name is changed
2060             extension should remain (FIXME should we allow editing extension? it will make problems with grouping)
2061             sidecar names are changed too, extensions are not changed
2062    DELETE
2063    UPDATE - file size, date or grouping has been changed
2064 */
2065
2066 gboolean file_data_add_ci(FileData *fd, FileDataChangeType type, const gchar *src, const gchar *dest)
2067 {
2068         FileDataChangeInfo *fdci;
2069
2070         if (fd->change) return FALSE;
2071
2072         fdci = g_new0(FileDataChangeInfo, 1);
2073
2074         fdci->type = type;
2075
2076         if (src)
2077                 fdci->source = g_strdup(src);
2078         else
2079                 fdci->source = g_strdup(fd->path);
2080
2081         if (dest)
2082                 fdci->dest = g_strdup(dest);
2083
2084         fd->change = fdci;
2085
2086         return TRUE;
2087 }
2088
2089 static void file_data_planned_change_remove(FileData *fd)
2090 {
2091         if (file_data_planned_change_hash &&
2092             (fd->change->type == FILEDATA_CHANGE_MOVE || fd->change->type == FILEDATA_CHANGE_RENAME))
2093                 {
2094                 if (g_hash_table_lookup(file_data_planned_change_hash, fd->change->dest) == fd)
2095                         {
2096                         DEBUG_1("planned change: removing %s -> %s", fd->change->dest, fd->path);
2097                         g_hash_table_remove(file_data_planned_change_hash, fd->change->dest);
2098                         file_data_unref(fd);
2099                         if (g_hash_table_size(file_data_planned_change_hash) == 0)
2100                                 {
2101                                 g_hash_table_destroy(file_data_planned_change_hash);
2102                                 file_data_planned_change_hash = nullptr;
2103                                 DEBUG_1("planned change: empty");
2104                                 }
2105                         }
2106                 }
2107 }
2108
2109
2110 void file_data_free_ci(FileData *fd)
2111 {
2112         FileDataChangeInfo *fdci = fd->change;
2113
2114         if (!fdci) return;
2115
2116         file_data_planned_change_remove(fd);
2117
2118         if (fdci->regroup_when_finished) file_data_disable_grouping(fd, FALSE);
2119
2120         g_free(fdci->source);
2121         g_free(fdci->dest);
2122
2123         g_free(fdci);
2124
2125         fd->change = nullptr;
2126 }
2127
2128 void file_data_set_regroup_when_finished(FileData *fd, gboolean enable)
2129 {
2130         FileDataChangeInfo *fdci = fd->change;
2131         if (!fdci) return;
2132         fdci->regroup_when_finished = enable;
2133 }
2134
2135 static gboolean file_data_sc_add_ci(FileData *fd, FileDataChangeType type)
2136 {
2137         GList *work;
2138
2139         if (fd->parent) fd = fd->parent;
2140
2141         if (fd->change) return FALSE;
2142
2143         work = fd->sidecar_files;
2144         while (work)
2145                 {
2146                 auto sfd = static_cast<FileData *>(work->data);
2147
2148                 if (sfd->change) return FALSE;
2149                 work = work->next;
2150                 }
2151
2152         file_data_add_ci(fd, type, nullptr, nullptr);
2153
2154         work = fd->sidecar_files;
2155         while (work)
2156                 {
2157                 auto sfd = static_cast<FileData *>(work->data);
2158
2159                 file_data_add_ci(sfd, type, nullptr, nullptr);
2160                 work = work->next;
2161                 }
2162
2163         return TRUE;
2164 }
2165
2166 static gboolean file_data_sc_check_ci(FileData *fd, FileDataChangeType type)
2167 {
2168         GList *work;
2169
2170         if (fd->parent) fd = fd->parent;
2171
2172         if (!fd->change || fd->change->type != type) return FALSE;
2173
2174         work = fd->sidecar_files;
2175         while (work)
2176                 {
2177                 auto sfd = static_cast<FileData *>(work->data);
2178
2179                 if (!sfd->change || sfd->change->type != type) return FALSE;
2180                 work = work->next;
2181                 }
2182
2183         return TRUE;
2184 }
2185
2186
2187 gboolean file_data_sc_add_ci_copy(FileData *fd, const gchar *dest_path)
2188 {
2189         if (!file_data_sc_add_ci(fd, FILEDATA_CHANGE_COPY)) return FALSE;
2190         file_data_sc_update_ci_copy(fd, dest_path);
2191         return TRUE;
2192 }
2193
2194 gboolean file_data_sc_add_ci_move(FileData *fd, const gchar *dest_path)
2195 {
2196         if (!file_data_sc_add_ci(fd, FILEDATA_CHANGE_MOVE)) return FALSE;
2197         file_data_sc_update_ci_move(fd, dest_path);
2198         return TRUE;
2199 }
2200
2201 gboolean file_data_sc_add_ci_rename(FileData *fd, const gchar *dest_path)
2202 {
2203         if (!file_data_sc_add_ci(fd, FILEDATA_CHANGE_RENAME)) return FALSE;
2204         file_data_sc_update_ci_rename(fd, dest_path);
2205         return TRUE;
2206 }
2207
2208 gboolean file_data_sc_add_ci_delete(FileData *fd)
2209 {
2210         return file_data_sc_add_ci(fd, FILEDATA_CHANGE_DELETE);
2211 }
2212
2213 gboolean file_data_sc_add_ci_unspecified(FileData *fd, const gchar *dest_path)
2214 {
2215         if (!file_data_sc_add_ci(fd, FILEDATA_CHANGE_UNSPECIFIED)) return FALSE;
2216         file_data_sc_update_ci_unspecified(fd, dest_path);
2217         return TRUE;
2218 }
2219
2220 gboolean file_data_add_ci_write_metadata(FileData *fd)
2221 {
2222         return file_data_add_ci(fd, FILEDATA_CHANGE_WRITE_METADATA, nullptr, nullptr);
2223 }
2224
2225 void file_data_sc_free_ci(FileData *fd)
2226 {
2227         GList *work;
2228
2229         if (fd->parent) fd = fd->parent;
2230
2231         file_data_free_ci(fd);
2232
2233         work = fd->sidecar_files;
2234         while (work)
2235                 {
2236                 auto sfd = static_cast<FileData *>(work->data);
2237
2238                 file_data_free_ci(sfd);
2239                 work = work->next;
2240                 }
2241 }
2242
2243 gboolean file_data_sc_add_ci_delete_list(GList *fd_list)
2244 {
2245         GList *work;
2246         gboolean ret = TRUE;
2247
2248         work = fd_list;
2249         while (work)
2250                 {
2251                 auto fd = static_cast<FileData *>(work->data);
2252
2253                 if (!file_data_sc_add_ci_delete(fd)) ret = FALSE;
2254                 work = work->next;
2255                 }
2256
2257         return ret;
2258 }
2259
2260 static void file_data_sc_revert_ci_list(GList *fd_list)
2261 {
2262         GList *work;
2263
2264         work = fd_list;
2265         while (work)
2266                 {
2267                 auto fd = static_cast<FileData *>(work->data);
2268
2269                 file_data_sc_free_ci(fd);
2270                 work = work->prev;
2271                 }
2272 }
2273
2274 static gboolean file_data_sc_add_ci_list_call_func(GList *fd_list, const gchar *dest, gboolean (*func)(FileData *, const gchar *))
2275 {
2276         GList *work;
2277
2278         work = fd_list;
2279         while (work)
2280                 {
2281                 auto fd = static_cast<FileData *>(work->data);
2282
2283                 if (!func(fd, dest))
2284                         {
2285                         file_data_sc_revert_ci_list(work->prev);
2286                         return FALSE;
2287                         }
2288                 work = work->next;
2289                 }
2290
2291         return TRUE;
2292 }
2293
2294 gboolean file_data_sc_add_ci_copy_list(GList *fd_list, const gchar *dest)
2295 {
2296         return file_data_sc_add_ci_list_call_func(fd_list, dest, file_data_sc_add_ci_copy);
2297 }
2298
2299 gboolean file_data_sc_add_ci_move_list(GList *fd_list, const gchar *dest)
2300 {
2301         return file_data_sc_add_ci_list_call_func(fd_list, dest, file_data_sc_add_ci_move);
2302 }
2303
2304 gboolean file_data_sc_add_ci_rename_list(GList *fd_list, const gchar *dest)
2305 {
2306         return file_data_sc_add_ci_list_call_func(fd_list, dest, file_data_sc_add_ci_rename);
2307 }
2308
2309 gboolean file_data_sc_add_ci_unspecified_list(GList *fd_list, const gchar *dest)
2310 {
2311         return file_data_sc_add_ci_list_call_func(fd_list, dest, file_data_sc_add_ci_unspecified);
2312 }
2313
2314 gboolean file_data_add_ci_write_metadata_list(GList *fd_list)
2315 {
2316         GList *work;
2317         gboolean ret = TRUE;
2318
2319         work = fd_list;
2320         while (work)
2321                 {
2322                 auto fd = static_cast<FileData *>(work->data);
2323
2324                 if (!file_data_add_ci_write_metadata(fd)) ret = FALSE;
2325                 work = work->next;
2326                 }
2327
2328         return ret;
2329 }
2330
2331 void file_data_free_ci_list(GList *fd_list)
2332 {
2333         GList *work;
2334
2335         work = fd_list;
2336         while (work)
2337                 {
2338                 auto fd = static_cast<FileData *>(work->data);
2339
2340                 file_data_free_ci(fd);
2341                 work = work->next;
2342                 }
2343 }
2344
2345 void file_data_sc_free_ci_list(GList *fd_list)
2346 {
2347         GList *work;
2348
2349         work = fd_list;
2350         while (work)
2351                 {
2352                 auto fd = static_cast<FileData *>(work->data);
2353
2354                 file_data_sc_free_ci(fd);
2355                 work = work->next;
2356                 }
2357 }
2358
2359 /*
2360  * update existing fd->change, it will be used from dialog callbacks for interactive editing
2361  * fails if fd->change does not exist or the change type does not match
2362  */
2363
2364 static void file_data_update_planned_change_hash(FileData *fd, const gchar *old_path, gchar *new_path)
2365 {
2366         FileDataChangeType type = fd->change->type;
2367
2368         if (type == FILEDATA_CHANGE_MOVE || type == FILEDATA_CHANGE_RENAME)
2369                 {
2370                 FileData *ofd;
2371
2372                 if (!file_data_planned_change_hash)
2373                         file_data_planned_change_hash = g_hash_table_new(g_str_hash, g_str_equal);
2374
2375                 if (old_path && g_hash_table_lookup(file_data_planned_change_hash, old_path) == fd)
2376                         {
2377                         DEBUG_1("planned change: removing %s -> %s", old_path, fd->path);
2378                         g_hash_table_remove(file_data_planned_change_hash, old_path);
2379                         file_data_unref(fd);
2380                         }
2381
2382                 ofd = static_cast<FileData *>(g_hash_table_lookup(file_data_planned_change_hash, new_path));
2383                 if (ofd != fd)
2384                         {
2385                         if (ofd)
2386                                 {
2387                                 DEBUG_1("planned change: replacing %s -> %s", new_path, ofd->path);
2388                                 g_hash_table_remove(file_data_planned_change_hash, new_path);
2389                                 file_data_unref(ofd);
2390                                 }
2391
2392                         DEBUG_1("planned change: inserting %s -> %s", new_path, fd->path);
2393                         file_data_ref(fd);
2394                         g_hash_table_insert(file_data_planned_change_hash, new_path, fd);
2395                         }
2396                 }
2397 }
2398
2399 static void file_data_update_ci_dest(FileData *fd, const gchar *dest_path)
2400 {
2401         gchar *old_path = fd->change->dest;
2402
2403         fd->change->dest = g_strdup(dest_path);
2404         file_data_update_planned_change_hash(fd, old_path, fd->change->dest);
2405         g_free(old_path);
2406 }
2407
2408 static void file_data_update_ci_dest_preserve_ext(FileData *fd, const gchar *dest_path)
2409 {
2410         const gchar *extension = registered_extension_from_path(fd->change->source);
2411         gchar *base = remove_extension_from_path(dest_path);
2412         gchar *old_path = fd->change->dest;
2413
2414         fd->change->dest = g_strconcat(base, fd->extended_extension ? fd->extended_extension : extension, NULL);
2415         file_data_update_planned_change_hash(fd, old_path, fd->change->dest);
2416
2417         g_free(old_path);
2418         g_free(base);
2419 }
2420
2421 static void file_data_sc_update_ci(FileData *fd, const gchar *dest_path)
2422 {
2423         GList *work;
2424         gchar *dest_path_full = nullptr;
2425
2426         if (fd->parent) fd = fd->parent;
2427
2428         if (!dest_path)
2429                 {
2430                 dest_path = fd->path;
2431                 }
2432         else if (!strchr(dest_path, G_DIR_SEPARATOR)) /* we got only filename, not a full path */
2433                 {
2434                 gchar *dir = remove_level_from_path(fd->path);
2435
2436                 dest_path_full = g_build_filename(dir, dest_path, NULL);
2437                 g_free(dir);
2438                 dest_path = dest_path_full;
2439                 }
2440         else if (fd->change->type != FILEDATA_CHANGE_RENAME && isdir(dest_path)) /* rename should not move files between directories */
2441                 {
2442                 dest_path_full = g_build_filename(dest_path, fd->name, NULL);
2443                 dest_path = dest_path_full;
2444                 }
2445
2446         file_data_update_ci_dest(fd, dest_path);
2447
2448         work = fd->sidecar_files;
2449         while (work)
2450                 {
2451                 auto sfd = static_cast<FileData *>(work->data);
2452
2453                 file_data_update_ci_dest_preserve_ext(sfd, dest_path);
2454                 work = work->next;
2455                 }
2456
2457         g_free(dest_path_full);
2458 }
2459
2460 static gboolean file_data_sc_check_update_ci(FileData *fd, const gchar *dest_path, FileDataChangeType type)
2461 {
2462         if (!file_data_sc_check_ci(fd, type)) return FALSE;
2463         file_data_sc_update_ci(fd, dest_path);
2464         return TRUE;
2465 }
2466
2467 gboolean file_data_sc_update_ci_copy(FileData *fd, const gchar *dest_path)
2468 {
2469         return file_data_sc_check_update_ci(fd, dest_path, FILEDATA_CHANGE_COPY);
2470 }
2471
2472 gboolean file_data_sc_update_ci_move(FileData *fd, const gchar *dest_path)
2473 {
2474         return file_data_sc_check_update_ci(fd, dest_path, FILEDATA_CHANGE_MOVE);
2475 }
2476
2477 gboolean file_data_sc_update_ci_rename(FileData *fd, const gchar *dest_path)
2478 {
2479         return file_data_sc_check_update_ci(fd, dest_path, FILEDATA_CHANGE_RENAME);
2480 }
2481
2482 gboolean file_data_sc_update_ci_unspecified(FileData *fd, const gchar *dest_path)
2483 {
2484         return file_data_sc_check_update_ci(fd, dest_path, FILEDATA_CHANGE_UNSPECIFIED);
2485 }
2486
2487 static gboolean file_data_sc_update_ci_list_call_func(GList *fd_list,
2488                                                       const gchar *dest,
2489                                                       gboolean (*func)(FileData *, const gchar *))
2490 {
2491         GList *work;
2492         gboolean ret = TRUE;
2493
2494         work = fd_list;
2495         while (work)
2496                 {
2497                 auto fd = static_cast<FileData *>(work->data);
2498
2499                 if (!func(fd, dest)) ret = FALSE;
2500                 work = work->next;
2501                 }
2502
2503         return ret;
2504 }
2505
2506 gboolean file_data_sc_update_ci_move_list(GList *fd_list, const gchar *dest)
2507 {
2508         return file_data_sc_update_ci_list_call_func(fd_list, dest, file_data_sc_update_ci_move);
2509 }
2510
2511 gboolean file_data_sc_update_ci_copy_list(GList *fd_list, const gchar *dest)
2512 {
2513         return file_data_sc_update_ci_list_call_func(fd_list, dest, file_data_sc_update_ci_copy);
2514 }
2515
2516 gboolean file_data_sc_update_ci_unspecified_list(GList *fd_list, const gchar *dest)
2517 {
2518         return file_data_sc_update_ci_list_call_func(fd_list, dest, file_data_sc_update_ci_unspecified);
2519 }
2520
2521
2522 /*
2523  * verify source and dest paths - dest image exists, etc.
2524  * it should detect all possible problems with the planned operation
2525  */
2526
2527 gint file_data_verify_ci(FileData *fd, GList *list)
2528 {
2529         gint ret = CHANGE_OK;
2530         gchar *dir;
2531         GList *work = nullptr;
2532         FileData *fd1 = nullptr;
2533
2534         if (!fd->change)
2535                 {
2536                 DEBUG_1("Change checked: no change info: %s", fd->path);
2537                 return ret;
2538                 }
2539
2540         if (!isname(fd->path))
2541                 {
2542                 /* this probably should not happen */
2543                 ret |= CHANGE_NO_SRC;
2544                 DEBUG_1("Change checked: file does not exist: %s", fd->path);
2545                 return ret;
2546                 }
2547
2548         dir = remove_level_from_path(fd->path);
2549
2550         if (fd->change->type != FILEDATA_CHANGE_DELETE &&
2551             fd->change->type != FILEDATA_CHANGE_MOVE && /* the unsaved metadata should survive move and rename operations */
2552             fd->change->type != FILEDATA_CHANGE_RENAME &&
2553             fd->change->type != FILEDATA_CHANGE_WRITE_METADATA &&
2554             fd->modified_xmp)
2555                 {
2556                 ret |= CHANGE_WARN_UNSAVED_META;
2557                 DEBUG_1("Change checked: unsaved metadata: %s", fd->path);
2558                 }
2559
2560         if (fd->change->type != FILEDATA_CHANGE_DELETE &&
2561             fd->change->type != FILEDATA_CHANGE_WRITE_METADATA &&
2562             !access_file(fd->path, R_OK))
2563                 {
2564                 ret |= CHANGE_NO_READ_PERM;
2565                 DEBUG_1("Change checked: no read permission: %s", fd->path);
2566                 }
2567         else if ((fd->change->type == FILEDATA_CHANGE_DELETE || fd->change->type == FILEDATA_CHANGE_MOVE) &&
2568                  !access_file(dir, W_OK))
2569                 {
2570                 ret |= CHANGE_NO_WRITE_PERM_DIR;
2571                 DEBUG_1("Change checked: source dir is readonly: %s", fd->path);
2572                 }
2573         else if (fd->change->type != FILEDATA_CHANGE_COPY &&
2574                  fd->change->type != FILEDATA_CHANGE_UNSPECIFIED &&
2575                  fd->change->type != FILEDATA_CHANGE_WRITE_METADATA &&
2576                  !access_file(fd->path, W_OK))
2577                 {
2578                 ret |= CHANGE_WARN_NO_WRITE_PERM;
2579                 DEBUG_1("Change checked: no write permission: %s", fd->path);
2580                 }
2581         /* WRITE_METADATA is special because it can be configured to silently write to ~/.geeqie/...
2582            - that means that there are no hard errors and warnings can be disabled
2583            - the destination is determined during the check
2584         */
2585         else if (fd->change->type == FILEDATA_CHANGE_WRITE_METADATA)
2586                 {
2587                 /* determine destination file */
2588                 gboolean have_dest = FALSE;
2589                 gchar *dest_dir = nullptr;
2590
2591                 if (options->metadata.save_in_image_file)
2592                         {
2593                         if (file_data_can_write_directly(fd))
2594                                 {
2595                                 /* we can write the file directly */
2596                                 if (access_file(fd->path, W_OK))
2597                                         {
2598                                         have_dest = TRUE;
2599                                         }
2600                                 else
2601                                         {
2602                                         if (options->metadata.warn_on_write_problems)
2603                                                 {
2604                                                 ret |= CHANGE_WARN_NO_WRITE_PERM;
2605                                                 DEBUG_1("Change checked: file is not writable: %s", fd->path);
2606                                                 }
2607                                         }
2608                                 }
2609                         else if (file_data_can_write_sidecar(fd))
2610                                 {
2611                                 /* we can write sidecar */
2612                                 gchar *sidecar = file_data_get_sidecar_path(fd, FALSE);
2613                                 if (access_file(sidecar, W_OK) || (!isname(sidecar) && access_file(dir, W_OK)))
2614                                         {
2615                                         file_data_update_ci_dest(fd, sidecar);
2616                                         have_dest = TRUE;
2617                                         }
2618                                 else
2619                                         {
2620                                         if (options->metadata.warn_on_write_problems)
2621                                                 {
2622                                                 ret |= CHANGE_WARN_NO_WRITE_PERM;
2623                                                 DEBUG_1("Change checked: file is not writable: %s", sidecar);
2624                                                 }
2625                                         }
2626                                 g_free(sidecar);
2627                                 }
2628                         }
2629
2630                 if (!have_dest)
2631                         {
2632                         /* write private metadata file under ~/.geeqie */
2633
2634                         /* If an existing metadata file exists, we will try writing to
2635                          * it's location regardless of the user's preference.
2636                          */
2637                         gchar *metadata_path = nullptr;
2638 #ifdef HAVE_EXIV2
2639                         /* but ignore XMP if we are not able to write it */
2640                         metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
2641 #endif
2642                         if (!metadata_path) metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
2643
2644                         if (metadata_path && !access_file(metadata_path, W_OK))
2645                                 {
2646                                 g_free(metadata_path);
2647                                 metadata_path = nullptr;
2648                                 }
2649
2650                         if (!metadata_path)
2651                                 {
2652                                 mode_t mode = 0755;
2653
2654                                 dest_dir = cache_get_location(CACHE_TYPE_METADATA, fd->path, FALSE, &mode);
2655                                 if (recursive_mkdir_if_not_exists(dest_dir, mode))
2656                                         {
2657                                         gchar *filename = g_strconcat(fd->name, options->metadata.save_legacy_format ? GQ_CACHE_EXT_METADATA : GQ_CACHE_EXT_XMP_METADATA, NULL);
2658
2659                                         metadata_path = g_build_filename(dest_dir, filename, NULL);
2660                                         g_free(filename);
2661                                         }
2662                                 }
2663                         if (access_file(metadata_path, W_OK) || (!isname(metadata_path) && access_file(dest_dir, W_OK)))
2664                                 {
2665                                 file_data_update_ci_dest(fd, metadata_path);
2666                                 have_dest = TRUE;
2667                                 }
2668                         else
2669                                 {
2670                                 ret |= CHANGE_NO_WRITE_PERM_DEST;
2671                                 DEBUG_1("Change checked: file is not writable: %s", metadata_path);
2672                                 }
2673                         g_free(metadata_path);
2674                         }
2675                 g_free(dest_dir);
2676                 }
2677
2678         if (fd->change->dest && fd->change->type != FILEDATA_CHANGE_WRITE_METADATA)
2679                 {
2680                 gboolean same;
2681                 gchar *dest_dir;
2682
2683                 same = (strcmp(fd->path, fd->change->dest) == 0);
2684
2685                 if (!same)
2686                         {
2687                         const gchar *dest_ext = registered_extension_from_path(fd->change->dest);
2688                         if (!dest_ext) dest_ext = "";
2689                         if (!options->file_filter.disable_file_extension_checks)
2690                                 {
2691                                 if (g_ascii_strcasecmp(fd->extension, dest_ext) != 0)
2692                                         {
2693                                         ret |= CHANGE_WARN_CHANGED_EXT;
2694                                         DEBUG_1("Change checked: source and destination have different extensions: %s -> %s", fd->path, fd->change->dest);
2695                                         }
2696                                 }
2697                         }
2698                 else
2699                         {
2700                         if (fd->change->type != FILEDATA_CHANGE_UNSPECIFIED) /** @FIXME this is now needed for running editors */
2701                                 {
2702                                 ret |= CHANGE_WARN_SAME;
2703                                 DEBUG_1("Change checked: source and destination are the same: %s -> %s", fd->path, fd->change->dest);
2704                                 }
2705                         }
2706
2707                 dest_dir = remove_level_from_path(fd->change->dest);
2708
2709                 if (!isdir(dest_dir))
2710                         {
2711                         ret |= CHANGE_NO_DEST_DIR;
2712                         DEBUG_1("Change checked: destination dir does not exist: %s -> %s", fd->path, fd->change->dest);
2713                         }
2714                 else if (!access_file(dest_dir, W_OK))
2715                         {
2716                         ret |= CHANGE_WARN_NO_WRITE_PERM_DEST_DIR;
2717                         DEBUG_1("Change checked: destination dir is readonly: %s -> %s", fd->path, fd->change->dest);
2718                         }
2719                 else if (!same)
2720                         {
2721                         if (isfile(fd->change->dest))
2722                                 {
2723                                 if (!access_file(fd->change->dest, W_OK))
2724                                         {
2725                                         ret |= CHANGE_NO_WRITE_PERM_DEST;
2726                                         DEBUG_1("Change checked: destination file exists and is readonly: %s -> %s", fd->path, fd->change->dest);
2727                                         }
2728                                 else
2729                                         {
2730                                         ret |= CHANGE_WARN_DEST_EXISTS;
2731                                         DEBUG_1("Change checked: destination exists: %s -> %s", fd->path, fd->change->dest);
2732                                         }
2733                                 }
2734                         else if (isdir(fd->change->dest))
2735                                 {
2736                                 ret |= CHANGE_DEST_EXISTS;
2737                                 DEBUG_1("Change checked: destination exists: %s -> %s", fd->path, fd->change->dest);
2738                                 }
2739                         }
2740
2741                 g_free(dest_dir);
2742                 }
2743
2744         /* During a rename operation, check if another planned destination file has
2745          * the same filename
2746          */
2747         if(fd->change->type == FILEDATA_CHANGE_RENAME ||
2748                                 fd->change->type == FILEDATA_CHANGE_COPY ||
2749                                 fd->change->type == FILEDATA_CHANGE_MOVE)
2750                 {
2751                 work = list;
2752                 while (work)
2753                         {
2754                         fd1 = static_cast<FileData *>(work->data);
2755                         work = work->next;
2756                         if (fd1 != nullptr && fd != fd1 )
2757                                 {
2758                                 if (!strcmp(fd->change->dest, fd1->change->dest))
2759                                         {
2760                                         ret |= CHANGE_DUPLICATE_DEST;
2761                                         }
2762                                 }
2763                         }
2764                 }
2765
2766         fd->change->error = ret;
2767         if (ret == 0) DEBUG_1("Change checked: OK: %s", fd->path);
2768
2769         g_free(dir);
2770         return ret;
2771 }
2772
2773
2774 gint file_data_sc_verify_ci(FileData *fd, GList *list)
2775 {
2776         GList *work;
2777         gint ret;
2778
2779         ret = file_data_verify_ci(fd, list);
2780
2781         work = fd->sidecar_files;
2782         while (work)
2783                 {
2784                 auto sfd = static_cast<FileData *>(work->data);
2785
2786                 ret |= file_data_verify_ci(sfd, list);
2787                 work = work->next;
2788                 }
2789
2790         return ret;
2791 }
2792
2793 gchar *file_data_get_error_string(gint error)
2794 {
2795         GString *result = g_string_new("");
2796
2797         if (error & CHANGE_NO_SRC)
2798                 {
2799                 if (result->len > 0) g_string_append(result, ", ");
2800                 g_string_append(result, _("file or directory does not exist"));
2801                 }
2802
2803         if (error & CHANGE_DEST_EXISTS)
2804                 {
2805                 if (result->len > 0) g_string_append(result, ", ");
2806                 g_string_append(result, _("destination already exists"));
2807                 }
2808
2809         if (error & CHANGE_NO_WRITE_PERM_DEST)
2810                 {
2811                 if (result->len > 0) g_string_append(result, ", ");
2812                 g_string_append(result, _("destination can't be overwritten"));
2813                 }
2814
2815         if (error & CHANGE_WARN_NO_WRITE_PERM_DEST_DIR)
2816                 {
2817                 if (result->len > 0) g_string_append(result, ", ");
2818                 g_string_append(result, _("destination directory is not writable"));
2819                 }
2820
2821         if (error & CHANGE_NO_DEST_DIR)
2822                 {
2823                 if (result->len > 0) g_string_append(result, ", ");
2824                 g_string_append(result, _("destination directory does not exist"));
2825                 }
2826
2827         if (error & CHANGE_NO_WRITE_PERM_DIR)
2828                 {
2829                 if (result->len > 0) g_string_append(result, ", ");
2830                 g_string_append(result, _("source directory is not writable"));
2831                 }
2832
2833         if (error & CHANGE_NO_READ_PERM)
2834                 {
2835                 if (result->len > 0) g_string_append(result, ", ");
2836                 g_string_append(result, _("no read permission"));
2837                 }
2838
2839         if (error & CHANGE_WARN_NO_WRITE_PERM)
2840                 {
2841                 if (result->len > 0) g_string_append(result, ", ");
2842                 g_string_append(result, _("file is readonly"));
2843                 }
2844
2845         if (error & CHANGE_WARN_DEST_EXISTS)
2846                 {
2847                 if (result->len > 0) g_string_append(result, ", ");
2848                 g_string_append(result, _("destination already exists and will be overwritten"));
2849                 }
2850
2851         if (error & CHANGE_WARN_SAME)
2852                 {
2853                 if (result->len > 0) g_string_append(result, ", ");
2854                 g_string_append(result, _("source and destination are the same"));
2855                 }
2856
2857         if (error & CHANGE_WARN_CHANGED_EXT)
2858                 {
2859                 if (result->len > 0) g_string_append(result, ", ");
2860                 g_string_append(result, _("source and destination have different extension"));
2861                 }
2862
2863         if (error & CHANGE_WARN_UNSAVED_META)
2864                 {
2865                 if (result->len > 0) g_string_append(result, ", ");
2866                 g_string_append(result, _("there are unsaved metadata changes for the file"));
2867                 }
2868
2869         if (error & CHANGE_DUPLICATE_DEST)
2870                 {
2871                 if (result->len > 0) g_string_append(result, ", ");
2872                 g_string_append(result, _("another destination file has the same filename"));
2873                 }
2874
2875         return g_string_free(result, FALSE);
2876 }
2877
2878 gint file_data_verify_ci_list(GList *list, gchar **desc, gboolean with_sidecars)
2879 {
2880         GList *work;
2881         gint all_errors = 0;
2882         gint common_errors = ~0;
2883         gint num;
2884         gint *errors;
2885         gint i;
2886
2887         if (!list) return 0;
2888
2889         num = g_list_length(list);
2890         errors = g_new(int, num);
2891         work = list;
2892         i = 0;
2893         while (work)
2894                 {
2895                 FileData *fd;
2896                 gint error;
2897
2898                 fd = static_cast<FileData *>(work->data);
2899                 work = work->next;
2900
2901                 error = with_sidecars ? file_data_sc_verify_ci(fd, list) : file_data_verify_ci(fd, list);
2902                 all_errors |= error;
2903                 common_errors &= error;
2904
2905                 errors[i] = error;
2906
2907                 i++;
2908                 }
2909
2910         if (desc && all_errors)
2911                 {
2912                 GList *work;
2913                 GString *result = g_string_new("");
2914
2915                 if (common_errors)
2916                         {
2917                         gchar *str = file_data_get_error_string(common_errors);
2918                         g_string_append(result, str);
2919                         g_string_append(result, "\n");
2920                         g_free(str);
2921                         }
2922
2923                 work = list;
2924                 i = 0;
2925                 while (work)
2926                         {
2927                         FileData *fd;
2928                         gint error;
2929
2930                         fd = static_cast<FileData *>(work->data);
2931                         work = work->next;
2932
2933                         error = errors[i] & ~common_errors;
2934
2935                         if (error)
2936                                 {
2937                                 gchar *str = file_data_get_error_string(error);
2938                                 g_string_append_printf(result, "%s: %s\n", fd->name, str);
2939                                 g_free(str);
2940                                 }
2941                         i++;
2942                         }
2943                 *desc = g_string_free(result, FALSE);
2944                 }
2945
2946         g_free(errors);
2947         return all_errors;
2948 }
2949
2950
2951 /*
2952  * perform the change described by FileFataChangeInfo
2953  * it is used for internal operations,
2954  * this function actually operates with files on the filesystem
2955  * it should implement safe delete
2956  */
2957
2958 static gboolean file_data_perform_move(FileData *fd)
2959 {
2960         g_assert(!strcmp(fd->change->source, fd->path));
2961         return move_file(fd->change->source, fd->change->dest);
2962 }
2963
2964 static gboolean file_data_perform_copy(FileData *fd)
2965 {
2966         g_assert(!strcmp(fd->change->source, fd->path));
2967         return copy_file(fd->change->source, fd->change->dest);
2968 }
2969
2970 static gboolean file_data_perform_delete(FileData *fd)
2971 {
2972         if (isdir(fd->path) && !islink(fd->path))
2973                 return rmdir_utf8(fd->path);
2974         else
2975                 if (options->file_ops.safe_delete_enable)
2976                         return file_util_safe_unlink(fd->path);
2977                 else
2978                         return unlink_file(fd->path);
2979 }
2980
2981 gboolean file_data_perform_ci(FileData *fd)
2982 {
2983         /** @FIXME When a directory that is a symbolic link is deleted,
2984          * at this point fd->change is null because no FileDataChangeInfo
2985          * has been set up. Therefore there is a seg. fault.
2986          * This code simply aborts the delete.
2987          */
2988         if (!fd->change)
2989                 {
2990                 return FALSE;
2991                 }
2992
2993         FileDataChangeType type = fd->change->type;
2994
2995         switch (type)
2996                 {
2997                 case FILEDATA_CHANGE_MOVE:
2998                         return file_data_perform_move(fd);
2999                 case FILEDATA_CHANGE_COPY:
3000                         return file_data_perform_copy(fd);
3001                 case FILEDATA_CHANGE_RENAME:
3002                         return file_data_perform_move(fd); /* the same as move */
3003                 case FILEDATA_CHANGE_DELETE:
3004                         return file_data_perform_delete(fd);
3005                 case FILEDATA_CHANGE_WRITE_METADATA:
3006                         return metadata_write_perform(fd);
3007                 case FILEDATA_CHANGE_UNSPECIFIED:
3008                         /* nothing to do here */
3009                         break;
3010                 }
3011         return TRUE;
3012 }
3013
3014
3015
3016 gboolean file_data_sc_perform_ci(FileData *fd)
3017 {
3018         GList *work;
3019         gboolean ret = TRUE;
3020         FileDataChangeType type = fd->change->type;
3021
3022         if (!file_data_sc_check_ci(fd, type)) return FALSE;
3023
3024         work = fd->sidecar_files;
3025         while (work)
3026                 {
3027                 auto sfd = static_cast<FileData *>(work->data);
3028
3029                 if (!file_data_perform_ci(sfd)) ret = FALSE;
3030                 work = work->next;
3031                 }
3032
3033         if (!file_data_perform_ci(fd)) ret = FALSE;
3034
3035         return ret;
3036 }
3037
3038 /*
3039  * updates FileData structure according to FileDataChangeInfo
3040  */
3041
3042 gboolean file_data_apply_ci(FileData *fd)
3043 {
3044         FileDataChangeType type = fd->change->type;
3045
3046         /** @FIXME delete ?*/
3047         if (type == FILEDATA_CHANGE_MOVE || type == FILEDATA_CHANGE_RENAME)
3048                 {
3049                 DEBUG_1("planned change: applying %s -> %s", fd->change->dest, fd->path);
3050                 file_data_planned_change_remove(fd);
3051
3052                 if (g_hash_table_lookup(file_data_pool, fd->change->dest))
3053                         {
3054                         /* this change overwrites another file which is already known to other modules
3055                            renaming fd would create duplicate FileData structure
3056                            the best thing we can do is nothing
3057                         */
3058                         /**  @FIXME maybe we could copy stuff like marks
3059                         */
3060                         DEBUG_1("can't rename fd, target exists %s -> %s", fd->change->dest, fd->path);
3061                         }
3062                 else
3063                         {
3064                         file_data_set_path(fd, fd->change->dest);
3065                         }
3066                 }
3067         file_data_increment_version(fd);
3068         file_data_send_notification(fd, NOTIFY_CHANGE);
3069
3070         return TRUE;
3071 }
3072
3073 gboolean file_data_sc_apply_ci(FileData *fd)
3074 {
3075         GList *work;
3076         FileDataChangeType type = fd->change->type;
3077
3078         if (!file_data_sc_check_ci(fd, type)) return FALSE;
3079
3080         work = fd->sidecar_files;
3081         while (work)
3082                 {
3083                 auto sfd = static_cast<FileData *>(work->data);
3084
3085                 file_data_apply_ci(sfd);
3086                 work = work->next;
3087                 }
3088
3089         file_data_apply_ci(fd);
3090
3091         return TRUE;
3092 }
3093
3094 static gboolean file_data_list_contains_whole_group(GList *list, FileData *fd)
3095 {
3096         GList *work;
3097         if (fd->parent) fd = fd->parent;
3098         if (!g_list_find(list, fd)) return FALSE;
3099
3100         work = fd->sidecar_files;
3101         while (work)
3102                 {
3103                 if (!g_list_find(list, work->data)) return FALSE;
3104                 work = work->next;
3105                 }
3106         return TRUE;
3107 }
3108
3109 GList *file_data_process_groups_in_selection(GList *list, gboolean ungroup, GList **ungrouped_list)
3110 {
3111         GList *out = nullptr;
3112         GList *work = list;
3113
3114         /* change partial groups to independent files */
3115         if (ungroup)
3116                 {
3117                 while (work)
3118                         {
3119                         auto fd = static_cast<FileData *>(work->data);
3120                         work = work->next;
3121
3122                         if (!file_data_list_contains_whole_group(list, fd))
3123                                 {
3124                                 file_data_disable_grouping(fd, TRUE);
3125                                 if (ungrouped_list)
3126                                         {
3127                                         *ungrouped_list = g_list_prepend(*ungrouped_list, file_data_ref(fd));
3128                                         }
3129                                 }
3130                         }
3131                 }
3132
3133         /* remove sidecars from the list,
3134            they can be still accessed via main_fd->sidecar_files */
3135         work = list;
3136         while (work)
3137                 {
3138                 auto fd = static_cast<FileData *>(work->data);
3139                 work = work->next;
3140
3141                 if (!fd->parent ||
3142                     (!ungroup && !file_data_list_contains_whole_group(list, fd)))
3143                         {
3144                         out = g_list_prepend(out, file_data_ref(fd));
3145                         }
3146                 }
3147
3148         filelist_free(list);
3149         out = g_list_reverse(out);
3150
3151         return out;
3152 }
3153
3154
3155
3156
3157
3158 /*
3159  * notify other modules about the change described by FileDataChangeInfo
3160  */
3161
3162 /* might use file_maint_ functions for now, later it should be changed to a system of callbacks */
3163 /** @FIXME do we need the ignore_list? It looks like a workaround for ineffective
3164    implementation in view-file-list.cc */
3165
3166
3167 struct NotifyIdleData {
3168         FileData *fd;
3169         NotifyType type;
3170 };
3171
3172
3173 struct NotifyData {
3174         FileDataNotifyFunc func;
3175         gpointer data;
3176         NotifyPriority priority;
3177 };
3178
3179 static GList *notify_func_list = nullptr;
3180
3181 static gint file_data_notify_sort(gconstpointer a, gconstpointer b)
3182 {
3183         auto nda = static_cast<const NotifyData *>(a);
3184         auto ndb = static_cast<const NotifyData *>(b);
3185
3186         if (nda->priority < ndb->priority) return -1;
3187         if (nda->priority > ndb->priority) return 1;
3188         return 0;
3189 }
3190
3191 gboolean file_data_register_notify_func(FileDataNotifyFunc func, gpointer data, NotifyPriority priority)
3192 {
3193         NotifyData *nd;
3194         GList *work = notify_func_list;
3195
3196         while (work)
3197                 {
3198                 auto nd = static_cast<NotifyData *>(work->data);
3199
3200                 if (nd->func == func && nd->data == data)
3201                         {
3202                         g_warning("Notify func already registered");
3203                         return FALSE;
3204                         }
3205                 work = work->next;
3206                 }
3207
3208         nd = g_new(NotifyData, 1);
3209         nd->func = func;
3210         nd->data = data;
3211         nd->priority = priority;
3212
3213         notify_func_list = g_list_insert_sorted(notify_func_list, nd, file_data_notify_sort);
3214         DEBUG_2("Notify func registered: %p", (void *)nd);
3215
3216         return TRUE;
3217 }
3218
3219 gboolean file_data_unregister_notify_func(FileDataNotifyFunc func, gpointer data)
3220 {
3221         GList *work = notify_func_list;
3222
3223         while (work)
3224                 {
3225                 auto nd = static_cast<NotifyData *>(work->data);
3226
3227                 if (nd->func == func && nd->data == data)
3228                         {
3229                         notify_func_list = g_list_delete_link(notify_func_list, work);
3230                         DEBUG_2("Notify func unregistered: %p", (void *)nd);
3231                         g_free(nd);
3232                         return TRUE;
3233                         }
3234                 work = work->next;
3235                 }
3236
3237         g_warning("Notify func not found");
3238         return FALSE;
3239 }
3240
3241 #pragma GCC diagnostic push
3242 #pragma GCC diagnostic ignored "-Wunused-function"
3243 gboolean file_data_send_notification_idle_cb_unused(gpointer data)
3244 {
3245         NotifyIdleData *nid = (NotifyIdleData *)data;
3246         GList *work = notify_func_list;
3247
3248         while (work)
3249                 {
3250                 NotifyData *nd = (NotifyData *)work->data;
3251
3252                 nd->func(nid->fd, nid->type, nd->data);
3253                 work = work->next;
3254                 }
3255         file_data_unref(nid->fd);
3256         g_free(nid);
3257         return FALSE;
3258 }
3259 #pragma GCC diagnostic pop
3260
3261 void file_data_send_notification(FileData *fd, NotifyType type)
3262 {
3263         GList *work = notify_func_list;
3264
3265         while (work)
3266                 {
3267                 auto nd = static_cast<NotifyData *>(work->data);
3268
3269                 nd->func(fd, type, nd->data);
3270                 work = work->next;
3271                 }
3272     /*
3273         NotifyIdleData *nid = g_new0(NotifyIdleData, 1);
3274         nid->fd = file_data_ref(fd);
3275         nid->type = type;
3276         g_idle_add_full(G_PRIORITY_HIGH, file_data_send_notification_idle_cb, nid, NULL);
3277     */
3278 }
3279
3280 static GHashTable *file_data_monitor_pool = nullptr;
3281 static guint realtime_monitor_id = 0; /* event source id */
3282
3283 static void realtime_monitor_check_cb(gpointer key, gpointer, gpointer)
3284 {
3285         auto fd = static_cast<FileData *>(key);
3286
3287         file_data_check_changed_files(fd);
3288
3289         DEBUG_1("monitor %s", fd->path);
3290 }
3291
3292 static gboolean realtime_monitor_cb(gpointer)
3293 {
3294         if (!options->update_on_time_change) return TRUE;
3295         g_hash_table_foreach(file_data_monitor_pool, realtime_monitor_check_cb, nullptr);
3296         return TRUE;
3297 }
3298
3299 gboolean file_data_register_real_time_monitor(FileData *fd)
3300 {
3301         gint count;
3302
3303         file_data_ref(fd);
3304
3305         if (!file_data_monitor_pool)
3306                 file_data_monitor_pool = g_hash_table_new(g_direct_hash, g_direct_equal);
3307
3308         count = GPOINTER_TO_INT(g_hash_table_lookup(file_data_monitor_pool, fd));
3309
3310         DEBUG_1("Register realtime %d %s", count, fd->path);
3311
3312         count++;
3313         g_hash_table_insert(file_data_monitor_pool, fd, GINT_TO_POINTER(count));
3314
3315         if (!realtime_monitor_id)
3316                 {
3317                 realtime_monitor_id = g_timeout_add(5000, realtime_monitor_cb, nullptr);
3318                 }
3319
3320         return TRUE;
3321 }
3322
3323 gboolean file_data_unregister_real_time_monitor(FileData *fd)
3324 {
3325         gint count;
3326
3327         g_assert(file_data_monitor_pool);
3328
3329         count = GPOINTER_TO_INT(g_hash_table_lookup(file_data_monitor_pool, fd));
3330
3331         DEBUG_1("Unregister realtime %d %s", count, fd->path);
3332
3333         g_assert(count > 0);
3334
3335         count--;
3336
3337         if (count == 0)
3338                 g_hash_table_remove(file_data_monitor_pool, fd);
3339         else
3340                 g_hash_table_insert(file_data_monitor_pool, fd, GINT_TO_POINTER(count));
3341
3342         file_data_unref(fd);
3343
3344         if (g_hash_table_size(file_data_monitor_pool) == 0)
3345                 {
3346                 g_source_remove(realtime_monitor_id);
3347                 realtime_monitor_id = 0;
3348                 return FALSE;
3349                 }
3350
3351         return TRUE;
3352 }
3353
3354 /*
3355  *-----------------------------------------------------------------------------
3356  * Saving marks list, clearing marks
3357  * Uses file_data_pool
3358  *-----------------------------------------------------------------------------
3359  */
3360
3361 static void marks_get_files(gpointer key, gpointer value, gpointer userdata)
3362 {
3363         auto file_name = static_cast<gchar *>(key);
3364         auto result = static_cast<GString *>(userdata);
3365         FileData *fd;
3366
3367         if (isfile(file_name))
3368                 {
3369                 fd = static_cast<FileData *>(value);
3370                 if (fd && fd->marks > 0)
3371                         {
3372                         g_string_append_printf(result, "%s,%i\n", fd->path, fd->marks);
3373                         }
3374                 }
3375 }
3376
3377 gboolean marks_list_load(const gchar *path)
3378 {
3379         FILE *f;
3380         gchar s_buf[1024];
3381         gchar *pathl;
3382         gchar *file_path;
3383         gchar *marks_value;
3384
3385         pathl = path_from_utf8(path);
3386         f = fopen(pathl, "r");
3387         g_free(pathl);
3388         if (!f) return FALSE;
3389
3390         /* first line must start with Marks comment */
3391         if (!fgets(s_buf, sizeof(s_buf), f) ||
3392                                         strncmp(s_buf, "#Marks", 6) != 0)
3393                 {
3394                 fclose(f);
3395                 return FALSE;
3396                 }
3397
3398         while (fgets(s_buf, sizeof(s_buf), f))
3399                 {
3400                 if (s_buf[0]=='#') continue;
3401                         file_path = strtok(s_buf, ",");
3402                         marks_value = strtok(nullptr, ",");
3403                         if (isfile(file_path))
3404                                 {
3405                                 FileData *fd = file_data_new_no_grouping(file_path);
3406                                 file_data_ref(fd);
3407                                 gint n = 0;
3408                                 while (n <= 9)
3409                                         {
3410                                         gint mark_no = 1 << n;
3411                                         if (atoi(marks_value) & mark_no)
3412                                                 {
3413                                                 file_data_set_mark(fd, n , 1);
3414                                                 }
3415                                         n++;
3416                                         }
3417                                 }
3418                 }
3419
3420         fclose(f);
3421         return TRUE;
3422 }
3423
3424 gboolean marks_list_save(gchar *path, gboolean save)
3425 {
3426         SecureSaveInfo *ssi;
3427         gchar *pathl;
3428
3429         pathl = path_from_utf8(path);
3430         ssi = secure_open(pathl);
3431         g_free(pathl);
3432         if (!ssi)
3433                 {
3434                 log_printf(_("Error: Unable to write marks lists to: %s\n"), path);
3435                 return FALSE;
3436                 }
3437
3438         secure_fprintf(ssi, "#Marks lists\n");
3439
3440         GString *marks = g_string_new("");
3441         if (save)
3442                 {
3443                 g_hash_table_foreach(file_data_pool, marks_get_files, marks);
3444                 }
3445         secure_fprintf(ssi, "%s", marks->str);
3446         g_string_free(marks, TRUE);
3447
3448         secure_fprintf(ssi, "#end\n");
3449         return (secure_close(ssi) == 0);
3450 }
3451
3452 static void marks_clear(gpointer key, gpointer value, gpointer)
3453 {
3454         auto file_name = static_cast<gchar *>(key);
3455         gint mark_no;
3456         gint n;
3457         FileData *fd;
3458
3459         if (isfile(file_name))
3460                 {
3461                 fd = static_cast<FileData *>(value);
3462                 if (fd && fd->marks > 0)
3463                         {
3464                         n = 0;
3465                         while (n <= 9)
3466                                 {
3467                                 mark_no = 1 << n;
3468                                 if (fd->marks & mark_no)
3469                                         {
3470                                         file_data_set_mark(fd, n , 0);
3471                                         }
3472                                 n++;
3473                                 }
3474                         }
3475                 }
3476 }
3477
3478 void marks_clear_all()
3479 {
3480         g_hash_table_foreach(file_data_pool, marks_clear, nullptr);
3481 }
3482
3483 void file_data_set_page_num(FileData *fd, gint page_num)
3484 {
3485         if (fd->page_total > 1 && page_num < 0)
3486                 {
3487                 fd->page_num = fd->page_total - 1;
3488                 }
3489         else if (fd->page_total > 1 && page_num <= fd->page_total)
3490                 {
3491                 fd->page_num = page_num - 1;
3492                 }
3493         else
3494                 {
3495                 fd->page_num = 0;
3496                 }
3497         file_data_send_notification(fd, NOTIFY_REREAD);
3498 }
3499
3500 void file_data_inc_page_num(FileData *fd)
3501 {
3502         if (fd->page_total > 0 && fd->page_num < fd->page_total - 1)
3503                 {
3504                 fd->page_num = fd->page_num + 1;
3505                 }
3506         else if (fd->page_total == 0)
3507                 {
3508                 fd->page_num = fd->page_num + 1;
3509                 }
3510         file_data_send_notification(fd, NOTIFY_REREAD);
3511 }
3512
3513 void file_data_dec_page_num(FileData *fd)
3514 {
3515         if (fd->page_num > 0)
3516                 {
3517                 fd->page_num = fd->page_num - 1;
3518                 }
3519         file_data_send_notification(fd, NOTIFY_REREAD);
3520 }
3521
3522 void file_data_set_page_total(FileData *fd, gint page_total)
3523 {
3524         fd->page_total = page_total;
3525 }
3526
3527 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */