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
le dossier choisi, sans parcourir ses sous-dossiers. Elles sont copiées vers
`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;
} 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(
Lardon3DAppState *state,
const char *source_directory,
Lardon3DImportResult *result
);
Lardon3DImportOutcome lardon3d_import_directory_controlled(
Lardon3DAppState *state,
const char *source_directory,
Lardon3DImportResult *result,
const Lardon3DImportControl *control
);
#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
#include <lardon3d/app_state.h>
#include <lardon3d/import_task.h>
void lardon3d_layout_draw(
const Lardon3DAppState *state,
const char *input_text,
const char *input_label,
const Lardon3DImportTaskSnapshot *import_snapshot,
int rows,
int cols
);

View file

@ -19,6 +19,7 @@ add_project_arguments(
)
ncursesw = dependency('ncursesw', required: true)
threads = dependency('threads')
executable(
'lardon3d',
@ -29,10 +30,11 @@ executable(
'src/tui.c',
'src/layout.c',
'src/import.c',
'src/import_task.c',
'src/project.c',
],
include_directories: include_directories('include'),
dependencies: [ncursesw],
dependencies: [ncursesw, threads],
)
import_test = executable(
@ -46,3 +48,17 @@ import_test = executable(
)
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;
} CandidateList;
typedef enum {
COPY_IMAGE_ERROR = -1,
COPY_IMAGE_COLLISION = 0,
COPY_IMAGE_CREATED = 1,
COPY_IMAGE_CANCELLED = 2,
} CopyImageOutcome;
static void
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
join_path(
char destination[PATH_MAX],
@ -416,7 +454,8 @@ static int
copy_image(
const char *source_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);
@ -445,8 +484,14 @@ copy_image(
}
bool success = true;
bool cancelled = false;
char buffer[64 * 1024];
for (;;) {
if (import_is_cancelled(control)) {
cancelled = true;
success = false;
break;
}
ssize_t read_count = read(source, buffer, sizeof(buffer));
if (read_count == 0) {
break;
@ -461,6 +506,11 @@ copy_image(
ssize_t written_total = 0;
while (written_total < read_count) {
if (import_is_cancelled(control)) {
cancelled = true;
success = false;
break;
}
ssize_t written = write(
destination,
buffer + written_total,
@ -491,11 +541,11 @@ copy_image(
}
if (!success) {
(void)unlink(destination_path);
return -1;
return cancelled ? COPY_IMAGE_CANCELLED : COPY_IMAGE_ERROR;
}
*destination_size = source_info.st_size;
return 1;
return COPY_IMAGE_CREATED;
}
static bool
@ -504,7 +554,9 @@ analyze_source_directory(
const char *absolute_source,
const char *originals_path,
Lardon3DImportResult *result,
CandidateList *candidates
CandidateList *candidates,
const Lardon3DImportControl *control,
bool *cancelled
)
{
DIR *directory = opendir(absolute_source);
@ -515,6 +567,10 @@ analyze_source_directory(
bool success = true;
for (;;) {
if (import_is_cancelled(control)) {
*cancelled = true;
break;
}
errno = 0;
struct dirent *entry = readdir(directory);
if (!entry) {
@ -600,7 +656,7 @@ rollback_created_files(
return removed;
}
static bool
static void
fail_import(
Lardon3DAppState *state,
Lardon3DImportResult *result,
@ -638,47 +694,80 @@ fail_import(
reason
);
}
return false;
}
bool
lardon3d_import_directory(
static Lardon3DImportOutcome
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,
const char *source_directory,
Lardon3DImportResult *result
Lardon3DImportResult *result,
const Lardon3DImportControl *control
)
{
if (!state || !result) {
return false;
return LARDON3D_IMPORT_FAILED;
}
*result = (Lardon3DImportResult) {0};
publish_progress(control, result, 0, "Analyse du dossier source...");
if (!state->project_loaded) {
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 absolute_source[PATH_MAX];
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)) {
return false;
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
if (has_forbidden_manifest_character(absolute_source)) {
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;
if (lstat(absolute_source, &source_directory_info) != 0) {
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)) {
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];
@ -686,35 +775,62 @@ lardon3d_import_directory(
if (!join_path(images_path, state->project_path, "images")
|| !join_path(originals_path, images_path, "originals")) {
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};
bool analysis_cancelled = false;
if (!analyze_source_directory(
state,
absolute_source,
originals_path,
result,
&candidates
&candidates,
control,
&analysis_cancelled
)) {
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)) {
free(candidates.items);
return false;
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
ManifestWriter manifest;
if (!manifest_begin(state, images_path, &manifest)) {
free(candidates.items);
return false;
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
bool success = true;
bool cancelled = false;
size_t processed = 0;
const char *failure_reason = NULL;
for (size_t index = 0; index < candidates.count; ++index) {
if (import_is_cancelled(control)) {
cancelled = true;
break;
}
ImportCandidate *candidate = &candidates.items[index];
char source_path[PATH_MAX];
char destination_path[PATH_MAX];
@ -730,13 +846,22 @@ lardon3d_import_directory(
}
off_t size = 0;
int copied = copy_image(source_path, destination_path, &size);
if (copied < 0) {
int copied = copy_image(
source_path,
destination_path,
&size,
control
);
if (copied == COPY_IMAGE_CANCELLED) {
cancelled = true;
break;
}
if (copied == COPY_IMAGE_ERROR) {
success = false;
failure_reason = "copie d'une image impossible";
break;
}
if (copied == 0) {
if (copied == COPY_IMAGE_COLLISION) {
++result->already_present;
struct stat destination_info;
bool destination_is_regular = false;
@ -746,6 +871,8 @@ lardon3d_import_directory(
destination_is_regular = true;
}
if (!destination_is_regular) {
++processed;
publish_progress(control, result, processed, "Import en cours...");
continue;
}
} else {
@ -773,8 +900,21 @@ lardon3d_import_directory(
failure_reason = "écriture du manifeste impossible";
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) {
manifest_abort(&manifest);
size_t removed = rollback_created_files(
@ -782,7 +922,20 @@ lardon3d_import_directory(
originals_path
);
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)) {
size_t removed = rollback_created_files(
@ -790,12 +943,14 @@ lardon3d_import_directory(
originals_path
);
free(candidates.items);
return fail_import(
(void)fail_import(
state,
result,
removed,
"mise à jour du manifeste impossible"
);
publish_progress(control, result, processed, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
free(candidates.items);
@ -808,5 +963,21 @@ lardon3d_import_directory(
result->already_present,
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 <stdio.h>
#include <string.h>
#include <lardon3d/layout.h>
@ -134,9 +135,62 @@ static void
draw_import_screen(
const char *input_text,
const char *input_label,
const Lardon3DImportTaskSnapshot *snapshot,
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(8, 4, columns - 6, "ESC : Accueil");
draw_text(9, 4, columns - 6, "Q : Quitter");
@ -148,6 +202,7 @@ draw_content(
const Lardon3DAppState *state,
const char *input_text,
const char *input_label,
const Lardon3DImportTaskSnapshot *import_snapshot,
int rows,
int columns
)
@ -156,6 +211,11 @@ draw_content(
const char *content;
const char *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 content_length = (int)strlen(content);
int journal_row = rows - 7;
@ -176,7 +236,12 @@ draw_content(
columns
);
} 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 {
draw_text(
(3 + journal_row) / 2,
@ -186,7 +251,12 @@ draw_content(
);
}
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);
}
@ -195,6 +265,7 @@ lardon3d_layout_draw(
const Lardon3DAppState *state,
const char *input_text,
const char *input_label,
const Lardon3DImportTaskSnapshot *import_snapshot,
int rows,
int columns
)
@ -209,6 +280,7 @@ lardon3d_layout_draw(
state,
input_text,
input_label,
import_snapshot,
rows,
columns
);

134
src/tui.c
View file

@ -4,7 +4,7 @@
#include <stdbool.h>
#include <stdio.h>
#include <lardon3d/import.h>
#include <lardon3d/import_task.h>
#include <lardon3d/layout.h>
#include <lardon3d/project.h>
#include <lardon3d/tui.h>
@ -28,7 +28,11 @@ typedef struct {
} TuiInput;
static void
redraw(const Lardon3DAppState *state, const TuiInput *input)
redraw(
const Lardon3DAppState *state,
const TuiInput *input,
Lardon3DImportTask *task
)
{
int rows;
int columns;
@ -41,7 +45,19 @@ redraw(const Lardon3DAppState *state, const TuiInput *input)
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
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(
input->mode != INPUT_NONE
&& 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
handle_active_input(
Lardon3DAppState *state,
TuiInput *input,
Lardon3DImportTask **task,
int key
)
{
@ -83,8 +137,7 @@ handle_active_input(
if (input->mode == INPUT_PROJECT_OPEN) {
(void)lardon3d_project_open(state, input->text);
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
Lardon3DImportResult result;
(void)lardon3d_import_directory(state, input->text, &result);
(void)start_import_task(state, input, task);
} else {
(void)lardon3d_project_create(state, input->text);
}
@ -105,8 +158,11 @@ handle_active_input(
if (input->mode == INPUT_PROJECT_OPEN) {
(void)lardon3d_project_open(state, input->text);
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
Lardon3DImportResult result;
(void)lardon3d_import_directory(state, input->text, &result);
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Erreur : chemin source trop long."
);
} else {
(void)lardon3d_project_create(state, input->text);
}
@ -127,9 +183,29 @@ static bool
handle_normal_input(
Lardon3DAppState *state,
TuiInput *input,
Lardon3DImportTask *task,
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') {
state->running = false;
return false;
@ -218,6 +294,7 @@ lardon3d_tui_init(void)
endwin();
return false;
}
timeout(75);
return true;
}
@ -230,23 +307,52 @@ lardon3d_tui_run(Lardon3DAppState *state)
}
TuiInput input = {0};
redraw(state, &input);
Lardon3DImportTask *task = NULL;
redraw(state, &input, task);
while (state->running) {
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
? handle_active_input(state, &input, key)
: handle_normal_input(state, &input, key);
if (key != ERR) {
should_redraw = (input.mode != INPUT_NONE
? handle_active_input(state, &input, &task, key)
: handle_normal_input(state, &input, task, key))
|| 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;
}

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;
}