feat: make standard image ingestion capture-safe

This commit is contained in:
fy59 2026-08-27 08:08:30 +02:00
parent 9f409645e3
commit 4b611ca723
3 changed files with 143 additions and 8 deletions

View file

@ -2510,6 +2510,80 @@ static const char image_select[] =
"a.state,a.created_at,i.producer_task_id,i.imported_at FROM images i JOIN image_assets a ON " "a.state,a.created_at,i.producer_task_id,i.imported_at FROM images i JOIN image_assets a ON "
"a.asset_id=i.asset_id "; "a.asset_id=i.asset_id ";
static Lardon3DProjectDbResult ensure_capture_for_registered_image_locked(
Lardon3DProjectDb *database, uint64_t scanset_id, uint64_t asset_id, uint64_t image_id) {
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database, "SELECT capture_id FROM capture_images WHERE image_id=?1", &statement);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)image_id);
int code = sqlite3_step(statement);
if (code == SQLITE_ROW) {
(void)sqlite3_finalize(statement);
return LARDON3D_PROJECT_DB_OK;
}
(void)sqlite3_finalize(statement);
if (code != SQLITE_DONE) {
return sqlite_result(database, code, "find capture for registered image");
}
result = prepare(
database, "INSERT INTO captures(scanset_id,created_at) "
"SELECT scanset_id,imported_at FROM images WHERE image_id=?1 AND scanset_id=?2",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)image_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)scanset_id);
result = step_done(database, statement, "create initial image capture");
statement = NULL;
}
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
if (sqlite3_changes(database->connection) != 1) {
return LARDON3D_PROJECT_DB_CORRUPT;
}
sqlite3_int64 capture_id = sqlite3_last_insert_rowid(database->connection);
if (capture_id <= 0) {
return LARDON3D_PROJECT_DB_CORRUPT;
}
result = prepare(database,
"INSERT INTO capture_assets(capture_id,asset_id,role) VALUES(?1,?2,1)",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)asset_id);
result = step_done(database, statement, "attach initial image source asset");
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database,
"INSERT INTO capture_images(capture_id,image_id) VALUES(?1,?2)",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)image_id);
result = step_done(database, statement, "attach initial image capture");
statement = NULL;
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database,
"INSERT INTO capture_selections(capture_id,image_id) VALUES(?1,?2)",
&statement);
}
if (result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, capture_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)image_id);
result = step_done(database, statement, "select initial capture image");
statement = NULL;
}
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_register_image_asset( Lardon3DProjectDbResult lardon3d_project_db_register_image_asset(
Lardon3DProjectDb *database, Lardon3DProjectDb *database,
const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], const char *asset_path, const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE], const char *asset_path,
@ -2682,6 +2756,10 @@ Lardon3DProjectDbResult lardon3d_project_db_register_image(
(void)sqlite3_finalize(statement); (void)sqlite3_finalize(statement);
statement = NULL; statement = NULL;
} }
if (result == LARDON3D_PROJECT_DB_OK) {
result = ensure_capture_for_registered_image_locked(database, scanset_id, (uint64_t)asset_id,
(uint64_t)image_id);
}
if (result == LARDON3D_PROJECT_DB_OK) { if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit image register"); result = execute(database, "COMMIT", "commit image register");
} }
@ -6346,12 +6424,14 @@ lardon3d_project_db_test_delete_catalog_identity(Lardon3DProjectDb *database, ui
Lardon3DProjectDbResult result = Lardon3DProjectDbResult result =
execute(database, "BEGIN IMMEDIATE", "begin catalog identity deletion test"); execute(database, "BEGIN IMMEDIATE", "begin catalog identity deletion test");
const char *sql[] = { const char *sql[] = {
("DELETE FROM captures WHERE capture_id IN "
"(SELECT capture_id FROM capture_images WHERE image_id=?1)"),
"DELETE FROM images WHERE image_id=?1", "DELETE FROM images WHERE image_id=?1",
"DELETE FROM image_assets WHERE asset_id=?1", "DELETE FROM image_assets WHERE asset_id=?1",
"DELETE FROM scansets WHERE scanset_id=?1", "DELETE FROM scansets WHERE scanset_id=?1",
}; };
const uint64_t ids[] = {image_id, asset_id, scanset_id}; const uint64_t ids[] = {image_id, image_id, asset_id, scanset_id};
for (size_t index = 0; index < 3 && result == LARDON3D_PROJECT_DB_OK; ++index) { for (size_t index = 0; index < 4 && result == LARDON3D_PROJECT_DB_OK; ++index) {
sqlite3_stmt *statement = NULL; sqlite3_stmt *statement = NULL;
result = prepare(database, sql[index], &statement); result = prepare(database, sql[index], &statement);
if (result == LARDON3D_PROJECT_DB_OK) { if (result == LARDON3D_PROJECT_DB_OK) {

View file

@ -114,6 +114,26 @@ import_thread(void *userdata)
return NULL; return NULL;
} }
static bool
has_initial_capture(Lardon3DProjectDb *database, const Lardon3DProjectDbImage *image,
const Lardon3DProjectDbImageAsset *asset, uint64_t scanset_id,
Lardon3DProjectDbCapture *capture)
{
if (lardon3d_project_db_find_capture_for_image(database, image->image_id, capture)
!= LARDON3D_PROJECT_DB_OK
|| capture->scanset_id != scanset_id) return false;
uint64_t selected_image = 0;
if (lardon3d_project_db_get_selected_capture_image(database, capture->capture_id,
&selected_image) != LARDON3D_PROJECT_DB_OK
|| selected_image != image->image_id) return false;
Lardon3DProjectDbCaptureAsset capture_asset;
size_t count = 0;
return lardon3d_project_db_list_capture_assets(database, capture->capture_id, 0,
&capture_asset, 1, &count) == LARDON3D_PROJECT_DB_OK
&& count == 1 && capture_asset.asset_id == asset->asset_id
&& capture_asset.role == LARDON3D_DB_CAPTURE_ASSET_SOURCE;
}
static bool static bool
run_test(void) run_test(void)
{ {
@ -139,6 +159,9 @@ run_test(void)
CHECK(lardon3d_image_catalog_create_scanset(&state, "Campagne générale", &a)); CHECK(lardon3d_image_catalog_create_scanset(&state, "Campagne générale", &a));
CHECK(lardon3d_image_catalog_create_scanset(&state, "Pièce démontée", &b)); CHECK(lardon3d_image_catalog_create_scanset(&state, "Pièce démontée", &b));
CHECK(a.scanset_id != b.scanset_id); CHECK(a.scanset_id != b.scanset_id);
Lardon3DProjectDbCapture preexisting_capture;
CHECK(lardon3d_project_db_create_capture(database, a.scanset_id, 1,
&preexisting_capture) == LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbScanSet scanset_page[2]; size_t scanset_count = 0; Lardon3DProjectDbScanSet scanset_page[2]; size_t scanset_count = 0;
CHECK(lardon3d_project_db_list_scansets(database, 0, scanset_page, 1, CHECK(lardon3d_project_db_list_scansets(database, 0, scanset_page, 1,
&scanset_count) == LARDON3D_PROJECT_DB_OK && scanset_count == 1 &scanset_count) == LARDON3D_PROJECT_DB_OK && scanset_count == 1
@ -163,14 +186,20 @@ run_test(void)
CHECK(lardon3d_image_catalog_import_file(&state, a.scanset_id, source_a, 0, CHECK(lardon3d_image_catalog_import_file(&state, a.scanset_id, source_a, 0,
&image_a, &asset_a) == LARDON3D_IMAGE_CATALOG_IMPORTED); &image_a, &asset_a) == LARDON3D_IMAGE_CATALOG_IMPORTED);
CHECK(canonical_asset_path(asset_a.path)); CHECK(canonical_asset_path(asset_a.path));
Lardon3DProjectDbCapture capture_a, capture_b, capture_different;
CHECK(has_initial_capture(database, &image_a, &asset_a, a.scanset_id, &capture_a));
CHECK(capture_a.capture_id != image_a.image_id);
CHECK(lardon3d_image_catalog_import_file(&state, a.scanset_id, source_b, 0, CHECK(lardon3d_image_catalog_import_file(&state, a.scanset_id, source_b, 0,
&duplicate_a, &duplicate_asset) == LARDON3D_IMAGE_CATALOG_ALREADY_PRESENT); &duplicate_a, &duplicate_asset) == LARDON3D_IMAGE_CATALOG_ALREADY_PRESENT);
CHECK(image_a.image_id == duplicate_a.image_id CHECK(image_a.image_id == duplicate_a.image_id
&& asset_a.asset_id == duplicate_asset.asset_id); && asset_a.asset_id == duplicate_asset.asset_id);
CHECK(has_initial_capture(database, &duplicate_a, &duplicate_asset, a.scanset_id, &capture_a));
CHECK(lardon3d_image_catalog_import_file(&state, b.scanset_id, source_b, 0, CHECK(lardon3d_image_catalog_import_file(&state, b.scanset_id, source_b, 0,
&image_b, &asset_b) == LARDON3D_IMAGE_CATALOG_IMPORTED); &image_b, &asset_b) == LARDON3D_IMAGE_CATALOG_IMPORTED);
CHECK(image_b.image_id != image_a.image_id && asset_b.asset_id == asset_a.asset_id); CHECK(image_b.image_id != image_a.image_id && asset_b.asset_id == asset_a.asset_id);
CHECK(strcmp(asset_b.path, asset_a.path) == 0); CHECK(strcmp(asset_b.path, asset_a.path) == 0);
CHECK(has_initial_capture(database, &image_b, &asset_b, b.scanset_id, &capture_b));
CHECK(capture_b.capture_id != capture_a.capture_id);
char different_dir[PATH_MAX], same_name[PATH_MAX]; char different_dir[PATH_MAX], same_name[PATH_MAX];
CHECK(join_path(different_dir, root, "different") && mkdir(different_dir, 0700) == 0); CHECK(join_path(different_dir, root, "different") && mkdir(different_dir, 0700) == 0);
@ -184,6 +213,9 @@ run_test(void)
&& different_asset.asset_id != asset_a.asset_id && different_asset.asset_id != asset_a.asset_id
&& canonical_asset_path(different_asset.path) && canonical_asset_path(different_asset.path)
&& strcmp(different_asset.path, asset_a.path) != 0); && strcmp(different_asset.path, asset_a.path) != 0);
CHECK(has_initial_capture(database, &different_image, &different_asset, a.scanset_id,
&capture_different));
CHECK(capture_different.capture_id != capture_a.capture_id);
char orphan_source[PATH_MAX], asset_root[PATH_MAX]; char orphan_source[PATH_MAX], asset_root[PATH_MAX];
CHECK(join_path(orphan_source, root, "orphan.jpg")); CHECK(join_path(orphan_source, root, "orphan.jpg"));

View file

@ -1066,22 +1066,24 @@ static bool run_test(void) {
/* S1: Capture is a catalog policy layer; image_id and asset_id stay immutable. */ /* S1: Capture is a catalog policy layer; image_id and asset_id stay immutable. */
Lardon3DProjectDbCapture capture, other_capture, mapped_capture; Lardon3DProjectDbCapture capture, other_capture, mapped_capture;
CHECK(lardon3d_project_db_create_capture(database, replacement_scanset.scanset_id, 20, CHECK(lardon3d_project_db_find_capture_for_image(database, pair_image.image_id, &capture) ==
&capture) == LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_create_capture(database, replacement_scanset.scanset_id, 21, CHECK(lardon3d_project_db_create_capture(database, replacement_scanset.scanset_id, 21,
&other_capture) == LARDON3D_PROJECT_DB_OK); &other_capture) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_create_capture(database, UINT64_C(999999), 22, &mapped_capture) == CHECK(lardon3d_project_db_create_capture(database, UINT64_C(999999), 22, &mapped_capture) ==
LARDON3D_PROJECT_DB_CONSTRAINT); LARDON3D_PROJECT_DB_CONSTRAINT);
Lardon3DProjectDbCapture capture_page[2]; Lardon3DProjectDbCapture capture_page[2];
size_t capture_count = 0; size_t capture_count = 0;
CHECK(lardon3d_project_db_list_captures(database, replacement_scanset.scanset_id, 0, CHECK(lardon3d_project_db_list_captures(database, replacement_scanset.scanset_id,
capture.capture_id - 1,
capture_page, 1, &capture_count) == capture_page, 1, &capture_count) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
capture_count == 1 && capture_page[0].capture_id == capture.capture_id); capture_count == 1 && capture_page[0].capture_id == capture.capture_id);
uint64_t selected_image = 0; uint64_t selected_image = 0;
CHECK(lardon3d_project_db_get_selected_capture_image(database, capture.capture_id, CHECK(lardon3d_project_db_get_selected_capture_image(database, capture.capture_id,
&selected_image) == &selected_image) ==
LARDON3D_PROJECT_DB_NOT_FOUND); LARDON3D_PROJECT_DB_OK &&
selected_image == pair_image.image_id);
unsigned char raw_hash[LARDON3D_PROJECT_DB_SHA256_SIZE] = {5}; unsigned char raw_hash[LARDON3D_PROJECT_DB_SHA256_SIZE] = {5};
char raw_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY]; char raw_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
asset_path_for_hash(raw_hash, raw_asset_path); asset_path_for_hash(raw_hash, raw_asset_path);
@ -1093,7 +1095,7 @@ static bool run_test(void) {
LARDON3D_DB_CAPTURE_ASSET_SOURCE) == LARDON3D_PROJECT_DB_OK); LARDON3D_DB_CAPTURE_ASSET_SOURCE) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_attach_capture_asset( CHECK(lardon3d_project_db_attach_capture_asset(
database, capture.capture_id, pair_image.asset_id, database, capture.capture_id, pair_image.asset_id,
LARDON3D_DB_CAPTURE_ASSET_SOURCE) == LARDON3D_PROJECT_DB_OK); LARDON3D_DB_CAPTURE_ASSET_SOURCE) == LARDON3D_PROJECT_DB_CONSTRAINT);
CHECK(lardon3d_project_db_attach_capture_asset( CHECK(lardon3d_project_db_attach_capture_asset(
database, capture.capture_id, raw_asset.asset_id, database, capture.capture_id, raw_asset.asset_id,
(Lardon3DProjectDbCaptureAssetRole)99) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); (Lardon3DProjectDbCaptureAssetRole)99) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
@ -1108,6 +1110,17 @@ static bool run_test(void) {
database, replacement_scanset.scanset_id, developed_hash, developed_asset_path, 3, database, replacement_scanset.scanset_id, developed_hash, developed_asset_path, 3,
"developed.png", "/source/developed.png", 0, 23, &identity_status, "developed.png", "/source/developed.png", 0, 23, &identity_status,
&developed_image) == LARDON3D_PROJECT_DB_OK); &developed_image) == LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbCapture developed_capture;
CHECK(lardon3d_project_db_find_capture_for_image(database, developed_image.image_id,
&developed_capture) == LARDON3D_PROJECT_DB_OK);
lardon3d_project_db_close(database);
database = NULL;
char remove_developed_capture[256];
CHECK(snprintf(remove_developed_capture, sizeof(remove_developed_capture),
"PRAGMA foreign_keys=ON;DELETE FROM captures WHERE capture_id=%llu",
(unsigned long long)developed_capture.capture_id) > 0);
CHECK(execute_test_sql(database_path, remove_developed_capture));
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_attach_capture_asset( CHECK(lardon3d_project_db_attach_capture_asset(
database, capture.capture_id, developed_image.asset_id, database, capture.capture_id, developed_image.asset_id,
LARDON3D_DB_CAPTURE_ASSET_DERIVED) == LARDON3D_PROJECT_DB_OK); LARDON3D_DB_CAPTURE_ASSET_DERIVED) == LARDON3D_PROJECT_DB_OK);
@ -1118,7 +1131,7 @@ static bool run_test(void) {
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
capture_asset_count == 3); capture_asset_count == 3);
CHECK(lardon3d_project_db_attach_capture_image(database, capture.capture_id, CHECK(lardon3d_project_db_attach_capture_image(database, capture.capture_id,
pair_image.image_id) == LARDON3D_PROJECT_DB_OK); pair_image.image_id) == LARDON3D_PROJECT_DB_CONSTRAINT);
CHECK(lardon3d_project_db_attach_capture_image(database, other_capture.capture_id, CHECK(lardon3d_project_db_attach_capture_image(database, other_capture.capture_id,
pair_image.image_id) == pair_image.image_id) ==
LARDON3D_PROJECT_DB_CONSTRAINT); LARDON3D_PROJECT_DB_CONSTRAINT);
@ -1141,6 +1154,16 @@ static bool run_test(void) {
&selected_image) == &selected_image) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
selected_image == developed_image.image_id && pair_image.image_id != developed_image.image_id); selected_image == developed_image.image_id && pair_image.image_id != developed_image.image_id);
CHECK(lardon3d_project_db_register_image(
database, replacement_scanset.scanset_id, third_hash, third_asset_path, 1,
"pair-a.jpg", "/source/pair-a.jpg", 0, 3, &identity_status,
&pair_image) == LARDON3D_PROJECT_DB_OK &&
identity_status == LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT);
CHECK(lardon3d_project_db_get_selected_capture_image(database, capture.capture_id,
&selected_image) ==
LARDON3D_PROJECT_DB_OK &&
selected_image == developed_image.image_id);
CHECK(query_integer(database_path, "SELECT count(*) FROM asset_derivations", 0));
Lardon3DProjectDbAssetDerivation derivation = { Lardon3DProjectDbAssetDerivation derivation = {
.parent_asset_id = raw_asset.asset_id, .parent_asset_id = raw_asset.asset_id,
.child_asset_id = developed_image.asset_id, .child_asset_id = developed_image.asset_id,