20#include <glib/gstdio.h>
34#define DT_FOLDER_SURVEY_STATE_FILE "folder-survey-state.ini"
91 GKeyFile *
state = g_key_file_new();
100 gpointer
value = NULL;
103 while(g_hash_table_iter_next(&iter, &
key, &
value))
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);
112 g_key_file_set_integer(
state, group,
"state", entry->
state);
117 gchar *contents = g_key_file_to_data(
state, &length, NULL);
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);
124 fprintf(stderr,
"[folder survey] failed to save state: %s\n",
error ?
error->message :
"unknown error");
125 g_clear_error(&
error);
128 g_object_unref(file);
130 g_key_file_unref(
state);
131 return success ? 0 : 1;
139 GKeyFile *
state = g_key_file_new();
140 GError *
error = NULL;
143 g_clear_error(&
error);
144 g_key_file_unref(
state);
153 gsize groups_count = 0;
154 gchar **
groups = g_key_file_get_groups(
state, &groups_count);
156 for(gsize
k = 0;
k < groups_count;
k++)
158 if(!strcmp(
groups[
k],
"survey"))
continue;
160 char *path = g_key_file_get_string(
state,
groups[
k],
"path", NULL);
188 g_key_file_unref(
state);
200 GQueue folders = G_QUEUE_INIT;
201 g_queue_push_tail(&folders, g_file_new_for_path(
folder));
205 while(!g_queue_is_empty(&folders))
207 GFile *current = g_queue_pop_head(&folders);
208 GError *enumeration_error = NULL;
209 GFileEnumerator *enumerator = g_file_enumerate_children(
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);
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);
225 GFileInfo *info = NULL;
228 while(g_file_enumerator_iterate(enumerator, &info, &child, NULL, &enumeration_error))
232 const GFileType
type = g_file_info_get_file_type(info);
233 if(
type == G_FILE_TYPE_DIRECTORY)
235 g_queue_push_tail(&folders, g_object_ref(child));
238 if(
type != G_FILE_TYPE_REGULAR)
continue;
240 char *path = g_file_get_path(child);
247 char *canonical_path = g_canonicalize_filename(path, NULL);
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;
262 fprintf(stderr,
"[folder survey] failed while enumerating files: %s\n", enumeration_error->message);
263 g_clear_error(&enumeration_error);
266 g_object_unref(enumerator);
270 while(!g_queue_is_empty(&folders)) g_object_unref(g_queue_pop_head(&folders));
293 GList *styles = NULL;
296 if((*
name)[0] !=
'\0') styles = g_list_prepend(styles, g_strdup(*
name));
300 return g_list_reverse(styles);
308 const guint generation = GPOINTER_TO_UINT(user_data);
309 char *path = g_canonicalize_filename(source, NULL);
340 g_hash_table_destroy(observed);
344 GList *imports = NULL;
349 g_hash_table_destroy(observed);
353 GHashTableIter previous_iter;
354 gpointer previous_path = NULL;
355 gpointer previous_entry = NULL;
358 while(g_hash_table_iter_next(&previous_iter, &previous_path, &previous_entry))
360 if(!g_hash_table_contains(observed, previous_path)) g_hash_table_iter_remove(&previous_iter);
363 GHashTableIter observed_iter;
364 gpointer observed_path = NULL;
365 gpointer observed_value = NULL;
366 g_hash_table_iter_init(&observed_iter, observed);
368 while(g_hash_table_iter_next(&observed_iter, &observed_path, &observed_value))
423 imports = g_list_prepend(imports, g_strdup(observed_path));
430 g_hash_table_destroy(observed);
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),
444 GDateTime *now = g_date_time_new_now_local();
445 date = g_date_time_format(now,
"%F");
446 g_date_time_unref(now);
454 .folder_survey =
TRUE,
460 .target_subfolder_pattern =
dt_conf_get_string(
"studio_capture/sub_directory_pattern"),
463 .elements = elements,
466 .callback_data = GUINT_TO_POINTER(params->generation),
467 .callback_data_free = NULL };
499 return G_SOURCE_CONTINUE;
507 return G_SOURCE_CONTINUE;
515 return G_SOURCE_CONTINUE;
524 return G_SOURCE_CONTINUE;
534 return G_SOURCE_CONTINUE;
538 return G_SOURCE_CONTINUE;
548 return G_SOURCE_REMOVE;
571 const int interval = CLAMP(
dt_conf_get_int(
"studio_capture/interval"), 2, 3600);
612 const gboolean folder_valid
615 const gboolean patterns_valid = !
IS_NULL_PTR(subfolder) && subfolder[0] !=
'\0' && !
IS_NULL_PTR(file_pattern)
616 && file_pattern[0] !=
'\0';
618 if(folder_valid && base_valid && patterns_valid)
623 GDateTime *now = g_date_time_new_now_local();
624 date = g_date_time_format(now,
"%F");
625 g_date_time_unref(now);
633 char *example = g_build_filename(
folder,
"example.raw", NULL);
637 .folder_survey =
TRUE,
639 .base_folder = g_strdup(base_folder),
640 .target_subfolder_pattern = g_strdup(subfolder),
641 .target_file_pattern = g_strdup(file_pattern),
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))
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);
666 path = g_strdup_printf(
".../%s", short_path);
671 g_object_unref(base);
672 g_object_unref(destination);
699 *message = _(
"The folder to survey does not exist.");
702 if(g_access(
folder, R_OK | X_OK) != 0)
704 *message = _(
"The folder to survey is not readable.");
712 *message = _(
"The project date is invalid.");
719 if(
IS_NULL_PTR(target) || target[0] ==
'\0' || !g_file_test(target, G_FILE_TEST_IS_DIR))
721 *message = _(
"The base directory does not exist.");
726 *message = _(
"The base directory is not writable.");
733 *message = _(
"The project directory naming pattern is empty.");
737 if(
IS_NULL_PTR(file_pattern) || file_pattern[0] ==
'\0')
739 *message = _(
"The file naming pattern is empty.");
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);
753 if(target_inside_source)
755 *message = _(
"The base directory destination cannot be inside the surveyed folder.");
762 *message = _(
"The configured destination path is invalid.");
784 char *canonical_folder = configured_folder && configured_folder[0]
785 ? g_canonicalize_filename(configured_folder, NULL)
807 if(!
IS_NULL_PTR(default_base_dir) && default_base_dir[0])
824 if(new_files <= 0)
return;
832 "[folder_survey] %d new file(s) pending, but no handler to ask about importing them\n",
860 const char *message = NULL;
868 char *canonical_folder = g_canonicalize_filename(configured_folder, NULL);
897 dt_control_log(_(
"Folder survey started: `%s` to `%s`."), canonical_folder, target);
901 dt_control_log(_(
"Folder survey started: `%s`."), canonical_folder);
957 g_hash_table_destroy(observed);
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))
979 g_hash_table_destroy(observed);
999 g_hash_table_destroy(observed);
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))
1022 g_hash_table_destroy(observed);
static void error(char *msg)
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,...)
struct dt_control_t * dt_control_get_global(void)
struct dt_view_manager_t * dt_view_manager_get_global(void)
void * dt_alloc_align(size_t size)
Allocate cacheline-aligned memory.
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
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
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 ...
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.
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)
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)
_lib_location_type_t type
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...
#define dt_free_align(ptr)
Release memory from dt_alloc_align() and set ptr to NULL.
static void dt_free_gpointer(gpointer ptr)
g_free() one pointer, with the signature GDestroyNotify wants.
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
#define DT_PATH_MAX
Buffer size for a filesystem path anywhere in Ansel.
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
struct dt_control_signal_t * dt_control_signal_get_global(void)
@ DT_SIGNAL_FOLDER_SURVEY_CHANGED
Raised when the folder survey starts or stops monitoring. no param, no returned value.
static const dt_aligned_pixel_simd_t value
const float uint32_t state[4]
dt_folder_survey_file_state_t state
gboolean baseline_initialized
gboolean was_active_last_session
gboolean dt_util_test_writable_dir(const char *path)
const dt_view_t * dt_view_manager_get_current_view(dt_view_manager_t *vm)