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
16#include "system/mem_alloc.h"
17#include "common/paths.h" // DT_PATH_MAX
19#include "control/settings.h"
20#include <glib/gstdio.h>
22
23#include "common/datetime.h"
25#include "common/conf.h"
26#include "control/control.h"
27#include "control/jobs.h"
29
30#include <gio/gio.h>
31#include "common/utility.h"
32#include "common/logging.h"
33
34#define DT_FOLDER_SURVEY_STATE_FILE "folder-survey-state.ini"
35
42
50
56
62
63typedef struct dt_folder_survey_t
64{
65 dt_pthread_mutex_t lock;
66 GHashTable *files;
67 char *folder;
70 guint timer;
72 gboolean initialized;
73 gboolean active;
75 gboolean scan_running;
76 gboolean shutting_down;
77 // TRUE when the previous application session quit while monitoring.
80
82
90{
91 GKeyFile *state = g_key_file_new();
92 g_key_file_set_string(state, "survey", "folder", _folder_survey.folder ? _folder_survey.folder : "");
93 g_key_file_set_boolean(state, "survey", "initialized", _folder_survey.baseline_initialized);
94 // Session marker: an application shutdown while monitoring leaves this TRUE
95 // so the next start can propose to resume the studio session.
96 g_key_file_set_boolean(state, "survey", "active", _folder_survey.active);
97
98 GHashTableIter iter;
99 gpointer key = NULL;
100 gpointer value = NULL;
101 g_hash_table_iter_init(&iter, _folder_survey.files);
102 // Serialize every known image path and the metadata used to detect changes.
103 while(g_hash_table_iter_next(&iter, &key, &value))
104 {
105 const char *path = (const char *)key;
107 char *group = g_compute_checksum_for_string(G_CHECKSUM_SHA256, path, -1);
108 g_key_file_set_string(state, group, "path", path);
109 g_key_file_set_uint64(state, group, "size", entry->size);
110 g_key_file_set_int64(state, group, "mtime", entry->mtime);
111 g_key_file_set_integer(state, group, "stable_scans", entry->stable_scans);
112 g_key_file_set_integer(state, group, "state", entry->state);
113 dt_free(group);
114 }
115
116 gsize length = 0;
117 gchar *contents = g_key_file_to_data(state, &length, NULL);
118 GFile *file = g_file_new_for_path(_folder_survey.state_path);
119 GError *error = NULL;
120 const gboolean success = g_file_replace_contents(file, contents, length, NULL, FALSE,
121 G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, &error);
122 if(!success)
123 {
124 fprintf(stderr, "[folder survey] failed to save state: %s\n", error ? error->message : "unknown error");
125 g_clear_error(&error);
126 }
127
128 g_object_unref(file);
129 dt_free(contents);
130 g_key_file_unref(state);
131 return success ? 0 : 1;
132}
133
138{
139 GKeyFile *state = g_key_file_new();
140 GError *error = NULL;
141 if(!g_key_file_load_from_file(state, _folder_survey.state_path, G_KEY_FILE_NONE, &error))
142 {
143 g_clear_error(&error);
144 g_key_file_unref(state);
145 return 0;
146 }
147
149 _folder_survey.folder = g_key_file_get_string(state, "survey", "folder", NULL);
150 _folder_survey.baseline_initialized = g_key_file_get_boolean(state, "survey", "initialized", NULL);
151 _folder_survey.was_active_last_session = g_key_file_get_boolean(state, "survey", "active", NULL);
152
153 gsize groups_count = 0;
154 gchar **groups = g_key_file_get_groups(state, &groups_count);
155 // Restore one image observation from each hashed path group.
156 for(gsize k = 0; k < groups_count; k++)
157 {
158 if(!strcmp(groups[k], "survey")) continue;
159
160 char *path = g_key_file_get_string(state, groups[k], "path", NULL);
161 if(IS_NULL_PTR(path) || path[0] == '\0')
162 {
163 dt_free(path);
164 continue;
165 }
166
167 dt_folder_survey_entry_t *entry = calloc(1, sizeof(dt_folder_survey_entry_t));
168 if(IS_NULL_PTR(entry))
169 {
170 dt_free(path);
171 continue;
172 }
173
174 entry->size = g_key_file_get_uint64(state, groups[k], "size", NULL);
175 entry->mtime = g_key_file_get_int64(state, groups[k], "mtime", NULL);
176 entry->stable_scans = g_key_file_get_integer(state, groups[k], "stable_scans", NULL);
177 entry->state = g_key_file_get_integer(state, groups[k], "state", NULL);
180 {
182 entry->stable_scans = 0;
183 }
184 g_hash_table_replace(_folder_survey.files, path, entry);
185 }
186
187 g_strfreev(groups);
188 g_key_file_unref(state);
189 return 0;
190}
191
198static int _folder_survey_collect(const char *folder, GHashTable *observed)
199{
200 GQueue folders = G_QUEUE_INIT;
201 g_queue_push_tail(&folders, g_file_new_for_path(folder));
202 int error = 0;
203
204 // Traverse every directory below the configured ingest root.
205 while(!g_queue_is_empty(&folders))
206 {
207 GFile *current = g_queue_pop_head(&folders);
208 GError *enumeration_error = NULL;
209 GFileEnumerator *enumerator = g_file_enumerate_children(
210 current,
211 G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE "," G_FILE_ATTRIBUTE_STANDARD_SIZE
212 "," G_FILE_ATTRIBUTE_TIME_MODIFIED,
213 G_FILE_QUERY_INFO_NONE, NULL, &enumeration_error);
214 g_object_unref(current);
215
216 if(IS_NULL_PTR(enumerator))
217 {
218 fprintf(stderr, "[folder survey] failed to enumerate folder: %s\n",
219 enumeration_error ? enumeration_error->message : "unknown error");
220 g_clear_error(&enumeration_error);
221 error = 1;
222 break;
223 }
224
225 GFileInfo *info = NULL;
226 GFile *child = NULL;
227 // Record supported regular images and queue child directories for traversal.
228 while(g_file_enumerator_iterate(enumerator, &info, &child, NULL, &enumeration_error))
229 {
230 if(IS_NULL_PTR(info) || IS_NULL_PTR(child)) break;
231
232 const GFileType type = g_file_info_get_file_type(info);
233 if(type == G_FILE_TYPE_DIRECTORY)
234 {
235 g_queue_push_tail(&folders, g_object_ref(child));
236 continue;
237 }
238 if(type != G_FILE_TYPE_REGULAR) continue;
239
240 char *path = g_file_get_path(child);
241 if(IS_NULL_PTR(path) || !dt_supported_image(path))
242 {
243 dt_free(path);
244 continue;
245 }
246
247 char *canonical_path = g_canonicalize_filename(path, NULL);
249 if(!IS_NULL_PTR(observation))
250 {
251 observation->size = g_file_info_get_size(info);
252 observation->mtime = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
253 g_hash_table_replace(observed, canonical_path, observation);
254 canonical_path = NULL;
255 }
256 dt_free(canonical_path);
257 dt_free(path);
258 }
259
260 if(!IS_NULL_PTR(enumeration_error))
261 {
262 fprintf(stderr, "[folder survey] failed while enumerating files: %s\n", enumeration_error->message);
263 g_clear_error(&enumeration_error);
264 error = 1;
265 }
266 g_object_unref(enumerator);
267 if(error) break;
268 }
269
270 while(!g_queue_is_empty(&folders)) g_object_unref(g_queue_pop_head(&folders));
271 return error;
272}
273
282{
284 if(IS_NULL_PTR(view)) return NULL;
285
287 if(IS_NULL_PTR(conf) || conf[0] == '\0')
288 {
289 dt_free(conf);
290 return NULL;
291 }
292
293 GList *styles = NULL;
294 gchar **names = g_strsplit(conf, DT_FOLDER_SURVEY_STYLES_SEPARATOR, -1);
295 for(gchar **name = names; *name; name++)
296 if((*name)[0] != '\0') styles = g_list_prepend(styles, g_strdup(*name));
297
298 g_strfreev(names);
299 dt_free(conf);
300 return g_list_reverse(styles);
301}
302
306static void _folder_survey_imported(const char *source, const gboolean success, gpointer user_data)
307{
308 const guint generation = GPOINTER_TO_UINT(user_data);
309 char *path = g_canonicalize_filename(source, NULL);
310
312 if(generation == _folder_survey.generation)
313 {
314 dt_folder_survey_entry_t *entry = g_hash_table_lookup(_folder_survey.files, path);
315 if(!IS_NULL_PTR(entry))
316 {
318 entry->stable_scans = success ? entry->stable_scans : 0;
320 }
321 }
323
324 dt_free(path);
325}
326
335{
337 GHashTable *observed = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
338 if(_folder_survey_collect(params->folder, observed))
339 {
340 g_hash_table_destroy(observed);
341 return 1;
342 }
343
344 GList *imports = NULL;
347 {
349 g_hash_table_destroy(observed);
350 return 0;
351 }
352
353 GHashTableIter previous_iter;
354 gpointer previous_path = NULL;
355 gpointer previous_entry = NULL;
356 g_hash_table_iter_init(&previous_iter, _folder_survey.files);
357 // Forget paths removed since the preceding scan so they can be detected if they reappear.
358 while(g_hash_table_iter_next(&previous_iter, &previous_path, &previous_entry))
359 {
360 if(!g_hash_table_contains(observed, previous_path)) g_hash_table_iter_remove(&previous_iter);
361 }
362
363 GHashTableIter observed_iter;
364 gpointer observed_path = NULL;
365 gpointer observed_value = NULL;
366 g_hash_table_iter_init(&observed_iter, observed);
367 // Compare each current image with its last size, timestamp, and import state.
368 while(g_hash_table_iter_next(&observed_iter, &observed_path, &observed_value))
369 {
370 const dt_folder_survey_observation_t *observation = observed_value;
371 dt_folder_survey_entry_t *entry = g_hash_table_lookup(_folder_survey.files, observed_path);
372
374 {
375 entry = calloc(1, sizeof(dt_folder_survey_entry_t));
376 if(IS_NULL_PTR(entry)) continue;
377 entry->size = observation->size;
378 entry->mtime = observation->mtime;
380 g_hash_table_replace(_folder_survey.files, g_strdup(observed_path), entry);
381 continue;
382 }
383
384 if(IS_NULL_PTR(entry))
385 {
386 entry = calloc(1, sizeof(dt_folder_survey_entry_t));
387 if(IS_NULL_PTR(entry)) continue;
388 entry->size = observation->size;
389 entry->mtime = observation->mtime;
391 g_hash_table_replace(_folder_survey.files, g_strdup(observed_path), entry);
392 continue;
393 }
394
396 {
397 // A producer may reuse a filename after the previous image was handled.
398 // Treat changed metadata at the same path as a new pending file.
399 if(entry->size != observation->size || entry->mtime != observation->mtime)
400 {
401 entry->size = observation->size;
402 entry->mtime = observation->mtime;
403 entry->stable_scans = 0;
405 }
406 continue;
407 }
408
409 if(entry->state == DT_FOLDER_SURVEY_FILE_QUEUED) continue;
410
411 if(entry->size != observation->size || entry->mtime != observation->mtime)
412 {
413 entry->size = observation->size;
414 entry->mtime = observation->mtime;
415 entry->stable_scans = 0;
416 continue;
417 }
418
419 entry->stable_scans++;
420 if(entry->stable_scans >= 1)
421 {
423 imports = g_list_prepend(imports, g_strdup(observed_path));
424 }
425 }
426
430 g_hash_table_destroy(observed);
431
432 if(!IS_NULL_PTR(imports))
433 {
434 imports = g_list_sort(imports, (GCompareFunc)g_strcmp0);
435 const int elements = g_list_length(imports);
436 dt_control_log(ngettext("Folder survey found %d new image to import.",
437 "Folder survey found %d new images to import.", elements),
438 elements);
439
440 char *date = dt_conf_get_string("studio_capture/datetime");
441 if(IS_NULL_PTR(date) || date[0] == '\0')
442 {
443 dt_free(date);
444 GDateTime *now = g_date_time_new_now_local();
445 date = g_date_time_format(now, "%F");
446 g_date_time_unref(now);
447 }
448
450 = { .imgs = imports,
451 .datetime = dt_string_to_datetime(date),
452 .copy = dt_conf_get_bool("studio_capture/copy"),
453 .delete_source = dt_conf_get_bool("studio_capture/delete_source"),
454 .folder_survey = TRUE,
455 .on_conflict = CLAMP(dt_conf_get_int("studio_capture/on_conflict"), DT_IMPORT_ONCONFLICT_SKIP,
458 .jobcode = dt_conf_get_string("studio_capture/jobcode"),
459 .base_folder = dt_conf_get_string("studio_capture/base_directory_pattern"),
460 .target_subfolder_pattern = dt_conf_get_string("studio_capture/sub_directory_pattern"),
461 .target_file_pattern = dt_conf_get_string("studio_capture/filename_pattern"),
462 .target_dir = NULL,
463 .elements = elements,
464 .discarded = NULL,
465 .file_imported = _folder_survey_imported,
466 .callback_data = GUINT_TO_POINTER(params->generation),
467 .callback_data_free = NULL };
468 dt_free(date);
469 if(dt_control_import(data)) return 1;
470 }
471
472 return 0;
473}
474
489
493static gboolean _folder_survey_scan(gpointer user_data)
494{
497 {
499 return G_SOURCE_CONTINUE;
500 }
502
503 char *folder = dt_conf_get_string("studio_capture/folder");
504 if(IS_NULL_PTR(folder) || folder[0] == '\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
505 {
507 return G_SOURCE_CONTINUE;
508 }
509
512 {
515 return G_SOURCE_CONTINUE;
516 }
518 dt_folder_survey_job_t *params = malloc(sizeof(dt_folder_survey_job_t));
519 if(IS_NULL_PTR(params))
520 {
524 return G_SOURCE_CONTINUE;
525 }
526 params->folder = folder;
527 params->generation = _folder_survey.generation;
529
531 if(IS_NULL_PTR(job))
532 {
534 return G_SOURCE_CONTINUE;
535 }
538 return G_SOURCE_CONTINUE;
539}
540
544static gboolean _folder_survey_scan_once(gpointer user_data)
545{
547 _folder_survey_scan(user_data);
548 return G_SOURCE_REMOVE;
549}
550
555{
556 if(_folder_survey.timer > 0)
557 {
558 g_source_remove(_folder_survey.timer);
560 }
562 {
563 g_source_remove(_folder_survey.immediate_scan);
565 }
567 const gboolean active = _folder_survey.active && !_folder_survey.shutting_down;
569 if(!active) return;
570
571 const int interval = CLAMP(dt_conf_get_int("studio_capture/interval"), 2, 3600);
572 _folder_survey.timer = g_timeout_add_seconds(interval, _folder_survey_scan, NULL);
574}
575
600
602{
603 if(!dt_conf_get_bool("studio_capture/copy")) return NULL;
604
605 char *folder = dt_conf_get_string("studio_capture/folder");
606 char *base_folder = dt_conf_get_string("studio_capture/base_directory_pattern");
607 char *subfolder = dt_conf_get_string("studio_capture/sub_directory_pattern");
608 char *file_pattern = dt_conf_get_string("studio_capture/filename_pattern");
609 char *date = dt_conf_get_string("studio_capture/datetime");
610 char *path = NULL;
611
612 const gboolean folder_valid
613 = !IS_NULL_PTR(folder) && folder[0] && g_file_test(folder, G_FILE_TEST_IS_DIR);
614 const gboolean base_valid = !IS_NULL_PTR(base_folder) && base_folder[0] && dt_util_test_writable_dir(base_folder);
615 const gboolean patterns_valid = !IS_NULL_PTR(subfolder) && subfolder[0] != '\0' && !IS_NULL_PTR(file_pattern)
616 && file_pattern[0] != '\0';
617
618 if(folder_valid && base_valid && patterns_valid)
619 {
620 if(IS_NULL_PTR(date) || date[0] == '\0')
621 {
622 dt_free(date);
623 GDateTime *now = g_date_time_new_now_local();
624 date = g_date_time_format(now, "%F");
625 g_date_time_unref(now);
626 }
627
628 char datetime_check[DT_DATETIME_LENGTH] = { 0 };
629 dt_image_t *img = dt_alloc_align(sizeof(dt_image_t)); // dt_image_t is 64-aligned, see #1212
630 if(dt_datetime_entry_to_exif(datetime_check, sizeof(datetime_check), date) && !IS_NULL_PTR(img))
631 {
632 dt_image_init(img);
633 char *example = g_build_filename(folder, "example.raw", NULL);
634 dt_control_import_t data = { .imgs = g_list_prepend(NULL, g_strdup(example)),
635 .datetime = dt_string_to_datetime(date),
636 .copy = TRUE,
637 .folder_survey = TRUE,
638 .jobcode = dt_conf_get_string("studio_capture/jobcode"),
639 .base_folder = g_strdup(base_folder),
640 .target_subfolder_pattern = g_strdup(subfolder),
641 .target_file_pattern = g_strdup(file_pattern),
642 .target_dir = NULL,
643 .elements = 1,
644 .discarded = NULL };
645 path = dt_build_filename_from_pattern(example, 1, img, &data);
646
647 // A valid destination must land inside the base directory.
648 if(!IS_NULL_PTR(path) && path[0])
649 {
650 GFile *base = g_file_new_for_path(base_folder);
651 GFile *destination = g_file_new_for_path(path);
652 if(!g_file_has_prefix(destination, base))
653 {
654 dt_free(path);
655 path = NULL;
656 }
657 else
658 {
659 // Full absolute paths get unwieldy in the UI preview: show only the base
660 // directory's own name (not its whole ancestry) plus what's built under it,
661 // e.g. ".../RAW Darktable/Scan/70 nouvel an/example.raw".
662 char *relative = g_file_get_relative_path(base, destination);
663 char *base_name = g_path_get_basename(base_folder);
664 char *short_path = g_build_filename(base_name, relative, NULL);
665 dt_free(path);
666 path = g_strdup_printf(".../%s", short_path);
667 dt_free(short_path);
668 dt_free(base_name);
669 dt_free(relative);
670 }
671 g_object_unref(base);
672 g_object_unref(destination);
673 }
674 else
675 {
676 dt_free(path);
677 path = NULL;
678 }
679
680 dt_free(example);
682 }
683 dt_free_align(img);
684 }
685
687 dt_free(base_folder);
688 dt_free(subfolder);
689 dt_free(file_pattern);
690 dt_free(date);
691 return path;
692}
693
694gboolean dt_folder_survey_can_start(const char **message)
695{
696 const char *folder = dt_conf_get_string_const("studio_capture/folder");
697 if(IS_NULL_PTR(folder) || folder[0] == '\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
698 {
699 *message = _("The folder to survey does not exist.");
700 return FALSE;
701 }
702 if(g_access(folder, R_OK | X_OK) != 0)
703 {
704 *message = _("The folder to survey is not readable.");
705 return FALSE;
706 }
707
708 const char *date = dt_conf_get_string_const("studio_capture/datetime");
709 char datetime[DT_DATETIME_LENGTH] = { 0 };
710 if(!IS_NULL_PTR(date) && date[0] && !dt_datetime_entry_to_exif(datetime, sizeof(datetime), date))
711 {
712 *message = _("The project date is invalid.");
713 return FALSE;
714 }
715
716 if(!dt_conf_get_bool("studio_capture/copy")) return TRUE;
717
718 const char *target = dt_conf_get_string_const("studio_capture/base_directory_pattern");
719 if(IS_NULL_PTR(target) || target[0] == '\0' || !g_file_test(target, G_FILE_TEST_IS_DIR))
720 {
721 *message = _("The base directory does not exist.");
722 return FALSE;
723 }
724 if(!dt_util_test_writable_dir(target))
725 {
726 *message = _("The base directory is not writable.");
727 return FALSE;
728 }
729
730 const char *subfolder = dt_conf_get_string_const("studio_capture/sub_directory_pattern");
731 if(IS_NULL_PTR(subfolder) || subfolder[0] == '\0')
732 {
733 *message = _("The project directory naming pattern is empty.");
734 return FALSE;
735 }
736 const char *file_pattern = dt_conf_get_string_const("studio_capture/filename_pattern");
737 if(IS_NULL_PTR(file_pattern) || file_pattern[0] == '\0')
738 {
739 *message = _("The file naming pattern is empty.");
740 return FALSE;
741 }
742
743 char *canonical_folder = g_canonicalize_filename(folder, NULL);
744 char *canonical_target = g_canonicalize_filename(target, NULL);
745 GFile *source_file = g_file_new_for_path(canonical_folder);
746 GFile *target_file = g_file_new_for_path(canonical_target);
747 const gboolean target_inside_source
748 = g_file_equal(source_file, target_file) || g_file_has_prefix(target_file, source_file);
749 g_object_unref(source_file);
750 g_object_unref(target_file);
751 dt_free(canonical_folder);
752 dt_free(canonical_target);
753 if(target_inside_source)
754 {
755 *message = _("The base directory destination cannot be inside the surveyed folder.");
756 return FALSE;
757 }
758
761 {
762 *message = _("The configured destination path is invalid.");
763 return FALSE;
764 }
766
767 return TRUE;
768}
769
771{
772 if(_folder_survey.initialized) return;
773
775 _folder_survey.files = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
776 char config_dir[DT_PATH_MAX] = { 0 };
777 dt_loc_get_user_config_dir(config_dir, sizeof(config_dir));
778 _folder_survey.state_path = g_build_filename(config_dir, DT_FOLDER_SURVEY_STATE_FILE, NULL);
780
783 char *configured_folder = dt_conf_get_string("studio_capture/folder");
784 char *canonical_folder = configured_folder && configured_folder[0]
785 ? g_canonicalize_filename(configured_folder, NULL)
786 : g_strdup("");
787 if(g_strcmp0(_folder_survey.folder, canonical_folder))
788 {
789 g_hash_table_remove_all(_folder_survey.files);
791 _folder_survey.folder = g_strdup(canonical_folder);
794 }
796 dt_free(canonical_folder);
797 dt_free(configured_folder);
798
799 // One-time seed: Studio Capture's base directory is its own setting, kept
800 // independent from the regular Import dialog past this point. But on its
801 // very first use it has never been set, so adopt whatever the regular
802 // Import dialog is currently configured with as a sensible starting value.
803 char *base_dir = dt_conf_get_string("studio_capture/base_directory_pattern");
804 if(IS_NULL_PTR(base_dir) || base_dir[0] == '\0')
805 {
806 char *default_base_dir = dt_conf_get_string("session/base_directory_pattern");
807 if(!IS_NULL_PTR(default_base_dir) && default_base_dir[0])
808 dt_conf_set_string("studio_capture/base_directory_pattern", default_base_dir);
809 dt_free(default_base_dir);
810 }
811 dt_free(base_dir);
812}
813
815
820
822{
823 const int new_files = dt_folder_survey_count_new_files();
824 if(new_files <= 0) return;
825
827 {
828 // Nobody registered to ask. Do NOT absorb: absorbing is the answer to a question that
829 // was never put, and it would silently make these files invisible to every later scan.
830 // Leaving the state untouched keeps them pending, to be offered whenever someone can ask.
832 "[folder_survey] %d new file(s) pending, but no handler to ask about importing them\n",
833 new_files);
834 return;
835 }
836
837 if(!_confirm_import_handler(new_files))
838 {
839 // Declined: absorb every currently observed file into the baseline so a later scan does
840 // not import it behind the user's back.
842 return;
843 }
844
845 // Accepted on a folder with no baseline yet: seed an initialized, empty one so the next
846 // scan treats those files as new pending imports instead of silently absorbing them.
849 {
852 }
854}
855
857{
859
860 const char *message = NULL;
861 if(!dt_folder_survey_can_start(&message))
862 {
863 dt_control_log("%s", message);
864 return 1;
865 }
866
867 char *configured_folder = dt_conf_get_string("studio_capture/folder");
868 char *canonical_folder = g_canonicalize_filename(configured_folder, NULL);
869 dt_conf_set_string("studio_capture/folder", canonical_folder);
870
872 if(g_strcmp0(_folder_survey.folder, canonical_folder))
873 {
874 // A different source restarts the comparison from a fresh baseline.
876 g_hash_table_remove_all(_folder_survey.files);
878 _folder_survey.folder = g_strdup(canonical_folder);
881 }
883
884 // Ask about images already in the folder before monitoring actually
885 // starts: `active` is still FALSE at this point, so no scan can run
886 // concurrently with this (blocking) prompt.
888
893
894 if(dt_conf_get_bool("studio_capture/copy"))
895 {
896 char *target = dt_conf_get_string("studio_capture/base_directory_pattern");
897 dt_control_log(_("Folder survey started: `%s` to `%s`."), canonical_folder, target);
898 dt_free(target);
899 }
900 else
901 dt_control_log(_("Folder survey started: `%s`."), canonical_folder);
902
903 dt_free(canonical_folder);
904 dt_free(configured_folder);
907 return 0;
908}
909
925
927{
928 if(!_folder_survey.initialized) return FALSE;
929
931 const gboolean active = _folder_survey.active;
933 return active;
934}
935
940
942{
943 if(!_folder_survey.initialized) return 0;
944
945 char *folder = dt_conf_get_string("studio_capture/folder");
946 if(IS_NULL_PTR(folder) || folder[0] == '\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
947 {
949 return 0;
950 }
951
952 GHashTable *observed = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
953 const int collect_error = _folder_survey_collect(folder, observed);
955 if(collect_error)
956 {
957 g_hash_table_destroy(observed);
958 return 0;
959 }
960
961 int count = 0;
963 GHashTableIter iter;
964 gpointer path = NULL;
965 gpointer value = NULL;
966 g_hash_table_iter_init(&iter, observed);
967 while(g_hash_table_iter_next(&iter, &path, &value))
968 {
969 // Without a baseline every observed file is a fresh candidate (a
970 // never-before-surveyed folder has nothing to compare against yet).
971 // With a baseline, only files not already marked done count (e.g. new
972 // arrivals while the application was closed).
973 const dt_folder_survey_entry_t *entry = g_hash_table_lookup(_folder_survey.files, path);
975 count++;
976 }
978
979 g_hash_table_destroy(observed);
980 return count;
981}
982
984{
985 if(!_folder_survey.initialized) return;
986
987 char *folder = dt_conf_get_string("studio_capture/folder");
988 if(IS_NULL_PTR(folder) || folder[0] == '\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
989 {
991 return;
992 }
993
994 GHashTable *observed = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
995 const int collect_error = _folder_survey_collect(folder, observed);
997 if(collect_error)
998 {
999 g_hash_table_destroy(observed);
1000 return;
1001 }
1002
1004 GHashTableIter iter;
1005 gpointer path = NULL;
1006 gpointer value = NULL;
1007 g_hash_table_iter_init(&iter, observed);
1008 while(g_hash_table_iter_next(&iter, &path, &value))
1009 {
1010 const dt_folder_survey_observation_t *observation = value;
1011 dt_folder_survey_entry_t *entry = calloc(1, sizeof(dt_folder_survey_entry_t));
1012 if(IS_NULL_PTR(entry)) continue;
1013 entry->size = observation->size;
1014 entry->mtime = observation->mtime;
1016 g_hash_table_replace(_folder_survey.files, g_strdup(path), entry);
1017 }
1021
1022 g_hash_table_destroy(observed);
1023}
1024
1026{
1027 // Consumed, not merely read: the resume question must be asked at most once per start,
1028 // whatever the answer, so clearing here is the point rather than a side effect.
1031 return TRUE;
1032}
1033
1040
1059
1061{
1063
1064 if(_folder_survey.timer > 0)
1065 {
1066 g_source_remove(_folder_survey.timer);
1068 }
1070 {
1071 g_source_remove(_folder_survey.immediate_scan);
1073 }
1074
1076 // Application shutdown, NOT a user stop: persist the active flag as-is so an
1077 // interrupted session can be proposed for resume on the next start.
1078 // shutting_down gates every scan and import path from here on.
1082}
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
int dt_conf_get_bool(const char *name)
gchar * dt_conf_get_string(const char *name)
Read the stored string for name as a private copy.
int dt_conf_get_int(const char *name)
Integer for name, clamped to the bounds declared in the XML.
void dt_conf_set_string(const char *name, const char *val)
const char * dt_conf_get_string_const(const char *name)
Borrow the stored string for name without copying it.
void dt_image_init(dt_image_t *img)
void dt_control_log(const char *msg,...)
Definition control.c:824
struct dt_control_t * dt_control_get_global(void)
Definition darktable.c:651
struct dt_view_manager_t * dt_view_manager_get_global(void)
Definition darktable.c:599
void * dt_alloc_align(size_t size)
Allocate cacheline-aligned memory.
Definition darktable.c:508
GDateTime * dt_string_to_datetime(const char *string)
Definition datetime.c:321
gboolean dt_datetime_entry_to_exif(char *exif, const size_t exif_size, const char *entry)
Definition datetime.c:333
#define DT_DATETIME_LENGTH
Definition datetime.h:38
GHashTable * names
GtkWidget* -> the name the application knows it by.
GtkWidget * preview
what the selected row actually captures
GtkWidget * folder
destination folder chooser
GtkWidget * view
the tree view
static int dt_pthread_mutex_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:127
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Initialise a mutex. With mutexattr NULL – which is how 54 of the 56 call sites in this tree spell it ...
Definition dtpthread.h:104
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:132
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:117
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.
static dt_folder_survey_confirm_import_handler_t _confirm_import_handler
#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()
void dt_folder_survey_forget_session()
Persist the cleared session marker, so resume is not proposed again.
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.
gboolean dt_folder_survey_take_session_marker()
Consume the "a session was monitoring when we last closed" marker.
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...
void dt_folder_survey_set_confirm_import_handler(dt_folder_survey_confirm_import_handler_t handler)
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
gboolean(* dt_folder_survey_confirm_import_handler_t)(int new_files)
#define DT_FOLDER_SURVEY_STYLES_CONF_KEY
gboolean dt_supported_image(const gchar *filename)
Definition darktable.c:381
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:78
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
int groups()
dt_job_t * dt_control_job_create(dt_job_execute_callback execute, const char *msg,...)
Definition jobs.c:137
int dt_control_add_job(dt_control_t *control, dt_job_queue_t queue_id, _dt_job_t *job)
Definition jobs.c:407
void * dt_control_job_get_params(const _dt_job_t *job)
Definition jobs.c:131
void dt_control_job_set_params(_dt_job_t *job, void *params, dt_job_destroy_callback callback)
Definition jobs.c:114
@ DT_JOB_QUEUE_SYSTEM_BG
Definition jobs.h:58
_lib_location_type_t type
Definition location.c:1
@ DT_DEBUG_CONTROL
Definition logging.h:52
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
float *const restrict const size_t k
#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 macros.h:96
#define dt_free_align(ptr)
Release memory from dt_alloc_align() and set ptr to NULL.
Definition mem_alloc.h:214
static void dt_free_gpointer(gpointer ptr)
g_free() one pointer, with the signature GDestroyNotify wants.
Definition mem_alloc.h:184
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
char * key
#define DT_PATH_MAX
Buffer size for a filesystem path anywhere in Ansel.
Definition paths.h:57
const char * name
Definition pdf.h:90
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
Definition signal.h:386
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:616
@ DT_SIGNAL_FOLDER_SURVEY_CHANGED
Raised when the folder survey starts or stops monitoring. no param, no returned value.
Definition signal.h:337
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
const float uint32_t state[4]
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
gboolean dt_util_test_writable_dir(const char *path)
Definition utility.c:348
const dt_view_t * dt_view_manager_get_current_view(dt_view_manager_t *vm)
Definition view.c:139