diff --git a/README.md b/README.md index 4d398b4..aa80d9f 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,8 @@ Le socle actuel comprend notamment : - tâches asynchrones annulables ; - gestionnaire de tâches ; - panneau d’activité GTK ; +- import groupé de preuves avec confirmation globale, révision individuelle, + copie contrôlée, empreinte SHA-256 et bilan détaillé ; - registre d’outils externes ; - exécution sécurisée par `GSubprocess` ; - exécution d’outils en tâche de fond ; diff --git a/docs/database/DATABASE_ARCHITECTURE.md b/docs/database/DATABASE_ARCHITECTURE.md index 40cd707..ef6164a 100644 --- a/docs/database/DATABASE_ARCHITECTURE.md +++ b/docs/database/DATABASE_ARCHITECTURE.md @@ -761,6 +761,11 @@ L'intégrité de la preuve est essentielle. Les métadonnées permettent notamment de suivre les empreintes cryptographiques. +L'import groupé conserve ce modèle : chaque fichier sélectionné est révisé et +importé séparément. Les imports sont exécutés séquentiellement afin d'éviter +les écritures SQLite concurrentes. L'échec d'un fichier n'annule pas les +preuves déjà importées et apparaît dans le bilan final. + --- ## 4.6 entites diff --git a/include/views/file_dialog.h b/include/views/file_dialog.h index bc4b40a..5449f8a 100644 --- a/include/views/file_dialog.h +++ b/include/views/file_dialog.h @@ -29,6 +29,23 @@ typedef void (*FileDialogCallback)( gpointer user_data ); +/** + * @brief Fonction appelée après une sélection multiple. + * + * paths est un tableau emprunté de chemins locaux possédés par le dialogue. + * Il n'est valide que pendant l'appel. Une annulation transmet NULL sans + * erreur. + * + * @param paths Chemins sélectionnés, ou NULL. + * @param error Erreur de sélection, ou NULL. + * @param user_data Données fournies lors de l'ouverture. + */ +typedef void (*FileDialogMultipleCallback)( + const GPtrArray *paths, + const GError *error, + gpointer user_data +); + /** * @brief Ouvre un dialogue permettant de sélectionner un fichier. * @@ -42,4 +59,16 @@ void file_dialog_select_file( gpointer user_data ); +/** + * @brief Ouvre un dialogue permettant de sélectionner plusieurs fichiers. + * @param parent Fenêtre parente, éventuellement NULL. + * @param callback Fonction appelée à la fermeture du dialogue. + * @param user_data Données transmises au callback. + */ +void file_dialog_select_files( + GtkWindow *parent, + FileDialogMultipleCallback callback, + gpointer user_data +); + #endif diff --git a/src/core/application.c b/src/core/application.c index 0a559d7..1d61966 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -123,8 +123,21 @@ typedef struct { Application *application; char *investigation_root_path; + gpointer batch_context; } ApplicationEvidenceImportContext; +/** @brief État d'un import groupé traité séquentiellement. */ +typedef struct +{ + Application *application; + GPtrArray *file_paths; + GPtrArray *evidence_types; + GPtrArray *failures; + guint current_index; + guint success_count; + guint skipped_count; +} ApplicationEvidenceBatchContext; + /** * @brief Contexte conservé jusqu'à la fin d'un chargement de graphe. */ @@ -191,6 +204,14 @@ static void application_present_error( const char *message ); +static void application_present_next_batch_metadata( + ApplicationEvidenceBatchContext *context +); + +static void application_evidence_batch_context_free( + ApplicationEvidenceBatchContext *context +); + /** @brief Libère les données de révision possédées par le dialogue. */ static void application_osint_review_context_free(gpointer user_data) { @@ -202,6 +223,24 @@ static void application_osint_review_context_free(gpointer user_data) g_free(context); } +/** @brief Démarre ou abandonne la préparation après confirmation globale. */ +static void application_on_batch_selection_confirmed( + gboolean confirmed, gpointer user_data +) +{ + ApplicationEvidenceBatchContext *context = user_data; + if (context == NULL) return; + if (confirmed) application_present_next_batch_metadata(context); + else + { + main_window_set_import_evidence_enabled( + context->application->main_window, TRUE); + main_window_set_status(context->application->main_window, + "Import groupé annulé avant toute copie."); + application_evidence_batch_context_free(context); + } +} + /** @brief Intègre les propositions confirmées puis recharge le graphe. */ static void application_on_osint_integration_confirmed( GPtrArray *selected_proposals, @@ -351,6 +390,46 @@ static void application_evidence_import_context_free( ); } +/** @brief Libère l'état possédé d'un import groupé. */ +static void application_evidence_batch_context_free( + ApplicationEvidenceBatchContext *context +) +{ + if (context == NULL) return; + g_clear_pointer(&context->file_paths, g_ptr_array_unref); + g_clear_pointer(&context->evidence_types, g_ptr_array_unref); + g_clear_pointer(&context->failures, g_ptr_array_unref); + g_free(context); +} + +/** @brief Affiche le bilan final puis libère l'import groupé. */ +static void application_finish_evidence_batch( + ApplicationEvidenceBatchContext *context +) +{ + GString *details = NULL; + char *message = NULL; + if (context == NULL) return; + details = g_string_new(NULL); + for (guint index = 0U; index < context->failures->len; index++) + g_string_append_printf(details, "- %s\n", + (const char *) g_ptr_array_index(context->failures, index)); + message = g_strdup_printf( + "%u preuve(s) importée(s), %u ignorée(s), %u échec(s).", + context->success_count, context->skipped_count, context->failures->len); + application_message_dialog_present_details( + main_window_get_window(context->application->main_window), + context->failures->len == 0U + ? APPLICATION_MESSAGE_DIALOG_INFORMATION + : APPLICATION_MESSAGE_DIALOG_WARNING, + "Import groupé terminé", message, + details->len > 0U ? details->str : "Tous les fichiers ont été importés."); + main_window_set_import_evidence_enabled( + context->application->main_window, TRUE); + g_free(message); g_string_free(details, TRUE); + application_evidence_batch_context_free(context); +} + /** * @brief Libère le contexte d'une vérification d'intégrité. */ @@ -2740,6 +2819,8 @@ static void application_on_evidence_import_completed( gboolean evidence_refreshed = FALSE; + ApplicationEvidenceBatchContext *batch_context = NULL; + if (task == NULL || context == NULL) { @@ -2749,16 +2830,17 @@ static void application_on_evidence_import_completed( application = context->application; + batch_context = context->batch_context; + if (application == NULL || application->main_window == NULL) { return; } - main_window_set_import_evidence_enabled( - application->main_window, - application->session != NULL - ); + if (batch_context == NULL) + main_window_set_import_evidence_enabled( + application->main_window, application->session != NULL); state = background_task_get_state( @@ -2775,6 +2857,14 @@ static void application_on_evidence_import_completed( if (evidence_record == NULL) { + if (batch_context != NULL) + { + g_ptr_array_add(batch_context->failures, g_strdup( + "Tâche terminée sans preuve exploitable.")); + batch_context->current_index++; + application_present_next_batch_metadata(batch_context); + return; + } application_present_error( application, "Import incomplet", @@ -2908,12 +2998,26 @@ static void application_on_evidence_import_completed( status_message ); + if (batch_context != NULL) + { + batch_context->success_count++; + batch_context->current_index++; + application_present_next_batch_metadata(batch_context); + } + return; } if (state == BACKGROUND_TASK_STATE_CANCELLED) { + if (batch_context != NULL) + { + batch_context->skipped_count++; + batch_context->current_index++; + application_present_next_batch_metadata(batch_context); + return; + } if (application_session_matches_root( application, context->investigation_root_path @@ -2940,13 +3044,23 @@ static void application_on_evidence_import_completed( : "erreur inconnue" ); - application_present_error( - application, - "Import impossible", - error != NULL - ? error->message - : "La preuve n’a pas pu être importée." - ); + if (batch_context != NULL) + { + const char *path = batch_context->current_index < + batch_context->file_paths->len + ? g_ptr_array_index(batch_context->file_paths, + batch_context->current_index) : "fichier inconnu"; + char *failure = g_strdup_printf("%s : %s", path, + error != NULL ? error->message : "échec de l'import"); + g_ptr_array_add(batch_context->failures, failure); + batch_context->current_index++; + application_present_next_batch_metadata(batch_context); + } + else + application_present_error( + application, "Import impossible", + error != NULL ? error->message + : "La preuve n’a pas pu être importée."); g_clear_error( &error @@ -3033,7 +3147,8 @@ static void application_start_evidence_import( const char *type_identifier, const char *collected_at, const char *source, - const char *description + const char *description, + ApplicationEvidenceBatchContext *batch_context ) { const InvestigationProject *project = NULL; @@ -3216,6 +3331,8 @@ static void application_start_evidence_import( context->application = application; + context->batch_context = batch_context; + context->investigation_root_path = g_strdup( root_path @@ -3426,7 +3543,8 @@ static void application_on_evidence_metadata_completed( ), evidence_import_dialog_result_get_description( result - ) + ), + NULL ); evidence_import_dialog_result_free( @@ -3673,6 +3791,127 @@ static void application_on_evidence_file_selected( ); } +/** @brief Traite les métadonnées du fichier courant d'un import groupé. */ +static void application_on_batch_metadata_completed( + EvidenceImportDialogResult *result, gpointer user_data +) +{ + ApplicationEvidenceBatchContext *context = user_data; + const char *file_path = NULL; + if (context == NULL) { evidence_import_dialog_result_free(result); return; } + if (result == NULL) + { + context->skipped_count++; + context->current_index++; + application_present_next_batch_metadata(context); + return; + } + file_path = g_ptr_array_index(context->file_paths, context->current_index); + application_start_evidence_import( + context->application, file_path, + evidence_import_dialog_result_get_type_identifier(result), + evidence_import_dialog_result_get_collected_at(result), + evidence_import_dialog_result_get_source(result), + evidence_import_dialog_result_get_description(result), context); + evidence_import_dialog_result_free(result); +} + +/** @brief Présente les métadonnées du prochain fichier, ou le bilan final. */ +static void application_present_next_batch_metadata( + ApplicationEvidenceBatchContext *context +) +{ + GError *error = NULL; + const char *file_path = NULL; + char *status = NULL; + if (context == NULL) return; + if (context->current_index >= context->file_paths->len) + { + application_finish_evidence_batch(context); + return; + } + file_path = g_ptr_array_index(context->file_paths, context->current_index); + status = g_strdup_printf("Préparation %u/%u : %s", + context->current_index + 1U, context->file_paths->len, file_path); + main_window_set_status(context->application->main_window, status); + g_free(status); + if (!evidence_import_dialog_present( + main_window_get_window(context->application->main_window), + file_path, context->evidence_types, + application_on_batch_metadata_completed, context, &error)) + { + char *failure = g_strdup_printf("%s : %s", file_path, + error != NULL ? error->message : "dialogue indisponible"); + g_ptr_array_add(context->failures, failure); + g_clear_error(&error); + context->current_index++; + application_present_next_batch_metadata(context); + } +} + +/** @brief Prépare un import groupé depuis les chemins sélectionnés. */ +static void application_on_evidence_files_selected( + const GPtrArray *paths, const GError *error, gpointer user_data +) +{ + Application *application = user_data; + ApplicationEvidenceBatchContext *context = NULL; + EvidenceTypeDao *dao = NULL; + GPtrArray *types = NULL; + GError *local_error = NULL; + if (application == NULL || application->main_window == NULL) return; + if (error != NULL) + { + application_present_error(application, "Sélection impossible", error->message); + return; + } + if (paths == NULL || paths->len == 0U) + { + main_window_set_status(application->main_window, "Import de preuve annulé."); + return; + } + if (paths->len == 1U) + { + application_on_evidence_file_selected( + g_ptr_array_index((GPtrArray *) paths, 0U), NULL, application); + return; + } + dao = evidence_type_dao_new( + investigation_session_get_database(application->session), &local_error); + if (dao != NULL) types = evidence_type_dao_list_all(dao, &local_error); + evidence_type_dao_free(dao); + if (types == NULL || types->len == 0U) + { + application_present_error(application, "Types de preuve indisponibles", + local_error != NULL ? local_error->message + : "Aucun type de preuve n'est disponible."); + g_clear_error(&local_error); + g_clear_pointer(&types, g_ptr_array_unref); + return; + } + context = g_new0(ApplicationEvidenceBatchContext, 1); + context->application = application; + context->file_paths = g_ptr_array_new_with_free_func(g_free); + context->evidence_types = types; + context->failures = g_ptr_array_new_with_free_func(g_free); + for (guint index = 0U; index < paths->len; index++) + g_ptr_array_add(context->file_paths, + g_strdup(g_ptr_array_index((GPtrArray *) paths, index))); + main_window_set_import_evidence_enabled(application->main_window, FALSE); + { + char *message = g_strdup_printf( + "%u fichiers ont été sélectionnés. Chaque fichier sera révisé " + "puis importé séparément avec sa propre empreinte SHA-256.", + paths->len); + application_message_dialog_present_confirmation( + main_window_get_window(application->main_window), + APPLICATION_MESSAGE_DIALOG_INFORMATION, + "Préparer l'import groupé", message, "Continuer", + application_on_batch_selection_confirmed, context); + g_free(message); + } +} + /** * @brief Traite la demande d'import d'une preuve. * @@ -3702,11 +3941,11 @@ static void application_on_import_evidence_requested( return; } - file_dialog_select_file( + file_dialog_select_files( main_window_get_window( application->main_window ), - application_on_evidence_file_selected, + application_on_evidence_files_selected, application ); } diff --git a/src/views/file_dialog.c b/src/views/file_dialog.c index a274d3d..4277573 100644 --- a/src/views/file_dialog.c +++ b/src/views/file_dialog.c @@ -16,6 +16,13 @@ typedef struct gpointer user_data; } FileDialogContext; +/** @brief Contexte conservé pendant une sélection multiple. */ +typedef struct +{ + FileDialogMultipleCallback callback; + gpointer user_data; +} FileDialogMultipleContext; + /** * @brief Libère le contexte du dialogue. */ @@ -234,3 +241,58 @@ void file_dialog_select_file( dialog ); } + +/** @brief Traite le résultat du sélecteur multiple. */ +static void file_dialog_on_files_selected( + GObject *source_object, GAsyncResult *result, gpointer user_data +) +{ + FileDialogMultipleContext *context = user_data; + GListModel *files = NULL; + GPtrArray *paths = NULL; + GError *error = NULL; + files = gtk_file_dialog_open_multiple_finish( + GTK_FILE_DIALOG(source_object), result, &error); + if (files == NULL) + { + if (error != NULL && g_error_matches( + error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED)) + context->callback(NULL, NULL, context->user_data); + else context->callback(NULL, error, context->user_data); + g_clear_error(&error); g_free(context); return; + } + paths = g_ptr_array_new_with_free_func(g_free); + for (guint index = 0U; index < g_list_model_get_n_items(files); index++) + { + GFile *file = g_list_model_get_item(files, index); + char *path = g_file_get_path(file); + if (path == NULL) + { + g_set_error_literal(&error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "Un fichier sélectionné ne possède pas de chemin local."); + g_object_unref(file); break; + } + g_ptr_array_add(paths, path); g_object_unref(file); + } + if (error != NULL) context->callback(NULL, error, context->user_data); + else context->callback(paths, NULL, context->user_data); + g_clear_error(&error); g_ptr_array_unref(paths); g_object_unref(files); + g_free(context); +} + +void file_dialog_select_files( + GtkWindow *parent, FileDialogMultipleCallback callback, gpointer user_data +) +{ + GtkFileDialog *dialog = NULL; + FileDialogMultipleContext *context = NULL; + if (callback == NULL) return; + context = g_new0(FileDialogMultipleContext, 1); + context->callback = callback; context->user_data = user_data; + dialog = gtk_file_dialog_new(); + gtk_file_dialog_set_title(dialog, "Sélectionner des fichiers de preuve"); + gtk_file_dialog_set_modal(dialog, TRUE); + gtk_file_dialog_open_multiple(dialog, parent, NULL, + file_dialog_on_files_selected, context); + g_object_unref(dialog); +}