feat: add asynchronous cancellable image import

This commit is contained in:
fy59 2026-08-06 19:35:58 +02:00
parent f2efe418cb
commit 8bb90ed05a
10 changed files with 1044 additions and 44 deletions

View file

@ -33,3 +33,5 @@ La variable `LARDON3D_PROJECTS_ROOT` permet de choisir un autre répertoire raci
L'import accepte les images JPEG, PNG, TIFF et HEIC présentes directement dans L'import accepte les images JPEG, PNG, TIFF et HEIC présentes directement dans
le dossier choisi, sans parcourir ses sous-dossiers. Elles sont copiées vers le dossier choisi, sans parcourir ses sous-dossiers. Elles sont copiées vers
`images/originals` et répertoriées dans `images/manifest.tsv`. `images/originals` et répertoriées dans `images/manifest.tsv`.
L'import s'exécute en arrière-plan afin que la TUI reste réactive. Pendant une
opération, la touche `C` demande son annulation.

View file

@ -13,10 +13,44 @@ typedef struct {
size_t ignored; size_t ignored;
} Lardon3DImportResult; } Lardon3DImportResult;
typedef struct {
size_t total;
size_t processed;
size_t copied;
size_t already_present;
size_t ignored;
const char *message;
} Lardon3DImportProgress;
typedef bool (*Lardon3DImportCancelled)(void *context);
typedef void (*Lardon3DImportProgressed)(
void *context,
const Lardon3DImportProgress *progress
);
typedef struct {
void *context;
Lardon3DImportCancelled is_cancelled;
Lardon3DImportProgressed progressed;
} Lardon3DImportControl;
typedef enum {
LARDON3D_IMPORT_FAILED = 0,
LARDON3D_IMPORT_SUCCEEDED,
LARDON3D_IMPORT_CANCELLED
} Lardon3DImportOutcome;
bool lardon3d_import_directory( bool lardon3d_import_directory(
Lardon3DAppState *state, Lardon3DAppState *state,
const char *source_directory, const char *source_directory,
Lardon3DImportResult *result Lardon3DImportResult *result
); );
Lardon3DImportOutcome lardon3d_import_directory_controlled(
Lardon3DAppState *state,
const char *source_directory,
Lardon3DImportResult *result,
const Lardon3DImportControl *control
);
#endif #endif

View file

@ -0,0 +1,44 @@
#ifndef LARDON3D_IMPORT_TASK_H
#define LARDON3D_IMPORT_TASK_H
#include <stdbool.h>
#include <stddef.h>
#include <lardon3d/app_state.h>
typedef enum {
LARDON3D_IMPORT_TASK_IDLE = 0,
LARDON3D_IMPORT_TASK_RUNNING,
LARDON3D_IMPORT_TASK_SUCCEEDED,
LARDON3D_IMPORT_TASK_CANCELLED,
LARDON3D_IMPORT_TASK_FAILED
} Lardon3DImportTaskStatus;
typedef struct {
Lardon3DImportTaskStatus status;
size_t total;
size_t processed;
size_t copied;
size_t already_present;
size_t ignored;
char message[256];
} Lardon3DImportTaskSnapshot;
typedef struct Lardon3DImportTask Lardon3DImportTask;
Lardon3DImportTask *lardon3d_import_task_create(void);
bool lardon3d_import_task_start(
Lardon3DImportTask *task,
const Lardon3DAppState *state,
const char *source_directory
);
void lardon3d_import_task_request_cancel(Lardon3DImportTask *task);
bool lardon3d_import_task_snapshot(
Lardon3DImportTask *task,
Lardon3DImportTaskSnapshot *snapshot
);
bool lardon3d_import_task_is_finished(Lardon3DImportTask *task);
bool lardon3d_import_task_join(Lardon3DImportTask *task);
void lardon3d_import_task_destroy(Lardon3DImportTask *task);
#endif

View file

@ -2,11 +2,13 @@
#define LARDON3D_LAYOUT_H #define LARDON3D_LAYOUT_H
#include <lardon3d/app_state.h> #include <lardon3d/app_state.h>
#include <lardon3d/import_task.h>
void lardon3d_layout_draw( void lardon3d_layout_draw(
const Lardon3DAppState *state, const Lardon3DAppState *state,
const char *input_text, const char *input_text,
const char *input_label, const char *input_label,
const Lardon3DImportTaskSnapshot *import_snapshot,
int rows, int rows,
int cols int cols
); );

View file

