From 6c34287feb33ed5d88d11a7ae1dfc909caf44884 Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Mon, 20 Jul 2026 00:04:50 +0200 Subject: [PATCH] feat: import evidence from GTK interface --- Makefile | 27 +- include/core/evidence_import_task.h | 89 +++ include/views/file_dialog.h | 45 ++ include/views/main_window.h | 39 +- src/core/application.c | 834 ++++++++++++++++++++++------ src/core/evidence_import_task.c | 663 ++++++++++++++++++++++ src/views/file_dialog.c | 236 ++++++++ src/views/main_window.c | 153 ++++- src/widgets/sidebar.c | 5 - tests/test_evidence_import_task.c | 798 ++++++++++++++++++++++++++ 10 files changed, 2658 insertions(+), 231 deletions(-) create mode 100644 include/core/evidence_import_task.h create mode 100644 include/views/file_dialog.h create mode 100644 src/core/evidence_import_task.c create mode 100644 src/views/file_dialog.c create mode 100644 tests/test_evidence_import_task.c diff --git a/Makefile b/Makefile index 47c9ac0..9bb8b76 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,7 @@ TEST_TOOL_INITIALIZER := tests/test_tool_initializer TEST_FILE_HASH := tests/test_file_hash TEST_EVIDENCE_COPY := tests/test_evidence_copy TEST_EVIDENCE_IMPORTER := tests/test_evidence_importer +TEST_EVIDENCE_IMPORT_TASK := tests/test_evidence_import_task all: $(TARGET) @@ -245,6 +246,25 @@ $(TEST_EVIDENCE_IMPORTER): \ -DEVIDENCE_IMPORTER_ENABLE_TEST_HOOKS \ $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_EVIDENCE_IMPORT_TASK): \ + tests/test_evidence_import_task.c \ + src/core/evidence_import_task.c \ + src/core/evidence_importer.c \ + src/core/evidence_copy.c \ + src/core/file_hash.c \ + src/core/background_task.c \ + src/core/task_manager.c \ + src/dao/evidence_dao.c \ + src/models/evidence_record.c \ + src/database/database.c \ + src/database/schema.c \ + src/database/statement.c \ + src/database/transaction.c \ + src/database/error.c + $(CC) $(TEST_CFLAGS) \ + -DEVIDENCE_IMPORTER_ENABLE_TEST_HOOKS \ + $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -268,7 +288,8 @@ test: \ $(TEST_TOOL_INITIALIZER) \ $(TEST_FILE_HASH) \ $(TEST_EVIDENCE_COPY) \ - $(TEST_EVIDENCE_IMPORTER) + $(TEST_EVIDENCE_IMPORTER) \ + $(TEST_EVIDENCE_IMPORT_TASK) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -293,6 +314,7 @@ test: \ @$(TEST_FILE_HASH) @$(TEST_EVIDENCE_COPY) @$(TEST_EVIDENCE_IMPORTER) + @$(TEST_EVIDENCE_IMPORT_TASK) @echo "Tous les tests sont valides." %.o: %.c @@ -325,7 +347,8 @@ clean: $(TEST_TOOL_INITIALIZER) \ $(TEST_FILE_HASH) \ $(TEST_EVIDENCE_COPY) \ - $(TEST_EVIDENCE_IMPORTER) + $(TEST_EVIDENCE_IMPORTER) \ + $(TEST_EVIDENCE_IMPORT_TASK) .PHONY: clean run test diff --git a/include/core/evidence_import_task.h b/include/core/evidence_import_task.h new file mode 100644 index 0000000..d4618b4 --- /dev/null +++ b/include/core/evidence_import_task.h @@ -0,0 +1,89 @@ +/****************************************************************************** + * @file evidence_import_task.h + * @brief Tâche asynchrone d’import d’une preuve. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_EVIDENCE_IMPORT_TASK_H +#define LABFY_INVESTIGATION_EVIDENCE_IMPORT_TASK_H + +#include "core/background_task.h" +#include "core/task_manager.h" + +#include + +/** + * @brief Codes d’erreur propres à la tâche d’import. + */ +typedef enum +{ + EVIDENCE_IMPORT_TASK_ERROR_INVALID_ARGUMENT, + EVIDENCE_IMPORT_TASK_ERROR_DATABASE, + EVIDENCE_IMPORT_TASK_ERROR_IMPORT +} EvidenceImportTaskError; + +/** + * @brief Domaine d’erreur du module. + */ +#define EVIDENCE_IMPORT_TASK_ERROR \ + evidence_import_task_error_quark() + +/** + * @brief Paramètres nécessaires à une tâche d’import. + * + * Toutes les chaînes sont copiées avant le démarrage du worker. + */ +typedef struct +{ + const char *database_path; + + const char *source_path; + const char *destination_directory; + const char *relative_directory; + + const char *type_identifier; + const char *collected_at; + const char *source; + const char *description; +} EvidenceImportTaskRequest; + +/** + * @brief Retourne le domaine d’erreur du module. + * + * @return Quark GLib du domaine d’erreur. + */ +GQuark evidence_import_task_error_quark(void); + +/** + * @brief Crée, enregistre et démarre une tâche d’import. + * + * La tâche ouvre sa propre connexion SQLite dans le thread secondaire. + * + * En cas de succès : + * + * - TaskManager conserve une référence ; + * - le worker conserve une référence pendant son exécution ; + * - l’appelant reçoit sa référence initiale et doit la libérer avec + * background_task_unref(). + * + * completion_data est transféré à BackgroundTask uniquement si le + * démarrage réussit. En cas d’échec, l’appelant en reste propriétaire. + * + * @param task_manager Gestionnaire recevant la tâche. + * @param request Paramètres de l’import. + * @param completion_callback Callback final facultatif. + * @param completion_data Données du callback final. + * @param completion_data_destroy Destructeur de completion_data. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouvelle tâche démarrée, ou NULL. + */ +BackgroundTask *evidence_import_task_start( + TaskManager *task_manager, + const EvidenceImportTaskRequest *request, + BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, + GDestroyNotify completion_data_destroy, + GError **error +); + +#endif diff --git a/include/views/file_dialog.h b/include/views/file_dialog.h new file mode 100644 index 0000000..bc4b40a --- /dev/null +++ b/include/views/file_dialog.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * @file file_dialog.h + * @brief Interface du dialogue de sélection d’un fichier de preuve. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_FILE_DIALOG_H +#define LABFY_INVESTIGATION_FILE_DIALOG_H + +#include + +/** + * @brief Fonction appelée après la fermeture du sélecteur. + * + * Cas possibles : + * + * - succès : file_path non NULL et error NULL ; + * - annulation : file_path NULL et error NULL ; + * - erreur : file_path NULL et error non NULL. + * + * Le chemin et l’erreur restent valides uniquement pendant l’appel. + * + * @param file_path Chemin local sélectionné, ou NULL. + * @param error Erreur de sélection, ou NULL. + * @param user_data Données transmises lors de l’ouverture. + */ +typedef void (*FileDialogCallback)( + const char *file_path, + const GError *error, + gpointer user_data +); + +/** + * @brief Ouvre un dialogue permettant de sélectionner un fichier. + * + * @param parent Fenêtre parente, éventuellement NULL. + * @param callback Fonction appelée lorsque le dialogue est terminé. + * @param user_data Données transmises au callback. + */ +void file_dialog_select_file( + GtkWindow *parent, + FileDialogCallback callback, + gpointer user_data +); + +#endif diff --git a/include/views/main_window.h b/include/views/main_window.h index 4c96478..29c81a6 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -27,6 +27,16 @@ typedef void (*MainWindowNewInvestigationCallback)( gpointer user_data ); +/** + * @brief Callback appelé lorsque l'utilisateur demande + * l'import d'une preuve. + * + * @param user_data Données utilisateur associées au callback. + */ +typedef void (*MainWindowImportEvidenceCallback)( + gpointer user_data +); + /** * @brief Définit le callback du bouton « Nouvelle enquête ». * @@ -163,16 +173,6 @@ typedef void (*MainWindowQuitCallback)( gpointer user_data ); -/** - * @brief Callback appelé lorsque l'utilisateur demande une tâche - * de démonstration. - * - * @param user_data Données utilisateur associées au callback. - */ -typedef void (*MainWindowDemoTaskCallback)( - gpointer user_data -); - /** * @brief Définit le callback du bouton « Ouvrir une enquête ». * @@ -204,20 +204,31 @@ void main_window_set_quit_callback( ); /** - * @brief Définit le callback du bouton « Tâche de test ». + * @brief Définit le callback de l'action « Importer une preuve ». * - * Ce bouton est temporaire et sert à valider le panneau d'activité. + * MainWindow transmet uniquement la demande au contrôleur. * * @param main_window Fenêtre principale. * @param callback Fonction appelée lors du clic. * @param user_data Données transmises au callback. */ -void main_window_set_demo_task_callback( +void main_window_set_import_evidence_callback( MainWindow *main_window, - MainWindowDemoTaskCallback callback, + MainWindowImportEvidenceCallback callback, gpointer user_data ); +/** + * @brief Active ou désactive l'action d'import d'une preuve. + * + * @param main_window Fenêtre principale. + * @param enabled TRUE pour autoriser l'import. + */ +void main_window_set_import_evidence_enabled( + MainWindow *main_window, + gboolean enabled +); + /** * @brief Libère les ressources de la fenêtre. * diff --git a/src/core/application.c b/src/core/application.c index 40884a0..234b593 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -18,14 +18,30 @@ #include "core/task_manager.h" #include "core/background_task.h" #include "core/tool_initializer.h" +#include "views/file_dialog.h" +#include "core/evidence_import_task.h" +#include "models/evidence_record.h" #include +#include /** * @brief Identifiant unique utilisé par GLib pour l'application. */ #define APPLICATION_ID "com.labfytools.investigation" +/** + * @brief Dossier physique recevant les documents importés. + */ +#define APPLICATION_EVIDENCE_DIRECTORY \ + "01_Preuves_Originales/Documents" + +/** + * @brief Type utilisé avant l’ajout du formulaire de métadonnées. + */ +#define APPLICATION_DEFAULT_EVIDENCE_TYPE \ + "document" + /** * @brief Erreurs produites lors du chargement d'une enquête. */ @@ -71,6 +87,78 @@ struct Application ToolInitializer *tool_initializer; }; +/** + * @brief Contexte conservé jusqu’à la fin d’un import. + */ +typedef struct +{ + Application *application; + char *investigation_root_path; +} ApplicationEvidenceImportContext; + +/** + * @brief Libère le contexte d’un import asynchrone. + */ +static void application_evidence_import_context_free( + gpointer user_data +) +{ + ApplicationEvidenceImportContext *context = + user_data; + + if (context == NULL) + { + return; + } + + g_free( + context->investigation_root_path + ); + + g_free( + context + ); +} + +/** + * @brief Vérifie que l’enquête active correspond à un chemin racine. + */ +static gboolean application_session_matches_root( + const Application *application, + const char *expected_root_path +) +{ + const InvestigationProject *project = NULL; + const char *current_root_path = NULL; + + if (application == NULL || + application->session == NULL || + expected_root_path == NULL) + { + return FALSE; + } + + project = + investigation_session_get_project( + application->session + ); + + if (project == NULL) + { + return FALSE; + } + + current_root_path = + investigation_project_get_root_path( + project + ); + + return g_strcmp0( + current_root_path, + expected_root_path + ) == 0; +} + static void application_present_error( Application *application, const char *title, @@ -359,6 +447,11 @@ static gboolean application_install_session( root_path ); + main_window_set_import_evidence_enabled( + application->main_window, + TRUE + ); + return TRUE; } @@ -775,265 +868,631 @@ static void application_on_open_investigation_requested( } /** - * @brief Exécute une tâche temporaire servant à tester le panneau. + * @brief Reconstruit et remplace l’arborescence active. * - * Cette fonction s'exécute dans un thread secondaire. + * Le nouveau modèle est entièrement construit avant que l’ancien + * soit détaché et libéré. * - * @param task Tâche en cours. - * @param cancellable Objet d'annulation. - * @param worker_data Données inutilisées. - * @param result Emplacement recevant le résultat. - * @param error Emplacement recevant une erreur. + * @param application Application concernée. + * @param investigation_root_path Racine à parcourir. * - * @return TRUE en cas de succès, sinon FALSE. + * @return TRUE si le remplacement a réussi. */ -static gboolean application_demo_task_worker( - BackgroundTask *task, - GCancellable *cancellable, - gpointer worker_data, - gpointer *result, - GError **error +static gboolean application_refresh_investigation_tree( + Application *application, + const char *investigation_root_path ) { - guint step = 0; - char status_message[64]; + InvestigationTreeModel *new_tree_model = NULL; + InvestigationTreeModel *old_tree_model = NULL; - (void) worker_data; - - if (task == NULL || - cancellable == NULL || - result == NULL || - error == NULL) + if (application == NULL || + application->main_window == NULL || + investigation_root_path == NULL || + investigation_root_path[0] == '\0') { return FALSE; } - for (step = 0; step <= 20; step++) + new_tree_model = + investigation_tree_builder_build( + investigation_root_path + ); + + if (new_tree_model == NULL) { - if (g_cancellable_set_error_if_cancelled( - cancellable, - error - )) - { - return FALSE; - } - - g_snprintf( - status_message, - sizeof(status_message), - "Étape %u sur 20", - step - ); - - background_task_report_progress( - task, - (double) step / 20.0, - status_message - ); - - if (step < 20) - { - g_usleep( - 500000 - ); - } + return FALSE; } - *result = g_strdup( - "Tâche de démonstration terminée." + old_tree_model = + application->tree_model; + + /* + * MainWindow transmet le nouveau modèle à la Sidebar. + * + * InvestigationTreeView détache d’abord son ancien modèle GTK, + * puis construit le nouveau. L’ancien modèle métier reste vivant + * pendant toute cette opération. + */ + main_window_set_tree_model( + application->main_window, + new_tree_model + ); + + application->tree_model = + new_tree_model; + + /* + * La vue ne référence plus les anciens nœuds. + */ + investigation_tree_model_free( + old_tree_model ); return TRUE; } /** - * @brief Journalise la fin de la tâche de démonstration. + * @brief Traite la fin d’une tâche d’import. * - * @param task Tâche terminée. - * @param user_data Données inutilisées. + * @param task Tâche d’import terminée. + * @param user_data Contexte ApplicationEvidenceImportContext. */ -static void application_demo_task_completed( +static void application_on_evidence_import_completed( BackgroundTask *task, gpointer user_data ) { + ApplicationEvidenceImportContext *context = + user_data; + + Application *application = NULL; + BackgroundTaskState state; - const char *result = NULL; + + EvidenceRecord *evidence_record = NULL; + + const char *original_name = NULL; + + char *status_message = NULL; + GError *error = NULL; - (void) user_data; - - if (task == NULL) + if (task == NULL || + context == NULL) { return; } - state = background_task_get_state( - task + application = + context->application; + + if (application == NULL || + application->main_window == NULL) + { + return; + } + + main_window_set_import_evidence_enabled( + application->main_window, + application->session != NULL ); - if (state == BACKGROUND_TASK_STATE_COMPLETED) - { - result = background_task_get_result( + state = + background_task_get_state( task ); - g_print( - "%s\n", - result != NULL - ? result - : "Tâche terminée." - ); - - return; - } - - if (state == BACKGROUND_TASK_STATE_CANCELLED) + if (state == + BACKGROUND_TASK_STATE_COMPLETED) { - g_print( - "Tâche de démonstration annulée.\n" + evidence_record = + background_task_get_result( + task + ); + + if (evidence_record == NULL) + { + application_present_error( + application, + "Import incomplet", + "La tâche est terminée sans fournir de preuve." + ); + + return; + } + + original_name = + evidence_record_get_original_name( + evidence_record + ); + + if (application_session_matches_root( + application, + context->investigation_root_path + )) + { + if (application_refresh_investigation_tree( + application, + context->investigation_root_path + )) + { + status_message = + g_strdup_printf( + "Preuve importée : %s", + original_name != NULL + ? original_name + : "(nom indisponible)" + ); + } + else + { + /* + * L’import reste valide même si le rafraîchissement visuel + * échoue. Le fichier et la ligne SQLite ne doivent surtout + * pas être présentés comme un échec. + */ + status_message = + g_strdup_printf( + "Preuve importée : %s — " + "arborescence non actualisée", + original_name != NULL + ? original_name + : "(nom indisponible)" + ); + + g_warning( + "La preuve a été importée, mais l’arborescence de '%s' " + "n’a pas pu être reconstruite.", + context->investigation_root_path + ); + } + + main_window_set_status( + application->main_window, + status_message + ); + } + g_message( + "Preuve importée dans '%s' : %s", + context->investigation_root_path, + original_name != NULL + ? original_name + : "(nom indisponible)" + ); + + g_free( + status_message ); return; } - error = background_task_dup_error( - task - ); + if (state == + BACKGROUND_TASK_STATE_CANCELLED) + { + if (application_session_matches_root( + application, + context->investigation_root_path + )) + { + main_window_set_status( + application->main_window, + "Import de preuve annulé." + ); + } + + return; + } + + error = + background_task_dup_error( + task + ); g_warning( - "La tâche de démonstration a échoué : %s", + "L’import de la preuve a échoué : %s", error != NULL ? error->message : "erreur inconnue" ); + application_present_error( + application, + "Import impossible", + error != NULL + ? error->message + : "La preuve n’a pas pu être importée." + ); + g_clear_error( &error ); } /** - * @brief Crée et lance une tâche de démonstration. + * @brief Prépare et démarre l’import asynchrone d’un fichier. + */ +static void application_start_evidence_import( + Application *application, + const char *file_path +) +{ + const InvestigationProject *project = NULL; + + const char *root_path = NULL; + const char *database_path = NULL; + + EvidenceImportTaskRequest request = + {0}; + + ApplicationEvidenceImportContext *context = + NULL; + + BackgroundTask *task = NULL; + + char *destination_directory = NULL; + char *source_name = NULL; + char *status_message = NULL; + + GError *error = NULL; + + if (application == NULL || + application->main_window == NULL || + application->session == NULL || + application->task_manager == NULL || + file_path == NULL || + file_path[0] == '\0') + { + return; + } + + project = + investigation_session_get_project( + application->session + ); + + if (project == NULL) + { + application_present_error( + application, + "Import impossible", + "La session ne fournit aucun projet d’enquête." + ); + + return; + } + + root_path = + investigation_project_get_root_path( + project + ); + + database_path = + investigation_project_get_database_path( + project + ); + + if (root_path == NULL || + root_path[0] == '\0' || + database_path == NULL || + database_path[0] == '\0') + { + application_present_error( + application, + "Import impossible", + "Les chemins de l’enquête sont invalides." + ); + + return; + } + + destination_directory = + g_build_filename( + root_path, + "01_Preuves_Originales", + "Documents", + NULL + ); + + if (destination_directory == NULL) + { + application_present_error( + application, + "Import impossible", + "Impossible de construire le dossier de destination." + ); + + return; + } + + /* + * Le dossier devrait exister dans une enquête normale. + * Cette création rend néanmoins l’import robuste face à un dossier + * supprimé accidentellement. + */ + if (g_mkdir_with_parents( + destination_directory, + 0700 + ) != 0) + { + status_message = + g_strdup_printf( + "Impossible de préparer le dossier '%s' : %s", + destination_directory, + g_strerror(errno) + ); + + application_present_error( + application, + "Import impossible", + status_message + ); + + g_free( + status_message + ); + + g_free( + destination_directory + ); + + return; + } + + context = + g_try_new0( + ApplicationEvidenceImportContext, + 1 + ); + + if (context == NULL) + { + application_present_error( + application, + "Import impossible", + "Impossible d’allouer le contexte de la tâche." + ); + + g_free( + destination_directory + ); + + return; + } + + context->application = + application; + + context->investigation_root_path = + g_strdup( + root_path + ); + + if (context->investigation_root_path == NULL) + { + application_evidence_import_context_free( + context + ); + + g_free( + destination_directory + ); + + application_present_error( + application, + "Import impossible", + "Impossible de conserver le chemin de l’enquête." + ); + + return; + } + + request.database_path = + database_path; + + request.source_path = + file_path; + + request.destination_directory = + destination_directory; + + request.relative_directory = + APPLICATION_EVIDENCE_DIRECTORY; + + request.type_identifier = + APPLICATION_DEFAULT_EVIDENCE_TYPE; + + request.collected_at = NULL; + request.source = NULL; + request.description = NULL; + + task = + evidence_import_task_start( + application->task_manager, + &request, + application_on_evidence_import_completed, + context, + application_evidence_import_context_free, + &error + ); + + if (task == NULL) + { + /* + * La tâche n’a pas pris possession du contexte. + */ + application_evidence_import_context_free( + context + ); + + application_present_error( + application, + "Import impossible", + error != NULL + ? error->message + : "La tâche d’import n’a pas pu être démarrée." + ); + + g_clear_error( + &error + ); + + g_free( + destination_directory + ); + + return; + } + + /* + * Le TaskManager et le worker conservent désormais leurs références. + */ + background_task_unref( + task + ); + + main_window_set_import_evidence_enabled( + application->main_window, + FALSE + ); + + source_name = + g_path_get_basename( + file_path + ); + + status_message = + g_strdup_printf( + "Import en cours : %s", + source_name != NULL + ? source_name + : file_path + ); + + main_window_set_status( + application->main_window, + status_message + ); + + g_free( + status_message + ); + + g_free( + source_name + ); + + g_free( + destination_directory + ); +} + +/** + * @brief Traite le fichier de preuve sélectionné. + * + * @param file_path Chemin local sélectionné, ou NULL. + * @param error Erreur de sélection, ou NULL. + * @param user_data Pointeur vers Application. + */ +static void application_on_evidence_file_selected( + const char *file_path, + const GError *error, + gpointer user_data +) +{ + Application *application = + user_data; + + if (application == NULL || + application->main_window == NULL) + { + return; + } + + /* + * Une annulation ne constitue pas une erreur. + */ + if (file_path == NULL && + error == NULL) + { + main_window_set_status( + application->main_window, + "Import de preuve annulé." + ); + + return; + } + + if (error != NULL) + { + g_warning( + "Impossible de sélectionner le fichier : %s", + error->message + ); + + application_present_error( + application, + "Sélection impossible", + error->message + ); + + return; + } + + if (file_path == NULL || + file_path[0] == '\0') + { + application_present_error( + application, + "Sélection impossible", + "Le chemin du fichier sélectionné est invalide." + ); + + return; + } + + application_start_evidence_import( + application, + file_path + ); +} + +/** + * @brief Traite la demande d'import d'une preuve. + * + * Le sélecteur de fichier sera ajouté à l'étape suivante. * * @param user_data Pointeur vers Application. */ -static void application_on_demo_task_requested( +static void application_on_import_evidence_requested( gpointer user_data ) { Application *application = user_data; - BackgroundTask *task = NULL; - GError *error = NULL; if (application == NULL || - application->task_manager == NULL) + application->main_window == NULL) { return; } - task = background_task_new( - "Tâche de démonstration" - ); - - if (task == NULL) + if (application->session == NULL) { - application_present_error( - application, - "Tâche impossible", - "La tâche de démonstration n'a pas pu être créée." + main_window_set_import_evidence_enabled( + application->main_window, + FALSE ); return; } - /* - * Le manager prend une référence supplémentaire. - */ - if (!task_manager_add( - application->task_manager, - task, - &error - )) - { - g_warning( - "Impossible d'ajouter la tâche : %s", - error != NULL - ? error->message - : "erreur inconnue" - ); - - application_present_error( - application, - "Tâche impossible", - error != NULL - ? error->message - : "La tâche n'a pas pu être ajoutée." - ); - - g_clear_error( - &error - ); - - background_task_unref( - task - ); - - return; - } - - if (!background_task_start( - task, - application_demo_task_worker, - NULL, - NULL, - g_free, - application_demo_task_completed, - NULL, - NULL, - &error - )) - { - g_warning( - "Impossible de démarrer la tâche : %s", - error != NULL - ? error->message - : "erreur inconnue" - ); - - application_present_error( - application, - "Démarrage impossible", - error != NULL - ? error->message - : "La tâche n'a pas pu être démarrée." - ); - - g_clear_error( - &error - ); - - task_manager_remove( - application->task_manager, - task - ); - - background_task_unref( - task - ); - - return; - } - - /* - * La référence locale n'est plus nécessaire : - * - * - TaskManager conserve une référence ; - * - BackgroundTask conserve une référence interne pendant - * l'exécution. - */ - background_task_unref( - task + file_dialog_select_file( + main_window_get_window( + application->main_window + ), + application_on_evidence_file_selected, + application ); } @@ -1183,12 +1642,17 @@ static void application_on_activate( application ); - main_window_set_demo_task_callback( + main_window_set_import_evidence_callback( application->main_window, - application_on_demo_task_requested, + application_on_import_evidence_requested, application ); + main_window_set_import_evidence_enabled( + application->main_window, + application->session != NULL + ); + main_window_set_quit_callback( application->main_window, application_on_quit_requested, diff --git a/src/core/evidence_import_task.c b/src/core/evidence_import_task.c new file mode 100644 index 0000000..e935fe2 --- /dev/null +++ b/src/core/evidence_import_task.c @@ -0,0 +1,663 @@ +/****************************************************************************** + * @file evidence_import_task.c + * @brief Exécution asynchrone de l’import d’une preuve. + ******************************************************************************/ + +#include "core/evidence_import_task.h" + +#include "core/evidence_importer.h" +#include "database/database.h" +#include "models/evidence_record.h" + +#include +#include + +/** + * @brief Copie privée des paramètres nécessaires au worker. + */ +typedef struct +{ + char *database_path; + + char *source_path; + char *destination_directory; + char *relative_directory; + + char *type_identifier; + char *collected_at; + char *source; + char *description; +} EvidenceImportTaskData; + +/** + * @brief Libère les données privées du worker. + */ +static void evidence_import_task_data_free( + gpointer user_data +) +{ + EvidenceImportTaskData *task_data = + user_data; + + if (task_data == NULL) + { + return; + } + + g_free( + task_data->description + ); + + g_free( + task_data->source + ); + + g_free( + task_data->collected_at + ); + + g_free( + task_data->type_identifier + ); + + g_free( + task_data->relative_directory + ); + + g_free( + task_data->destination_directory + ); + + g_free( + task_data->source_path + ); + + g_free( + task_data->database_path + ); + + g_free( + task_data + ); +} + +/** + * @brief Vérifie les paramètres obligatoires. + */ +static gboolean evidence_import_task_request_is_valid( + const EvidenceImportTaskRequest *request +) +{ + if (request == NULL) + { + return FALSE; + } + + if (request->database_path == NULL || + request->database_path[0] == '\0') + { + return FALSE; + } + + if (request->source_path == NULL || + request->source_path[0] == '\0') + { + return FALSE; + } + + if (request->destination_directory == NULL || + request->destination_directory[0] == '\0') + { + return FALSE; + } + + if (request->relative_directory == NULL || + request->relative_directory[0] == '\0') + { + return FALSE; + } + + if (request->type_identifier == NULL || + request->type_identifier[0] == '\0') + { + return FALSE; + } + + return TRUE; +} + +/** + * @brief Copie les paramètres avant le lancement du thread. + */ +static EvidenceImportTaskData * +evidence_import_task_data_new( + const EvidenceImportTaskRequest *request +) +{ + EvidenceImportTaskData *task_data = + NULL; + + if (!evidence_import_task_request_is_valid( + request + )) + { + return NULL; + } + + task_data = + g_try_new0( + EvidenceImportTaskData, + 1 + ); + + if (task_data == NULL) + { + return NULL; + } + + task_data->database_path = + g_strdup( + request->database_path + ); + + task_data->source_path = + g_strdup( + request->source_path + ); + + task_data->destination_directory = + g_strdup( + request->destination_directory + ); + + task_data->relative_directory = + g_strdup( + request->relative_directory + ); + + task_data->type_identifier = + g_strdup( + request->type_identifier + ); + + task_data->collected_at = + g_strdup( + request->collected_at + ); + + task_data->source = + g_strdup( + request->source + ); + + task_data->description = + g_strdup( + request->description + ); + + return task_data; +} + +GQuark evidence_import_task_error_quark(void) +{ + return g_quark_from_static_string( + "evidence-import-task-error-quark" + ); +} + +/** + * @brief Traduit l’annulation métier en annulation GIO. + * + * BackgroundTask reconnaît une annulation uniquement avec : + * + * G_IO_ERROR / G_IO_ERROR_CANCELLED. + */ +static void evidence_import_task_propagate_import_error( + GError **error, + GError *import_error +) +{ + if (import_error != NULL && + g_error_matches( + import_error, + EVIDENCE_IMPORTER_ERROR, + EVIDENCE_IMPORTER_ERROR_CANCELLED + )) + { + g_clear_error( + &import_error + ); + + g_set_error_literal( + error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED, + "L’import de la preuve a été annulé." + ); + + return; + } + + if (import_error != NULL) + { + g_propagate_error( + error, + import_error + ); + + return; + } + + g_set_error_literal( + error, + EVIDENCE_IMPORT_TASK_ERROR, + EVIDENCE_IMPORT_TASK_ERROR_IMPORT, + "L’import a échoué sans fournir d’erreur." + ); +} + +/** + * @brief Exécute l’import dans le thread secondaire. + */ +static gboolean evidence_import_task_worker( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +) +{ + EvidenceImportTaskData *task_data = + worker_data; + + EvidenceImportRequest import_request = + {0}; + + Database *database = NULL; + + EvidenceImporter *evidence_importer = + NULL; + + EvidenceRecord *evidence_record = + NULL; + + GError *importer_error = NULL; + GError *import_error = NULL; + + if (task == NULL || + cancellable == NULL || + task_data == NULL || + result == NULL || + error == NULL) + { + return FALSE; + } + + *result = NULL; + + if (g_cancellable_set_error_if_cancelled( + cancellable, + error + )) + { + return FALSE; + } + + background_task_report_progress( + task, + 0.05, + "Préparation de l’import" + ); + + database = + database_open( + task_data->database_path + ); + + if (database == NULL) + { + g_set_error( + error, + EVIDENCE_IMPORT_TASK_ERROR, + EVIDENCE_IMPORT_TASK_ERROR_DATABASE, + "Impossible d’ouvrir la base SQLite : %s", + task_data->database_path + ); + + return FALSE; + } + + if (g_cancellable_set_error_if_cancelled( + cancellable, + error + )) + { + database_close( + database + ); + + return FALSE; + } + + evidence_importer = + evidence_importer_new( + database, + &importer_error + ); + + if (evidence_importer == NULL) + { + if (importer_error != NULL) + { + g_propagate_prefixed_error( + error, + importer_error, + "Impossible de préparer l’import : " + ); + } + else + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_TASK_ERROR, + EVIDENCE_IMPORT_TASK_ERROR_IMPORT, + "Impossible de créer le service d’import." + ); + } + + database_close( + database + ); + + return FALSE; + } + + import_request.source_path = + task_data->source_path; + + import_request.destination_directory = + task_data->destination_directory; + + import_request.relative_directory = + task_data->relative_directory; + + import_request.type_identifier = + task_data->type_identifier; + + import_request.collected_at = + task_data->collected_at; + + import_request.source = + task_data->source; + + import_request.description = + task_data->description; + + background_task_report_progress( + task, + 0.20, + "Copie et vérification de la preuve" + ); + + evidence_record = + evidence_importer_import( + evidence_importer, + &import_request, + cancellable, + &import_error + ); + + if (evidence_record == NULL) + { + evidence_import_task_propagate_import_error( + error, + import_error + ); + + evidence_importer_free( + evidence_importer + ); + + database_close( + database + ); + + return FALSE; + } + + background_task_report_progress( + task, + 1.0, + "Preuve importée" + ); + + evidence_importer_free( + evidence_importer + ); + + database_close( + database + ); + + *result = + evidence_record; + + return TRUE; +} + +/** + * @brief Transmet une erreur secondaire au code appelant. + */ +static void evidence_import_task_propagate_start_error( + GError **error, + GError *start_error, + const char *prefix +) +{ + if (start_error == NULL) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_TASK_ERROR, + EVIDENCE_IMPORT_TASK_ERROR_IMPORT, + "La tâche d’import n’a pas pu être démarrée." + ); + + return; + } + + if (error != NULL) + { + g_propagate_prefixed_error( + error, + start_error, + "%s", + prefix + ); + + return; + } + + g_clear_error( + &start_error + ); +} + +/** + * @brief Libère le résultat EvidenceRecord d’une tâche. + * + * Cette fonction adapte evidence_record_free() au type GDestroyNotify. + * + * @param user_data Pointeur vers EvidenceRecord. + */ +static void evidence_import_task_result_free( + gpointer user_data +) +{ + EvidenceRecord *evidence_record = + user_data; + + evidence_record_free( + evidence_record + ); +} + +BackgroundTask *evidence_import_task_start( + TaskManager *task_manager, + const EvidenceImportTaskRequest *request, + BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, + GDestroyNotify completion_data_destroy, + GError **error +) +{ + EvidenceImportTaskData *task_data = + NULL; + + BackgroundTask *task = NULL; + + char *source_name = NULL; + char *task_title = NULL; + + GError *start_error = NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (task_manager == NULL || + !evidence_import_task_request_is_valid( + request + )) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_TASK_ERROR, + EVIDENCE_IMPORT_TASK_ERROR_INVALID_ARGUMENT, + "Les paramètres de la tâche d’import sont invalides." + ); + + return NULL; + } + + task_data = + evidence_import_task_data_new( + request + ); + + if (task_data == NULL) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_TASK_ERROR, + EVIDENCE_IMPORT_TASK_ERROR_IMPORT, + "Impossible d’allouer les données de la tâche d’import." + ); + + return NULL; + } + + source_name = + g_path_get_basename( + request->source_path + ); + + task_title = + g_strdup_printf( + "Importer la preuve : %s", + source_name + ); + + task = + background_task_new( + task_title + ); + + g_free( + task_title + ); + + g_free( + source_name + ); + + if (task == NULL) + { + evidence_import_task_data_free( + task_data + ); + + g_set_error_literal( + error, + EVIDENCE_IMPORT_TASK_ERROR, + EVIDENCE_IMPORT_TASK_ERROR_IMPORT, + "Impossible de créer la tâche d’import." + ); + + return NULL; + } + + if (!task_manager_add( + task_manager, + task, + &start_error + )) + { + evidence_import_task_data_free( + task_data + ); + + background_task_unref( + task + ); + + evidence_import_task_propagate_start_error( + error, + start_error, + "Impossible d’ajouter la tâche : " + ); + + return NULL; + } + + if (!background_task_start( + task, + evidence_import_task_worker, + task_data, + evidence_import_task_data_free, + evidence_import_task_result_free, + completion_callback, + completion_data, + completion_data_destroy, + &start_error + )) + { + /* + * background_task_start() n’a pas pris possession + * de task_data ni de completion_data en cas d’échec. + */ + evidence_import_task_data_free( + task_data + ); + + task_manager_remove( + task_manager, + task + ); + + background_task_unref( + task + ); + + evidence_import_task_propagate_start_error( + error, + start_error, + "Impossible de démarrer la tâche : " + ); + + return NULL; + } + + return task; +} diff --git a/src/views/file_dialog.c b/src/views/file_dialog.c new file mode 100644 index 0000000..a274d3d --- /dev/null +++ b/src/views/file_dialog.c @@ -0,0 +1,236 @@ +/****************************************************************************** + * @file file_dialog.c + * @brief Implémentation du dialogue de sélection d’un fichier de preuve. + ******************************************************************************/ + +#include "views/file_dialog.h" + +#include + +/** + * @brief Contexte conservé pendant l’opération asynchrone. + */ +typedef struct +{ + FileDialogCallback callback; + gpointer user_data; +} FileDialogContext; + +/** + * @brief Libère le contexte du dialogue. + */ +static void file_dialog_context_free( + FileDialogContext *context +) +{ + g_free( + context + ); +} + +/** + * @brief Transmet le résultat au code appelant. + */ +static void file_dialog_emit_result( + FileDialogContext *context, + const char *file_path, + const GError *error +) +{ + if (context == NULL || + context->callback == NULL) + { + return; + } + + context->callback( + file_path, + error, + context->user_data + ); +} + +/** + * @brief Traite le résultat du sélecteur de fichier. + */ +static void file_dialog_on_file_selected( + GObject *source_object, + GAsyncResult *result, + gpointer user_data +) +{ + GtkFileDialog *dialog = + GTK_FILE_DIALOG( + source_object + ); + + FileDialogContext *context = + user_data; + + GFile *file = NULL; + + char *file_path = NULL; + + GError *error = NULL; + GError *path_error = NULL; + + if (context == NULL) + { + return; + } + + file = + gtk_file_dialog_open_finish( + dialog, + result, + &error + ); + + if (file == NULL) + { + /* + * Fermer volontairement le dialogue n’est pas une erreur. + */ + if (error != NULL && + g_error_matches( + error, + GTK_DIALOG_ERROR, + GTK_DIALOG_ERROR_DISMISSED + )) + { + file_dialog_emit_result( + context, + NULL, + NULL + ); + } + else + { + file_dialog_emit_result( + context, + NULL, + error + ); + } + + g_clear_error( + &error + ); + + file_dialog_context_free( + context + ); + + return; + } + + file_path = + g_file_get_path( + file + ); + + /* + * g_file_get_path() retourne NULL pour une ressource qui ne + * correspond pas à un fichier local. + */ + if (file_path == NULL) + { + g_set_error_literal( + &path_error, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "Le fichier sélectionné ne possède pas de chemin local." + ); + + file_dialog_emit_result( + context, + NULL, + path_error + ); + + g_clear_error( + &path_error + ); + + g_object_unref( + file + ); + + file_dialog_context_free( + context + ); + + return; + } + + file_dialog_emit_result( + context, + file_path, + NULL + ); + + g_free( + file_path + ); + + g_object_unref( + file + ); + + file_dialog_context_free( + context + ); +} + +void file_dialog_select_file( + GtkWindow *parent, + FileDialogCallback callback, + gpointer user_data +) +{ + GtkFileDialog *dialog = NULL; + + FileDialogContext *context = NULL; + + if (callback == NULL) + { + return; + } + + context = + g_new0( + FileDialogContext, + 1 + ); + + context->callback = + callback; + + context->user_data = + user_data; + + dialog = + gtk_file_dialog_new(); + + gtk_file_dialog_set_title( + dialog, + "Sélectionner un fichier de preuve" + ); + + gtk_file_dialog_set_modal( + dialog, + TRUE + ); + + gtk_file_dialog_open( + dialog, + parent, + NULL, + file_dialog_on_file_selected, + context + ); + + g_object_unref( + dialog + ); +} diff --git a/src/views/main_window.c b/src/views/main_window.c index c673c4a..0a9dc3a 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -58,7 +58,7 @@ struct MainWindow GtkWidget *action_bar; GtkWidget *new_investigation_button; GtkWidget *open_investigation_button; - GtkWidget *demo_task_button; + GtkWidget *import_evidence_button; GtkWidget *content_paned; GtkWidget *main_paned; GtkWidget *status_label; @@ -80,17 +80,18 @@ struct MainWindow gpointer open_investigation_user_data; + MainWindowImportEvidenceCallback + import_evidence_callback; + + gpointer + import_evidence_user_data; + MainWindowQuitCallback quit_callback; gpointer quit_user_data; - MainWindowDemoTaskCallback - demo_task_callback; - - gpointer - demo_task_user_data; }; /** @@ -146,12 +147,12 @@ static void main_window_on_open_investigation_clicked( } /** - * @brief Transmet la demande de tâche de démonstration au contrôleur. + * @brief Transmet la demande d'import d'une preuve au contrôleur. * * @param button Bouton ayant reçu le clic. * @param user_data Pointeur vers MainWindow. */ -static void main_window_on_demo_task_clicked( +static void main_window_on_import_evidence_clicked( GtkButton *button, gpointer user_data ) @@ -161,13 +162,13 @@ static void main_window_on_demo_task_clicked( (void) button; if (main_window == NULL || - main_window->demo_task_callback == NULL) + main_window->import_evidence_callback == NULL) { return; } - main_window->demo_task_callback( - main_window->demo_task_user_data + main_window->import_evidence_callback( + main_window->import_evidence_user_data ); } @@ -197,6 +198,54 @@ static void main_window_on_quit_clicked( ); } +/** + * @brief Crée un bouton d'action compact avec une icône. + * + * @param icon_name Nom de l'icône symbolique GTK. + * @param tooltip_text Texte de l'infobulle. + * + * @return Nouveau bouton GTK. + */ +static GtkWidget *main_window_create_action_button( + const char *icon_name, + const char *tooltip_text +) +{ + GtkWidget *button = NULL; + + if (icon_name == NULL || + icon_name[0] == '\0') + { + return NULL; + } + + button = + gtk_button_new_from_icon_name( + icon_name + ); + + if (button == NULL) + { + return NULL; + } + + gtk_widget_add_css_class( + button, + "flat" + ); + + if (tooltip_text != NULL && + tooltip_text[0] != '\0') + { + gtk_widget_set_tooltip_text( + button, + tooltip_text + ); + } + + return button; +} + MainWindow *main_window_new( GtkApplication *application, TaskManager *task_manager @@ -206,6 +255,7 @@ MainWindow *main_window_new( GtkWidget *sidebar_widget = NULL; GtkWidget *workspace_widget = NULL; GtkWidget *task_panel_widget = NULL; + GtkWidget *action_spacer = NULL; if (application == NULL || task_manager == NULL) @@ -252,7 +302,7 @@ MainWindow *main_window_new( */ main_window->action_bar = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, - 8 + 4 ); gtk_widget_set_margin_start( @@ -276,18 +326,46 @@ MainWindow *main_window_new( ); main_window->new_investigation_button = - gtk_button_new_with_label( - "Nouvelle enquête" + main_window_create_action_button( + "document-new-symbolic", + "Créer une nouvelle enquête" ); main_window->open_investigation_button = - gtk_button_new_with_label( + main_window_create_action_button( + "document-open-symbolic", "Ouvrir une enquête" ); - main_window->demo_task_button = - gtk_button_new_with_label( - "Tâche de test" + main_window->import_evidence_button = + main_window_create_action_button( + "document-save-symbolic", + "Importer une preuve" + ); + + main_window->quit_button = + main_window_create_action_button( + "application-exit-symbolic", + "Quitter Labfy Investigation" + ); + + action_spacer = + gtk_box_new( + GTK_ORIENTATION_HORIZONTAL, + 0 + ); + + gtk_widget_set_hexpand( + action_spacer, + TRUE + ); + + /* + * Aucun import n'est autorisé sans enquête ouverte. + */ + gtk_widget_set_sensitive( + main_window->import_evidence_button, + FALSE ); main_window->quit_button = @@ -307,7 +385,12 @@ MainWindow *main_window_new( gtk_box_append( GTK_BOX(main_window->action_bar), - main_window->demo_task_button + main_window->import_evidence_button + ); + + gtk_box_append( + GTK_BOX(main_window->action_bar), + action_spacer ); gtk_box_append( @@ -334,10 +417,10 @@ MainWindow *main_window_new( ); g_signal_connect( - main_window->demo_task_button, + main_window->import_evidence_button, "clicked", G_CALLBACK( - main_window_on_demo_task_clicked + main_window_on_import_evidence_clicked ), main_window ); @@ -795,9 +878,9 @@ void main_window_set_open_investigation_callback( main_window->open_investigation_user_data = user_data; } -void main_window_set_demo_task_callback( +void main_window_set_import_evidence_callback( MainWindow *main_window, - MainWindowDemoTaskCallback callback, + MainWindowImportEvidenceCallback callback, gpointer user_data ) { @@ -806,8 +889,28 @@ void main_window_set_demo_task_callback( return; } - main_window->demo_task_callback = callback; - main_window->demo_task_user_data = user_data; + main_window->import_evidence_callback = + callback; + + main_window->import_evidence_user_data = + user_data; +} + +void main_window_set_import_evidence_enabled( + MainWindow *main_window, + gboolean enabled +) +{ + if (main_window == NULL || + main_window->import_evidence_button == NULL) + { + return; + } + + gtk_widget_set_sensitive( + main_window->import_evidence_button, + enabled + ); } void main_window_set_quit_callback( diff --git a/src/widgets/sidebar.c b/src/widgets/sidebar.c index b3e0989..4cd364a 100644 --- a/src/widgets/sidebar.c +++ b/src/widgets/sidebar.c @@ -146,11 +146,6 @@ void sidebar_set_tree_model( return; } - if (sidebar == NULL) - { - return; - } - investigation_tree_view_set_model( sidebar->tree_view, tree_model diff --git a/tests/test_evidence_import_task.c b/tests/test_evidence_import_task.c new file mode 100644 index 0000000..2216dd9 --- /dev/null +++ b/tests/test_evidence_import_task.c @@ -0,0 +1,798 @@ +/****************************************************************************** + * @file test_evidence_import_task.c + * @brief Tests de la tâche asynchrone d’import d’une preuve. + ******************************************************************************/ + +#include "core/evidence_import_task.h" + +#include "core/background_task.h" +#include "core/task_manager.h" +#include "dao/evidence_dao.h" +#include "database/database.h" +#include "models/evidence_record.h" +#include "core/evidence_importer_test.h" + +#include +#include +#include + +#include +#include + +#define TEST_RELATIVE_DIRECTORY \ + "01_Preuves_Originales/Documents" + +/** + * @brief Contexte utilisé pour attendre la fin asynchrone. + */ +typedef struct +{ + GMainLoop *main_loop; + + gboolean completed; + gboolean timed_out; +} TestCompletionContext; + +/** + * @brief Environnement temporaire d’un test. + */ +typedef struct +{ + char *root_directory; + char *database_path; + + char *source_directory; + char *evidence_directory; + char *destination_directory; + + TaskManager *task_manager; +} TestEvidenceImportTaskFixture; + +/** + * @brief Quitte la boucle principale lorsque la tâche se termine. + */ +static void test_evidence_import_task_completed( + BackgroundTask *task, + gpointer user_data +) +{ + TestCompletionContext *context = + user_data; + + assert(task != NULL); + assert(context != NULL); + + context->completed = TRUE; + + g_main_loop_quit( + context->main_loop + ); +} + +/** + * @brief Empêche un test asynchrone de rester bloqué. + */ +static gboolean test_evidence_import_task_timeout( + gpointer user_data +) +{ + TestCompletionContext *context = + user_data; + + assert(context != NULL); + + context->timed_out = TRUE; + + g_main_loop_quit( + context->main_loop + ); + + return G_SOURCE_REMOVE; +} + +/** + * @brief Attend la fin d’une tâche avec un timeout. + */ +static void test_evidence_import_task_wait( + TestCompletionContext *context +) +{ + guint timeout_source_id = 0; + + assert(context != NULL); + assert(context->main_loop != NULL); + + timeout_source_id = + g_timeout_add_seconds( + 5, + test_evidence_import_task_timeout, + context + ); + + assert(timeout_source_id != 0); + + g_main_loop_run( + context->main_loop + ); + + if (!context->timed_out) + { + assert( + g_source_remove( + timeout_source_id + ) + ); + } + + assert(!context->timed_out); + assert(context->completed); +} + +/** + * @brief Compte les fichiers présents dans la destination. + */ +static guint test_evidence_import_task_count_destination_entries( + const TestEvidenceImportTaskFixture *fixture +) +{ + GDir *directory = NULL; + + const char *entry_name = NULL; + + guint entry_count = 0; + + GError *error = NULL; + + assert(fixture != NULL); + + directory = + g_dir_open( + fixture->destination_directory, + 0, + &error + ); + + assert(directory != NULL); + assert(error == NULL); + + while ((entry_name = g_dir_read_name(directory)) != NULL) + { + assert(entry_name[0] != '\0'); + + entry_count++; + } + + g_dir_close( + directory + ); + + return entry_count; +} + +/** + * @brief Crée une enquête temporaire. + */ +static TestEvidenceImportTaskFixture +test_evidence_import_task_fixture_create(void) +{ + TestEvidenceImportTaskFixture fixture = + {0}; + + GError *error = NULL; + + fixture.root_directory = + g_dir_make_tmp( + "labfy-evidence-import-task-test-XXXXXX", + &error + ); + + assert(fixture.root_directory != NULL); + assert(error == NULL); + + fixture.database_path = + g_build_filename( + fixture.root_directory, + "Enquete.sqlite", + NULL + ); + + fixture.source_directory = + g_build_filename( + fixture.root_directory, + "sources", + NULL + ); + + fixture.evidence_directory = + g_build_filename( + fixture.root_directory, + "01_Preuves_Originales", + NULL + ); + + fixture.destination_directory = + g_build_filename( + fixture.evidence_directory, + "Documents", + NULL + ); + + assert(fixture.database_path != NULL); + assert(fixture.source_directory != NULL); + assert(fixture.evidence_directory != NULL); + assert(fixture.destination_directory != NULL); + + assert( + g_mkdir( + fixture.source_directory, + 0700 + ) == 0 + ); + + assert( + g_mkdir( + fixture.evidence_directory, + 0700 + ) == 0 + ); + + assert( + g_mkdir( + fixture.destination_directory, + 0700 + ) == 0 + ); + + assert( + database_initialize( + fixture.database_path, + "Enquete_EvidenceImportTask", + fixture.root_directory + ) + ); + + fixture.task_manager = + task_manager_new(); + + assert(fixture.task_manager != NULL); + + return fixture; +} + +/** + * @brief Détruit une fixture déjà débarrassée de ses fichiers. + */ +static void test_evidence_import_task_fixture_clear( + TestEvidenceImportTaskFixture *fixture +) +{ + assert(fixture != NULL); + + task_manager_free( + fixture->task_manager + ); + + assert( + g_remove( + fixture->database_path + ) == 0 + ); + + assert( + g_rmdir( + fixture->destination_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->evidence_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->source_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->root_directory + ) == 0 + ); + + g_free( + fixture->destination_directory + ); + + g_free( + fixture->evidence_directory + ); + + g_free( + fixture->source_directory + ); + + g_free( + fixture->database_path + ); + + g_free( + fixture->root_directory + ); +} + +/** + * @brief Vérifie le refus des paramètres invalides. + */ +static void test_evidence_import_task_invalid_arguments(void) +{ + EvidenceImportTaskRequest request = + {0}; + + BackgroundTask *task = NULL; + + GError *error = NULL; + + task = + evidence_import_task_start( + NULL, + &request, + NULL, + NULL, + NULL, + &error + ); + + assert(task == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_IMPORT_TASK_ERROR + ); + + assert( + error->code == + EVIDENCE_IMPORT_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +/** + * @brief Vérifie un import asynchrone complet. + */ +static void test_evidence_import_task_success(void) +{ + TestEvidenceImportTaskFixture fixture = + test_evidence_import_task_fixture_create(); + + TestCompletionContext completion_context = + {0}; + + EvidenceImportTaskRequest request = + {0}; + + BackgroundTask *task = NULL; + + EvidenceRecord *evidence_record = NULL; + + Database *database = NULL; + EvidenceDao *evidence_dao = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + + guint64 evidence_count = 0; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "preuve_asynchrone.txt", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "preuve asynchrone", + -1, + &error + ) + ); + + assert(error == NULL); + + completion_context.main_loop = + g_main_loop_new( + NULL, + FALSE + ); + + assert(completion_context.main_loop != NULL); + + request.database_path = + fixture.database_path; + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + request.source = + "Test automatisé EvidenceImportTask."; + + request.description = + "Import réalisé depuis un thread secondaire."; + + assert( + test_evidence_import_task_count_destination_entries( + &fixture + ) == 0 + ); + + task = + evidence_import_task_start( + fixture.task_manager, + &request, + test_evidence_import_task_completed, + &completion_context, + NULL, + &error + ); + + assert(task != NULL); + assert(error == NULL); + + assert( + task_manager_get_count( + fixture.task_manager + ) == 1 + ); + + test_evidence_import_task_wait( + &completion_context + ); + + assert( + test_evidence_import_task_count_destination_entries( + &fixture + ) == 1 + ); + + assert( + background_task_get_state( + task + ) == + BACKGROUND_TASK_STATE_COMPLETED + ); + + assert( + background_task_get_progress( + task + ) == 1.0 + ); + + assert( + background_task_dup_error( + task + ) == NULL + ); + + evidence_record = + background_task_get_result( + task + ); + + assert(evidence_record != NULL); + + assert( + strcmp( + evidence_record_get_original_name( + evidence_record + ), + "preuve_asynchrone.txt" + ) == 0 + ); + + assert( + evidence_record_get_integrity_status( + evidence_record + ) == + EVIDENCE_INTEGRITY_STATUS_VALID + ); + + destination_path = + g_build_filename( + fixture.destination_directory, + evidence_record_get_internal_name( + evidence_record + ), + NULL + ); + + assert(destination_path != NULL); + + assert( + g_file_test( + destination_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + /* + * Le worker a fermé sa connexion. + * Une nouvelle connexion permet de vérifier la persistance. + */ + database = + database_open( + fixture.database_path + ); + + assert(database != NULL); + + evidence_dao = + evidence_dao_new( + database, + &error + ); + + assert(evidence_dao != NULL); + assert(error == NULL); + + assert( + evidence_dao_count( + evidence_dao, + &evidence_count, + &error + ) + ); + + assert(error == NULL); + assert(evidence_count == 1); + + evidence_dao_free( + evidence_dao + ); + + database_close( + database + ); + + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + g_main_loop_unref( + completion_context.main_loop + ); + + /* + * La tâche reste encore possédée par le manager. + */ + background_task_unref( + task + ); + + test_evidence_import_task_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie l’annulation après la copie de la preuve. + */ +static void test_evidence_import_task_cancelled_after_copy(void) +{ + TestEvidenceImportTaskFixture fixture = + test_evidence_import_task_fixture_create(); + + TestCompletionContext completion_context = + {0}; + + EvidenceImportTaskRequest request = + {0}; + + BackgroundTask *task = NULL; + + char *source_path = NULL; + + guint64 evidence_count = 42; + + Database *database = NULL; + EvidenceDao *evidence_dao = NULL; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "preuve_annulee.txt", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "preuve annulée après copie", + -1, + &error + ) + ); + + assert(error == NULL); + + completion_context.main_loop = + g_main_loop_new( + NULL, + FALSE + ); + + assert(completion_context.main_loop != NULL); + + request.database_path = + fixture.database_path; + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + evidence_importer_test_reset_hooks(); + + evidence_importer_test_set_cancel_after_copy( + TRUE + ); + + task = + evidence_import_task_start( + fixture.task_manager, + &request, + test_evidence_import_task_completed, + &completion_context, + NULL, + &error + ); + + assert(task != NULL); + assert(error == NULL); + + test_evidence_import_task_wait( + &completion_context + ); + + assert( + background_task_get_state( + task + ) == + BACKGROUND_TASK_STATE_CANCELLED + ); + + assert( + test_evidence_import_task_count_destination_entries( + &fixture + ) == 0 + ); + + database = + database_open( + fixture.database_path + ); + + assert(database != NULL); + + evidence_dao = + evidence_dao_new( + database, + &error + ); + + assert(evidence_dao != NULL); + assert(error == NULL); + + assert( + evidence_dao_count( + evidence_dao, + &evidence_count, + &error + ) + ); + + assert(error == NULL); + assert(evidence_count == 0); + + evidence_dao_free( + evidence_dao + ); + + database_close( + database + ); + + /* + * Le fichier original reste intact. + */ + assert( + g_file_test( + source_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + evidence_importer_test_reset_hooks(); + + background_task_unref( + task + ); + + g_main_loop_unref( + completion_context.main_loop + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + source_path + ); + + test_evidence_import_task_fixture_clear( + &fixture + ); +} + +int main(void) +{ + test_evidence_import_task_invalid_arguments(); + test_evidence_import_task_success(); + test_evidence_import_task_cancelled_after_copy(); + + printf( + "EvidenceImportTask : premiers tests valides.\n" + ); + + return 0; +}