31#define DT_FOLDER_SURVEY_STATE_FILE "folder-survey-state.ini"
88 GKeyFile *
state = g_key_file_new();
97 gpointer
value = NULL;
100 while(g_hash_table_iter_next(&iter, &
key, &
value))
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);
109 g_key_file_set_integer(
state, group,
"state", entry->
state);
114 gchar *contents = g_key_file_to_data(
state, &length, NULL);
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);
121 fprintf(stderr,
"[folder survey] failed to save state: %s\n",
error ?
error->message :
"unknown error");
122 g_clear_error(&
error);
125 g_object_unref(file);
127 g_key_file_unref(
state);
128 return success ? 0 : 1;
136 GKeyFile *
state = g_key_file_new();
137 GError *
error = NULL;
140 g_clear_error(&
error);
141 g_key_file_unref(
state);
150 gsize groups_count = 0;
151 gchar **
groups = g_key_file_get_groups(
state, &groups_count);
153 for(gsize
k = 0;
k < groups_count;
k++)
155 if(!strcmp(
groups[
k],
"survey"))
continue;
157 char *path = g_key_file_get_string(
state,
groups[
k],
"path", NULL);
185 g_key_file_unref(
state);
197 GQueue folders = G_QUEUE_INIT;
198 g_queue_push_tail(&folders, g_file_new_for_path(folder));
202 while(!g_queue_is_empty(&folders))
204 GFile *current = g_queue_pop_head(&folders);
205 GError *enumeration_error = NULL;
206 GFileEnumerator *enumerator = g_file_enumerate_children(
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);
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);
222 GFileInfo *info = NULL;
225 while(g_file_enumerator_iterate(enumerator, &info, &child, NULL, &enumeration_error))
229 const GFileType
type = g_file_info_get_file_type(info);
230 if(
type == G_FILE_TYPE_DIRECTORY)
232 g_queue_push_tail(&folders, g_object_ref(child));
235 if(
type != G_FILE_TYPE_REGULAR)
continue;
237 char *path = g_file_get_path(child);
244 char *canonical_path = g_canonicalize_filename(path, NULL);
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;
259 fprintf(stderr,
"[folder survey] failed while enumerating files: %s\n", enumeration_error->message);
260 g_clear_error(&enumeration_error);
263 g_object_unref(enumerator);
267 while(!g_queue_is_empty(&folders)) g_object_unref(g_queue_pop_head(&folders));
290 GList *styles = NULL;
293 if((*
name)[0] !=
'\0') styles = g_list_prepend(styles, g_strdup(*
name));
297 return g_list_reverse(styles);
305 const guint generation = GPOINTER_TO_UINT(user_data);
306 char *path = g_canonicalize_filename(source, NULL);
337 g_hash_table_destroy(observed);
341 GList *imports = NULL;
346 g_hash_table_destroy(observed);
350 GHashTableIter previous_iter;
351 gpointer previous_path = NULL;
352 gpointer previous_entry = NULL;
355 while(g_hash_table_iter_next(&previous_iter, &previous_path, &previous_entry))
357 if(!g_hash_table_contains(observed, previous_path)) g_hash_table_iter_remove(&previous_iter);
360 GHashTableIter observed_iter;
361 gpointer observed_path = NULL;
362 gpointer observed_value = NULL;
363 g_hash_table_iter_init(&observed_iter, observed);
365 while(g_hash_table_iter_next(&observed_iter, &observed_path, &observed_value))
420 imports = g_list_prepend(imports, g_strdup(observed_path));
427 g_hash_table_destroy(observed);
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),
441 GDateTime *now = g_date_time_new_now_local();
442 date = g_date_time_format(now,
"%F");
443 g_date_time_unref(now);
451 .folder_survey =
TRUE,
457 .target_subfolder_pattern =
dt_conf_get_string(
"studio_capture/sub_directory_pattern"),
460 .elements = elements,
463 .callback_data = GUINT_TO_POINTER(params->generation),
464 .callback_data_free = NULL };
496 return G_SOURCE_CONTINUE;
501 if(
IS_NULL_PTR(folder) || folder[0] ==
'\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
504 return G_SOURCE_CONTINUE;
512 return G_SOURCE_CONTINUE;
521 return G_SOURCE_CONTINUE;
523 params->folder = folder;
531 return G_SOURCE_CONTINUE;
535 return G_SOURCE_CONTINUE;
545 return G_SOURCE_REMOVE;
568 const int interval = CLAMP(
dt_conf_get_int(
"studio_capture/interval"), 2, 3600);
609 const gboolean folder_valid
610 = !
IS_NULL_PTR(folder) && folder[0] && g_file_test(folder, G_FILE_TEST_IS_DIR);
612 const gboolean patterns_valid = !
IS_NULL_PTR(subfolder) && subfolder[0] !=
'\0' && !
IS_NULL_PTR(file_pattern)
613 && file_pattern[0] !=
'\0';
615 if(folder_valid && base_valid && patterns_valid)
620 GDateTime *now = g_date_time_new_now_local();
621 date = g_date_time_format(now,
"%F");
622 g_date_time_unref(now);
630 char *example = g_build_filename(folder,
"example.raw", NULL);
634 .folder_survey =
TRUE,
636 .base_folder = g_strdup(base_folder),
637 .target_subfolder_pattern = g_strdup(subfolder),
638 .target_file_pattern = g_strdup(file_pattern),
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))
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);
663 path = g_strdup_printf(
".../%s", short_path);
668 g_object_unref(base);
669 g_object_unref(destination);
694 if(
IS_NULL_PTR(folder) || folder[0] ==
'\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
696 *message = _(
"The folder to survey does not exist.");
699 if(g_access(folder, R_OK | X_OK) != 0)
701 *message = _(
"The folder to survey is not readable.");
709 *message = _(
"The project date is invalid.");
716 if(
IS_NULL_PTR(target) || target[0] ==
'\0' || !g_file_test(target, G_FILE_TEST_IS_DIR))
718 *message = _(
"The base directory does not exist.");
723 *message = _(
"The base directory is not writable.");
730 *message = _(
"The project directory naming pattern is empty.");
734 if(
IS_NULL_PTR(file_pattern) || file_pattern[0] ==
'\0')
736 *message = _(
"The file naming pattern is empty.");
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);
750 if(target_inside_source)
752 *message = _(
"The base directory destination cannot be inside the surveyed folder.");
759 *message = _(
"The configured destination path is invalid.");
781 char *canonical_folder = configured_folder && configured_folder[0]
782 ? g_canonicalize_filename(configured_folder, NULL)
804 if(!
IS_NULL_PTR(default_base_dir) && default_base_dir[0])
829 if(new_files <= 0)
return;
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?",
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),
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);
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"))
851 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(delete_check)));
852 gtk_widget_destroy(dialog);
854 if(import_now != GTK_RESPONSE_YES)
873 const char *message = NULL;
881 char *canonical_folder = g_canonicalize_filename(configured_folder, NULL);
910 dt_control_log(_(
"Folder survey started: `%s` to `%s`."), canonical_folder, target);
914 dt_control_log(_(
"Folder survey started: `%s`."), canonical_folder);
959 if(
IS_NULL_PTR(folder) || folder[0] ==
'\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
970 g_hash_table_destroy(observed);
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))
992 g_hash_table_destroy(observed);
1001 if(
IS_NULL_PTR(folder) || folder[0] ==
'\0' || !g_file_test(folder, G_FILE_TEST_IS_DIR))
1012 g_hash_table_destroy(observed);
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))
1035 g_hash_table_destroy(observed);
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);
1054 if(resume != GTK_RESPONSE_YES)
1061 return G_SOURCE_REMOVE;
1071 return G_SOURCE_REMOVE;
static void error(char *msg)
void dt_image_init(dt_image_t *img)
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,...)
uint32_t view(const dt_view_t *self)
gboolean dt_supported_image(const gchar *filename)
check if file is a supported image
static void dt_free_gpointer(gpointer ptr)
static const dt_aligned_pixel_simd_t value
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
GDateTime * dt_string_to_datetime(const char *string)
gboolean dt_datetime_entry_to_exif(char *exif, const size_t exif_size, const char *entry)
#define DT_DATETIME_LENGTH
static int dt_pthread_mutex_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
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
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.
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
@ DT_IMPORT_ONCONFLICT_SKIP
dt_job_t * dt_control_job_create(dt_job_execute_callback execute, const char *msg,...)
int dt_control_add_job(dt_control_t *control, dt_job_queue_t queue_id, _dt_job_t *job)
void * dt_control_job_get_params(const _dt_job_t *job)
void dt_control_job_set_params(_dt_job_t *job, void *params, dt_job_destroy_callback callback)
float *const restrict const size_t k
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
@ DT_SIGNAL_FOLDER_SURVEY_CHANGED
Raised when the folder survey starts or stops monitoring. no param, no returned value.
struct _GtkWidget GtkWidget
const float uint32_t state[4]
struct dt_gui_gtk_t * gui
struct dt_control_signal_t * signals
struct dt_view_manager_t * view_manager
struct dt_control_t * control
dt_folder_survey_file_state_t state
gboolean baseline_initialized
gboolean was_active_last_session
gboolean dt_util_test_writable_dir(const char *path)
int dt_view_manager_switch(dt_view_manager_t *vm, const char *view_name)
const dt_view_t * dt_view_manager_get_current_view(dt_view_manager_t *vm)