@ -19,6 +19,7 @@ add_project_arguments(
) )
ncursesw = dependency('ncursesw', required: true) ncursesw = dependency('ncursesw', required: true)
threads = dependency('threads')
executable( executable(
'lardon3d', 'lardon3d',
@ -29,10 +30,11 @@ executable(
'src/tui.c', 'src/tui.c',
'src/layout.c', 'src/layout.c',
'src/import.c', 'src/import.c',
'src/import_task.c',
'src/project.c', 'src/project.c',
], ],
include_directories: include_directories('include'), include_directories: include_directories('include'),
dependencies: [ncursesw], dependencies: [ncursesw, threads],
) )
import_test = executable( import_test = executable(
@ -46,3 +48,17 @@ import_test = executable(
) )
test('import', import_test) test('import', import_test)
import_task_test = executable(
'test-import-task',
sources: [
'tests/test_import_task.c',
'src/app_state.c',
'src/import.c',
'src/import_task.c',
],
include_directories: include_directories('include'),
dependencies: [threads],
)
test('import-task', import_task_test, timeout: 30)

View file

@ -37,6 +37,13 @@ typedef struct {
size_t capacity; size_t capacity;
} CandidateList; } CandidateList;
typedef enum {
COPY_IMAGE_ERROR = -1,
COPY_IMAGE_COLLISION = 0,
COPY_IMAGE_CREATED = 1,
COPY_IMAGE_CANCELLED = 2,
} CopyImageOutcome;
static void static void
set_status(Lardon3DAppState *state, const char *message) set_status(Lardon3DAppState *state, const char *message)
{ {
@ -48,6 +55,37 @@ set_status(Lardon3DAppState *state, const char *message)
); );
} }
static bool
import_is_cancelled(const Lardon3DImportControl *control)
{
return control && control->is_cancelled
&& control->is_cancelled(control->context);
}
static void
publish_progress(
const Lardon3DImportControl *control,
const Lardon3DImportResult *result,
size_t processed,
const char *message
)
{
if (!control || !control->progressed) {
return;
}
const Lardon3DImportProgress progress = {
.total = result->admissible_found,
.processed = processed <= result->admissible_found
? processed
: result->admissible_found,
.copied = result->copied,
.already_present = result->already_present,
.ignored = result->ignored,
.message = message,
};
control->progressed(control->context, &progress);
}
static bool static bool
join_path( join_path(
char destination[PATH_MAX], char destination[PATH_MAX],
@ -416,7 +454,8 @@ static int
copy_image( copy_image(
const char *source_path, const char *source_path,
const char *destination_path, const char *destination_path,
off_t *destination_size off_t *destination_size,
const Lardon3DImportControl *control
) )
{ {
int source = open(source_path, O_RDONLY | O_NOFOLLOW); int source = open(source_path, O_RDONLY | O_NOFOLLOW);
@ -445,8 +484,14 @@ copy_image(
} }
bool success = true; bool success = true;
bool cancelled = false;
char buffer[64 * 1024]; char buffer[64 * 1024];
for (;;) { for (;;) {
if (import_is_cancelled(control)) {
cancelled = true;
success = false;
break;
}
ssize_t read_count = read(source, buffer, sizeof(buffer)); ssize_t read_count = read(source, buffer, sizeof(buffer));
if (read_count == 0) { if (read_count == 0) {
break; break;
@ -461,6 +506,11 @@ copy_image(
ssize_t written_total = 0; ssize_t written_total = 0;
while (written_total < read_count) { while (written_total < read_count) {
if (import_is_cancelled(control)) {
cancelled = true;
success = false;
break;
}
ssize_t written = write( ssize_t written = write(
destination, destination,
buffer + written_total, buffer + written_total,
@ -491,11 +541,11 @@ copy_image(
} }
if (!success) { if (!success) {
(void)unlink(destination_path); (void)unlink(destination_path);
return -1; return cancelled ? COPY_IMAGE_CANCELLED : COPY_IMAGE_ERROR;
} }
*destination_size = source_info.st_size; *destination_size = source_info.st_size;
return 1; return COPY_IMAGE_CREATED;
} }
static bool static bool
@ -504,7 +554,9 @@ analyze_source_directory(
const char *absolute_source, const char *absolute_source,
const char *originals_path, const char *originals_path,
Lardon3DImportResult *result, Lardon3DImportResult *result,
CandidateList *candidates CandidateList *candidates,
const Lardon3DImportControl *control,
bool *cancelled
) )
{ {
DIR *directory = opendir(absolute_source); DIR *directory = opendir(absolute_source);
@ -515,6 +567,10 @@ analyze_source_directory(
bool success = true; bool success = true;
for (;;) { for (;;) {
if (import_is_cancelled(control)) {
*cancelled = true;
break;
}
errno = 0; errno = 0;
struct dirent *entry = readdir(directory); struct dirent *entry = readdir(directory);
if (!entry) { if (!entry) {
@ -600,7 +656,7 @@ rollback_created_files(
return removed; return removed;
} }
static bool static void
fail_import( fail_import(
Lardon3DAppState *state, Lardon3DAppState *state,
Lardon3DImportResult *result, Lardon3DImportResult *result,
@ -638,47 +694,80 @@ fail_import(
reason reason
); );
} }
return false;
} }
bool static Lardon3DImportOutcome
lardon3d_import_directory( cancel_import(
Lardon3DAppState *state,
Lardon3DImportResult *result,
size_t processed,
ManifestWriter *manifest,
CandidateList *candidates,
const char *originals_path,
const Lardon3DImportControl *control
)
{
manifest_abort(manifest);
size_t removed = rollback_created_files(candidates, originals_path);
result->copied -= removed;
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Import annulé : %zu sur %zu fichiers traités.",
processed,
result->admissible_found
);
publish_progress(control, result, processed, state->status_message);
free(candidates->items);
return LARDON3D_IMPORT_CANCELLED;
}
Lardon3DImportOutcome
lardon3d_import_directory_controlled(
Lardon3DAppState *state, Lardon3DAppState *state,
const char *source_directory, const char *source_directory,
Lardon3DImportResult *result Lardon3DImportResult *result,
const Lardon3DImportControl *control
) )
{ {
if (!state || !result) { if (!state || !result) {
return false; return LARDON3D_IMPORT_FAILED;
} }
*result = (Lardon3DImportResult) {0}; *result = (Lardon3DImportResult) {0};
publish_progress(control, result, 0, "Analyse du dossier source...");
if (!state->project_loaded) { if (!state->project_loaded) {
set_status(state, "Aucun projet chargé."); set_status(state, "Aucun projet chargé.");
return false; publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
} }
char trimmed_source[PATH_MAX]; char trimmed_source[PATH_MAX];
char absolute_source[PATH_MAX]; char absolute_source[PATH_MAX];
if (!trim_source_path(state, source_directory, trimmed_source)) { if (!trim_source_path(state, source_directory, trimmed_source)) {
return false; publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
} }
if (!resolve_source_path(state, trimmed_source, absolute_source)) { if (!resolve_source_path(state, trimmed_source, absolute_source)) {
return false; publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
} }
if (has_forbidden_manifest_character(absolute_source)) { if (has_forbidden_manifest_character(absolute_source)) {
set_status(state, "Erreur : chemin source incompatible avec le manifeste."); set_status(state, "Erreur : chemin source incompatible avec le manifeste.");
return false; publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
} }
struct stat source_directory_info; struct stat source_directory_info;
if (lstat(absolute_source, &source_directory_info) != 0) { if (lstat(absolute_source, &source_directory_info) != 0) {
set_status(state, "Erreur : dossier source inexistant."); set_status(state, "Erreur : dossier source inexistant.");
return false; publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
} }
if (!S_ISDIR(source_directory_info.st_mode)) { if (!S_ISDIR(source_directory_info.st_mode)) {
set_status(state, "Erreur : la source n'est pas un dossier."); set_status(state, "Erreur : la source n'est pas un dossier.");
return false; publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
} }
char images_path[PATH_MAX]; char images_path[PATH_MAX];
@ -686,35 +775,62 @@ lardon3d_import_directory(
if (!join_path(images_path, state->project_path, "images") if (!join_path(images_path, state->project_path, "images")
|| !join_path(originals_path, images_path, "originals")) { || !join_path(originals_path, images_path, "originals")) {
set_status(state, "Erreur : chemin du projet trop long."); set_status(state, "Erreur : chemin du projet trop long.");
return false; publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
} }
CandidateList candidates = {0}; CandidateList candidates = {0};
bool analysis_cancelled = false;
if (!analyze_source_directory( if (!analyze_source_directory(
state, state,
absolute_source, absolute_source,
originals_path, originals_path,
result, result,
&candidates &candidates,
control,
&analysis_cancelled
)) { )) {
free(candidates.items); free(candidates.items);
return false; publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
if (analysis_cancelled) {
free(candidates.items);
set_status(state, "Import annulé : 0 fichier traité.");
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_CANCELLED;
}
publish_progress(control, result, 0, "Import en cours...");
if (import_is_cancelled(control)) {
free(candidates.items);
set_status(state, "Import annulé : 0 fichier traité.");
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_CANCELLED;
} }
if (!ensure_originals_directory(state, images_path, originals_path)) { if (!ensure_originals_directory(state, images_path, originals_path)) {
free(candidates.items); free(candidates.items);
return false; publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
} }
ManifestWriter manifest; ManifestWriter manifest;
if (!manifest_begin(state, images_path, &manifest)) { if (!manifest_begin(state, images_path, &manifest)) {
free(candidates.items); free(candidates.items);
return false; publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
} }
bool success = true; bool success = true;
bool cancelled = false;
size_t processed = 0;
const char *failure_reason = NULL; const char *failure_reason = NULL;
for (size_t index = 0; index < candidates.count; ++index) { for (size_t index = 0; index < candidates.count; ++index) {
if (import_is_cancelled(control)) {
cancelled = true;
break;
}
ImportCandidate *candidate = &candidates.items[index]; ImportCandidate *candidate = &candidates.items[index];
char source_path[PATH_MAX]; char source_path[PATH_MAX];
char destination_path[PATH_MAX]; char destination_path[PATH_MAX];
@ -730,13 +846,22 @@ lardon3d_import_directory(
} }
off_t size = 0; off_t size = 0;
int copied = copy_image(source_path, destination_path, &size); int copied = copy_image(
if (copied < 0) { source_path,
destination_path,
&size,
control
);
if (copied == COPY_IMAGE_CANCELLED) {
cancelled = true;
break;
}
if (copied == COPY_IMAGE_ERROR) {
success = false; success = false;
failure_reason = "copie d'une image impossible"; failure_reason = "copie d'une image impossible";
break; break;
} }
if (copied == 0) { if (copied == COPY_IMAGE_COLLISION) {
++result->already_present; ++result->already_present;
struct stat destination_info; struct stat destination_info;
bool destination_is_regular = false; bool destination_is_regular = false;
@ -746,6 +871,8 @@ lardon3d_import_directory(
destination_is_regular = true; destination_is_regular = true;
} }
if (!destination_is_regular) { if (!destination_is_regular) {
++processed;
publish_progress(control, result, processed, "Import en cours...");
continue; continue;
} }
} else { } else {
@ -773,8 +900,21 @@ lardon3d_import_directory(
failure_reason = "écriture du manifeste impossible"; failure_reason = "écriture du manifeste impossible";
break; break;
} }
++processed;
publish_progress(control, result, processed, "Import en cours...");
} }
if (cancelled) {
return cancel_import(
state,
result,
processed,
&manifest,
&candidates,
originals_path,
control
);
}
if (!success) { if (!success) {
manifest_abort(&manifest); manifest_abort(&manifest);
size_t removed = rollback_created_files( size_t removed = rollback_created_files(
@ -782,7 +922,20 @@ lardon3d_import_directory(
originals_path originals_path
); );
free(candidates.items); free(candidates.items);
return fail_import(state, result, removed, failure_reason); (void)fail_import(state, result, removed, failure_reason);
publish_progress(control, result, processed, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
if (import_is_cancelled(control)) {
return cancel_import(
state,
result,
processed,
&manifest,
&candidates,
originals_path,
control
);
} }
if (!manifest_commit(state, &manifest)) { if (!manifest_commit(state, &manifest)) {
size_t removed = rollback_created_files( size_t removed = rollback_created_files(
@ -790,12 +943,14 @@ lardon3d_import_directory(
originals_path originals_path
); );
free(candidates.items); free(candidates.items);
return fail_import( (void)fail_import(
state, state,
result, result,
removed, removed,
"mise à jour du manifeste impossible" "mise à jour du manifeste impossible"
); );
publish_progress(control, result, processed, state->status_message);
return LARDON3D_IMPORT_FAILED;
} }
free(candidates.items); free(candidates.items);
@ -808,5 +963,21 @@ lardon3d_import_directory(
result->already_present, result->already_present,
result->already_present == 1 ? "" : "s" result->already_present == 1 ? "" : "s"
); );
return true; publish_progress(control, result, processed, "Import terminé.");
return LARDON3D_IMPORT_SUCCEEDED;
}
bool
lardon3d_import_directory(
Lardon3DAppState *state,
const char *source_directory,
Lardon3DImportResult *result
)
{
return lardon3d_import_directory_controlled(
state,
source_directory,
result,
NULL
) == LARDON3D_IMPORT_SUCCEEDED;
} }

229
src/import_task.c Normal file
View file

@ -0,0 +1,229 @@
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <lardon3d/import.h>
#include <lardon3d/import_task.h>
struct Lardon3DImportTask {
pthread_mutex_t mutex;
pthread_t thread;
bool thread_started;
bool joined;
bool cancel_requested;
Lardon3DAppState worker_state;
char source_directory[PATH_MAX];
Lardon3DImportTaskSnapshot snapshot;
};
static void
copy_message(char destination[256], const char *message)
{
(void)snprintf(destination, 256, "%s", message ? message : "");
}
static bool
task_is_cancelled(void *context)
{
Lardon3DImportTask *task = context;
bool cancelled;
(void)pthread_mutex_lock(&task->mutex);
cancelled = task->cancel_requested;
(void)pthread_mutex_unlock(&task->mutex);
return cancelled;
}
static void
task_progressed(void *context, const Lardon3DImportProgress *progress)
{
Lardon3DImportTask *task = context;
(void)pthread_mutex_lock(&task->mutex);
task->snapshot.total = progress->total;
task->snapshot.processed = progress->processed;
task->snapshot.copied = progress->copied;
task->snapshot.already_present = progress->already_present;
task->snapshot.ignored = progress->ignored;
copy_message(task->snapshot.message, progress->message);
(void)pthread_mutex_unlock(&task->mutex);
}
static void *
run_import(void *argument)
{
Lardon3DImportTask *task = argument;
Lardon3DImportResult result;
const Lardon3DImportControl control = {
.context = task,
.is_cancelled = task_is_cancelled,
.progressed = task_progressed,
};
Lardon3DImportOutcome outcome = lardon3d_import_directory_controlled(
&task->worker_state,
task->source_directory,
&result,
&control
);
(void)pthread_mutex_lock(&task->mutex);
task->snapshot.status = outcome == LARDON3D_IMPORT_SUCCEEDED
? LARDON3D_IMPORT_TASK_SUCCEEDED
: outcome == LARDON3D_IMPORT_CANCELLED
? LARDON3D_IMPORT_TASK_CANCELLED
: LARDON3D_IMPORT_TASK_FAILED;
task->snapshot.total = result.admissible_found;
task->snapshot.copied = result.copied;
task->snapshot.already_present = result.already_present;
copy_message(task->snapshot.message, task->worker_state.status_message);
(void)pthread_mutex_unlock(&task->mutex);
return NULL;
}
Lardon3DImportTask *
lardon3d_import_task_create(void)
{
Lardon3DImportTask *task = calloc(1, sizeof(*task));
if (!task) {
return NULL;
}
if (pthread_mutex_init(&task->mutex, NULL) != 0) {
free(task);
return NULL;
}
task->snapshot.status = LARDON3D_IMPORT_TASK_IDLE;
return task;
}
bool
lardon3d_import_task_start(
Lardon3DImportTask *task,
const Lardon3DAppState *state,
const char *source_directory
)
{
if (!task || !state || !source_directory) {
return false;
}
(void)pthread_mutex_lock(&task->mutex);
if (task->thread_started) {
(void)pthread_mutex_unlock(&task->mutex);
return false;
}
int written = snprintf(
task->source_directory,
sizeof(task->source_directory),
"%s",
source_directory
);
if (written < 0 || (size_t)written >= sizeof(task->source_directory)) {
copy_message(task->snapshot.message, "Erreur : chemin source trop long.");
task->snapshot.status = LARDON3D_IMPORT_TASK_FAILED;
(void)pthread_mutex_unlock(&task->mutex);
return false;
}
task->worker_state = *state;
task->cancel_requested = false;
task->joined = false;
task->snapshot = (Lardon3DImportTaskSnapshot) {
.status = LARDON3D_IMPORT_TASK_RUNNING,
.message = "Analyse du dossier source...",
};
task->thread_started = true;
int error = pthread_create(&task->thread, NULL, run_import, task);
if (error != 0) {
task->thread_started = false;
task->snapshot.status = LARDON3D_IMPORT_TASK_FAILED;
copy_message(
task->snapshot.message,
"Erreur : impossible de lancer la tâche d'import."
);
(void)pthread_mutex_unlock(&task->mutex);
return false;
}
(void)pthread_mutex_unlock(&task->mutex);
return true;
}
void
lardon3d_import_task_request_cancel(Lardon3DImportTask *task)
{
if (!task) {
return;
}
(void)pthread_mutex_lock(&task->mutex);
if (task->snapshot.status == LARDON3D_IMPORT_TASK_RUNNING) {
task->cancel_requested = true;
copy_message(task->snapshot.message, "Annulation demandée...");
}
(void)pthread_mutex_unlock(&task->mutex);
}
bool
lardon3d_import_task_snapshot(
Lardon3DImportTask *task,
Lardon3DImportTaskSnapshot *snapshot
)
{
if (!task || !snapshot) {
return false;
}
(void)pthread_mutex_lock(&task->mutex);
*snapshot = task->snapshot;
(void)pthread_mutex_unlock(&task->mutex);
return true;
}
bool
lardon3d_import_task_is_finished(Lardon3DImportTask *task)
{
Lardon3DImportTaskSnapshot snapshot;
if (!lardon3d_import_task_snapshot(task, &snapshot)) {
return false;
}
return snapshot.status == LARDON3D_IMPORT_TASK_SUCCEEDED
|| snapshot.status == LARDON3D_IMPORT_TASK_CANCELLED
|| snapshot.status == LARDON3D_IMPORT_TASK_FAILED;
}
bool
lardon3d_import_task_join(Lardon3DImportTask *task)
{
if (!task) {
return false;
}
(void)pthread_mutex_lock(&task->mutex);
bool started = task->thread_started;
bool joined = task->joined;
bool should_join = started && !joined;
pthread_t thread = task->thread;
(void)pthread_mutex_unlock(&task->mutex);
if (!should_join) {
return started && joined;
}
if (pthread_join(thread, NULL) != 0) {
return false;
}
(void)pthread_mutex_lock(&task->mutex);
task->joined = true;
(void)pthread_mutex_unlock(&task->mutex);
return true;
}
void
lardon3d_import_task_destroy(Lardon3DImportTask *task)
{
if (!task) {
return;
}
if (task->thread_started && !task->joined) {
lardon3d_import_task_request_cancel(task);
if (!lardon3d_import_task_join(task)) {
return;
}
}
(void)pthread_mutex_destroy(&task->mutex);
free(task);
}

View file

@ -1,4 +1,5 @@
#include <ncurses.h> #include <ncurses.h>
#include <stdio.h>
#include <string.h> #include <string.h>
#include <lardon3d/layout.h> #include <lardon3d/layout.h>
@ -134,9 +135,62 @@ static void
draw_import_screen( draw_import_screen(
const char *input_text, const char *input_text,
const char *input_label, const char *input_label,
const Lardon3DImportTaskSnapshot *snapshot,
int columns int columns
) )
{ {
if (snapshot && snapshot->status == LARDON3D_IMPORT_TASK_RUNNING) {
draw_text(6, 4, columns - 6, "Import en cours");
char line[128];
(void)snprintf(
line,
sizeof(line),
"Fichiers : %zu / %zu",
snapshot->processed,
snapshot->total
);
draw_text(7, 4, columns - 6, line);
(void)snprintf(line, sizeof(line), "Copiés : %zu", snapshot->copied);
draw_text(8, 4, columns - 6, line);
(void)snprintf(
line,
sizeof(line),
"Déjà présents : %zu",
snapshot->already_present
);
draw_text(9, 4, columns - 6, line);
(void)snprintf(line, sizeof(line), "Ignorés : %zu", snapshot->ignored);
draw_text(10, 4, columns - 6, line);
int percent = snapshot->total == 0
? 0
: (int)((snapshot->processed * 100) / snapshot->total);
if (percent > 100) {
percent = 100;
}
int bar_width = columns - 18;
if (bar_width > 40) {
bar_width = 40;
}
if (bar_width < 1) {
bar_width = 1;
}
int filled = (bar_width * percent) / 100;
char bar[64];
bar[0] = '[';
for (int index = 0; index < bar_width; ++index) {
bar[index + 1] = index < filled ? '#' : '-';
}
(void)snprintf(
bar + bar_width + 1,
sizeof(bar) - (size_t)bar_width - 1,
"] %d %%",
percent
);
draw_text(11, 4, columns - 6, bar);
draw_text(12, 4, columns - 6, "C : Annuler l'import");
return;
}
draw_text(7, 4, columns - 6, "I : Importer des images"); draw_text(7, 4, columns - 6, "I : Importer des images");
draw_text(8, 4, columns - 6, "ESC : Accueil"); draw_text(8, 4, columns - 6, "ESC : Accueil");
draw_text(9, 4, columns - 6, "Q : Quitter"); draw_text(9, 4, columns - 6, "Q : Quitter");
@ -148,6 +202,7 @@ draw_content(
const Lardon3DAppState *state, const Lardon3DAppState *state,
const char *input_text, const char *input_text,
const char *input_label, const char *input_label,
const Lardon3DImportTaskSnapshot *import_snapshot,
int rows, int rows,
int columns int columns
) )
@ -156,6 +211,11 @@ draw_content(
const char *content; const char *content;
const char *footer; const char *footer;
screen_texts(state->screen, &title, &content, &footer); screen_texts(state->screen, &title, &content, &footer);
bool import_running = import_snapshot
&& import_snapshot->status == LARDON3D_IMPORT_TASK_RUNNING;
if (import_running) {
footer = "C Annuler l'import Q désactivé";
}
int title_length = (int)strlen(title); int title_length = (int)strlen(title);
int content_length = (int)strlen(content); int content_length = (int)strlen(content);
int journal_row = rows - 7; int journal_row = rows - 7;
@ -176,7 +236,12 @@ draw_content(
columns columns
); );
} else if (state->screen == LARDON3D_SCREEN_IMPORT) { } else if (state->screen == LARDON3D_SCREEN_IMPORT) {
draw_import_screen(input_text, input_label, columns); draw_import_screen(
input_text,
input_label,
import_snapshot,
columns
);
} else { } else {
draw_text( draw_text(
(3 + journal_row) / 2, (3 + journal_row) / 2,
@ -186,7 +251,12 @@ draw_content(
); );
} }
draw_text(journal_row + 1, 2, columns - 4, "Journal"); draw_text(journal_row + 1, 2, columns - 4, "Journal");
draw_text(journal_row + 2, 4, columns - 6, state->status_message); draw_text(
journal_row + 2,
4,
columns - 6,
import_running ? import_snapshot->message : state->status_message
);
draw_text(rows - 2, 2, columns - 4, footer); draw_text(rows - 2, 2, columns - 4, footer);
} }
@ -195,6 +265,7 @@ lardon3d_layout_draw(
const Lardon3DAppState *state, const Lardon3DAppState *state,
const char *input_text, const char *input_text,
const char *input_label, const char *input_label,
const Lardon3DImportTaskSnapshot *import_snapshot,
int rows, int rows,
int columns int columns
) )
@ -209,6 +280,7 @@ lardon3d_layout_draw(
state, state,
input_text, input_text,
input_label, input_label,
import_snapshot,
rows, rows,
columns columns
); );

134
src/tui.c
View file

@ -4,7 +4,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <stdio.h>
#include <lardon3d/import.h> #include <lardon3d/import_task.h>
#include <lardon3d/layout.h> #include <lardon3d/layout.h>
#include <lardon3d/project.h> #include <lardon3d/project.h>
#include <lardon3d/tui.h> #include <lardon3d/tui.h>
@ -28,7 +28,11 @@ typedef struct {
} TuiInput; } TuiInput;
static void static void
redraw(const Lardon3DAppState *state, const TuiInput *input) redraw(
const Lardon3DAppState *state,
const TuiInput *input,
Lardon3DImportTask *task
)
{ {
int rows; int rows;
int columns; int columns;
@ -41,7 +45,19 @@ redraw(const Lardon3DAppState *state, const TuiInput *input)
} else if (input->mode == INPUT_IMPORT_DIRECTORY) { } else if (input->mode == INPUT_IMPORT_DIRECTORY) {
label = "Dossier source :"; label = "Dossier source :";
} }
lardon3d_layout_draw(state, text, label, rows, columns); Lardon3DImportTaskSnapshot snapshot;
const Lardon3DImportTaskSnapshot *displayed_snapshot = NULL;
if (task && lardon3d_import_task_snapshot(task, &snapshot)) {
displayed_snapshot = &snapshot;
}
lardon3d_layout_draw(
state,
text,
label,
displayed_snapshot,
rows,
columns
);
(void)curs_set( (void)curs_set(
input->mode != INPUT_NONE input->mode != INPUT_NONE
&& rows >= MINIMUM_ROWS && rows >= MINIMUM_ROWS
@ -51,10 +67,48 @@ redraw(const Lardon3DAppState *state, const TuiInput *input)
); );
} }
static bool
start_import_task(
Lardon3DAppState *state,
TuiInput *input,
Lardon3DImportTask **task
)
{
*task = lardon3d_import_task_create();
if (!*task) {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Erreur : impossible de créer la tâche d'import."
);
input->mode = INPUT_NONE;
return false;
}
if (!lardon3d_import_task_start(*task, state, input->text)) {
Lardon3DImportTaskSnapshot snapshot;
if (lardon3d_import_task_snapshot(*task, &snapshot)
&& snapshot.message[0]) {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"%s",
snapshot.message
);
}
lardon3d_import_task_destroy(*task);
*task = NULL;
input->mode = INPUT_NONE;
return false;
}
input->mode = INPUT_NONE;
return true;
}
static bool static bool
handle_active_input( handle_active_input(
Lardon3DAppState *state, Lardon3DAppState *state,
TuiInput *input, TuiInput *input,
Lardon3DImportTask **task,
int key int key
) )
{ {
@ -83,8 +137,7 @@ handle_active_input(
if (input->mode == INPUT_PROJECT_OPEN) { if (input->mode == INPUT_PROJECT_OPEN) {
(void)lardon3d_project_open(state, input->text); (void)lardon3d_project_open(state, input->text);
} else if (input->mode == INPUT_IMPORT_DIRECTORY) { } else if (input->mode == INPUT_IMPORT_DIRECTORY) {
Lardon3DImportResult result; (void)start_import_task(state, input, task);
(void)lardon3d_import_directory(state, input->text, &result);
} else { } else {
(void)lardon3d_project_create(state, input->text); (void)lardon3d_project_create(state, input->text);
} }
@ -105,8 +158,11 @@ handle_active_input(
if (input->mode == INPUT_PROJECT_OPEN) { if (input->mode == INPUT_PROJECT_OPEN) {
(void)lardon3d_project_open(state, input->text); (void)lardon3d_project_open(state, input->text);
} else if (input->mode == INPUT_IMPORT_DIRECTORY) { } else if (input->mode == INPUT_IMPORT_DIRECTORY) {
Lardon3DImportResult result; (void)snprintf(
(void)lardon3d_import_directory(state, input->text, &result); state->status_message,
sizeof(state->status_message),
"Erreur : chemin source trop long."
);
} else { } else {
(void)lardon3d_project_create(state, input->text); (void)lardon3d_project_create(state, input->text);
} }
@ -127,9 +183,29 @@ static bool
handle_normal_input( handle_normal_input(
Lardon3DAppState *state, Lardon3DAppState *state,
TuiInput *input, TuiInput *input,
Lardon3DImportTask *task,
int key int key
) )
{ {
if (task) {
if (key == 'c' || key == 'C') {
lardon3d_import_task_request_cancel(task);
} else if (key == 'q' || key == 'Q') {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Quitter est désactivé pendant l'import."
);
} else if (key == 27) {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Accueil indisponible pendant l'import."
);
}
return key == KEY_RESIZE || key == 'c' || key == 'C'
|| key == 'q' || key == 'Q' || key == 27;
}
if (key == 'q' || key == 'Q') { if (key == 'q' || key == 'Q') {
state->running = false; state->running = false;
return false; return false;
@ -218,6 +294,7 @@ lardon3d_tui_init(void)
endwin(); endwin();
return false; return false;
} }
timeout(75);
return true; return true;
} }
@ -230,23 +307,52 @@ lardon3d_tui_run(Lardon3DAppState *state)
} }
TuiInput input = {0}; TuiInput input = {0};
redraw(state, &input); Lardon3DImportTask *task = NULL;
redraw(state, &input, task);
while (state->running) { while (state->running) {
int key = getch(); int key = getch();
if (key == ERR) {
return false; bool should_redraw = task != NULL;
if (task && lardon3d_import_task_is_finished(task)) {
Lardon3DImportTaskSnapshot snapshot;
if (!lardon3d_import_task_join(task)
|| !lardon3d_import_task_snapshot(task, &snapshot)) {
lardon3d_import_task_destroy(task);
return false;
}
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"%s",
snapshot.message
);
lardon3d_import_task_destroy(task);
task = NULL;
should_redraw = true;
} }
bool should_redraw = input.mode != INPUT_NONE if (key != ERR) {
? handle_active_input(state, &input, key) should_redraw = (input.mode != INPUT_NONE
: handle_normal_input(state, &input, key); ? handle_active_input(state, &input, &task, key)
: handle_normal_input(state, &input, task, key))
|| should_redraw;
}
if (should_redraw) { if (should_redraw) {
redraw(state, &input); redraw(state, &input, task);
} }
} }
if (task) {
lardon3d_import_task_request_cancel(task);
if (!lardon3d_import_task_join(task)) {
lardon3d_import_task_destroy(task);
return false;
}
lardon3d_import_task_destroy(task);
}
return true; return true;
} }

324
tests/test_import_task.c Normal file
View file

@ -0,0 +1,324 @@
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <lardon3d/app_state.h>
#include <lardon3d/import_task.h>
#define CHECK(condition) \
do { \
if (!(condition)) { \
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \
return false; \
} \
} while (0)
static bool
join_path(char destination[PATH_MAX], const char *parent, const char *child)
{
int written = snprintf(destination, PATH_MAX, "%s/%s", parent, child);
return written >= 0 && (size_t)written < PATH_MAX;
}
static bool
write_all(int descriptor, const void *data, size_t size)
{
const char *bytes = data;
size_t total = 0;
while (total < size) {
ssize_t written = write(descriptor, bytes + total, size - total);
if (written < 0 && errno == EINTR) {
continue;
}
if (written <= 0) {
return false;
}
total += (size_t)written;
}
return true;
}
static bool
create_file(const char *path, size_t size)
{
int descriptor = open(path, O_WRONLY | O_CREAT | O_EXCL, 0644);
if (descriptor < 0) {
return false;
}
char block[64 * 1024];
(void)memset(block, 'L', sizeof(block));
bool success = true;
while (size > 0) {
size_t chunk = size < sizeof(block) ? size : sizeof(block);
if (!write_all(descriptor, block, chunk)) {
success = false;
break;
}
size -= chunk;
}
if (close(descriptor) != 0) {
success = false;
}
return success;
}
static bool
read_file(const char *path, char *content, size_t capacity)
{
int descriptor = open(path, O_RDONLY);
if (descriptor < 0 || capacity == 0) {
return false;
}
size_t total = 0;
while (total + 1 < capacity) {
ssize_t count = read(descriptor, content + total, capacity - total - 1);
if (count == 0) {
break;
}
if (count < 0 && errno == EINTR) {
continue;
}
if (count < 0) {
(void)close(descriptor);
return false;
}
total += (size_t)count;
}
content[total] = '\0';
return close(descriptor) == 0;
}
static bool
remove_tree(const char *path)
{
struct stat info;
if (lstat(path, &info) != 0) {
return errno == ENOENT;
}
if (!S_ISDIR(info.st_mode)) {
return unlink(path) == 0;
}
DIR *directory = opendir(path);
if (!directory) {
return false;
}
bool success = true;
for (struct dirent *entry = readdir(directory);
entry;
entry = readdir(directory)) {
if (strcmp(entry->d_name, ".") == 0
|| strcmp(entry->d_name, "..") == 0) {
continue;
}
char child[PATH_MAX];
if (!join_path(child, path, entry->d_name) || !remove_tree(child)) {
success = false;
}
}
if (closedir(directory) != 0 || rmdir(path) != 0) {
success = false;
}
return success;
}
static bool
has_temporary_file(const char *images_path)
{
DIR *directory = opendir(images_path);
if (!directory) {
return true;
}
bool found = false;
for (struct dirent *entry = readdir(directory);
entry;
entry = readdir(directory)) {
if (strncmp(entry->d_name, ".manifest.tsv.tmp.", 18) == 0) {
found = true;
}
}
if (closedir(directory) != 0) {
found = true;
}
return found;
}
static bool
wait_until_finished(
Lardon3DImportTask *task,
Lardon3DImportTaskSnapshot *snapshot
)
{
const struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
for (size_t attempt = 0; attempt < 10000; ++attempt) {
if (!lardon3d_import_task_snapshot(task, snapshot)
|| snapshot->processed > snapshot->total) {
return false;
}
if (snapshot->status != LARDON3D_IMPORT_TASK_RUNNING) {
return true;
}
(void)nanosleep(&pause, NULL);
}
return false;
}
static bool
run_success_tests(
Lardon3DAppState *state,
const char *source,
const char *manifest
)
{
Lardon3DImportTask *unused = lardon3d_import_task_create();
CHECK(unused);
CHECK(!lardon3d_import_task_start(NULL, state, source));
CHECK(!lardon3d_import_task_start(unused, NULL, source));
CHECK(!lardon3d_import_task_start(unused, state, NULL));
lardon3d_import_task_destroy(unused);
Lardon3DImportTask *task = lardon3d_import_task_create();
CHECK(task);
CHECK(lardon3d_import_task_start(task, state, source));
CHECK(!lardon3d_import_task_start(task, state, source));
Lardon3DImportTaskSnapshot snapshot;
CHECK(wait_until_finished(task, &snapshot));
CHECK(snapshot.status == LARDON3D_IMPORT_TASK_SUCCEEDED);
CHECK(snapshot.total == 2);
CHECK(snapshot.processed == 2);
CHECK(snapshot.copied == 2);
CHECK(snapshot.already_present == 0);
CHECK(lardon3d_import_task_join(task));
CHECK(lardon3d_import_task_join(task));
lardon3d_import_task_destroy(task);
CHECK(access(manifest, F_OK) == 0);
task = lardon3d_import_task_create();
CHECK(task && lardon3d_import_task_start(task, state, source));
CHECK(wait_until_finished(task, &snapshot));
CHECK(snapshot.status == LARDON3D_IMPORT_TASK_SUCCEEDED);
CHECK(snapshot.copied == 0);
CHECK(snapshot.already_present == 2);
CHECK(lardon3d_import_task_join(task));
lardon3d_import_task_destroy(task);
return true;
}
static bool
run_cancellation_test(
Lardon3DAppState *state,
const char *source,
const char *images,
const char *originals,
const char *manifest
)
{
char manifest_before[8192];
CHECK(read_file(manifest, manifest_before, sizeof(manifest_before)));
Lardon3DImportTask *task = lardon3d_import_task_create();
CHECK(task && lardon3d_import_task_start(task, state, source));
Lardon3DImportTaskSnapshot snapshot;
const struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
bool copy_observed = false;
for (size_t attempt = 0; attempt < 10000; ++attempt) {
CHECK(lardon3d_import_task_snapshot(task, &snapshot));
CHECK(snapshot.processed <= snapshot.total);
if (snapshot.status != LARDON3D_IMPORT_TASK_RUNNING) {
break;
}
if (snapshot.copied > 0) {
copy_observed = true;
break;
}
(void)nanosleep(&pause, NULL);
}
CHECK(copy_observed);
lardon3d_import_task_request_cancel(task);
CHECK(wait_until_finished(task, &snapshot));
CHECK(snapshot.status == LARDON3D_IMPORT_TASK_CANCELLED);
CHECK(lardon3d_import_task_join(task));
lardon3d_import_task_destroy(task);
char manifest_after[8192];
CHECK(read_file(manifest, manifest_after, sizeof(manifest_after)));
CHECK(strcmp(manifest_before, manifest_after) == 0);
CHECK(!has_temporary_file(images));
for (size_t index = 0; index < 8; ++index) {
char filename[64];
CHECK(snprintf(filename, sizeof(filename), "large-%zu.jpg", index) > 0);
char destination[PATH_MAX];
CHECK(join_path(destination, originals, filename));
CHECK(access(destination, F_OK) != 0);
}
return true;
}
static bool
run_test(void)
{
char base[] = "/tmp/lardon3d-import-task-test.XXXXXX";
CHECK(mkdtemp(base));
char project[PATH_MAX];
char images[PATH_MAX];
char originals[PATH_MAX];
char source[PATH_MAX];
char large_source[PATH_MAX];
char manifest[PATH_MAX];
CHECK(join_path(project, base, "project"));
CHECK(join_path(images, project, "images"));
CHECK(join_path(originals, images, "originals"));
CHECK(join_path(source, base, "source"));
CHECK(join_path(large_source, base, "large-source"));
CHECK(join_path(manifest, images, "manifest.tsv"));
CHECK(mkdir(project, 0755) == 0);
CHECK(mkdir(images, 0755) == 0);
CHECK(mkdir(source, 0755) == 0);
CHECK(mkdir(large_source, 0755) == 0);
char path[PATH_MAX];
CHECK(join_path(path, source, "one.jpg"));
CHECK(create_file(path, 3));
CHECK(join_path(path, source, "two.png"));
CHECK(create_file(path, 7));
for (size_t index = 0; index < 8; ++index) {
char filename[64];
CHECK(snprintf(filename, sizeof(filename), "large-%zu.jpg", index) > 0);
CHECK(join_path(path, large_source, filename));
CHECK(create_file(path, 16 * 1024 * 1024));
}
Lardon3DAppState state;
lardon3d_app_state_init(&state);
state.project_loaded = true;
CHECK(snprintf(
state.project_path,
sizeof(state.project_path),
"%s",
project
) > 0);
CHECK(run_success_tests(&state, source, manifest));
CHECK(run_cancellation_test(
&state,
large_source,
images,
originals,
manifest
));
CHECK(remove_tree(base));
return true;
}
int
main(void)
{
return run_test() ? EXIT_SUCCESS : EXIT_FAILURE;
}