feat: add safe image import with manifest
This commit is contained in:
parent
2cc632c75e
commit
f2efe418cb
9 changed files with 1264 additions and 63 deletions
|
|
@ -29,3 +29,7 @@ La TUI s'affiche directement dans le terminal courant. Appuyez sur `q` ou `Q` po
|
||||||
|
|
||||||
Les projets sont enregistrés par défaut dans `~/Documents/Lardon/Projets3D`.
|
Les projets sont enregistrés par défaut dans `~/Documents/Lardon/Projets3D`.
|
||||||
La variable `LARDON3D_PROJECTS_ROOT` permet de choisir un autre répertoire racine à l'exécution.
|
La variable `LARDON3D_PROJECTS_ROOT` permet de choisir un autre répertoire racine à l'exécution.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
|
||||||
22
include/lardon3d/import.h
Normal file
22
include/lardon3d/import.h
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#ifndef LARDON3D_IMPORT_H
|
||||||
|
#define LARDON3D_IMPORT_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include <lardon3d/app_state.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
size_t admissible_found;
|
||||||
|
size_t copied;
|
||||||
|
size_t already_present;
|
||||||
|
size_t ignored;
|
||||||
|
} Lardon3DImportResult;
|
||||||
|
|
||||||
|
bool lardon3d_import_directory(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const char *source_directory,
|
||||||
|
Lardon3DImportResult *result
|
||||||
|
);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
void lardon3d_layout_draw(
|
void lardon3d_layout_draw(
|
||||||
const Lardon3DAppState *state,
|
const Lardon3DAppState *state,
|
||||||
const char *project_name_input,
|
const char *input_text,
|
||||||
const char *project_input_label,
|
const char *input_label,
|
||||||
int rows,
|
int rows,
|
||||||
int cols
|
int cols
|
||||||
);
|
);
|
||||||
|
|
|
||||||
14
meson.build
14
meson.build
|
|
@ -11,6 +11,7 @@ project(
|
||||||
)
|
)
|
||||||
|
|
||||||
add_project_arguments(
|
add_project_arguments(
|
||||||
|
'-D_POSIX_C_SOURCE=200809L',
|
||||||
'-Wpedantic',
|
'-Wpedantic',
|
||||||
'-Wconversion',
|
'-Wconversion',
|
||||||
'-Wshadow',
|
'-Wshadow',
|
||||||
|
|
@ -27,8 +28,21 @@ executable(
|
||||||
'src/app_state.c',
|
'src/app_state.c',
|
||||||
'src/tui.c',
|
'src/tui.c',
|
||||||
'src/layout.c',
|
'src/layout.c',
|
||||||
|
'src/import.c',
|
||||||
'src/project.c',
|
'src/project.c',
|
||||||
],
|
],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
dependencies: [ncursesw],
|
dependencies: [ncursesw],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import_test = executable(
|
||||||
|
'test-import',
|
||||||
|
sources: [
|
||||||
|
'tests/test_import.c',
|
||||||
|
'src/app_state.c',
|
||||||
|
'src/import.c',
|
||||||
|
],
|
||||||
|
include_directories: include_directories('include'),
|
||||||
|
)
|
||||||
|
|
||||||
|
test('import', import_test)
|
||||||
|
|
|
||||||
812
src/import.c
Normal file
812
src/import.c
Normal file
|
|
@ -0,0 +1,812 @@
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <lardon3d/import.h>
|
||||||
|
|
||||||
|
enum {
|
||||||
|
MANIFEST_LINE_CAPACITY = PATH_MAX + 512,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
FILE *file;
|
||||||
|
char temporary_path[PATH_MAX];
|
||||||
|
char final_path[PATH_MAX];
|
||||||
|
bool previous_exists;
|
||||||
|
} ManifestWriter;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char filename[NAME_MAX + 1];
|
||||||
|
bool created;
|
||||||
|
} ImportCandidate;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
ImportCandidate *items;
|
||||||
|
size_t count;
|
||||||
|
size_t capacity;
|
||||||
|
} CandidateList;
|
||||||
|
|
||||||
|
static void
|
||||||
|
set_status(Lardon3DAppState *state, const char *message)
|
||||||
|
{
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"%s",
|
||||||
|
message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
trim_source_path(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const char *input,
|
||||||
|
char output[PATH_MAX]
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!input) {
|
||||||
|
set_status(state, "Erreur : dossier source vide.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *start = input;
|
||||||
|
while (*start && isspace((unsigned char)*start)) {
|
||||||
|
++start;
|
||||||
|
}
|
||||||
|
const char *end = input + strlen(input);
|
||||||
|
while (end > start && isspace((unsigned char)end[-1])) {
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t length = (size_t)(end - start);
|
||||||
|
if (length == 0) {
|
||||||
|
set_status(state, "Erreur : dossier source vide.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (length >= PATH_MAX) {
|
||||||
|
set_status(state, "Erreur : chemin source trop long.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
(void)memcpy(output, start, length);
|
||||||
|
output[length] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
resolve_source_path(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const char *source,
|
||||||
|
char absolute_source[PATH_MAX]
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (source[0] == '/') {
|
||||||
|
int written = snprintf(absolute_source, PATH_MAX, "%s", source);
|
||||||
|
if (written >= 0 && (size_t)written < PATH_MAX) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
char current_directory[PATH_MAX];
|
||||||
|
if (getcwd(current_directory, sizeof(current_directory))
|
||||||
|
&& join_path(absolute_source, current_directory, source)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_status(state, "Erreur : chemin source trop long ou inaccessible.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
has_supported_extension(const char *filename)
|
||||||
|
{
|
||||||
|
const char *extension = strrchr(filename, '.');
|
||||||
|
if (!extension) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strcasecmp(extension, ".jpg") == 0
|
||||||
|
|| strcasecmp(extension, ".jpeg") == 0
|
||||||
|
|| strcasecmp(extension, ".png") == 0
|
||||||
|
|| strcasecmp(extension, ".tif") == 0
|
||||||
|
|| strcasecmp(extension, ".tiff") == 0
|
||||||
|
|| strcasecmp(extension, ".heic") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
has_forbidden_manifest_character(const char *text)
|
||||||
|
{
|
||||||
|
return strchr(text, '\t') || strchr(text, '\n') || strchr(text, '\r');
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
candidate_list_append(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
CandidateList *candidates,
|
||||||
|
const char *filename
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (candidates->count == candidates->capacity) {
|
||||||
|
size_t capacity = candidates->capacity == 0
|
||||||
|
? 16
|
||||||
|
: candidates->capacity * 2;
|
||||||
|
if (capacity < candidates->capacity
|
||||||
|
|| capacity > SIZE_MAX / sizeof(*candidates->items)) {
|
||||||
|
set_status(state, "Erreur : trop de fichiers à importer.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void *items = realloc(
|
||||||
|
candidates->items,
|
||||||
|
capacity * sizeof(*candidates->items)
|
||||||
|
);
|
||||||
|
if (!items) {
|
||||||
|
set_status(state, "Erreur : mémoire insuffisante pour l'import.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
candidates->items = items;
|
||||||
|
candidates->capacity = capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportCandidate *candidate = &candidates->items[candidates->count];
|
||||||
|
int written = snprintf(
|
||||||
|
candidate->filename,
|
||||||
|
sizeof(candidate->filename),
|
||||||
|
"%s",
|
||||||
|
filename
|
||||||
|
);
|
||||||
|
if (written < 0 || (size_t)written >= sizeof(candidate->filename)) {
|
||||||
|
set_status(state, "Erreur : nom de fichier trop long.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
candidate->created = false;
|
||||||
|
++candidates->count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
ensure_originals_directory(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
char images_path[PATH_MAX],
|
||||||
|
char originals_path[PATH_MAX]
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct stat info;
|
||||||
|
if (lstat(images_path, &info) != 0 || !S_ISDIR(info.st_mode)) {
|
||||||
|
set_status(state, "Erreur : dossier images absent ou invalide.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (lstat(originals_path, &info) == 0) {
|
||||||
|
if (!S_ISDIR(info.st_mode)) {
|
||||||
|
set_status(state, "Erreur : images/originals n'est pas un dossier.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (errno != ENOENT || mkdir(originals_path, 0755) != 0) {
|
||||||
|
set_status(state, "Erreur : impossible de créer images/originals.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
manifest_line_is_valid(const char *line)
|
||||||
|
{
|
||||||
|
const char *first_tab = strchr(line, '\t');
|
||||||
|
if (!first_tab || first_tab == line) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const char *second_tab = strchr(first_tab + 1, '\t');
|
||||||
|
if (!second_tab || second_tab == first_tab + 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (strchr(second_tab + 1, '\t') || !strchr(second_tab + 1, '\n')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const char *digit = first_tab + 1; digit < second_tab; ++digit) {
|
||||||
|
if (!isdigit((unsigned char)*digit)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
manifest_abort(ManifestWriter *writer)
|
||||||
|
{
|
||||||
|
if (writer->file) {
|
||||||
|
(void)fclose(writer->file);
|
||||||
|
writer->file = NULL;
|
||||||
|
}
|
||||||
|
if (writer->temporary_path[0]) {
|
||||||
|
(void)unlink(writer->temporary_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
manifest_begin(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const char *images_path,
|
||||||
|
ManifestWriter *writer
|
||||||
|
)
|
||||||
|
{
|
||||||
|
*writer = (ManifestWriter) {0};
|
||||||
|
if (!join_path(writer->final_path, images_path, "manifest.tsv")
|
||||||
|
|| !join_path(
|
||||||
|
writer->temporary_path,
|
||||||
|
images_path,
|
||||||
|
".manifest.tsv.tmp.XXXXXX"
|
||||||
|
)) {
|
||||||
|
set_status(state, "Erreur : chemin du manifeste trop long.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *previous = NULL;
|
||||||
|
int previous_descriptor = open(
|
||||||
|
writer->final_path,
|
||||||
|
O_RDONLY | O_NOFOLLOW
|
||||||
|
);
|
||||||
|
if (previous_descriptor >= 0) {
|
||||||
|
struct stat info;
|
||||||
|
if (fstat(previous_descriptor, &info) != 0 || !S_ISREG(info.st_mode)) {
|
||||||
|
(void)close(previous_descriptor);
|
||||||
|
set_status(state, "Erreur : manifest.tsv invalide.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
previous = fdopen(previous_descriptor, "r");
|
||||||
|
if (!previous) {
|
||||||
|
(void)close(previous_descriptor);
|
||||||
|
set_status(state, "Erreur : impossible de lire manifest.tsv.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
writer->previous_exists = true;
|
||||||
|
} else if (errno != ENOENT) {
|
||||||
|
set_status(state, "Erreur : impossible de lire manifest.tsv.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int descriptor = mkstemp(writer->temporary_path);
|
||||||
|
if (descriptor < 0) {
|
||||||
|
if (previous) {
|
||||||
|
(void)fclose(previous);
|
||||||
|
}
|
||||||
|
set_status(state, "Erreur : impossible de préparer manifest.tsv.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
writer->file = fdopen(descriptor, "w");
|
||||||
|
if (!writer->file) {
|
||||||
|
(void)close(descriptor);
|
||||||
|
if (previous) {
|
||||||
|
(void)fclose(previous);
|
||||||
|
}
|
||||||
|
manifest_abort(writer);
|
||||||
|
set_status(state, "Erreur : impossible d'écrire manifest.tsv.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = fputs(
|
||||||
|
"filename\tsize_bytes\tsource_path\n",
|
||||||
|
writer->file
|
||||||
|
) >= 0;
|
||||||
|
if (previous) {
|
||||||
|
char line[MANIFEST_LINE_CAPACITY];
|
||||||
|
if (!fgets(line, sizeof(line), previous)
|
||||||
|
|| strcmp(line, "filename\tsize_bytes\tsource_path\n") != 0) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
while (success && fgets(line, sizeof(line), previous)) {
|
||||||
|
if (!manifest_line_is_valid(line)
|
||||||
|
|| fputs(line, writer->file) < 0) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ferror(previous) || fclose(previous) != 0) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
manifest_abort(writer);
|
||||||
|
set_status(state, "Erreur : manifest.tsv invalide ou illisible.");
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
manifest_contains(const ManifestWriter *writer, const char *filename)
|
||||||
|
{
|
||||||
|
if (!writer->previous_exists) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int descriptor = open(writer->final_path, O_RDONLY | O_NOFOLLOW);
|
||||||
|
if (descriptor < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
FILE *file = fdopen(descriptor, "r");
|
||||||
|
if (!file) {
|
||||||
|
(void)close(descriptor);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int found = 0;
|
||||||
|
char line[MANIFEST_LINE_CAPACITY];
|
||||||
|
(void)fgets(line, sizeof(line), file);
|
||||||
|
size_t filename_length = strlen(filename);
|
||||||
|
while (fgets(line, sizeof(line), file)) {
|
||||||
|
const char *tab = strchr(line, '\t');
|
||||||
|
if (tab && (size_t)(tab - line) == filename_length
|
||||||
|
&& memcmp(line, filename, filename_length) == 0) {
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ferror(file) || fclose(file) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
manifest_append(
|
||||||
|
ManifestWriter *writer,
|
||||||
|
const char *filename,
|
||||||
|
off_t size,
|
||||||
|
const char *source_path
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return fprintf(
|
||||||
|
writer->file,
|
||||||
|
"%s\t%" PRIdMAX "\t%s\n",
|
||||||
|
filename,
|
||||||
|
(intmax_t)size,
|
||||||
|
source_path
|
||||||
|
) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
manifest_commit(Lardon3DAppState *state, ManifestWriter *writer)
|
||||||
|
{
|
||||||
|
bool success = fflush(writer->file) == 0;
|
||||||
|
if (success) {
|
||||||
|
success = fsync(fileno(writer->file)) == 0;
|
||||||
|
}
|
||||||
|
if (fclose(writer->file) != 0) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
writer->file = NULL;
|
||||||
|
if (success) {
|
||||||
|
success = rename(writer->temporary_path, writer->final_path) == 0;
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
(void)unlink(writer->temporary_path);
|
||||||
|
set_status(state, "Erreur : impossible de mettre à jour manifest.tsv.");
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
copy_image(
|
||||||
|
const char *source_path,
|
||||||
|
const char *destination_path,
|
||||||
|
off_t *destination_size
|
||||||
|
)
|
||||||
|
{
|
||||||
|
int source = open(source_path, O_RDONLY | O_NOFOLLOW);
|
||||||
|
if (source < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct stat source_info;
|
||||||
|
if (fstat(source, &source_info) != 0 || !S_ISREG(source_info.st_mode)) {
|
||||||
|
(void)close(source);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
mode_t mode = source_info.st_mode & 0666;
|
||||||
|
if (mode == 0) {
|
||||||
|
mode = 0644;
|
||||||
|
}
|
||||||
|
int destination = open(
|
||||||
|
destination_path,
|
||||||
|
O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW,
|
||||||
|
mode
|
||||||
|
);
|
||||||
|
if (destination < 0) {
|
||||||
|
(void)close(source);
|
||||||
|
return errno == EEXIST ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = true;
|
||||||
|
char buffer[64 * 1024];
|
||||||
|
for (;;) {
|
||||||
|
ssize_t read_count = read(source, buffer, sizeof(buffer));
|
||||||
|
if (read_count == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (read_count < 0) {
|
||||||
|
if (errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t written_total = 0;
|
||||||
|
while (written_total < read_count) {
|
||||||
|
ssize_t written = write(
|
||||||
|
destination,
|
||||||
|
buffer + written_total,
|
||||||
|
(size_t)(read_count - written_total)
|
||||||
|
);
|
||||||
|
if (written < 0 && errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (written <= 0) {
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
written_total += written;
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
success = fsync(destination) == 0;
|
||||||
|
}
|
||||||
|
if (close(source) != 0) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
if (close(destination) != 0) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
(void)unlink(destination_path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*destination_size = source_info.st_size;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
analyze_source_directory(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const char *absolute_source,
|
||||||
|
const char *originals_path,
|
||||||
|
Lardon3DImportResult *result,
|
||||||
|
CandidateList *candidates
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DIR *directory = opendir(absolute_source);
|
||||||
|
if (!directory) {
|
||||||
|
set_status(state, "Erreur : impossible d'ouvrir le dossier source.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = true;
|
||||||
|
for (;;) {
|
||||||
|
errno = 0;
|
||||||
|
struct dirent *entry = readdir(directory);
|
||||||
|
if (!entry) {
|
||||||
|
if (errno != 0) {
|
||||||
|
set_status(state, "Erreur : lecture du dossier source impossible.");
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (strcmp(entry->d_name, ".") == 0
|
||||||
|
|| strcmp(entry->d_name, "..") == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
char source_path[PATH_MAX];
|
||||||
|
if (!join_path(source_path, absolute_source, entry->d_name)) {
|
||||||
|
set_status(state, "Erreur : chemin source trop long.");
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct stat source_info;
|
||||||
|
if (lstat(source_path, &source_info) != 0
|
||||||
|
|| !S_ISREG(source_info.st_mode)
|
||||||
|
|| !has_supported_extension(entry->d_name)) {
|
||||||
|
++result->ignored;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++result->admissible_found;
|
||||||
|
|
||||||
|
if (has_forbidden_manifest_character(entry->d_name)) {
|
||||||
|
set_status(
|
||||||
|
state,
|
||||||
|
"Erreur : nom de fichier incompatible avec le manifeste."
|
||||||
|
);
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
char destination_path[PATH_MAX];
|
||||||
|
if (!join_path(destination_path, originals_path, entry->d_name)) {
|
||||||
|
set_status(state, "Erreur : chemin destination trop long.");
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!candidate_list_append(state, candidates, entry->d_name)) {
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closedir(directory) != 0) {
|
||||||
|
set_status(state, "Erreur : fermeture du dossier source impossible.");
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t
|
||||||
|
rollback_created_files(
|
||||||
|
CandidateList *candidates,
|
||||||
|
const char *originals_path
|
||||||
|
)
|
||||||
|
{
|
||||||
|
size_t removed = 0;
|
||||||
|
for (size_t index = candidates->count; index > 0; --index) {
|
||||||
|
ImportCandidate *candidate = &candidates->items[index - 1];
|
||||||
|
if (!candidate->created) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
char destination_path[PATH_MAX];
|
||||||
|
if (join_path(
|
||||||
|
destination_path,
|
||||||
|
originals_path,
|
||||||
|
candidate->filename
|
||||||
|
)
|
||||||
|
&& unlink(destination_path) == 0) {
|
||||||
|
candidate->created = false;
|
||||||
|
++removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
fail_import(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
Lardon3DImportResult *result,
|
||||||
|
size_t removed,
|
||||||
|
const char *reason
|
||||||
|
)
|
||||||
|
{
|
||||||
|
size_t copied_before_rollback = result->copied;
|
||||||
|
result->copied -= removed;
|
||||||
|
if (copied_before_rollback > 0 && result->copied == 0) {
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"Import annulé : %zu copie%s retirée%s après erreur (%s).",
|
||||||
|
removed,
|
||||||
|
removed == 1 ? "" : "s",
|
||||||
|
removed == 1 ? "" : "s",
|
||||||
|
reason
|
||||||
|
);
|
||||||
|
} else if (result->copied > 0) {
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"Erreur critique : %zu copie%s conservée%s sans manifeste (%s).",
|
||||||
|
result->copied,
|
||||||
|
result->copied == 1 ? "" : "s",
|
||||||
|
result->copied == 1 ? "" : "s",
|
||||||
|
reason
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"Erreur d'import : %s.",
|
||||||
|
reason
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_import_directory(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const char *source_directory,
|
||||||
|
Lardon3DImportResult *result
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!state || !result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*result = (Lardon3DImportResult) {0};
|
||||||
|
|
||||||
|
if (!state->project_loaded) {
|
||||||
|
set_status(state, "Aucun projet chargé.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char trimmed_source[PATH_MAX];
|
||||||
|
char absolute_source[PATH_MAX];
|
||||||
|
if (!trim_source_path(state, source_directory, trimmed_source)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!resolve_source_path(state, trimmed_source, absolute_source)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (has_forbidden_manifest_character(absolute_source)) {
|
||||||
|
set_status(state, "Erreur : chemin source incompatible avec le manifeste.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct stat source_directory_info;
|
||||||
|
if (lstat(absolute_source, &source_directory_info) != 0) {
|
||||||
|
set_status(state, "Erreur : dossier source inexistant.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!S_ISDIR(source_directory_info.st_mode)) {
|
||||||
|
set_status(state, "Erreur : la source n'est pas un dossier.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char images_path[PATH_MAX];
|
||||||
|
char originals_path[PATH_MAX];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
CandidateList candidates = {0};
|
||||||
|
if (!analyze_source_directory(
|
||||||
|
state,
|
||||||
|
absolute_source,
|
||||||
|
originals_path,
|
||||||
|
result,
|
||||||
|
&candidates
|
||||||
|
)) {
|
||||||
|
free(candidates.items);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ensure_originals_directory(state, images_path, originals_path)) {
|
||||||
|
free(candidates.items);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ManifestWriter manifest;
|
||||||
|
if (!manifest_begin(state, images_path, &manifest)) {
|
||||||
|
free(candidates.items);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = true;
|
||||||
|
const char *failure_reason = NULL;
|
||||||
|
for (size_t index = 0; index < candidates.count; ++index) {
|
||||||
|
ImportCandidate *candidate = &candidates.items[index];
|
||||||
|
char source_path[PATH_MAX];
|
||||||
|
char destination_path[PATH_MAX];
|
||||||
|
if (!join_path(source_path, absolute_source, candidate->filename)
|
||||||
|
|| !join_path(
|
||||||
|
destination_path,
|
||||||
|
originals_path,
|
||||||
|
candidate->filename
|
||||||
|
)) {
|
||||||
|
success = false;
|
||||||
|
failure_reason = "chemin de fichier trop long";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
off_t size = 0;
|
||||||
|
int copied = copy_image(source_path, destination_path, &size);
|
||||||
|
if (copied < 0) {
|
||||||
|
success = false;
|
||||||
|
failure_reason = "copie d'une image impossible";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (copied == 0) {
|
||||||
|
++result->already_present;
|
||||||
|
struct stat destination_info;
|
||||||
|
bool destination_is_regular = false;
|
||||||
|
if (lstat(destination_path, &destination_info) == 0
|
||||||
|
&& S_ISREG(destination_info.st_mode)) {
|
||||||
|
size = destination_info.st_size;
|
||||||
|
destination_is_regular = true;
|
||||||
|
}
|
||||||
|
if (!destination_is_regular) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
++result->copied;
|
||||||
|
candidate->created = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int already_listed = manifest_contains(
|
||||||
|
&manifest,
|
||||||
|
candidate->filename
|
||||||
|
);
|
||||||
|
if (already_listed < 0) {
|
||||||
|
success = false;
|
||||||
|
failure_reason = "lecture du manifeste impossible";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!already_listed
|
||||||
|
&& !manifest_append(
|
||||||
|
&manifest,
|
||||||
|
candidate->filename,
|
||||||
|
size,
|
||||||
|
source_path
|
||||||
|
)) {
|
||||||
|
success = false;
|
||||||
|
failure_reason = "écriture du manifeste impossible";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
manifest_abort(&manifest);
|
||||||
|
size_t removed = rollback_created_files(
|
||||||
|
&candidates,
|
||||||
|
originals_path
|
||||||
|
);
|
||||||
|
free(candidates.items);
|
||||||
|
return fail_import(state, result, removed, failure_reason);
|
||||||
|
}
|
||||||
|
if (!manifest_commit(state, &manifest)) {
|
||||||
|
size_t removed = rollback_created_files(
|
||||||
|
&candidates,
|
||||||
|
originals_path
|
||||||
|
);
|
||||||
|
free(candidates.items);
|
||||||
|
return fail_import(
|
||||||
|
state,
|
||||||
|
result,
|
||||||
|
removed,
|
||||||
|
"mise à jour du manifeste impossible"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
free(candidates.items);
|
||||||
|
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"Import terminé : %zu copiée%s, %zu déjà présente%s.",
|
||||||
|
result->copied,
|
||||||
|
result->copied == 1 ? "" : "s",
|
||||||
|
result->already_present,
|
||||||
|
result->already_present == 1 ? "" : "s"
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
78
src/layout.c
78
src/layout.c
|
|
@ -69,6 +69,7 @@ screen_texts(
|
||||||
case LARDON3D_SCREEN_IMPORT:
|
case LARDON3D_SCREEN_IMPORT:
|
||||||
*title = "Import";
|
*title = "Import";
|
||||||
*content = "Import des images";
|
*content = "Import des images";
|
||||||
|
*footer = "I Importer des images ESC Accueil Q Quit";
|
||||||
break;
|
break;
|
||||||
case LARDON3D_SCREEN_VIEWER:
|
case LARDON3D_SCREEN_VIEWER:
|
||||||
*title = "Viewer";
|
*title = "Viewer";
|
||||||
|
|
@ -87,10 +88,37 @@ screen_texts(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
draw_input_field(
|
||||||
|
const char *input_text,
|
||||||
|
const char *input_label,
|
||||||
|
int row,
|
||||||
|
int columns
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!input_text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_text(row, 4, columns - 6, input_label);
|
||||||
|
(void)mvaddch(row + 1, 2, '[');
|
||||||
|
(void)mvaddch(row + 1, columns - 3, ']');
|
||||||
|
|
||||||
|
int available = columns - 8;
|
||||||
|
size_t length = strlen(input_text);
|
||||||
|
const char *visible = input_text;
|
||||||
|
if (length > (size_t)available) {
|
||||||
|
visible += length - (size_t)available;
|
||||||
|
length = (size_t)available;
|
||||||
|
}
|
||||||
|
draw_text(row + 1, 4, available, visible);
|
||||||
|
(void)move(row + 1, 4 + (int)length);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
draw_project_screen(
|
draw_project_screen(
|
||||||
const char *project_name_input,
|
const char *input_text,
|
||||||
const char *project_input_label,
|
const char *input_label,
|
||||||
int columns
|
int columns
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
@ -99,31 +127,27 @@ draw_project_screen(
|
||||||
draw_text(8, 4, columns - 6, "C : Fermer le projet");
|
draw_text(8, 4, columns - 6, "C : Fermer le projet");
|
||||||
draw_text(9, 4, columns - 6, "ESC : Accueil");
|
draw_text(9, 4, columns - 6, "ESC : Accueil");
|
||||||
draw_text(10, 4, columns - 6, "Q : Quitter");
|
draw_text(10, 4, columns - 6, "Q : Quitter");
|
||||||
|
draw_input_field(input_text, input_label, 11, columns);
|
||||||
if (!project_name_input) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_text(11, 4, columns - 6, project_input_label);
|
static void
|
||||||
(void)mvaddch(12, 2, '[');
|
draw_import_screen(
|
||||||
(void)mvaddch(12, columns - 3, ']');
|
const char *input_text,
|
||||||
|
const char *input_label,
|
||||||
int available = columns - 8;
|
int columns
|
||||||
size_t length = strlen(project_name_input);
|
)
|
||||||
const char *visible = project_name_input;
|
{
|
||||||
if (length > (size_t)available) {
|
draw_text(7, 4, columns - 6, "I : Importer des images");
|
||||||
visible += length - (size_t)available;
|
draw_text(8, 4, columns - 6, "ESC : Accueil");
|
||||||
length = (size_t)available;
|
draw_text(9, 4, columns - 6, "Q : Quitter");
|
||||||
}
|
draw_input_field(input_text, input_label, 10, columns);
|
||||||
draw_text(12, 4, available, visible);
|
|
||||||
(void)move(12, 4 + (int)length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
draw_content(
|
draw_content(
|
||||||
const Lardon3DAppState *state,
|
const Lardon3DAppState *state,
|
||||||
const char *project_name_input,
|
const char *input_text,
|
||||||
const char *project_input_label,
|
const char *input_label,
|
||||||
int rows,
|
int rows,
|
||||||
int columns
|
int columns
|
||||||
)
|
)
|
||||||
|
|
@ -147,10 +171,12 @@ draw_content(
|
||||||
}
|
}
|
||||||
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
||||||
draw_project_screen(
|
draw_project_screen(
|
||||||
project_name_input,
|
input_text,
|
||||||
project_input_label,
|
input_label,
|
||||||
columns
|
columns
|
||||||
);
|
);
|
||||||
|
} else if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
||||||
|
draw_import_screen(input_text, input_label, columns);
|
||||||
} else {
|
} else {
|
||||||
draw_text(
|
draw_text(
|
||||||
(3 + journal_row) / 2,
|
(3 + journal_row) / 2,
|
||||||
|
|
@ -167,8 +193,8 @@ draw_content(
|
||||||
void
|
void
|
||||||
lardon3d_layout_draw(
|
lardon3d_layout_draw(
|
||||||
const Lardon3DAppState *state,
|
const Lardon3DAppState *state,
|
||||||
const char *project_name_input,
|
const char *input_text,
|
||||||
const char *project_input_label,
|
const char *input_label,
|
||||||
int rows,
|
int rows,
|
||||||
int columns
|
int columns
|
||||||
)
|
)
|
||||||
|
|
@ -181,8 +207,8 @@ lardon3d_layout_draw(
|
||||||
draw_frame(rows, columns);
|
draw_frame(rows, columns);
|
||||||
draw_content(
|
draw_content(
|
||||||
state,
|
state,
|
||||||
project_name_input,
|
input_text,
|
||||||
project_input_label,
|
input_label,
|
||||||
rows,
|
rows,
|
||||||
columns
|
columns
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
#define _POSIX_C_SOURCE 200809L
|
|
||||||
|
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
|
|
||||||
98
src/tui.c
98
src/tui.c
|
|
@ -4,40 +4,46 @@
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include <lardon3d/import.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>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
PROJECT_INPUT_CAPACITY = 256,
|
|
||||||
MINIMUM_ROWS = 20,
|
MINIMUM_ROWS = 20,
|
||||||
MINIMUM_COLUMNS = 72,
|
MINIMUM_COLUMNS = 72,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
INPUT_NONE = 0,
|
||||||
|
INPUT_PROJECT_CREATE,
|
||||||
|
INPUT_PROJECT_OPEN,
|
||||||
|
INPUT_IMPORT_DIRECTORY,
|
||||||
|
} InputMode;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
enum {
|
InputMode mode;
|
||||||
PROJECT_INPUT_NONE = 0,
|
char text[PATH_MAX];
|
||||||
PROJECT_INPUT_CREATE,
|
|
||||||
PROJECT_INPUT_OPEN,
|
|
||||||
} mode;
|
|
||||||
char text[PROJECT_INPUT_CAPACITY];
|
|
||||||
size_t length;
|
size_t length;
|
||||||
} ProjectInput;
|
} TuiInput;
|
||||||
|
|
||||||
static void
|
static void
|
||||||
redraw(const Lardon3DAppState *state, const ProjectInput *input)
|
redraw(const Lardon3DAppState *state, const TuiInput *input)
|
||||||
{
|
{
|
||||||
int rows;
|
int rows;
|
||||||
int columns;
|
int columns;
|
||||||
getmaxyx(stdscr, rows, columns);
|
getmaxyx(stdscr, rows, columns);
|
||||||
|
|
||||||
const char *text = input->mode != PROJECT_INPUT_NONE ? input->text : NULL;
|
const char *text = input->mode != INPUT_NONE ? input->text : NULL;
|
||||||
const char *label = input->mode == PROJECT_INPUT_OPEN
|
const char *label = "Nom du nouveau projet :";
|
||||||
? "Nom du dossier projet :"
|
if (input->mode == INPUT_PROJECT_OPEN) {
|
||||||
: "Nom du nouveau projet :";
|
label = "Nom du dossier projet :";
|
||||||
|
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
||||||
|
label = "Dossier source :";
|
||||||
|
}
|
||||||
lardon3d_layout_draw(state, text, label, rows, columns);
|
lardon3d_layout_draw(state, text, label, rows, columns);
|
||||||
(void)curs_set(
|
(void)curs_set(
|
||||||
input->mode != PROJECT_INPUT_NONE
|
input->mode != INPUT_NONE
|
||||||
&& rows >= MINIMUM_ROWS
|
&& rows >= MINIMUM_ROWS
|
||||||
&& columns >= MINIMUM_COLUMNS
|
&& columns >= MINIMUM_COLUMNS
|
||||||
? 1
|
? 1
|
||||||
|
|
@ -46,9 +52,9 @@ redraw(const Lardon3DAppState *state, const ProjectInput *input)
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
handle_project_input(
|
handle_active_input(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
ProjectInput *input,
|
TuiInput *input,
|
||||||
int key
|
int key
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
@ -57,10 +63,13 @@ handle_project_input(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key == 27) {
|
if (key == 27) {
|
||||||
const char *message = input->mode == PROJECT_INPUT_OPEN
|
const char *message = "Création du projet annulée.";
|
||||||
? "Ouverture du projet annulée."
|
if (input->mode == INPUT_PROJECT_OPEN) {
|
||||||
: "Création du projet annulée.";
|
message = "Ouverture du projet annulée.";
|
||||||
input->mode = PROJECT_INPUT_NONE;
|
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
||||||
|
message = "Import annulé.";
|
||||||
|
}
|
||||||
|
input->mode = INPUT_NONE;
|
||||||
(void)snprintf(
|
(void)snprintf(
|
||||||
state->status_message,
|
state->status_message,
|
||||||
sizeof(state->status_message),
|
sizeof(state->status_message),
|
||||||
|
|
@ -71,12 +80,15 @@ handle_project_input(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key == '\n' || key == '\r' || key == KEY_ENTER) {
|
if (key == '\n' || key == '\r' || key == KEY_ENTER) {
|
||||||
if (input->mode == PROJECT_INPUT_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) {
|
||||||
|
Lardon3DImportResult result;
|
||||||
|
(void)lardon3d_import_directory(state, input->text, &result);
|
||||||
} else {
|
} else {
|
||||||
(void)lardon3d_project_create(state, input->text);
|
(void)lardon3d_project_create(state, input->text);
|
||||||
}
|
}
|
||||||
input->mode = PROJECT_INPUT_NONE;
|
input->mode = INPUT_NONE;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,12 +102,15 @@ handle_project_input(
|
||||||
|
|
||||||
if (key >= 0 && key <= UCHAR_MAX && isprint((unsigned char)key)) {
|
if (key >= 0 && key <= UCHAR_MAX && isprint((unsigned char)key)) {
|
||||||
if (input->length + 1 >= sizeof(input->text)) {
|
if (input->length + 1 >= sizeof(input->text)) {
|
||||||
if (input->mode == PROJECT_INPUT_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) {
|
||||||
|
Lardon3DImportResult result;
|
||||||
|
(void)lardon3d_import_directory(state, input->text, &result);
|
||||||
} else {
|
} else {
|
||||||
(void)lardon3d_project_create(state, input->text);
|
(void)lardon3d_project_create(state, input->text);
|
||||||
}
|
}
|
||||||
input->mode = PROJECT_INPUT_NONE;
|
input->mode = INPUT_NONE;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,7 +126,7 @@ handle_project_input(
|
||||||
static bool
|
static bool
|
||||||
handle_normal_input(
|
handle_normal_input(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
ProjectInput *input,
|
TuiInput *input,
|
||||||
int key
|
int key
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
@ -141,8 +156,8 @@ handle_normal_input(
|
||||||
case 'n':
|
case 'n':
|
||||||
case 'N':
|
case 'N':
|
||||||
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
||||||
*input = (ProjectInput) {
|
*input = (TuiInput) {
|
||||||
.mode = PROJECT_INPUT_CREATE,
|
.mode = INPUT_PROJECT_CREATE,
|
||||||
.text = "",
|
.text = "",
|
||||||
.length = 0,
|
.length = 0,
|
||||||
};
|
};
|
||||||
|
|
@ -152,8 +167,27 @@ handle_normal_input(
|
||||||
case 'o':
|
case 'o':
|
||||||
case 'O':
|
case 'O':
|
||||||
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
||||||
*input = (ProjectInput) {
|
*input = (TuiInput) {
|
||||||
.mode = PROJECT_INPUT_OPEN,
|
.mode = INPUT_PROJECT_OPEN,
|
||||||
|
.text = "",
|
||||||
|
.length = 0,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
case 'i':
|
||||||
|
case 'I':
|
||||||
|
if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
||||||
|
if (!state->project_loaded) {
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"Aucun projet chargé."
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
*input = (TuiInput) {
|
||||||
|
.mode = INPUT_IMPORT_DIRECTORY,
|
||||||
.text = "",
|
.text = "",
|
||||||
.length = 0,
|
.length = 0,
|
||||||
};
|
};
|
||||||
|
|
@ -195,7 +229,7 @@ lardon3d_tui_run(Lardon3DAppState *state)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ProjectInput input = {0};
|
TuiInput input = {0};
|
||||||
redraw(state, &input);
|
redraw(state, &input);
|
||||||
|
|
||||||
while (state->running) {
|
while (state->running) {
|
||||||
|
|
@ -204,8 +238,8 @@ lardon3d_tui_run(Lardon3DAppState *state)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool should_redraw = input.mode != PROJECT_INPUT_NONE
|
bool should_redraw = input.mode != INPUT_NONE
|
||||||
? handle_project_input(state, &input, key)
|
? handle_active_input(state, &input, key)
|
||||||
: handle_normal_input(state, &input, key);
|
: handle_normal_input(state, &input, key);
|
||||||
|
|
||||||
if (should_redraw) {
|
if (should_redraw) {
|
||||||
|
|
|
||||||
291
tests/test_import.c
Normal file
291
tests/test_import.c
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
#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 <unistd.h>
|
||||||
|
|
||||||
|
#include <lardon3d/app_state.h>
|
||||||
|
#include <lardon3d/import.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
|
||||||
|
create_directory(const char *path)
|
||||||
|
{
|
||||||
|
return mkdir(path, 0755) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
create_file(const char *path, const char *content)
|
||||||
|
{
|
||||||
|
int descriptor = open(path, O_WRONLY | O_CREAT | O_EXCL, 0644);
|
||||||
|
if (descriptor < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t length = strlen(content);
|
||||||
|
size_t written_total = 0;
|
||||||
|
while (written_total < length) {
|
||||||
|
ssize_t written = write(
|
||||||
|
descriptor,
|
||||||
|
content + written_total,
|
||||||
|
length - written_total
|
||||||
|
);
|
||||||
|
if (written < 0 && errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (written <= 0) {
|
||||||
|
(void)close(descriptor);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
written_total += (size_t)written;
|
||||||
|
}
|
||||||
|
return close(descriptor) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
file_equals(const char *path, const char *expected)
|
||||||
|
{
|
||||||
|
char content[256];
|
||||||
|
return read_file(path, content, sizeof(content))
|
||||||
|
&& strcmp(content, expected) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
manifest_has_unique_filenames(const char *path, size_t expected_entries)
|
||||||
|
{
|
||||||
|
FILE *file = fopen(path, "r");
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char filenames[16][NAME_MAX + 1];
|
||||||
|
size_t count = 0;
|
||||||
|
char line[PATH_MAX + 512];
|
||||||
|
bool valid = fgets(line, sizeof(line), file)
|
||||||
|
&& strcmp(line, "filename\tsize_bytes\tsource_path\n") == 0;
|
||||||
|
while (valid && fgets(line, sizeof(line), file)) {
|
||||||
|
char *tab = strchr(line, '\t');
|
||||||
|
if (!tab || count >= 16) {
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
*tab = '\0';
|
||||||
|
for (size_t index = 0; index < count; ++index) {
|
||||||
|
if (strcmp(filenames[index], line) == 0) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int written = snprintf(
|
||||||
|
filenames[count],
|
||||||
|
sizeof(filenames[count]),
|
||||||
|
"%s",
|
||||||
|
line
|
||||||
|
);
|
||||||
|
if (written < 0 || (size_t)written >= sizeof(filenames[count])) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
if (ferror(file) || fclose(file) != 0) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
return valid && count == expected_entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
has_manifest_temporary(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
|
||||||
|
run_test(void)
|
||||||
|
{
|
||||||
|
char base[] = "/tmp/lardon3d-import-test.XXXXXX";
|
||||||
|
CHECK(mkdtemp(base));
|
||||||
|
|
||||||
|
char project[PATH_MAX];
|
||||||
|
char images[PATH_MAX];
|
||||||
|
char originals[PATH_MAX];
|
||||||
|
char valid_source[PATH_MAX];
|
||||||
|
char tab_source[PATH_MAX];
|
||||||
|
char newline_source[PATH_MAX];
|
||||||
|
CHECK(join_path(project, base, "project"));
|
||||||
|
CHECK(join_path(images, project, "images"));
|
||||||
|
CHECK(join_path(originals, images, "originals"));
|
||||||
|
CHECK(join_path(valid_source, base, "valid"));
|
||||||
|
CHECK(join_path(tab_source, base, "tab"));
|
||||||
|
CHECK(join_path(newline_source, base, "newline"));
|
||||||
|
CHECK(create_directory(project));
|
||||||
|
CHECK(create_directory(images));
|
||||||
|
CHECK(create_directory(valid_source));
|
||||||
|
CHECK(create_directory(tab_source));
|
||||||
|
CHECK(create_directory(newline_source));
|
||||||
|
|
||||||
|
char path[PATH_MAX];
|
||||||
|
CHECK(join_path(path, valid_source, "one.jpg"));
|
||||||
|
CHECK(create_file(path, "one"));
|
||||||
|
CHECK(join_path(path, valid_source, "two.PNG"));
|
||||||
|
CHECK(create_file(path, "two-two"));
|
||||||
|
|
||||||
|
Lardon3DAppState state;
|
||||||
|
lardon3d_app_state_init(&state);
|
||||||
|
state.project_loaded = true;
|
||||||
|
CHECK(snprintf(
|
||||||
|
state.project_path,
|
||||||
|
sizeof(state.project_path),
|
||||||
|
"%s",
|
||||||
|
project
|
||||||
|
) > 0);
|
||||||
|
|
||||||
|
Lardon3DImportResult result;
|
||||||
|
CHECK(lardon3d_import_directory(&state, valid_source, &result));
|
||||||
|
CHECK(result.admissible_found == 2);
|
||||||
|
CHECK(result.copied == 2);
|
||||||
|
CHECK(result.already_present == 0);
|
||||||
|
|
||||||
|
char manifest[PATH_MAX];
|
||||||
|
CHECK(join_path(manifest, images, "manifest.tsv"));
|
||||||
|
char manifest_before[8192];
|
||||||
|
CHECK(read_file(manifest, manifest_before, sizeof(manifest_before)));
|
||||||
|
|
||||||
|
CHECK(join_path(path, tab_source, "would-copy.jpg"));
|
||||||
|
CHECK(create_file(path, "must-not-copy"));
|
||||||
|
CHECK(join_path(path, tab_source, "bad\tname.jpg"));
|
||||||
|
CHECK(create_file(path, "invalid"));
|
||||||
|
CHECK(!lardon3d_import_directory(&state, tab_source, &result));
|
||||||
|
CHECK(join_path(path, originals, "would-copy.jpg"));
|
||||||
|
CHECK(access(path, F_OK) != 0);
|
||||||
|
char manifest_after[8192];
|
||||||
|
CHECK(read_file(manifest, manifest_after, sizeof(manifest_after)));
|
||||||
|
CHECK(strcmp(manifest_before, manifest_after) == 0);
|
||||||
|
CHECK(!has_manifest_temporary(images));
|
||||||
|
|
||||||
|
CHECK(join_path(path, newline_source, "also-not-copied.jpg"));
|
||||||
|
CHECK(create_file(path, "must-not-copy-either"));
|
||||||
|
CHECK(join_path(path, newline_source, "bad\nname.png"));
|
||||||
|
CHECK(create_file(path, "invalid"));
|
||||||
|
CHECK(!lardon3d_import_directory(&state, newline_source, &result));
|
||||||
|
CHECK(join_path(path, originals, "also-not-copied.jpg"));
|
||||||
|
CHECK(access(path, F_OK) != 0);
|
||||||
|
CHECK(read_file(manifest, manifest_after, sizeof(manifest_after)));
|
||||||
|
CHECK(strcmp(manifest_before, manifest_after) == 0);
|
||||||
|
CHECK(!has_manifest_temporary(images));
|
||||||
|
|
||||||
|
CHECK(join_path(path, valid_source, "three.tiff"));
|
||||||
|
CHECK(create_file(path, "three-three-three"));
|
||||||
|
CHECK(lardon3d_import_directory(&state, valid_source, &result));
|
||||||
|
CHECK(result.copied == 1);
|
||||||
|
CHECK(result.already_present == 2);
|
||||||
|
CHECK(lardon3d_import_directory(&state, valid_source, &result));
|
||||||
|
CHECK(result.copied == 0);
|
||||||
|
CHECK(result.already_present == 3);
|
||||||
|
CHECK(manifest_has_unique_filenames(manifest, 3));
|
||||||
|
CHECK(join_path(path, originals, "one.jpg"));
|
||||||
|
CHECK(file_equals(path, "one"));
|
||||||
|
CHECK(join_path(path, originals, "two.PNG"));
|
||||||
|
CHECK(file_equals(path, "two-two"));
|
||||||
|
CHECK(join_path(path, originals, "three.tiff"));
|
||||||
|
CHECK(file_equals(path, "three-three-three"));
|
||||||
|
CHECK(!has_manifest_temporary(images));
|
||||||
|
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
CHECK(join_path(path, originals, "two.PNG"));
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
CHECK(join_path(path, originals, "one.jpg"));
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
CHECK(unlink(manifest) == 0);
|
||||||
|
CHECK(rmdir(originals) == 0);
|
||||||
|
CHECK(rmdir(images) == 0);
|
||||||
|
CHECK(rmdir(project) == 0);
|
||||||
|
|
||||||
|
CHECK(join_path(path, valid_source, "three.tiff"));
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
CHECK(join_path(path, valid_source, "two.PNG"));
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
CHECK(join_path(path, valid_source, "one.jpg"));
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
CHECK(rmdir(valid_source) == 0);
|
||||||
|
|
||||||
|
CHECK(join_path(path, tab_source, "bad\tname.jpg"));
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
CHECK(join_path(path, tab_source, "would-copy.jpg"));
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
CHECK(rmdir(tab_source) == 0);
|
||||||
|
|
||||||
|
CHECK(join_path(path, newline_source, "bad\nname.png"));
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
CHECK(join_path(path, newline_source, "also-not-copied.jpg"));
|
||||||
|
CHECK(unlink(path) == 0);
|
||||||
|
CHECK(rmdir(newline_source) == 0);
|
||||||
|
CHECK(rmdir(base) == 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main(void)
|
||||||
|
{
|
||||||
|
return run_test() ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue