Bug fix: Collections and unmounted drives
[geeqie.git] / src / collect-io.cc
1 /*
2  * Copyright (C) 2004 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Author: John Ellis
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 #include "collect-io.h"
23
24 #include <cstdio>
25 #include <cstring>
26
27 #include <gdk-pixbuf/gdk-pixbuf.h>
28 #include <gdk/gdk.h>
29 #include <glib-object.h>
30 #include <mntent.h>
31
32 #include <config.h>
33
34 #include "collect.h"
35 #include "debug.h"
36 #include "filedata.h"
37 #include "intl.h"
38 #include "layout-util.h"
39 #include "main-defines.h"
40 #include "options.h"
41 #include "secure-save.h"
42 #include "thumb.h"
43 #include "ui-fileops.h"
44 #include "ui-utildlg.h"
45
46 #define GQ_COLLECTION_MARKER "#" GQ_APPNAME
47
48 enum {
49         GQ_COLLECTION_FAIL_MIN =     300,
50         GQ_COLLECTION_FAIL_PERCENT = 98,
51         GQ_COLLECTION_READ_BUFSIZE = 4096
52 };
53
54 struct CollectManagerEntry;
55
56 static void collection_load_thumb_step(CollectionData *cd);
57 static gboolean collection_save_private(CollectionData *cd, const gchar *path);
58
59 static CollectManagerEntry *collect_manager_get_entry(const gchar *path);
60 static void collect_manager_entry_reset(CollectManagerEntry *entry);
61 static gint collect_manager_process_action(CollectManagerEntry *entry, gchar **path_ptr);
62
63 namespace
64 {
65
66 gboolean scan_geometry(gchar *buffer, GdkRectangle &window)
67 {
68         gint nx;
69         gint ny;
70         gint nw;
71         gint nh;
72
73         if (sscanf(buffer, "%d %d %d %d", &nx, &ny, &nw, &nh) != 4) return FALSE;
74
75         window.x = nx;
76         window.y = ny;
77         window.width = nw;
78         window.height = nh;
79
80         return TRUE;
81 }
82
83 }
84
85 static gboolean collection_load_private(CollectionData *cd, const gchar *path, CollectionLoadFlags flags)
86 {
87         gchar s_buf[GQ_COLLECTION_READ_BUFSIZE];
88         FILE *f;
89         gchar *pathl;
90         gboolean limit_failures = TRUE;
91         gboolean success = TRUE;
92         gboolean has_official_header = FALSE;
93         gboolean has_geometry_header = FALSE;
94         gboolean has_gqview_header   = FALSE;
95         gboolean need_header     = TRUE;
96         guint total = 0;
97         guint fail = 0;
98         gboolean changed = FALSE;
99         CollectManagerEntry *entry = nullptr;
100         guint flush = !!(flags & COLLECTION_LOAD_FLUSH);
101         guint append = !!(flags & COLLECTION_LOAD_APPEND);
102         guint only_geometry = !!(flags & COLLECTION_LOAD_GEOMETRY);
103         gboolean reading_extended_filename = FALSE;
104         gchar *buffer2;
105
106         if (!only_geometry)
107                 {
108                 collection_load_stop(cd);
109
110                 if (flush)
111                         collect_manager_flush();
112                 else
113                         entry = collect_manager_get_entry(path);
114
115                 if (!append)
116                         {
117                         g_list_free_full(cd->list, reinterpret_cast<GDestroyNotify>(collection_info_free));
118                         cd->list = nullptr;
119                         }
120                 }
121
122         if (!path && !cd->path) return FALSE;
123
124         if (!path) path = cd->path;
125
126         pathl = path_from_utf8(path);
127
128         DEBUG_1("collection load: append=%d flush=%d only_geometry=%d path=%s", append, flush, only_geometry, pathl);
129
130         /* load it */
131         f = fopen(pathl, "r");
132         g_free(pathl);
133         if (!f)
134                 {
135                 log_printf("Failed to open collection file: \"%s\"\n", path);
136                 return FALSE;
137                 }
138
139         GString *extended_filename_buffer = g_string_new(nullptr);
140         while (fgets(s_buf, sizeof(s_buf), f))
141                 {
142                 gchar *buf;
143                 gchar *p = s_buf;
144
145                 if (!reading_extended_filename)
146                         {
147                         /* Skip whitespaces and empty lines */
148                         while (*p && g_ascii_isspace(*p)) p++;
149                         if (*p == '\n' || *p == '\r') continue;
150
151                         /* Parse comments */
152                         if (*p == '#')
153                                 {
154                                 if (!need_header) continue;
155                                 if (g_ascii_strncasecmp(p, GQ_COLLECTION_MARKER, strlen(GQ_COLLECTION_MARKER)) == 0)
156                                         {
157                                         /* Looks like an official collection, allow unchecked input.
158                                          * All this does is allow adding files that may not exist,
159                                          * which is needed for the collection manager to work.
160                                          * Also unofficial files abort after too many invalid entries.
161                                          */
162                                         has_official_header = TRUE;
163                                         limit_failures = FALSE;
164                                         }
165                                 else if (strncmp(p, "#geometry:", 10 ) == 0 && scan_geometry(p + 10, cd->window))
166                                         {
167                                         has_geometry_header = TRUE;
168                                         cd->window_read = TRUE;
169                                         if (only_geometry) break;
170                                         }
171                                 else if (g_ascii_strncasecmp(p, "#GQview collection", strlen("#GQview collection")) == 0)
172                                         {
173                                         /* As 2008/04/15 there is no difference between our collection file format
174                                          * and GQview 2.1.5 collection file format so ignore failures as well. */
175                                         has_gqview_header = TRUE;
176                                         limit_failures = FALSE;
177                                         }
178                                 need_header = (!has_official_header && !has_gqview_header) || !has_geometry_header;
179                                 continue;
180                                 }
181
182                         if (only_geometry) continue;
183                         }
184
185                 /* Read filenames */
186                 /** @todo This is not safe! */
187                 /* Updated: anything within double quotes is considered a filename */
188                 if (!reading_extended_filename)
189                         {
190                         /* not yet reading filename */
191                         while (*p && *p != '"') p++;
192                         if (*p) p++;
193                         /* start of filename read */
194                         buf = p;
195                         while (*p && *p != '"') p++;
196                         if (p[0] != '"')
197                                 {
198                                 /* first part of extended filename */
199                                 g_string_append(extended_filename_buffer, buf);
200                                 reading_extended_filename = TRUE;
201                                 continue;
202                                 }
203                         }
204                 else
205                         {
206                         buf = p;
207                         while (*p && *p != '"') p++;
208                         if (p[0] != '"')
209                                 {
210                                 /* end of extended filename still not found */
211                                 g_string_append(extended_filename_buffer, buf);
212                                 continue;
213                                 }
214
215                         /* end of extended filename found */
216                         g_string_append_len(extended_filename_buffer, buf, p - buf);
217                         reading_extended_filename = FALSE;
218                         }
219
220                 if (extended_filename_buffer->len > 0)
221                         {
222                         buffer2 = g_strdup(extended_filename_buffer->str);
223                         g_string_erase(extended_filename_buffer, 0, -1);
224                         }
225                 else
226                         {
227                         *p = 0;
228                         buffer2 = g_strdup(buf);
229                         }
230
231                 if (*buffer2)
232                         {
233                         gboolean valid;
234
235                         if (!flush)
236                                 changed |= collect_manager_process_action(entry, &buffer2);
237
238                         valid = (buffer2[0] == G_DIR_SEPARATOR && collection_add_check(cd, file_data_new_simple(buffer2), FALSE, TRUE));
239                         if (!valid)
240                                 {
241                                 log_printf("Warning: Collection: %s Invalid file: %s", cd->name, buffer2);
242                                 DEBUG_1("collection invalid file: %s", buffer2);
243                                 }
244
245                         total++;
246                         if (!valid)
247                                 {
248                                 /* If the file path has the prefix home, tmp or usr it was on the local file system and has been deleted. Ignore it. */
249                                 if (!g_str_has_prefix(buffer2, "/home") && !g_str_has_prefix(buffer2, "/tmp") && !g_str_has_prefix(buffer2, "/usr"))
250                                         {
251                                         /* The file was on a mounted drive and either has been deleted or the drive is not mounted */
252                                         struct mntent *mount_entry;
253                                         FILE *mount_entries;
254                                         gboolean found = FALSE;
255
256                                         mount_entries = setmntent("/proc/mounts", "r");
257                                         if (mount_entries == nullptr)
258                                                 {
259                                                 /* It is assumed this will never fail */
260                                                 perror("setmntent");
261                                                 exit(1);
262                                                 }
263
264                                         while (nullptr != (mount_entry = getmntent(mount_entries)))
265                                                 {
266                                                 if (g_strcmp0(mount_entry->mnt_dir, G_DIR_SEPARATOR_S) != 0)
267                                                         {
268                                                         if (g_str_has_prefix(buffer2, mount_entry->mnt_dir))
269                                                                 {
270                                                                 log_printf("%s was a file on a mounted filesystem but has been deleted: %s", buffer2, cd->name);
271                                                                 found = TRUE;
272                                                                 break;
273                                                                 }
274                                                         }
275                                                 }
276                                         endmntent(mount_entries);
277
278                                         if (!found)
279                                                 {
280                                                 log_printf("%s is a file on an unmounted filesystem: %s", buffer2, cd->path);
281                                                 gchar *text = g_strdup_printf(_("This Collection cannot be opened because it contains a link to a file on a drive which is not yet mounted.\n\nCollection: %s\nFile: %s\n"), cd->path, buffer2);
282                                                 warning_dialog(_("Cannot open Collection"), text, GQ_ICON_DIALOG_WARNING, nullptr);
283                                                 g_free(text);
284
285                                                 collection_window_close_by_collection(cd);
286                                                 success = FALSE;
287                                                 break;
288                                                 }
289                                         }
290                                 else
291                                         {
292                                         log_printf("%s was a file on local filesystem but has been deleted: %s", buffer2, cd->name);
293                                         }
294
295                                 fail++;
296                                 if (limit_failures && fail > GQ_COLLECTION_FAIL_MIN && fail * 100 / total > GQ_COLLECTION_FAIL_PERCENT)
297                                         {
298                                         log_printf("%d invalid filenames in unofficial collection file, closing: %s\n", fail, path);
299                                         success = FALSE;
300                                         break;
301                                         }
302                                 }
303                         }
304                 g_free(buffer2);
305                 }
306
307         g_string_free(extended_filename_buffer, TRUE);
308
309         DEBUG_1("collection files: total = %d fail = %d official=%d gqview=%d geometry=%d", total, fail, has_official_header, has_gqview_header, has_geometry_header);
310
311         fclose(f);
312         if (only_geometry) return has_geometry_header;
313
314         if (!flush)
315                 {
316                 gchar *buf = nullptr;
317                 while (collect_manager_process_action(entry, &buf))
318                         {
319                         collection_add_check(cd, file_data_new_group(buf), FALSE, TRUE);
320                         changed = TRUE;
321                         g_free(buf);
322                         buf = nullptr;
323                         }
324                 }
325
326         cd->list = collection_list_sort(cd->list, cd->sort_method);
327
328         if (!flush && changed && success)
329                 collection_save_private(cd, path);
330
331         if (!flush)
332                 collect_manager_entry_reset(entry);
333
334         if (!append) cd->changed = FALSE;
335
336         return success;
337 }
338
339 gboolean collection_load(CollectionData *cd, const gchar *path, CollectionLoadFlags flags)
340 {
341         if (collection_load_private(cd, path, static_cast<CollectionLoadFlags>(flags | COLLECTION_LOAD_FLUSH)))
342                 {
343                 layout_recent_add_path(cd->path);
344                 return TRUE;
345                 }
346
347         return FALSE;
348 }
349
350 static void collection_load_thumb_do(CollectionData *cd)
351 {
352         GdkPixbuf *pixbuf;
353
354         if (!cd->thumb_loader || !g_list_find(cd->list, cd->thumb_info)) return;
355
356         pixbuf = thumb_loader_get_pixbuf(cd->thumb_loader);
357         collection_info_set_thumb(cd->thumb_info, pixbuf);
358         g_object_unref(pixbuf);
359
360         if (cd->info_updated_func) cd->info_updated_func(cd, cd->thumb_info, cd->info_updated_data);
361 }
362
363 static void collection_load_thumb_error_cb(ThumbLoader *, gpointer data)
364 {
365         auto cd = static_cast<CollectionData *>(data);
366
367         collection_load_thumb_do(cd);
368         collection_load_thumb_step(cd);
369 }
370
371 static void collection_load_thumb_done_cb(ThumbLoader *, gpointer data)
372 {
373         auto cd = static_cast<CollectionData *>(data);
374
375         collection_load_thumb_do(cd);
376         collection_load_thumb_step(cd);
377 }
378
379 static void collection_load_thumb_step(CollectionData *cd)
380 {
381         GList *work;
382         CollectInfo *ci;
383
384         if (!cd->list)
385                 {
386                 collection_load_stop(cd);
387                 return;
388                 }
389
390         work = cd->list;
391         ci = static_cast<CollectInfo *>(work->data);
392         work = work->next;
393         /* find first unloaded thumb */
394         while (work && ci->pixbuf)
395                 {
396                 ci = static_cast<CollectInfo *>(work->data);
397                 work = work->next;
398                 }
399
400         if (!ci || ci->pixbuf)
401                 {
402                 /* done */
403                 collection_load_stop(cd);
404
405                 /* send a NULL CollectInfo to notify end */
406                 if (cd->info_updated_func) cd->info_updated_func(cd, nullptr, cd->info_updated_data);
407
408                 return;
409                 }
410
411         /* setup loader and call it */
412         cd->thumb_info = ci;
413         thumb_loader_free(cd->thumb_loader);
414         cd->thumb_loader = thumb_loader_new(options->thumbnails.max_width, options->thumbnails.max_height);
415         thumb_loader_set_callbacks(cd->thumb_loader,
416                                    collection_load_thumb_done_cb,
417                                    collection_load_thumb_error_cb,
418                                    nullptr,
419                                    cd);
420
421         /* start it */
422         if (!thumb_loader_start(cd->thumb_loader, ci->fd))
423                 {
424                 /* error, handle it, do next */
425                 DEBUG_1("error loading thumb for %s", ci->fd->path);
426                 collection_load_thumb_do(cd);
427                 collection_load_thumb_step(cd);
428                 }
429 }
430
431 void collection_load_thumb_idle(CollectionData *cd)
432 {
433         if (!cd->thumb_loader) collection_load_thumb_step(cd);
434 }
435
436 gboolean collection_load_begin(CollectionData *cd, const gchar *path, CollectionLoadFlags flags)
437 {
438         if (!collection_load(cd, path, flags)) return FALSE;
439
440         collection_load_thumb_idle(cd);
441
442         return TRUE;
443 }
444
445 void collection_load_stop(CollectionData *cd)
446 {
447         if (!cd->thumb_loader) return;
448
449         thumb_loader_free(cd->thumb_loader);
450         cd->thumb_loader = nullptr;
451 }
452
453 static gboolean collection_save_private(CollectionData *cd, const gchar *path)
454 {
455         SecureSaveInfo *ssi;
456         GList *work;
457         gchar *pathl;
458
459         if (!path && !cd->path) return FALSE;
460
461         if (!path)
462                 {
463                 path = cd->path;
464                 }
465
466
467         pathl = path_from_utf8(path);
468         ssi = secure_open(pathl);
469         g_free(pathl);
470         if (!ssi)
471                 {
472                 log_printf(_("failed to open collection (write) \"%s\"\n"), path);
473                 return FALSE;
474                 }
475
476         secure_fprintf(ssi, "%s collection\n", GQ_COLLECTION_MARKER);
477         secure_fprintf(ssi, "#created with %s version %s\n", GQ_APPNAME, VERSION);
478
479         collection_update_geometry(cd);
480         if (cd->window_read)
481                 {
482                 secure_fprintf(ssi, "#geometry: %d %d %d %d\n", cd->window.x, cd->window.y, cd->window.width, cd->window.height);
483                 }
484
485         work = cd->list;
486         while (work && secsave_errno == SS_ERR_NONE)
487                 {
488                 auto ci = static_cast<CollectInfo *>(work->data);
489                 secure_fprintf(ssi, "\"%s\"\n", ci->fd->path);
490                 work = work->next;
491                 }
492
493         secure_fprintf(ssi, "#end\n");
494
495         if (secure_close(ssi))
496                 {
497                 log_printf(_("error saving collection file: %s\nerror: %s\n"), path,
498                             secsave_strerror(secsave_errno));
499                 return FALSE;
500                 }
501
502         if (!cd->path || strcmp(path, cd->path) != 0)
503                 {
504                 gchar *buf = cd->path;
505                 cd->path = g_strdup(path);
506                 path = cd->path;
507                 g_free(buf);
508
509                 g_free(cd->name);
510                 cd->name = g_strdup(filename_from_path(cd->path));
511
512                 collection_path_changed(cd);
513                 }
514
515         cd->changed = FALSE;
516
517         return TRUE;
518 }
519
520 gboolean collection_save(CollectionData *cd, const gchar *path)
521 {
522         if (collection_save_private(cd, path))
523                 {
524                 layout_recent_add_path(cd->path);
525                 return TRUE;
526                 }
527
528         return FALSE;
529 }
530
531 gboolean collection_load_only_geometry(CollectionData *cd, const gchar *path)
532 {
533         return collection_load(cd, path, COLLECTION_LOAD_GEOMETRY);
534 }
535
536
537 /*
538  *-------------------------------------------------------------------
539  * collection manager
540  *-------------------------------------------------------------------
541  */
542
543 enum {
544         COLLECT_MANAGER_ACTIONS_PER_IDLE = 1000,
545         COLLECT_MANAGER_FLUSH_DELAY =      10000
546 };
547
548 struct CollectManagerEntry
549 {
550         gchar *path;
551         GList *add_list;
552         GHashTable *oldpath_hash;
553         GHashTable *newpath_hash;
554         gboolean empty;
555 };
556
557 enum CollectManagerType {
558         COLLECTION_MANAGER_UPDATE,
559         COLLECTION_MANAGER_ADD,
560         COLLECTION_MANAGER_REMOVE
561 };
562
563 struct CollectManagerAction
564 {
565         gchar *oldpath;
566         gchar *newpath;
567
568         CollectManagerType type;
569
570         gint ref;
571 };
572
573
574 static GList *collection_manager_entry_list = nullptr;
575 static GList *collection_manager_action_list = nullptr;
576 static GList *collection_manager_action_tail = nullptr;
577 static guint collection_manager_timer_id = 0; /* event source id */
578
579
580 static CollectManagerAction *collect_manager_action_new(const gchar *oldpath, const gchar *newpath,
581                                                         CollectManagerType type)
582 {
583         CollectManagerAction *action;
584
585         action = g_new0(CollectManagerAction, 1);
586         action->ref = 1;
587
588         action->oldpath = g_strdup(oldpath);
589         action->newpath = g_strdup(newpath);
590
591         action->type = type;
592
593         return action;
594 }
595
596 static void collect_manager_action_ref(CollectManagerAction *action)
597 {
598         action->ref++;
599 }
600
601 static void collect_manager_action_unref(CollectManagerAction *action)
602 {
603         action->ref--;
604
605         if (action->ref > 0) return;
606
607         g_free(action->oldpath);
608         g_free(action->newpath);
609         g_free(action);
610 }
611
612 static void collect_manager_entry_free_data(CollectManagerEntry *entry)
613 {
614         g_list_free_full(entry->add_list, reinterpret_cast<GDestroyNotify>(collect_manager_action_unref));
615         if (g_hash_table_size(entry->oldpath_hash) > 0)
616                 g_hash_table_destroy(entry->oldpath_hash);
617         else
618                 g_hash_table_unref(entry->oldpath_hash);
619         if (g_hash_table_size(entry->newpath_hash) > 0)
620                 g_hash_table_destroy(entry->newpath_hash);
621         else
622                 g_hash_table_unref(entry->newpath_hash);
623 }
624
625 static void collect_manager_entry_init_data(CollectManagerEntry *entry)
626 {
627         entry->add_list = nullptr;
628         entry->oldpath_hash = g_hash_table_new_full(g_str_hash, g_str_equal, nullptr, reinterpret_cast<GDestroyNotify>(collect_manager_action_unref));
629         entry->newpath_hash = g_hash_table_new(g_str_hash, g_str_equal);
630         entry->empty = TRUE;
631
632 }
633
634 static CollectManagerEntry *collect_manager_entry_new(const gchar *path)
635 {
636         CollectManagerEntry *entry;
637
638         entry = g_new0(CollectManagerEntry, 1);
639         entry->path = g_strdup(path);
640         collect_manager_entry_init_data(entry);
641
642         collection_manager_entry_list = g_list_append(collection_manager_entry_list, entry);
643
644         return entry;
645 }
646
647
648 static void collect_manager_entry_free(CollectManagerEntry *entry)
649 {
650         collection_manager_entry_list = g_list_remove(collection_manager_entry_list, entry);
651
652         collect_manager_entry_free_data(entry);
653
654         g_free(entry->path);
655         g_free(entry);
656 }
657
658 static void collect_manager_entry_reset(CollectManagerEntry *entry)
659 {
660         collect_manager_entry_free_data(entry);
661         collect_manager_entry_init_data(entry);
662 }
663
664 static CollectManagerEntry *collect_manager_get_entry(const gchar *path)
665 {
666         GList *work;
667
668         work = collection_manager_entry_list;
669         while (work)
670                 {
671                 CollectManagerEntry *entry;
672
673                 entry = static_cast<CollectManagerEntry *>(work->data);
674                 work = work->next;
675                 if (strcmp(entry->path, path) == 0)
676                         {
677                         return entry;
678                         }
679                 }
680         return nullptr;
681
682 }
683
684 static void collect_manager_entry_add_action(CollectManagerEntry *entry, CollectManagerAction *action)
685 {
686
687         CollectManagerAction *orig_action;
688
689         entry->empty = FALSE;
690
691         if (action->oldpath == nullptr)
692                 {
693                 /* add file */
694                 if (action->newpath == nullptr)
695                         {
696                         return;
697                         }
698
699                 orig_action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->newpath_hash, action->newpath));
700                 if (orig_action)
701                         {
702                         /* target already exists */
703                         log_printf("collection manager failed to add another action for target %s in collection %s\n",
704                                 action->newpath, entry->path);
705                         return;
706                         }
707                 entry->add_list = g_list_append(entry->add_list, action);
708                 g_hash_table_insert(entry->newpath_hash, action->newpath, action);
709                 collect_manager_action_ref(action);
710                 return;
711                 }
712
713         orig_action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->newpath_hash, action->oldpath));
714         if (orig_action)
715                 {
716                 /* new action with the same file */
717                 CollectManagerAction *new_action = collect_manager_action_new(orig_action->oldpath, action->newpath, action->type);
718
719                 if (new_action->oldpath)
720                         {
721                         g_hash_table_steal(entry->oldpath_hash, orig_action->oldpath);
722                         g_hash_table_insert(entry->oldpath_hash, new_action->oldpath, new_action);
723                         }
724                 else
725                         {
726                         GList *work = g_list_find(entry->add_list, orig_action);
727                         work->data = new_action;
728                         }
729
730                 g_hash_table_steal(entry->newpath_hash, orig_action->newpath);
731                 if (new_action->newpath)
732                         {
733                         g_hash_table_insert(entry->newpath_hash, new_action->newpath, new_action);
734                         }
735                 collect_manager_action_unref(orig_action);
736                 return;
737                 }
738
739
740         orig_action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->oldpath_hash, action->oldpath));
741         if (orig_action)
742                 {
743                 /* another action for the same source, ignore */
744                 log_printf("collection manager failed to add another action for source %s in collection %s\n",
745                         action->oldpath, entry->path);
746                 return;
747                 }
748
749         g_hash_table_insert(entry->oldpath_hash, action->oldpath, action);
750         if (action->newpath)
751                 {
752                 g_hash_table_insert(entry->newpath_hash, action->newpath, action);
753                 }
754         collect_manager_action_ref(action);
755 }
756
757 static gboolean collect_manager_process_action(CollectManagerEntry *entry, gchar **path_ptr)
758 {
759         gchar *path = *path_ptr;
760         CollectManagerAction *action;
761
762         if (path == nullptr)
763                 {
764                 /* get new files */
765                 if (entry->add_list)
766                         {
767                         action = static_cast<CollectManagerAction *>(entry->add_list->data);
768                         g_assert(action->oldpath == nullptr);
769                         entry->add_list = g_list_remove(entry->add_list, action);
770                         path = g_strdup(action->newpath);
771                         g_hash_table_remove(entry->newpath_hash, path);
772                         collect_manager_action_unref(action);
773                         }
774                 *path_ptr = path;
775                 return (path != nullptr);
776                 }
777
778         action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->oldpath_hash, path));
779
780         if (action)
781                 {
782                 strcpy(*path_ptr, action->newpath);
783                 return TRUE;
784                 }
785
786         return FALSE; /* no change */
787 }
788
789 static void collect_manager_refresh()
790 {
791         GList *list;
792         GList *work;
793         FileData *dir_fd;
794
795         dir_fd = file_data_new_dir(get_collections_dir());
796         filelist_read(dir_fd, &list, nullptr);
797         file_data_unref(dir_fd);
798
799         work = collection_manager_entry_list;
800         while (work && list)
801                 {
802                 CollectManagerEntry *entry;
803                 GList *list_step;
804
805                 entry = static_cast<CollectManagerEntry *>(work->data);
806                 work = work->next;
807
808                 list_step = list;
809                 while (list_step && entry)
810                         {
811                         FileData *fd;
812
813                         fd = static_cast<FileData *>(list_step->data);
814                         list_step = list_step->next;
815
816                         if (strcmp(fd->path, entry->path) == 0)
817                                 {
818                                 list = g_list_remove(list, fd);
819                                 file_data_unref(fd);
820
821                                 entry = nullptr;
822                                 }
823                         else
824                                 {
825                                 collect_manager_entry_free(entry);
826
827                                 entry = nullptr;
828                                 }
829                         }
830                 }
831
832         work = list;
833         while (work)
834                 {
835                 FileData *fd;
836
837                 fd = static_cast<FileData *>(work->data);
838                 work = work->next;
839
840                 collect_manager_entry_new(fd->path);
841                 }
842
843         filelist_free(list);
844 }
845
846 static void collect_manager_process_actions(gint max)
847 {
848         if (collection_manager_action_list) DEBUG_1("collection manager processing actions");
849
850         while (collection_manager_action_list != nullptr && max > 0)
851                 {
852                 CollectManagerAction *action;
853                 GList *work;
854
855                 action = static_cast<CollectManagerAction *>(collection_manager_action_list->data);
856                 work = collection_manager_entry_list;
857                 while (work)
858                         {
859                         CollectManagerEntry *entry;
860
861                         entry = static_cast<CollectManagerEntry *>(work->data);
862                         work = work->next;
863
864                         if (action->type == COLLECTION_MANAGER_UPDATE)
865                                 {
866                                 collect_manager_entry_add_action(entry, action);
867                                 }
868                         else if (action->oldpath && action->newpath &&
869                                  strcmp(action->newpath, entry->path) == 0)
870                                 {
871                                 /* convert action to standard add format */
872                                 g_free(action->newpath);
873                                 if (action->type == COLLECTION_MANAGER_ADD)
874                                         {
875                                         action->newpath = action->oldpath;
876                                         action->oldpath = nullptr;
877                                         }
878                                 else if (action->type == COLLECTION_MANAGER_REMOVE)
879                                         {
880                                         action->newpath = nullptr;
881                                         }
882                                 collect_manager_entry_add_action(entry, action);
883                                 }
884
885                         max--;
886                         }
887
888                 if (action->type != COLLECTION_MANAGER_UPDATE &&
889                     action->oldpath && action->newpath)
890                         {
891                         log_printf("collection manager failed to %s %s for collection %s\n",
892                                 (action->type == COLLECTION_MANAGER_ADD) ? "add" : "remove",
893                                 action->oldpath, action->newpath);
894                         }
895
896                 if (collection_manager_action_tail == collection_manager_action_list)
897                         {
898                         collection_manager_action_tail = nullptr;
899                         }
900                 collection_manager_action_list = g_list_remove(collection_manager_action_list, action);
901                 collect_manager_action_unref(action);
902                 }
903 }
904
905 static gboolean collect_manager_process_entry(CollectManagerEntry *entry)
906 {
907         CollectionData *cd;
908
909         if (entry->empty) return FALSE;
910
911         cd = collection_new(entry->path);
912         (void) collection_load_private(cd, entry->path, COLLECTION_LOAD_NONE);
913
914         collection_unref(cd);
915
916         return TRUE;
917 }
918
919 static gboolean collect_manager_process_entry_list()
920 {
921         GList *work;
922
923         work = collection_manager_entry_list;
924         while (work)
925                 {
926                 CollectManagerEntry *entry;
927
928                 entry = static_cast<CollectManagerEntry *>(work->data);
929                 work = work->next;
930                 if (collect_manager_process_entry(entry)) return TRUE;
931                 }
932
933         return FALSE;
934 }
935
936
937
938 static gboolean collect_manager_process_cb(gpointer)
939 {
940         if (collection_manager_action_list) collect_manager_refresh();
941         collect_manager_process_actions(COLLECT_MANAGER_ACTIONS_PER_IDLE);
942         if (collection_manager_action_list) return G_SOURCE_CONTINUE;
943
944         if (collect_manager_process_entry_list()) return G_SOURCE_CONTINUE;
945
946         DEBUG_1("collection manager is up to date");
947         return G_SOURCE_REMOVE;
948 }
949
950 static gboolean collect_manager_timer_cb(gpointer)
951 {
952         DEBUG_1("collection manager timer expired");
953
954         g_idle_add_full(G_PRIORITY_LOW, collect_manager_process_cb, nullptr, nullptr);
955
956         collection_manager_timer_id = 0;
957         return FALSE;
958 }
959
960 static void collect_manager_timer_push(gint stop)
961 {
962         if (collection_manager_timer_id)
963                 {
964                 if (!stop) return;
965
966                 g_source_remove(collection_manager_timer_id);
967                 collection_manager_timer_id = 0;
968                 }
969
970         if (!stop)
971                 {
972                 collection_manager_timer_id = g_timeout_add(COLLECT_MANAGER_FLUSH_DELAY,
973                                                             collect_manager_timer_cb, nullptr);
974                 DEBUG_1("collection manager timer started");
975                 }
976 }
977
978 static void collect_manager_add_action(CollectManagerAction *action)
979 {
980         if (!action) return;
981
982         /* we keep track of the list's tail to keep this a n(1) operation */
983
984         if (collection_manager_action_tail)
985                 {
986                 collection_manager_action_tail = g_list_append(collection_manager_action_tail, action);
987                 collection_manager_action_tail = collection_manager_action_tail->next;
988                 }
989         else
990                 {
991                 collection_manager_action_list = g_list_append(collection_manager_action_list, action);
992                 collection_manager_action_tail = collection_manager_action_list;
993                 }
994
995         collect_manager_timer_push(FALSE);
996 }
997
998 void collect_manager_moved(FileData *fd)
999 {
1000         CollectManagerAction *action;
1001         const gchar *oldpath = fd->change->source;
1002         const gchar *newpath = fd->change->dest;
1003
1004         action = collect_manager_action_new(oldpath, newpath, COLLECTION_MANAGER_UPDATE);
1005         collect_manager_add_action(action);
1006 }
1007
1008 void collect_manager_add(FileData *fd, const gchar *collection)
1009 {
1010         CollectManagerAction *action;
1011         CollectWindow *cw;
1012
1013         if (!fd || !collection) return;
1014
1015         cw = collection_window_find_by_path(collection);
1016         if (cw)
1017                 {
1018                 if (collection_list_find_fd(cw->cd->list, fd) == nullptr)
1019                         {
1020                         collection_add(cw->cd, fd, FALSE);
1021                         }
1022                 return;
1023                 }
1024
1025         action = collect_manager_action_new(fd->path, collection, COLLECTION_MANAGER_ADD);
1026         collect_manager_add_action(action);
1027 }
1028
1029 void collect_manager_remove(FileData *fd, const gchar *collection)
1030 {
1031         CollectManagerAction *action;
1032         CollectWindow *cw;
1033
1034         if (!fd || !collection) return;
1035
1036         cw = collection_window_find_by_path(collection);
1037         if (cw)
1038                 {
1039                 while (collection_remove(cw->cd, fd));
1040                 return;
1041                 }
1042
1043         action = collect_manager_action_new(fd->path, collection, COLLECTION_MANAGER_REMOVE);
1044         collect_manager_add_action(action);
1045 }
1046
1047 void collect_manager_flush()
1048 {
1049         collect_manager_timer_push(TRUE);
1050
1051         DEBUG_1("collection manager flushing");
1052         while (collect_manager_process_cb(nullptr));
1053 }
1054
1055 void collect_manager_notify_cb(FileData *fd, NotifyType type, gpointer)
1056 {
1057         if (!(type & NOTIFY_CHANGE) || !fd->change) return;
1058
1059         DEBUG_1("Notify collect_manager: %s %04x", fd->path, type);
1060         switch (fd->change->type)
1061                 {
1062                 case FILEDATA_CHANGE_MOVE:
1063                         collect_manager_moved(fd);
1064                         break;
1065                 case FILEDATA_CHANGE_COPY:
1066                         break;
1067                 case FILEDATA_CHANGE_RENAME:
1068                         collect_manager_moved(fd);
1069                         break;
1070                 case FILEDATA_CHANGE_DELETE:
1071                 case FILEDATA_CHANGE_UNSPECIFIED:
1072                 case FILEDATA_CHANGE_WRITE_METADATA:
1073                         break;
1074                 }
1075 }
1076
1077 static gint collection_manager_sort_cb(gconstpointer a, gconstpointer b)
1078 {
1079         auto char_a = static_cast<const gchar *>(a);
1080         auto char_b = static_cast<const gchar *>(b);
1081
1082         return g_strcmp0(char_a, char_b);
1083 }
1084
1085 /**
1086  * @brief Creates sorted list of collections
1087  * @param[out] names_exc sorted list of collections names excluding extension
1088  * @param[out] names_inc sorted list of collections names including extension
1089  * @param[out] paths sorted list of collection paths
1090  *
1091  * Lists of type gchar.
1092  * Used lists must be freed with string_list_free()
1093  */
1094 void collect_manager_list(GList **names_exc, GList **names_inc, GList **paths)
1095 {
1096         FileData *dir_fd;
1097         GList *list = nullptr;
1098         gchar *name;
1099         FileData *fd;
1100         gchar *filename;
1101
1102         if (names_exc == nullptr && names_inc == nullptr && paths == nullptr)
1103                 {
1104                 return;
1105                 }
1106
1107         dir_fd = file_data_new_dir((get_collections_dir()));
1108
1109         filelist_read(dir_fd, &list, nullptr);
1110
1111         while (list)
1112                 {
1113                 fd = static_cast<FileData *>(list->data);
1114                 filename = g_strdup(filename_from_path(fd->path));
1115
1116                 if (file_extension_match(filename, GQ_COLLECTION_EXT))
1117                         {
1118                         name = remove_extension_from_path(filename);
1119
1120                         if (names_exc != nullptr)
1121                                 {
1122                                 *names_exc = g_list_insert_sorted(*names_exc, g_strdup(name),
1123                                                                                         collection_manager_sort_cb);
1124                                 *names_exc = g_list_first(*names_exc);
1125                                 }
1126                         if (names_inc != nullptr)
1127                                 {
1128                                 *names_inc = g_list_insert_sorted(*names_inc,filename,
1129                                                                                         collection_manager_sort_cb);
1130                                 *names_inc = g_list_first(*names_inc);
1131                                 }
1132                         if (paths != nullptr)
1133                                 {
1134                                 *paths = g_list_insert_sorted(*paths, g_strdup(fd->path),
1135                                                                                         collection_manager_sort_cb);
1136                                 *paths = g_list_first(*paths);
1137                                 }
1138                         g_free(name);
1139                         }
1140                 list = list->next;
1141                 g_free(filename);
1142                 }
1143
1144         filelist_free(list);
1145 }
1146 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */