1264 lines
37 KiB
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;
|
|
}
|