feat: add asynchronous cancellable image import
This commit is contained in:
parent
f2efe418cb
commit
8bb90ed05a
10 changed files with 1044 additions and 44 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
44
include/lardon3d/import_task.h
Normal file
44
include/lardon3d/import_task.h
Normal 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
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
18
meson.build
18
meson.build
|
|
@ -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)
|
||||
|
|
|
|||
225
src/import.c
225
src/import.c
|
|
@ -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
229
src/import_task.c
Normal 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);
|
||||
}
|
||||
76
src/layout.c
76
src/layout.c
|
|
@ -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
134
src/tui.c
|
|
@ -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
324
tests/test_import_task.c
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue