lardon3d/src/import.c

1264 lines
37 KiB
C

#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>
#include <lardon3d/image_catalog.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;
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)
{
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"%s",
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],
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, "Error: source directory is empty.");
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, "Error: source directory is empty.");
return false;
}
if (length >= PATH_MAX) {
set_status(state, "Error: source path is too 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, "Error: source path is too long or 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, "Error: too many files to import.");
return false;
}
void *items = realloc(
candidates->items,
capacity * sizeof(*candidates->items)
);
if (!items) {
set_status(state, "Error: insufficient memory for 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, "Error: filename is too 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, "Error: project path is too long.");
return false;
}
struct stat info;
if (lstat(images_path, &info) != 0 || !S_ISDIR(info.st_mode)) {
set_status(state, "Error: images directory is missing or invalid.");
return false;
}
if (lstat(originals_path, &info) == 0) {
if (!S_ISDIR(info.st_mode)) {
set_status(state, "Error: images/originals is not a directory.");
return false;
}
return true;
}
if (errno != ENOENT || mkdir(originals_path, 0755) != 0) {
set_status(state, "Error: unable to create 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, "Error: manifest path is too 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, "Error: invalid manifest.tsv.");
return false;
}
previous = fdopen(previous_descriptor, "r");
if (!previous) {
(void)close(previous_descriptor);
set_status(state, "Error: unable to read manifest.tsv.");
return false;
}
writer->previous_exists = true;
} else if (errno != ENOENT) {
set_status(state, "Error: unable to read manifest.tsv.");
return false;
}
int descriptor = mkstemp(writer->temporary_path);
if (descriptor < 0) {
if (previous) {
(void)fclose(previous);
}
set_status(state, "Error: unable to prepare 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, "Error: unable to write 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, "Error: manifest.tsv is invalid or unreadable.");
}
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, "Error: unable to update manifest.tsv.");
}
return success;
}
static ssize_t
read_retry(int descriptor, void *buffer, size_t capacity)
{
ssize_t count;
do {
count = read(descriptor, buffer, capacity);
} while (count < 0 && errno == EINTR);
return count;
}
static int
copy_image(
const char *source_path,
const char *destination_path,
off_t *destination_size,
const Lardon3DImportControl *control
)
{
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) {
if (errno != EEXIST) {
(void)close(source);
return COPY_IMAGE_ERROR;
}
destination = open(destination_path, O_RDONLY | O_NOFOLLOW);
struct stat destination_info;
bool identical = destination >= 0
&& fstat(destination, &destination_info) == 0
&& S_ISREG(destination_info.st_mode)
&& destination_info.st_size == source_info.st_size;
char source_buffer[64 * 1024], destination_buffer[64 * 1024];
while (identical) {
ssize_t source_count = read_retry(source, source_buffer,
sizeof(source_buffer));
ssize_t destination_count = read_retry(destination,
destination_buffer, sizeof(destination_buffer));
if (source_count < 0 || destination_count < 0
|| source_count != destination_count) {
identical = false;
break;
}
if (source_count == 0) break;
if (memcmp(source_buffer, destination_buffer,
(size_t)source_count) != 0) {
identical = false;
}
}
if (destination >= 0 && close(destination) != 0) identical = false;
if (close(source) != 0) identical = false;
if (!identical) return COPY_IMAGE_ERROR;
*destination_size = source_info.st_size;
return COPY_IMAGE_COLLISION;
}
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;
}
if (read_count < 0) {
if (errno == EINTR) {
continue;
}
success = false;
break;
}
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,
(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 cancelled ? COPY_IMAGE_CANCELLED : COPY_IMAGE_ERROR;
}
*destination_size = source_info.st_size;
return COPY_IMAGE_CREATED;
}
static bool
analyze_source_directory(
Lardon3DAppState *state,
const char *absolute_source,
const char *originals_path,
Lardon3DImportResult *result,
CandidateList *candidates,
const Lardon3DImportControl *control,
bool *cancelled
)
{
DIR *directory = opendir(absolute_source);
if (!directory) {
set_status(state, "Error: unable to open source directory.");
return false;
}
bool success = true;
for (;;) {
if (import_is_cancelled(control)) {
*cancelled = true;
break;
}
errno = 0;
struct dirent *entry = readdir(directory);
if (!entry) {
if (errno != 0) {
set_status(state, "Error: unable to read source directory.");
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, "Error: source path is too 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,
"Error: filename is incompatible with the manifest."
);
success = false;
break;
}
char destination_path[PATH_MAX];
if (!join_path(destination_path, originals_path, entry->d_name)) {
set_status(state, "Error: destination path is too long.");
success = false;
break;
}
if (!candidate_list_append(state, candidates, entry->d_name)) {
success = false;
break;
}
}
if (closedir(directory) != 0) {
set_status(state, "Error: unable to close source directory.");
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 void
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 cancelled: %zu %s removed after error (%s).",
removed,
removed == 1 ? "copy" : "copies",
reason
);
} else if (result->copied > 0) {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Critical error: %zu %s retained without manifest (%s).",
result->copied,
result->copied == 1 ? "copy" : "copies",
reason
);
} else {
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Import error: %s.",
reason
);
}
}
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 cancelled: %zu of %zu files processed.",
processed,
result->admissible_found
);
publish_progress(control, result, processed, state->status_message);
free(candidates->items);
return LARDON3D_IMPORT_CANCELLED;
}
Lardon3DImportOutcome
lardon3d_import_directory_batch(
Lardon3DAppState *state,
const char *source_directory,
size_t batch_size,
Lardon3DImportResult *result,
const Lardon3DImportControl *control,
bool *complete
)
{
if (complete) *complete = false;
if (!state || !result || !complete || batch_size == 0
|| !state->project_loaded) {
return LARDON3D_IMPORT_FAILED;
}
*result = (Lardon3DImportResult) {0};
char trimmed_source[PATH_MAX], absolute_source[PATH_MAX];
if (!trim_source_path(state, source_directory, trimmed_source)
|| !resolve_source_path(state, trimmed_source, absolute_source)
|| has_forbidden_manifest_character(absolute_source)) {
return LARDON3D_IMPORT_FAILED;
}
struct stat source_info;
if (lstat(absolute_source, &source_info) != 0
|| !S_ISDIR(source_info.st_mode)) {
set_status(state, "Error: source directory is missing or invalid.");
return LARDON3D_IMPORT_FAILED;
}
char images_path[PATH_MAX], originals_path[PATH_MAX];
if (!join_path(images_path, state->project_path, "images")
|| !join_path(originals_path, images_path, "originals")
|| !ensure_originals_directory(state, images_path, originals_path)) {
return LARDON3D_IMPORT_FAILED;
}
ManifestWriter manifest;
if (!manifest_begin(state, images_path, &manifest)) {
return LARDON3D_IMPORT_FAILED;
}
DIR *directory = opendir(absolute_source);
if (!directory) {
manifest_abort(&manifest);
set_status(state, "Error: unable to open source directory.");
return LARDON3D_IMPORT_FAILED;
}
CandidateList created = {0};
bool success = true, cancelled = false;
size_t added = 0;
for (;;) {
if (import_is_cancelled(control)) {
cancelled = true;
break;
}
errno = 0;
struct dirent *entry = readdir(directory);
if (!entry) {
if (errno != 0) success = false;
break;
}
if (strcmp(entry->d_name, ".") == 0
|| strcmp(entry->d_name, "..") == 0) continue;
char source_path[PATH_MAX], destination_path[PATH_MAX];
struct stat item_info;
if (!join_path(source_path, absolute_source, entry->d_name)) {
success = false; break;
}
if (lstat(source_path, &item_info) != 0
|| !S_ISREG(item_info.st_mode)
|| !has_supported_extension(entry->d_name)) {
++result->ignored;
continue;
}
++result->admissible_found;
if (has_forbidden_manifest_character(entry->d_name)) {
success = false; break;
}
int listed = manifest_contains(&manifest, entry->d_name);
if (listed < 0) {
success = false; break;
}
if (listed > 0) {
++result->processed;
++result->already_present;
continue;
}
if (added >= batch_size
|| !join_path(destination_path, originals_path, entry->d_name)) {
if (added < batch_size) success = false;
continue;
}
off_t size = 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; break;
}
if (copied == COPY_IMAGE_COLLISION) {
struct stat destination_info;
if (lstat(destination_path, &destination_info) != 0
|| !S_ISREG(destination_info.st_mode)) {
success = false; break;
}
size = destination_info.st_size;
++result->already_present;
} else {
if (!candidate_list_append(state, &created, entry->d_name)) {
(void)unlink(destination_path);
success = false; break;
}
created.items[created.count - 1].created = true;
++result->copied;
}
if (!manifest_append(&manifest, entry->d_name, size, source_path)) {
success = false; break;
}
++added;
++result->newly_manifested;
++result->processed;
}
if (closedir(directory) != 0) success = false;
if (!success || cancelled) {
manifest_abort(&manifest);
(void)rollback_created_files(&created, originals_path);
free(created.items);
set_status(state, cancelled ? "Import cancelled at a safe boundary."
: "Error during an import batch.");
return cancelled ? LARDON3D_IMPORT_CANCELLED : LARDON3D_IMPORT_FAILED;
}
if (!manifest_commit(state, &manifest)) {
(void)rollback_created_files(&created, originals_path);
free(created.items);
return LARDON3D_IMPORT_FAILED;
}
free(created.items);
*complete = result->processed == result->admissible_found;
set_status(state, *complete ? "Import completed." : "Import batch published.");
publish_progress(control, result, result->processed, state->status_message);
return LARDON3D_IMPORT_SUCCEEDED;
}
Lardon3DImportOutcome
lardon3d_import_directory_controlled(
Lardon3DAppState *state,
const char *source_directory,
Lardon3DImportResult *result,
const Lardon3DImportControl *control
)
{
if (!state || !result) {
return LARDON3D_IMPORT_FAILED;
}
*result = (Lardon3DImportResult) {0};
publish_progress(control, result, 0, "Analyse du dossier source...");
if (!state->project_loaded) {
set_status(state, "No project loaded.");
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)) {
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
if (!resolve_source_path(state, trimmed_source, absolute_source)) {
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
if (has_forbidden_manifest_character(absolute_source)) {
set_status(state, "Error: source path is incompatible with the manifest.");
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, "Error: source directory does not exist.");
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
if (!S_ISDIR(source_directory_info.st_mode)) {
set_status(state, "Error: source is not a directory.");
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
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, "Error: project path is too long.");
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,
control,
&analysis_cancelled
)) {
free(candidates.items);
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
if (analysis_cancelled) {
free(candidates.items);
set_status(state, "Import cancelled: 0 files processed.");
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 cancelled: 0 files processed.");
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_CANCELLED;
}
if (!ensure_originals_directory(state, images_path, originals_path)) {
free(candidates.items);
publish_progress(control, result, 0, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
ManifestWriter manifest;
if (!manifest_begin(state, images_path, &manifest)) {
free(candidates.items);
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];
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,
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 == COPY_IMAGE_COLLISION) {
++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) {
++processed;
publish_progress(control, result, processed, "Import en cours...");
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 = "unable to write manifest";
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(
&candidates,
originals_path
);
free(candidates.items);
(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(
&candidates,
originals_path
);
free(candidates.items);
(void)fail_import(
state,
result,
removed,
"unable to update manifest"
);
publish_progress(control, result, processed, state->status_message);
return LARDON3D_IMPORT_FAILED;
}
free(candidates.items);
(void)snprintf(
state->status_message,
sizeof(state->status_message),
"Import completed: %zu copied, %zu already present.",
result->copied,
result->already_present
);
publish_progress(control, result, processed, "Import completed.");
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;
}
Lardon3DImportOutcome
lardon3d_import_directory_batch_to_scanset(
Lardon3DAppState *state,
uint64_t scanset_id,
uint64_t producer_task_id,
const char *source_directory,
size_t batch_size,
Lardon3DImportResult *result,
const Lardon3DImportControl *control,
bool *complete
)
{
if (!state || !state->project_loaded || !state->project_db || !result
|| !complete || scanset_id == 0 || producer_task_id == 0
|| batch_size == 0) return LARDON3D_IMPORT_FAILED;
*result = (Lardon3DImportResult){0};
*complete = false;
char trimmed[PATH_MAX], source[PATH_MAX];
if (!trim_source_path(state, source_directory, trimmed)
|| !resolve_source_path(state, trimmed, source)) return LARDON3D_IMPORT_FAILED;
struct stat directory_info;
if (lstat(source, &directory_info) != 0 || !S_ISDIR(directory_info.st_mode)
|| S_ISLNK(directory_info.st_mode)) {
set_status(state, "Error: source directory is missing or invalid.");
return LARDON3D_IMPORT_FAILED;
}
DIR *directory = opendir(source);
if (!directory) return LARDON3D_IMPORT_FAILED;
bool success = true, cancelled = false, remaining = false;
size_t registered = 0;
ManifestWriter legacy_manifest = {0};
bool legacy_manifest_active = false;
char legacy_images[PATH_MAX], legacy_originals[PATH_MAX];
bool legacy_paths = ensure_originals_directory(state, legacy_images,
legacy_originals);
for (;;) {
if (import_is_cancelled(control)) { cancelled = true; break; }
errno = 0;
struct dirent *entry = readdir(directory);
if (!entry) { if (errno) success = false; break; }
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue;
char path[PATH_MAX]; struct stat information;
if (!join_path(path, source, entry->d_name)) { success = false; break; }
if (lstat(path, &information) != 0 || !S_ISREG(information.st_mode)
|| S_ISLNK(information.st_mode) || !has_supported_extension(entry->d_name)) {
++result->ignored; continue;
}
++result->admissible_found;
if (registered >= batch_size) { remaining = true; continue; }
Lardon3DProjectDbImage image;
Lardon3DProjectDbImageAsset asset;
Lardon3DImageCatalogImportResult imported =
lardon3d_image_catalog_import_file(state, scanset_id, path,
producer_task_id, &image, &asset);
if (imported == LARDON3D_IMAGE_CATALOG_ALREADY_PRESENT) {
++result->already_present;
++result->processed;
} else if (imported == LARDON3D_IMAGE_CATALOG_IMPORTED) {
++registered;
++result->newly_manifested;
++result->copied;
++result->processed;
if (legacy_paths && (!legacy_manifest_active
? manifest_begin(state, legacy_images, &legacy_manifest)
: true)) {
legacy_manifest_active = true;
int listed = manifest_contains(&legacy_manifest, entry->d_name);
char asset_path[PATH_MAX], legacy_path[PATH_MAX];
if (listed == 0
&& join_path(asset_path, state->project_path, asset.path)
&& join_path(legacy_path, legacy_originals, entry->d_name)
&& link(asset_path, legacy_path) == 0
&& !manifest_append(&legacy_manifest, entry->d_name,
(off_t)asset.size_bytes, path)) {
(void)unlink(legacy_path);
manifest_abort(&legacy_manifest);
legacy_manifest_active = false;
legacy_paths = false;
}
}
} else {
success = false; break;
}
}
if (closedir(directory) != 0) success = false;
if (legacy_manifest_active && !manifest_commit(state, &legacy_manifest)) {
legacy_manifest_active = false;
}
if (cancelled) {
set_status(state, "Import cancelled at a safe boundary.");
return LARDON3D_IMPORT_CANCELLED;
}
if (!success) {
set_status(state, "Error during an import batch.");
return LARDON3D_IMPORT_FAILED;
}
*complete = !remaining;
set_status(state, *complete ? "Import completed." : "Import batch published.");
publish_progress(control, result, result->processed, state->status_message);
return LARDON3D_IMPORT_SUCCEEDED;
}