Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
folder_survey.c
Go to the documentation of this file.
1/*
2 This file is part of the Ansel project.
3 Copyright (C) 2026 Guillaume STUTIN.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14*/
15
17
18#include "common/darktable.h"
19#include "common/datetime.h"
21#include "common/imageio.h"
22#include "control/conf.h"
23#include "control/control.h"
24#include "control/jobs.h"
26#include "gui/gtk.h"
27#include "views/view.h"
28
29#include <gio/gio.h>
30
31#define DT_FOLDER_SURVEY_STATE_FILE "folder-survey-state.ini"
32
39
47
53
59
60typedef struct dt_folder_survey_t
61{
62 dt_pthread_mutex_t lock;
63 GHashTable *files;
64 char *folder;
67 guint timer;
69 gboolean initialized;
70 gboolean active;
72 gboolean scan_running;
73 gboolean shutting_down;
74 // TRUE when the previous application session quit while monitoring.
77
79
87{
88 GKeyFile *state = g_key_file_new();
89 g_key_file_set_string(state, "survey", "folder", _folder_survey.folder ? _folder_survey.folder : "");
90 g_key_file_set_boolean(state, "survey", "initialized", _folder_survey.baseline_initialized);
91 // Session marker: an application shutdown while monitoring leaves this TRUE
92 // so the next start can propose to resume the studio session.
93 g_key_file_set_boolean(state, "survey", "active", _folder_survey.active);
94
95 GHashTableIter iter;
96 gpointer key = NULL;
97 gpointer value = NULL;
98 g_hash_table_iter_init(&iter, _folder_survey.files);
99 // Serialize every known image path and the metadata used to detect changes.
100 while(g_hash_table_iter_next(&iter, &key, &value))
101 {
102 const char *path = (const char *)key;
104 char *group = g_compute_checksum_for_string(G_CHECKSUM_SHA256, path, -1);
105 g_key_file_set_string(state, group, "path", path);
106 g_key_file_set_uint64(state, group, "size", entry->size);
107 g_key_file_set_int64(state, group, "mtime", entry->mtime);
108 g_key_file_set_integer(state, group, "stable_scans", entry->stable_scans);
109 g_key_file_set_integer(state, group, "state", entry->state);
110 dt_free(group);
111 }
112
113 gsize length = 0;
114 gchar *contents = g_key_file_to_data(state, &length, NULL);
115 GFile *file = g_file_new_for_path(_folder_survey.state_path);
116 GError *error = NULL;
117 const gboolean success = g_file_replace_contents(file, contents, length, NULL, FALSE,
118 G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, &error);
119 if(!success)
120 {
121 fprintf(stderr, "[folder survey] failed to save state: %s\n", error ? error->message : "unknown error");
122 g_clear_error(&error);
123 }
124
125 g_object_unref(file);
126 dt_free(contents);
127 g_key_file_unref(state);
128 return success ? 0 : 1;
129}
130
135{
136 GKeyFile *state = g_key_file_new();
137 GError *error = NULL;
138 if(!g_key_file_load_from_file(state, _folder_survey.state_path, G_KEY_FILE_NONE, &error))
139 {
140 g_clear_error(&error);
141 g_key_file_unref(state);
142 return 0;
143 }
144
146 _folder_survey.folder = g_key_file_get_string(state, "survey", "folder", NULL);
147 _folder_survey.baseline_initialized = g_key_file_get_boolean(state, "survey", "initialized", NULL);
148 _folder_survey.was_active_last_session = g_key_file_get_boolean(state, "survey", "active", NULL);
149
150 gsize groups_count = 0;
151 gchar **groups = g_key_file_get_groups(state, &groups_count);
152 // Restore one image observation from each hashed path group.
153 for(gsize k = 0; k < groups_count; k++)
154 {
155 if(!strcmp(groups[k], "survey")) continue;
156
157 char *path = g_key_file_get_string(state, groups[k], "path", NULL);
158 if(IS_NULL_PTR(path) || path[0] == '\0')
159 {
160 dt_free(path);
161 continue;
162 }
163
164 dt_folder_survey_entry_t *entry = calloc(1, sizeof(dt_folder_survey_entry_t));
165 if(IS_NULL_PTR(entry))
166 {
167 dt_free(path);
168 continue;
169 }
170
171 entry->size = g_key_file_get_uint64(state, groups[k], "size", NULL);
172 entry->mtime = g_key_file_get_int64(state, groups[k], "mtime", NULL);
173 entry->stable_scans = g_key_file_get_integer(state, groups[k], "stable_scans", NULL);
174 entry->state = g_key_file_get_integer(state, groups[k], "state", NULL);
177 {
179 entry->stable_scans = 0;
180 }
181 g_hash_table_replace(_folder_survey.files, path, entry);
182 }
183
184 g_strfreev(groups);
185 g_key_file_unref(state);
186 return 0;
187}
188
195static int _folder_survey_collect(const char *folder, GHashTable *observed)
196{
197 GQueue folders = G_QUEUE_INIT;
198 g_queue_push_tail(&folders, g_file_new_for_path(folder));
199 int error = 0;
200
201 // Traverse every directory below the configured ingest root.
202 while(!g_queue_is_empty(&folders))
203 {
204 GFile *current = g_queue_pop_head(&folders);
205 GError *enumeration_error = NULL;
206 GFileEnumerator *enumerator = g_file_enumerate_children(
207 current,
208 G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE "," G_FILE_ATTRIBUTE_STANDARD_SIZE
209 "," G_FILE_ATTRIBUTE_TIME_MODIFIED,
210 G_FILE_QUERY_INFO_NONE, NULL, &enumeration_error);
211 g_object_unref(current);
212
213 if(IS_NULL_PTR(enumerator))
214 {
215 fprintf(stderr, "[folder survey] failed to enumerate folder: %s\n",
216 enumeration_error ? enumeration_error->message : "unknown error");
217 g_clear_error(&enumeration_error);
218 error = 1;
219 break;
220 }
221
222 GFileInfo *info = NULL;
223 GFile *child = NULL;
224 // Record supported regular images and queue child directories for traversal.
225 while(g_file_enumerator_iterate(enumerator, &info, &child, NULL, &enumeration_error))
226 {
227 if(IS_NULL_PTR(info) || IS_NULL_PTR(child)) break;
228
229 const GFileType type = g_file_info_get_file_type(info);
230 if(type == G_FILE_TYPE_DIRECTORY)
231 {
232 g_queue_push_tail(&folders, g_object_ref(child));
233 continue;
234 }
235 if(type != G_FILE_TYPE_REGULAR) continue;
236
237 char *path = g_file_get_path(child);
238 if(IS_NULL_PTR(path) || !dt_supported_image(path))
239 {
240 dt_free(path);
241 continue;
242 }
243
244 char *canonical_path = g_canonicalize_filename(path, NULL);
246 if(!IS_NULL_PTR(observation))
247 {
248 observation->size = g_file_info_get_size(info);
249 observation->mtime = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
250 g_hash_table_replace(observed, canonical_path, observation);
251 canonical_path = NULL;
252 }
253 dt_free(canonical_path);
254 dt_free(path);
255 }
256
257 if(!IS_NULL_PTR(enumeration_error))
258 {
259 fprintf(stderr, "[folder survey] failed while enumerating files: %s\n", enumeration_error->message);
260 g_clear_error(&enumeration_error);
261 error = 1;
262 }
263 g_object_unref(enumerator);
264 if(error) break;
265 }
266
267 while(!g_queue_is_empty(&folders)) g_object_unref(g_queue_pop_head(&folders));
268 return error;
269}
270
279{
281 if(IS_NULL_PTR(view)) return NULL;
282
284 if(IS_NULL_PTR(conf) || conf[0] == '\0')
285 {
286 dt_free(conf);
287 return NULL;
288 }
289
290 GList *styles = NULL;
291 gchar **names = g_strsplit(conf, DT_FOLDER_SURVEY_STYLES_SEPARATOR, -1);
292 for(gchar **name = names; *name; name++)
293 if((*name)[0] != '\0') styles = g_list_prepend(styles, g_strdup(*name));
294
295 g_strfreev(names);
296 dt_free(conf);
297 return g_list_reverse(styles);
298}
299
303static void _folder_survey_imported(const char *source, const gboolean success, gpointer user_data)
304{
305 const guint generation = GPOINTER_TO_UINT(user_data);
306 char *path = g_canonicalize_filename(source, NULL);
307
309 if(generation == _folder_survey.generation)
310 {
311 dt_folder_survey_entry_t *entry = g_hash_table_lookup(_folder_survey.files, path);
312 if(!IS_NULL_PTR(entry))
313 {
315 entry->stable_scans = success ? entry->stable_scans : 0;
317 }
318 }
320
321 dt_free(path);
322}
323
332{
334 GHashTable *observed = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
335 if(_folder_survey_collect(params->folder, observed))
336 {
337 g_hash_table_destroy(observed);
338 return 1;
339 }
340
341 GList *imports = NULL;
344 {
346 g_hash_table_destroy(observed);
347 return 0;
348 }
349
350 GHashTableIter previous_iter;
351 gpointer previous_path = NULL;
352 gpointer previous_entry = NULL;
353 g_hash_table_iter_init(&previous_iter, _folder_survey.files);
354 // Forget paths removed since the preceding scan so they can be detected if they reappear.
355 while(g_hash_table_iter_next(&previous_iter, &previous_path, &previous_entry))
356 {
357 if(!g_hash_table_contains(observed, previous_path)) g_hash_table_iter_remove(&previous_iter);
358 }
359
360 GHashTableIter observed_iter;
361 gpointer observed_path = NULL;
362 gpointer observed_value = NULL;
363 g_hash_table_iter_init(&observed_iter, observed);
364 // Compare each current image with its last size, timestamp, and import state.
365 while(g_hash_table_iter_next(&observed_iter, &observed_path, &observed_value))
366 {
367 const dt_folder_survey_observation_t *observation = observed_value;
368 dt_folder_survey_entry_t *entry = g_hash_table_lookup(_folder_survey.files, observed_path);
369
371 {
372 entry = calloc(1, sizeof(dt_folder_survey_entry_t));
373 if(IS_NULL_PTR(entry)) continue;
374 entry->size = observation->size;
375 entry->mtime = observation->mtime;
377 g_hash_table_replace(_folder_survey.files, g_strdup(observed_path), entry);
378 continue;
379 }
380
381 if(IS_NULL_PTR(entry))
382 {
383 entry = calloc(1, sizeof(dt_folder_survey_entry_t));
384 if(IS_NULL_PTR(entry)) continue;
385 entry->size = observation->size;
386 entry->mtime = observation->mtime;
388 g_hash_table_replace(_folder_survey.files, g_strdup(observed_path), entry);
389 continue;
390 }
391
393 {
394 // A producer may reuse a filename after the previous image was handled.
395 // Treat changed metadata at the same path as a new pending file.
396 if(entry->size != observation->size || entry->mtime != observation->mtime)
397 {
398 entry->size = observation->size;
399 entry->mtime = observation->mtime;
400 entry->stable_scans = 0;
402 }
403 continue;
404 }
405
406 if(entry->state == DT_FOLDER_SURVEY_FILE_QUEUED) continue;
407
408 if(entry->size != observation->size || entry->mtime != observation->mtime)
409 {
410 entry->size = observation->size;
411 entry->mtime = observation->mtime;
412 entry->stable_scans = 0;
413 continue;
414 }
415
416 entry->stable_scans++;
417 if(entry->stable_scans >= 1)
418 {
420 imports = g_list_prepend(imports, g_strdup(observed_path));
421 }
422 }
423
427 g_hash_table_destroy(observed);
428
429 if(!IS_NULL_PTR(imports))
430 {
431 imports = g_list_sort(imports, (GCompareFunc)g_strcmp0);
432 const int elements = g_list_length(imports);
433 dt_control_log(ngettext("Folder survey found %d new image to import.",
434 "Folder survey found %d new images to import.", elements),
435 elements);
436
437 char *date = dt_conf_get_string("studio_capture/datetime");
438 if(IS_NULL_PTR(date) || date[0] == '\0')
439 {
440 dt_free(date);
441 GDateTime *now = g_date_time_new_now_local();
442 date = g_date_time_format(now, "%F");
443 g_date_time_unref(now);
444 }
445
447 = { .imgs = imports,
448 .datetime = dt_string_to_datetime(date),
449 .copy = dt_conf_get_bool("studio_capture/copy"),
450 .delete_source = dt_conf_get_bool("studio_capture/delete_source"),
451 .folder_survey = TRUE,
452 .on_conflict = CLAMP(dt_conf_get_int("studio_capture/on_conflict"), DT_IMPORT_ONCONFLICT_SKIP,
455 .jobcode = dt_conf_get_string("studio_capture/jobcode"),
456 .base_folder = dt_conf_get_string("studio_capture/base_directory_pattern"),
457 .target_subfolder_pattern = dt_conf_get_string("studio_capture/sub_directory_pattern"),
458 .target_file_pattern = dt_conf_get_string("studio_capture/filename_pattern"),
459 .target_dir = NULL,
460 .elements = elements,
461 .discarded = NULL,
462 .file_imported = _folder_survey_imported,
463 .callback_data = GUINT_TO_POINTER(params->generation),
464 .callback_data_free = NULL };
465 dt_free(date);
466 if(dt_control_import(data)) return 1;
467 }
468
469 return 0;
470}
471
486
490static gboolean _folder_survey_scan(gpointer user_data)
491{
494 {
496 return G_SOURCE_CONTINUE;
497 }
499
500 char *folder = dt_conf_get_string("studio_capture/folder");
501 if(IS_NULL_PTR(folder) || folder[0] == '\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
502 {
503 dt_free(folder);
504 return G_SOURCE_CONTINUE;
505 }
506
509 {
511 dt_free(folder);
512 return G_SOURCE_CONTINUE;
513 }
515 dt_folder_survey_job_t *params = malloc(sizeof(dt_folder_survey_job_t));
516 if(IS_NULL_PTR(params))
517 {
520 dt_free(folder);
521 return G_SOURCE_CONTINUE;
522 }
523 params->folder = folder;
524 params->generation = _folder_survey.generation;
526
528 if(IS_NULL_PTR(job))
529 {
531 return G_SOURCE_CONTINUE;
532 }
535 return G_SOURCE_CONTINUE;
536}
537
541static gboolean _folder_survey_scan_once(gpointer user_data)
542{
544 _folder_survey_scan(user_data);
545 return G_SOURCE_REMOVE;
546}
547
552{
553 if(_folder_survey.timer > 0)
554 {
555 g_source_remove(_folder_survey.timer);
557 }
559 {
560 g_source_remove(_folder_survey.immediate_scan);
562 }
564 const gboolean active = _folder_survey.active && !_folder_survey.shutting_down;
566 if(!active) return;
567
568 const int interval = CLAMP(dt_conf_get_int("studio_capture/interval"), 2, 3600);
569 _folder_survey.timer = g_timeout_add_seconds(interval, _folder_survey_scan, NULL);
571}
572
597
599{
600 if(!dt_conf_get_bool("studio_capture/copy")) return NULL;
601
602 char *folder = dt_conf_get_string("studio_capture/folder");
603 char *base_folder = dt_conf_get_string("studio_capture/base_directory_pattern");
604 char *subfolder = dt_conf_get_string("studio_capture/sub_directory_pattern");
605 char *file_pattern = dt_conf_get_string("studio_capture/filename_pattern");
606 char *date = dt_conf_get_string("studio_capture/datetime");
607 char *path = NULL;
608
609 const gboolean folder_valid
610 = !IS_NULL_PTR(folder) && folder[0] && g_file_test(folder, G_FILE_TEST_IS_DIR);
611 const gboolean base_valid = !IS_NULL_PTR(base_folder) && base_folder[0] && dt_util_test_writable_dir(base_folder);
612 const gboolean patterns_valid = !IS_NULL_PTR(subfolder) && subfolder[0] != '\0' && !IS_NULL_PTR(file_pattern)
613 && file_pattern[0] != '\0';
614
615 if(folder_valid && base_valid && patterns_valid)
616 {
617 if(IS_NULL_PTR(date) || date[0] == '\0')
618 {
619 dt_free(date);
620 GDateTime *now = g_date_time_new_now_local();
621 date = g_date_time_format(now, "%F");
622 g_date_time_unref(now);
623 }
624
625 char datetime_check[DT_DATETIME_LENGTH] = { 0 };
626 dt_image_t *img = malloc(sizeof(dt_image_t));
627 if(dt_datetime_entry_to_exif(datetime_check, sizeof(datetime_check), date) && !IS_NULL_PTR(img))
628 {
629 dt_image_init(img);
630 char *example = g_build_filename(folder, "example.raw", NULL);
631 dt_control_import_t data = { .imgs = g_list_prepend(NULL, g_strdup(example)),
632 .datetime = dt_string_to_datetime(date),
633 .copy = TRUE,
634 .folder_survey = TRUE,
635 .jobcode = dt_conf_get_string("studio_capture/jobcode"),
636 .base_folder = g_strdup(base_folder),
637 .target_subfolder_pattern = g_strdup(subfolder),
638 .target_file_pattern = g_strdup(file_pattern),
639 .target_dir = NULL,
640 .elements = 1,
641 .discarded = NULL };
642 path = dt_build_filename_from_pattern(example, 1, img, &data);
643
644 // A valid destination must land inside the base directory.
645 if(!IS_NULL_PTR(path) && path[0])
646 {
647 GFile *base = g_file_new_for_path(base_folder);
648 GFile *destination = g_file_new_for_path(path);
649 if(!g_file_has_prefix(destination, base))
650 {
651 dt_free(path);
652 path = NULL;
653 }
654 else
655 {
656 // Full absolute paths get unwieldy in the UI preview: show only the base
657 // directory's own name (not its whole ancestry) plus what's built under it,
658 // e.g. ".../RAW Darktable/Scan/70 nouvel an/example.raw".
659 char *relative = g_file_get_relative_path(base, destination);
660 char *base_name = g_path_get_basename(base_folder);
661 char *short_path = g_build_filename(base_name, relative, NULL);
662 dt_free(path);
663 path = g_strdup_printf(".../%s", short_path);
664 dt_free(short_path);
665 dt_free(base_name);
666 dt_free(relative);
667 }
668 g_object_unref(base);
669 g_object_unref(destination);
670 }
671 else
672 {
673 dt_free(path);
674 path = NULL;
675 }
676
677 dt_free(example);
679 }
680 dt_free(img);
681 }
682
683 dt_free(folder);
684 dt_free(base_folder);
685 dt_free(subfolder);
686 dt_free(file_pattern);
687 dt_free(date);
688 return path;
689}
690
691gboolean dt_folder_survey_can_start(const char **message)
692{
693 const char *folder = dt_conf_get_string_const("studio_capture/folder");
694 if(IS_NULL_PTR(folder) || folder[0] == '\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
695 {
696 *message = _("The folder to survey does not exist.");
697 return FALSE;
698 }
699 if(g_access(folder, R_OK | X_OK) != 0)
700 {
701 *message = _("The folder to survey is not readable.");
702 return FALSE;
703 }
704
705 const char *date = dt_conf_get_string_const("studio_capture/datetime");
706 char datetime[DT_DATETIME_LENGTH] = { 0 };
707 if(!IS_NULL_PTR(date) && date[0] && !dt_datetime_entry_to_exif(datetime, sizeof(datetime), date))
708 {
709 *message = _("The project date is invalid.");
710 return FALSE;
711 }
712
713 if(!dt_conf_get_bool("studio_capture/copy")) return TRUE;
714
715 const char *target = dt_conf_get_string_const("studio_capture/base_directory_pattern");
716 if(IS_NULL_PTR(target) || target[0] == '\0' || !g_file_test(target, G_FILE_TEST_IS_DIR))
717 {
718 *message = _("The base directory does not exist.");
719 return FALSE;
720 }
721 if(!dt_util_test_writable_dir(target))
722 {
723 *message = _("The base directory is not writable.");
724 return FALSE;
725 }
726
727 const char *subfolder = dt_conf_get_string_const("studio_capture/sub_directory_pattern");
728 if(IS_NULL_PTR(subfolder) || subfolder[0] == '\0')
729 {
730 *message = _("The project directory naming pattern is empty.");
731 return FALSE;
732 }
733 const char *file_pattern = dt_conf_get_string_const("studio_capture/filename_pattern");
734 if(IS_NULL_PTR(file_pattern) || file_pattern[0] == '\0')
735 {
736 *message = _("The file naming pattern is empty.");
737 return FALSE;
738 }
739
740 char *canonical_folder = g_canonicalize_filename(folder, NULL);
741 char *canonical_target = g_canonicalize_filename(target, NULL);
742 GFile *source_file = g_file_new_for_path(canonical_folder);
743 GFile *target_file = g_file_new_for_path(canonical_target);
744 const gboolean target_inside_source
745 = g_file_equal(source_file, target_file) || g_file_has_prefix(target_file, source_file);
746 g_object_unref(source_file);
747 g_object_unref(target_file);
748 dt_free(canonical_folder);
749 dt_free(canonical_target);
750 if(target_inside_source)
751 {
752 *message = _("The base directory destination cannot be inside the surveyed folder.");
753 return FALSE;
754 }
755
756 char *preview = dt_folder_survey_destination_preview();
757 if(IS_NULL_PTR(preview))
758 {
759 *message = _("The configured destination path is invalid.");
760 return FALSE;
761 }
762 dt_free(preview);
763
764 return TRUE;
765}
766
768{
769 if(_folder_survey.initialized) return;
770
772 _folder_survey.files = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
773 char config_dir[PATH_MAX] = { 0 };
774 dt_loc_get_user_config_dir(config_dir, sizeof(config_dir));
775 _folder_survey.state_path = g_build_filename(config_dir, DT_FOLDER_SURVEY_STATE_FILE, NULL);
777
780 char *configured_folder = dt_conf_get_string("studio_capture/folder");
781 char *canonical_folder = configured_folder && configured_folder[0]
782 ? g_canonicalize_filename(configured_folder, NULL)
783 : g_strdup("");
784 if(g_strcmp0(_folder_survey.folder, canonical_folder))
785 {
786 g_hash_table_remove_all(_folder_survey.files);
788 _folder_survey.folder = g_strdup(canonical_folder);
791 }
793 dt_free(canonical_folder);
794 dt_free(configured_folder);
795
796 // One-time seed: Studio Capture's base directory is its own setting, kept
797 // independent from the regular Import dialog past this point. But on its
798 // very first use it has never been set, so adopt whatever the regular
799 // Import dialog is currently configured with as a sensible starting value.
800 char *base_dir = dt_conf_get_string("studio_capture/base_directory_pattern");
801 if(IS_NULL_PTR(base_dir) || base_dir[0] == '\0')
802 {
803 char *default_base_dir = dt_conf_get_string("session/base_directory_pattern");
804 if(!IS_NULL_PTR(default_base_dir) && default_base_dir[0])
805 dt_conf_set_string("studio_capture/base_directory_pattern", default_base_dir);
806 dt_free(default_base_dir);
807 }
808 dt_free(base_dir);
809}
810
827{
828 const int new_files = dt_folder_survey_count_new_files();
829 if(new_files <= 0) return;
830
831 GtkWindow *parent = GTK_WINDOW(dt_ui_main_window(darktable.gui->ui));
832 GtkWidget *dialog = gtk_message_dialog_new(
833 parent, GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
834 ngettext("%d image in the surveyed folder is not in the library yet.\nImport it now?",
835 "%d images in the surveyed folder are not in the library yet.\nImport them now?",
836 new_files),
837 new_files);
838
839 GtkWidget *delete_check
840 = gtk_check_button_new_with_label(_("Delete the originals after verifying the complete copies"));
841 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(delete_check),
842 dt_conf_get_bool("studio_capture/delete_source"));
843 gtk_widget_set_sensitive(delete_check, dt_conf_get_bool("studio_capture/copy"));
844 gtk_box_pack_start(GTK_BOX(gtk_message_dialog_get_message_area(GTK_MESSAGE_DIALOG(dialog))), delete_check,
846 gtk_widget_show_all(dialog);
847
848 const int import_now = gtk_dialog_run(GTK_DIALOG(dialog));
849 if(import_now == GTK_RESPONSE_YES && dt_conf_get_bool("studio_capture/copy"))
850 dt_conf_set_bool("studio_capture/delete_source",
851 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(delete_check)));
852 gtk_widget_destroy(dialog);
853
854 if(import_now != GTK_RESPONSE_YES)
855 {
857 return;
858 }
859
862 {
865 }
867}
868
870{
872
873 const char *message = NULL;
874 if(!dt_folder_survey_can_start(&message))
875 {
876 dt_control_log("%s", message);
877 return 1;
878 }
879
880 char *configured_folder = dt_conf_get_string("studio_capture/folder");
881 char *canonical_folder = g_canonicalize_filename(configured_folder, NULL);
882 dt_conf_set_string("studio_capture/folder", canonical_folder);
883
885 if(g_strcmp0(_folder_survey.folder, canonical_folder))
886 {
887 // A different source restarts the comparison from a fresh baseline.
889 g_hash_table_remove_all(_folder_survey.files);
891 _folder_survey.folder = g_strdup(canonical_folder);
894 }
896
897 // Ask about images already in the folder before monitoring actually
898 // starts: `active` is still FALSE at this point, so no scan can run
899 // concurrently with this (blocking) prompt.
901
906
907 if(dt_conf_get_bool("studio_capture/copy"))
908 {
909 char *target = dt_conf_get_string("studio_capture/base_directory_pattern");
910 dt_control_log(_("Folder survey started: `%s` to `%s`."), canonical_folder, target);
911 dt_free(target);
912 }
913 else
914 dt_control_log(_("Folder survey started: `%s`."), canonical_folder);
915
916 dt_free(canonical_folder);
917 dt_free(configured_folder);
920 return 0;
921}
922
924{
925 if(!_folder_survey.initialized) return;
926
928 const gboolean active = _folder_survey.active;
930
932 if(active)
933 {
934 dt_control_log(_("Folder survey stopped."));
936 }
937}
938
940{
941 if(!_folder_survey.initialized) return FALSE;
942
944 const gboolean active = _folder_survey.active;
946 return active;
947}
948
953
955{
956 if(!_folder_survey.initialized) return 0;
957
958 char *folder = dt_conf_get_string("studio_capture/folder");
959 if(IS_NULL_PTR(folder) || folder[0] == '\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
960 {
961 dt_free(folder);
962 return 0;
963 }
964
965 GHashTable *observed = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
966 const int collect_error = _folder_survey_collect(folder, observed);
967 dt_free(folder);
968 if(collect_error)
969 {
970 g_hash_table_destroy(observed);
971 return 0;
972 }
973
974 int count = 0;
976 GHashTableIter iter;
977 gpointer path = NULL;
978 gpointer value = NULL;
979 g_hash_table_iter_init(&iter, observed);
980 while(g_hash_table_iter_next(&iter, &path, &value))
981 {
982 // Without a baseline every observed file is a fresh candidate (a
983 // never-before-surveyed folder has nothing to compare against yet).
984 // With a baseline, only files not already marked done count (e.g. new
985 // arrivals while the application was closed).
986 const dt_folder_survey_entry_t *entry = g_hash_table_lookup(_folder_survey.files, path);
988 count++;
989 }
991
992 g_hash_table_destroy(observed);
993 return count;
994}
995
997{
998 if(!_folder_survey.initialized) return;
999
1000 char *folder = dt_conf_get_string("studio_capture/folder");
1001 if(IS_NULL_PTR(folder) || folder[0] == '\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
1002 {
1003 dt_free(folder);
1004 return;
1005 }
1006
1007 GHashTable *observed = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
1008 const int collect_error = _folder_survey_collect(folder, observed);
1009 dt_free(folder);
1010 if(collect_error)
1011 {
1012 g_hash_table_destroy(observed);
1013 return;
1014 }
1015
1017 GHashTableIter iter;
1018 gpointer path = NULL;
1019 gpointer value = NULL;
1020 g_hash_table_iter_init(&iter, observed);
1021 while(g_hash_table_iter_next(&iter, &path, &value))
1022 {
1023 const dt_folder_survey_observation_t *observation = value;
1024 dt_folder_survey_entry_t *entry = calloc(1, sizeof(dt_folder_survey_entry_t));
1025 if(IS_NULL_PTR(entry)) continue;
1026 entry->size = observation->size;
1027 entry->mtime = observation->mtime;
1029 g_hash_table_replace(_folder_survey.files, g_strdup(path), entry);
1030 }
1034
1035 g_hash_table_destroy(observed);
1036}
1037
1039{
1040 if(!dt_folder_survey_session_was_active()) return G_SOURCE_REMOVE;
1042
1043 char *folder = dt_conf_get_string("studio_capture/folder");
1044 GtkWindow *parent = GTK_WINDOW(dt_ui_main_window(darktable.gui->ui));
1045
1046 GtkWidget *dialog = gtk_message_dialog_new(
1047 parent, GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
1048 _("A studio capture session was monitoring `%s` when Ansel was last closed.\n"
1049 "Resume the session?"),
1050 folder ? folder : "");
1051 const int resume = gtk_dialog_run(GTK_DIALOG(dialog));
1052 gtk_widget_destroy(dialog);
1053
1054 if(resume != GTK_RESPONSE_YES)
1055 {
1056 // Clear the persisted marker so the question is not asked again.
1060 dt_free(folder);
1061 return G_SOURCE_REMOVE;
1062 }
1063
1065
1066 // dt_folder_survey_start() itself offers to import any images already
1067 // sitting in the folder, covering files that appeared while Ansel was
1068 // closed the same way it covers a plain session start.
1070 dt_free(folder);
1071 return G_SOURCE_REMOVE;
1072}
1073
1092
1094{
1096
1097 if(_folder_survey.timer > 0)
1098 {
1099 g_source_remove(_folder_survey.timer);
1101 }
1103 {
1104 g_source_remove(_folder_survey.immediate_scan);
1106 }
1107
1109 // Application shutdown, NOT a user stop: persist the active flag as-is so an
1110 // interrupted session can be proposed for resume on the next start.
1111 // shutting_down gates every scan and import path from here on.
1115}
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void dt_image_init(dt_image_t *img)
char * key
int type
char * name
void dt_conf_set_bool(const char *name, int val)
int dt_conf_get_bool(const char *name)
gchar * dt_conf_get_string(const char *name)
int dt_conf_get_int(const char *name)
void dt_conf_set_string(const char *name, const char *val)
const char * dt_conf_get_string_const(const char *name)
void dt_control_log(const char *msg,...)
Definition control.c:777
uint32_t view(const dt_view_t *self)
Definition darkroom.c:228
darktable_t darktable
Definition darktable.c:183
gboolean dt_supported_image(const gchar *filename)
check if file is a supported image
Definition darktable.c:350
static void dt_free_gpointer(gpointer ptr)
Definition darktable.h:475
#define dt_free(ptr)
Definition darktable.h:468
static const dt_aligned_pixel_simd_t value
Definition darktable.h:589
#define PATH_MAX
Definition darktable.h:1179
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition darktable.h:293
GDateTime * dt_string_to_datetime(const char *string)
Definition datetime.c:307
gboolean dt_datetime_entry_to_exif(char *exif, const size_t exif_size, const char *entry)
Definition datetime.c:319
#define DT_DATETIME_LENGTH
Definition datetime.h:37
static int dt_pthread_mutex_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:384
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Definition dtpthread.h:369
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:389
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:374
void dt_loc_get_user_config_dir(char *configdir, size_t bufsize)
static void _folder_survey_job_cleanup(void *data)
Release one scan job and allow the next periodic scan to start.
static gboolean _folder_survey_scan(gpointer user_data)
Queue one background scan without overlapping the previous scan.
static void _folder_survey_imported(const char *source, const gboolean success, gpointer user_data)
Update one persisted entry when its asynchronous import completes.
static int32_t _folder_survey_job_run(dt_job_t *job)
Compare the current directory contents with the prior survey loop.
static void _folder_survey_reschedule()
Recreate the periodic source after a frequency or state change.
dt_folder_survey_file_state_t
@ DT_FOLDER_SURVEY_FILE_QUEUED
@ DT_FOLDER_SURVEY_FILE_PENDING
@ DT_FOLDER_SURVEY_FILE_DONE
int dt_folder_survey_start()
Validate the persisted configuration and start monitoring.
void dt_folder_survey_cleanup()
Release folder survey state after control workers have stopped.
gboolean dt_folder_survey_session_was_active()
TRUE when the previous application session quit while monitoring.
char * dt_folder_survey_destination_preview()
Build the expanded destination path of a sample file from the current configuration,...
static dt_folder_survey_t _folder_survey
gboolean dt_folder_survey_is_active()
TRUE while the periodic folder scan is running.
static gboolean _folder_survey_scan_once(gpointer user_data)
Run the immediate post-configuration scan only once.
#define DT_FOLDER_SURVEY_STATE_FILE
void dt_folder_survey_stop()
Stop new scans before control workers begin shutting down.
static void _folder_survey_offer_pending_import()
Ask whether images already sitting in the source folder should be imported right away,...
void dt_folder_survey_absorb_new_files()
Record every file currently in the surveyed folder as already handled, so restarting the survey will ...
static int _folder_survey_collect(const char *folder, GHashTable *observed)
Recursively collect supported images and their stability metadata.
gboolean dt_folder_survey_can_start(const char **message)
Check, without any side effect, whether the persisted configuration has everything dt_folder_survey_s...
static int _folder_survey_save_locked()
Persist the complete survey baseline while the survey lock is held.
void dt_folder_survey_init()
Initialize the folder survey state from the persisted configuration.
static GList * _folder_survey_styles_for_import()
Read the ordered auto-apply style list from conf.
void dt_folder_survey_halt()
User-requested stop: end monitoring and clear the persisted session marker so the next application st...
gboolean dt_folder_survey_propose_resume()
Propose to resume an interrupted studio session at application start.
static void _folder_survey_deactivate()
Stop periodic scans without discarding the persisted comparison state.
static int _folder_survey_load_locked()
Load the previous file list and convert interrupted imports to retries.
int dt_folder_survey_count_new_files()
Count files in the surveyed folder that are unknown to the persisted baseline, i.e....
#define DT_FOLDER_SURVEY_STYLES_SEPARATOR
#define DT_FOLDER_SURVEY_STYLES_CONF_KEY
GtkWidget * dt_ui_main_window(dt_ui_t *ui)
get the main window widget
#define DT_GUI_BOX_SPACING
Definition gtk.h:109
gchar * dt_build_filename_from_pattern(const char *const filename, const int index, dt_image_t *img, dt_control_import_t *data)
Build a full path for a given image file, given a pattern.
Definition import_jobs.c:76
int dt_control_import(dt_control_import_t data)
Process a list of images to import with or without copying the files on an arbitrary hard-drive.
void dt_control_import_data_free(dt_control_import_t *data)
@ DT_IMPORT_ONCONFLICT_UNIQUE
Definition import_jobs.h:39
@ DT_IMPORT_ONCONFLICT_SKIP
Definition import_jobs.h:37
dt_job_t * dt_control_job_create(dt_job_execute_callback execute, const char *msg,...)
Definition jobs.c:135
int dt_control_add_job(dt_control_t *control, dt_job_queue_t queue_id, _dt_job_t *job)
Definition jobs.c:405
void * dt_control_job_get_params(const _dt_job_t *job)
Definition jobs.c:129
void dt_control_job_set_params(_dt_job_t *job, void *params, dt_job_destroy_callback callback)
Definition jobs.c:112
@ DT_JOB_QUEUE_SYSTEM_BG
Definition jobs.h:57
float *const restrict const size_t k
int groups()
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
Definition signal.h:366
@ DT_SIGNAL_FOLDER_SURVEY_CHANGED
Raised when the folder survey starts or stops monitoring. no param, no returned value.
Definition signal.h:330
struct _GtkWidget GtkWidget
Definition splash.h:29
const float uint32_t state[4]
struct dt_gui_gtk_t * gui
Definition darktable.h:793
struct dt_control_signal_t * signals
Definition darktable.h:792
struct dt_view_manager_t * view_manager
Definition darktable.h:790
struct dt_control_t * control
Definition darktable.h:791
gint64 mtime
guint64 size
dt_folder_survey_file_state_t state
int stable_scans
dt_pthread_mutex_t lock
GHashTable * files
gboolean baseline_initialized
gboolean was_active_last_session
dt_ui_t * ui
Definition gtk.h:165
gboolean dt_util_test_writable_dir(const char *path)
Definition utility.c:344
int dt_view_manager_switch(dt_view_manager_t *vm, const char *view_name)
Definition view.c:236
const dt_view_t * dt_view_manager_get_current_view(dt_view_manager_t *vm)
Definition view.c:141