feat(catalog): add persistent scan sets and image assets
This commit is contained in:
parent
36d083f727
commit
0f99415317
27 changed files with 1686 additions and 193 deletions
|
|
@ -22,11 +22,11 @@ Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais "ensemble
|
||||||
|
|
||||||
- **Project** : cycle de vie persistant, identité stable et Project Database ouverte
|
- **Project** : cycle de vie persistant, identité stable et Project Database ouverte
|
||||||
- **Import** : premier task kind de production, exécuté par la file générique en lots bornés et reprenables
|
- **Import** : premier task kind de production, exécuté par la file générique en lots bornés et reprenables
|
||||||
- **Image Catalog** : indexage des métadonnées d'images
|
- **ScanSet / Image Catalog v1** : acquisitions, images logiques, provenance et assets SHA-256 persistants et paginés
|
||||||
- **Image View** : vues triées et filtrées pour la TUI
|
- **Image View** : vues triées et filtrées pour la TUI
|
||||||
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
|
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
|
||||||
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
|
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
|
||||||
- **Project Database v3** : identité, tâches typées/checkpoints, paramètres d'import et inventaire d'artefacts SQLite
|
- **Project Database v4** : tâches, ScanSets, catalogue image, assets et migrations SQLite
|
||||||
- **Task Kind Registry** : identité métier durable et reconstruction runtime explicite
|
- **Task Kind Registry** : identité métier durable et reconstruction runtime explicite
|
||||||
- **Recovery projet** : reprise automatique sélective et bornée des imports récupérables
|
- **Recovery projet** : reprise automatique sélective et bornée des imports récupérables
|
||||||
- **Task Queue** : file FIFO avec sélection adaptative et backpressure
|
- **Task Queue** : file FIFO avec sélection adaptative et backpressure
|
||||||
|
|
|
||||||
|
|
@ -44,15 +44,15 @@ des fichiers admissibles et maintenance d'un manifeste cohérent.
|
||||||
**Statut :** IMPLEMENTED
|
**Statut :** IMPLEMENTED
|
||||||
|
|
||||||
### Import Task
|
### Import Task
|
||||||
Wrapper asynchrone de l'import avec états, progression et annulation
|
Premier type métier persistant. Il s'exécute par lots bornés dans le scheduler
|
||||||
coopérative. Exécute l'import dans un worker dédié (non encore migré vers
|
générique, cible explicitement un ScanSet et peut être reconstruit puis repris.
|
||||||
le scheduler générique).
|
|
||||||
|
|
||||||
**Statut :** IMPLEMENTED
|
**Statut :** IMPLEMENTED
|
||||||
|
|
||||||
### Image Catalog
|
### ScanSet et Image Catalog
|
||||||
Chargement et indexage en mémoire des métadonnées d'images depuis le
|
Catalogue SQLite persistant séparant acquisition, image logique et asset
|
||||||
manifeste du projet. Fournit un accès structuré aux images.
|
physique SHA-256. Les parcours persistants sont paginés ; l'ancien catalogue
|
||||||
|
mémoire depuis `manifest.tsv` reste une façade legacy pour la TUI.
|
||||||
|
|
||||||
**Statut :** IMPLEMENTED
|
**Statut :** IMPLEMENTED
|
||||||
|
|
||||||
|
|
@ -120,6 +120,6 @@ frontière de séquence connue.
|
||||||
- Absence de DAG de dépendances.
|
- Absence de DAG de dépendances.
|
||||||
- Absence de priorités.
|
- Absence de priorités.
|
||||||
- Absence de pools de workers multiples (CPU/GPU/IO).
|
- Absence de pools de workers multiples (CPU/GPU/IO).
|
||||||
- Import non migré vers le scheduler générique.
|
- La TUI legacy ne sélectionne pas encore explicitement ses ScanSets.
|
||||||
- Persistance des tâches et checkpoints non implémentée.
|
- La réconciliation globale des assets/checkpoints orphelins n'est pas implémentée.
|
||||||
- Viewer et publication live non implémentés.
|
- Viewer et publication live non implémentés.
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ par le `time_t` local avant conversion. Le format reste donc lisible entre
|
||||||
plateformes uniquement pour les valeurs communes à leurs domaines `size_t` et
|
plateformes uniquement pour les valeurs communes à leurs domaines `size_t` et
|
||||||
`time_t`.
|
`time_t`.
|
||||||
|
|
||||||
## Project Database v3
|
## Project Database v4
|
||||||
|
|
||||||
SQLite contient l'état logique interrogable et les références aux fichiers ;
|
SQLite contient l'état logique interrogable et les références aux fichiers ;
|
||||||
les checkpoints et artefacts volumineux restent externes. L'enregistrement du
|
les checkpoints et artefacts volumineux restent externes. L'enregistrement du
|
||||||
|
|
@ -136,7 +136,7 @@ non durable, absent, invalide, version inconnue et erreur d'I/O. Aucune réparat
|
||||||
ou suppression silencieuse n'est effectuée.
|
ou suppression silencieuse n'est effectuée.
|
||||||
|
|
||||||
Le format checkpoint reste en version 1 et ne contient pas de `task_kind`. Le
|
Le format checkpoint reste en version 1 et ne contient pas de `task_kind`. Le
|
||||||
schéma SQLite v3 conserve `task_kind` et `task_kind_version` dans le résumé
|
schéma SQLite v4 conserve `task_kind` et `task_kind_version` dans le résumé
|
||||||
logique interrogable. La migration v1→v2 laisse ces deux colonnes à `NULL` : une
|
logique interrogable. La migration v1→v2 laisse ces deux colonnes à `NULL` : une
|
||||||
tâche legacy reste inspectable mais ne peut pas être reconstruite ou resoumise.
|
tâche legacy reste inspectable mais ne peut pas être reconstruite ou resoumise.
|
||||||
Un kind inconnu ou une version non supportée est diagnostiqué sans exécuter de
|
Un kind inconnu ou une version non supportée est diagnostiqué sans exécuter de
|
||||||
|
|
@ -147,8 +147,8 @@ code.
|
||||||
**IMPLEMENTED** — modèle durable, codec v1, lecture validée, publication
|
**IMPLEMENTED** — modèle durable, codec v1, lecture validée, publication
|
||||||
atomique et restauration sûre d'une tâche isolée.
|
atomique et restauration sûre d'une tâche isolée.
|
||||||
|
|
||||||
**IMPLEMENTED** — Project Database v3 pour identité, résumés de tâches typées,
|
**IMPLEMENTED** — Project Database v4 pour identité, tâches typées, ScanSets,
|
||||||
références checkpoint et artefacts génériques.
|
images logiques, assets SHA-256, références checkpoint et artefacts génériques.
|
||||||
|
|
||||||
**IMPLEMENTED** — registry statique bornée et reconstruction explicite avec
|
**IMPLEMENTED** — registry statique bornée et reconstruction explicite avec
|
||||||
ownership du userdata.
|
ownership du userdata.
|
||||||
|
|
@ -156,21 +156,39 @@ ownership du userdata.
|
||||||
**IMPLEMENTED** — API projet de sauvegarde fichier+DB et inventaire validé au
|
**IMPLEMENTED** — API projet de sauvegarde fichier+DB et inventaire validé au
|
||||||
redémarrage.
|
redémarrage.
|
||||||
|
|
||||||
**IMPLEMENTED** — `import.images` persiste son chemin source absolu dans une
|
**IMPLEMENTED** — `import.images` persiste son chemin source absolu et son
|
||||||
table dédiée et publie un checkpoint après chaque lot validé. Le manifeste
|
`scanset_id`, puis publie un checkpoint après chaque lot validé. Le catalogue
|
||||||
publié rend le rejeu idempotent à la granularité d'une image.
|
SQLite rend le rejeu idempotent à la granularité du contenu dans un ScanSet.
|
||||||
|
|
||||||
Le chemin source absolu est l'intention durable v1 : il doit rester accessible
|
Le chemin source absolu est l'intention durable v1 : il doit rester accessible
|
||||||
après redémarrage et un projet déplacé ne rend pas une source externe portable.
|
après redémarrage et un projet déplacé ne rend pas une source externe portable.
|
||||||
Une source absente ou devenue non-répertoire fait échouer proprement la
|
Une source absente ou devenue non-répertoire fait échouer proprement la
|
||||||
reconstruction. Une image déjà inscrite au manifeste est un résultat validé et
|
reconstruction. Après import terminé, l'image dépend de l'asset géré, plus de la
|
||||||
les modifications ultérieures de sa source sont ignorées. Pour la fenêtre
|
source. Le SHA-256 est calculé pendant la copie avec un tampon fixe de 64 Kio.
|
||||||
« copie publiée, manifeste non publié », la reprise n'adopte la destination
|
L'asset est publié sans écrasement sous
|
||||||
orpheline qu'après comparaison octet par octet avec la source ; une collision
|
`assets/images/<prefix>/<sha256>`, puis seulement enregistré `READY` dans une
|
||||||
différente est une erreur. Le manifeste est republié atomiquement avant le
|
transaction SQLite. Un asset concurrent déjà présent n'est adopté qu'après
|
||||||
checkpoint de fin de lot. Si ce checkpoint ou sa transaction DB échoue, le
|
rehash complet et vérification de taille. Si SQLite échoue après publication,
|
||||||
manifeste demeure la frontière idempotente plus récente et un checkpoint
|
le fichier reste orphelin pour une future réconciliation ; aucune transaction
|
||||||
orphelin peut subsister selon le protocole filesystem puis SQLite.
|
FS+SQLite n'est revendiquée.
|
||||||
|
|
||||||
|
Les identités publiées `scanset_id`, `image_id` et `asset_id` utilisent les
|
||||||
|
séquences SQLite `AUTOINCREMENT` : une valeur issue d'une transaction validée
|
||||||
|
n'est jamais réattribuée à un autre objet, même après suppression de la ligne.
|
||||||
|
Une valeur réservée par une transaction annulée n'est pas une identité publiée.
|
||||||
|
|
||||||
|
`manifest.tsv` reste supporté par l'ancien chemin d'import/catalogue. Le chemin
|
||||||
|
persistant entretient une projection best-effort par hardlinks pour la TUI,
|
||||||
|
mais SQLite est le commit logique canonique : la correction de la reprise ne
|
||||||
|
dépend plus de l'ordre de `readdir()` ni du nom de fichier.
|
||||||
|
|
||||||
|
La migration v3 vers v4 ne transforme pas les lignes du manifeste historique
|
||||||
|
en images cataloguées : elles ne contiennent pas toutes les preuves exigées par
|
||||||
|
le modèle v4. Le marqueur durable `legacy_image_catalog_pending` rend cette
|
||||||
|
situation visible. Une tâche récupérable peut repeupler le catalogue par rejeu
|
||||||
|
si sa source existe encore ; sinon les fichiers et le manifeste restent une
|
||||||
|
projection legacy, explicitement non cataloguée. Une tâche v3 déjà terminée
|
||||||
|
n'est pas rejouée automatiquement.
|
||||||
|
|
||||||
**IMPLEMENTED** — reprise automatique sélective à l'ouverture : pagination
|
**IMPLEMENTED** — reprise automatique sélective à l'ouverture : pagination
|
||||||
bornée, validation checkpoint/kind, reconstruction production et enqueue sans
|
bornée, validation checkpoint/kind, reconstruction production et enqueue sans
|
||||||
|
|
@ -184,5 +202,8 @@ terminale n'appartient pas à la requête de reprise.
|
||||||
**NOT_YET_WIRED** — autosave complet, réconciliation des fichiers orphelins et
|
**NOT_YET_WIRED** — autosave complet, réconciliation des fichiers orphelins et
|
||||||
retry piloté par l'utilisateur pour les sources indisponibles.
|
retry piloté par l'utilisateur pour les sources indisponibles.
|
||||||
|
|
||||||
**PLANNED** — catalogue d'artefacts photogrammétriques réels, migrations v4+ et
|
**NOT_YET_WIRED** — migration de la vue TUI en mémoire vers la pagination
|
||||||
reprise avec dépendances.
|
SQLite, scrub des assets et réconciliation globale des orphelins.
|
||||||
|
|
||||||
|
**PLANNED** — Feature Store, Visual Index, migrations v5+ et reprise avec
|
||||||
|
dépendances.
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,8 @@ La base de données projet stocke les métadonnées de reconstruction et les rel
|
||||||
## Relations
|
## Relations
|
||||||
|
|
||||||
- Project → Scan Set (1:N)
|
- Project → Scan Set (1:N)
|
||||||
- Scan Set → Image (N:M)
|
- Scan Set → Image logique (1:N)
|
||||||
|
- Image logique → Image Asset (N:1)
|
||||||
- Image → Feature Set (1:N)
|
- Image → Feature Set (1:N)
|
||||||
- Image → Visual Signature (1:N)
|
- Image → Visual Signature (1:N)
|
||||||
- Candidate Pair → Image (2)
|
- Candidate Pair → Image (2)
|
||||||
|
|
@ -167,9 +168,9 @@ référence vers le fichier checkpoint. Le fichier checkpoint validé reste la
|
||||||
source complète pour `lardon3d_task_restore()` ; la DB seule ne reconstruit
|
source complète pour `lardon3d_task_restore()` ; la DB seule ne reconstruit
|
||||||
jamais une tâche. Un écart ou un fichier invalide interdit la reprise.
|
jamais une tâche. Un écart ou un fichier invalide interdit la reprise.
|
||||||
|
|
||||||
## Schéma v3 implémenté
|
## Schéma v4 implémenté
|
||||||
|
|
||||||
- `metadata(key PRIMARY KEY, value)` contient `schema_version=3` et
|
- `metadata(key PRIMARY KEY, value)` contient `schema_version=4` et
|
||||||
`next_task_id`, prochain ID durable allouable.
|
`next_task_id`, prochain ID durable allouable.
|
||||||
- `project(singleton=1, stable_id UNIQUE, name, created_at, updated_at)` décrit
|
- `project(singleton=1, stable_id UNIQUE, name, created_at, updated_at)` décrit
|
||||||
l'unique identité logique de la DB.
|
l'unique identité logique de la DB.
|
||||||
|
|
@ -182,22 +183,41 @@ jamais une tâche. Un écart ou un fichier invalide interdit la reprise.
|
||||||
- `artifacts(artifact_id PRIMARY KEY, kind, path, state, size_bytes,
|
- `artifacts(artifact_id PRIMARY KEY, kind, path, state, size_bytes,
|
||||||
producer_task_id REFERENCES tasks, created_at, updated_at)` inventorie des
|
producer_task_id REFERENCES tasks, created_at, updated_at)` inventorie des
|
||||||
fichiers externes. Les états v1 sont `STAGED` et `READY`.
|
fichiers externes. Les états v1 sont `STAGED` et `READY`.
|
||||||
|
- `scansets(scanset_id INTEGER PRIMARY KEY AUTOINCREMENT, name, created_at, updated_at)`
|
||||||
|
représente les acquisitions logiques, y compris les ScanSets vides.
|
||||||
|
- `image_assets(asset_id INTEGER PRIMARY KEY AUTOINCREMENT, sha256 UNIQUE, path UNIQUE,
|
||||||
|
size_bytes, state, created_at)` décrit les contenus physiques `READY`.
|
||||||
|
- `images(image_id INTEGER PRIMARY KEY AUTOINCREMENT, scanset_id REFERENCES scansets,
|
||||||
|
asset_id REFERENCES image_assets, original_name, source_path,
|
||||||
|
producer_task_id REFERENCES tasks, imported_at)` décrit les images logiques.
|
||||||
|
`UNIQUE(scanset_id,asset_id)` interdit les doublons de contenu dans une même
|
||||||
|
acquisition sans fusionner deux acquisitions différentes.
|
||||||
- `image_import_tasks(task_id PRIMARY KEY REFERENCES tasks ON DELETE CASCADE,
|
- `image_import_tasks(task_id PRIMARY KEY REFERENCES tasks ON DELETE CASCADE,
|
||||||
source_path)` conserve l'unique paramètre métier v1 de `import.images`.
|
source_path, scanset_id REFERENCES scansets)` conserve les paramètres
|
||||||
|
métier immuables de `import.images`.
|
||||||
|
|
||||||
Les indexes portent uniquement sur `tasks(recovery_state, task_id)`,
|
`AUTOINCREMENT` est volontairement limité à ces trois identités publiées. Il
|
||||||
`artifacts(state, artifact_id)` et `artifacts(producer_task_id)`.
|
empêche la réutilisation d'un ID issu d'une transaction validée même si sa ligne
|
||||||
|
maximale est supprimée plus tard. Le coût de `sqlite_sequence` est accepté pour
|
||||||
|
garantir qu'un futur Feature Store, match ou track ne voie jamais son identifiant
|
||||||
|
désigner un autre objet. Les IDs de transactions rollbackées ne sont pas
|
||||||
|
considérés publiés et peuvent être réutilisés.
|
||||||
|
|
||||||
|
Les indexes ajoutés en v4 sont `images(scanset_id,image_id)`, pagination réelle,
|
||||||
|
et `images(producer_task_id,image_id)`, recherche par tâche productrice. Le
|
||||||
|
SHA-256 et le chemin asset sont déjà indexés par leurs contraintes `UNIQUE`.
|
||||||
|
|
||||||
## Ouverture et migrations
|
## Ouverture et migrations
|
||||||
|
|
||||||
Une DB vide reçoit directement le schéma v3 dans une transaction
|
Une DB vide reçoit directement le schéma v4 dans une transaction
|
||||||
`BEGIN IMMEDIATE`. Une DB v1 reçoit transactionnellement les colonnes nullable
|
`BEGIN IMMEDIATE`. Une DB v1 reçoit transactionnellement les colonnes nullable
|
||||||
`task_kind` et `task_kind_version`, puis les migrations v2→v3. Les anciennes lignes restent
|
`task_kind` et `task_kind_version`, puis les migrations v2→v3. Les anciennes lignes restent
|
||||||
`NULL/NULL`, sans type inventé et sans perte des projets, tâches, checkpoints ou
|
`NULL/NULL`, sans type inventé et sans perte des projets, tâches, checkpoints ou
|
||||||
artefacts. Une interruption ou erreur provoque un rollback complet. Une DB v2
|
artefacts. Une interruption ou erreur provoque un rollback complet. Les DB v1,
|
||||||
est migrée vers v3 ; une DB v3 est validée puis ouverte. Une version future est refusée et une DB contenant
|
v2 et v3 sont migrées séquentiellement vers v4. Une version future est refusée et une DB contenant
|
||||||
des tables sans métadonnée de version est considérée corrompue. La fonction
|
des tables sans métadonnée de version est considérée corrompue. La fonction
|
||||||
interne de migration ne connaît que `0 → 3`, `1 → 2 → 3` et `2 → 3`.
|
interne de migration ne connaît que `0 → 4`, `1 → 2 → 3 → 4`,
|
||||||
|
`2 → 3 → 4` et `3 → 4`.
|
||||||
|
|
||||||
Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` :
|
Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` :
|
||||||
|
|
||||||
|
|
@ -227,7 +247,53 @@ UPDATE metadata SET value=3
|
||||||
WHERE key='schema_version' AND value=2;
|
WHERE key='schema_version' AND value=2;
|
||||||
```
|
```
|
||||||
|
|
||||||
Configuration v3 : `foreign_keys=ON`, `journal_mode=DELETE`,
|
Migration v3→v4 exacte, dans la même transaction :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE scansets(
|
||||||
|
scanset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(scanset_id>0),
|
||||||
|
name TEXT NOT NULL CHECK(length(name)>0 AND length(name)<256),
|
||||||
|
created_at INTEGER NOT NULL CHECK(created_at>=0),
|
||||||
|
updated_at INTEGER NOT NULL CHECK(updated_at>=created_at)
|
||||||
|
);
|
||||||
|
CREATE TABLE image_assets(
|
||||||
|
asset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(asset_id>0),
|
||||||
|
sha256 BLOB NOT NULL UNIQUE CHECK(length(sha256)=32),
|
||||||
|
path TEXT NOT NULL UNIQUE CHECK(length(path)>0 AND length(path)<4096),
|
||||||
|
size_bytes INTEGER NOT NULL CHECK(size_bytes>=0),
|
||||||
|
state INTEGER NOT NULL CHECK(state=1),
|
||||||
|
created_at INTEGER NOT NULL CHECK(created_at>=0)
|
||||||
|
);
|
||||||
|
CREATE TABLE images(
|
||||||
|
image_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(image_id>0),
|
||||||
|
scanset_id INTEGER NOT NULL REFERENCES scansets(scanset_id),
|
||||||
|
asset_id INTEGER NOT NULL REFERENCES image_assets(asset_id),
|
||||||
|
original_name TEXT NOT NULL CHECK(length(original_name)>0 AND length(original_name)<256),
|
||||||
|
source_path TEXT NOT NULL CHECK(length(source_path)>0 AND length(source_path)<4096),
|
||||||
|
producer_task_id INTEGER REFERENCES tasks(task_id),
|
||||||
|
imported_at INTEGER NOT NULL CHECK(imported_at>=0),
|
||||||
|
UNIQUE(scanset_id,asset_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX images_scanset_idx ON images(scanset_id,image_id);
|
||||||
|
CREATE INDEX images_producer_idx ON images(producer_task_id,image_id);
|
||||||
|
ALTER TABLE image_import_tasks
|
||||||
|
ADD COLUMN scanset_id INTEGER REFERENCES scansets(scanset_id);
|
||||||
|
INSERT INTO scansets(name,created_at,updated_at)
|
||||||
|
SELECT 'Imports antérieurs à ScanSet v1',0,0
|
||||||
|
WHERE EXISTS(SELECT 1 FROM image_import_tasks);
|
||||||
|
UPDATE image_import_tasks
|
||||||
|
SET scanset_id=(SELECT scanset_id FROM scansets
|
||||||
|
WHERE name='Imports antérieurs à ScanSet v1'
|
||||||
|
ORDER BY scanset_id LIMIT 1)
|
||||||
|
WHERE scanset_id IS NULL;
|
||||||
|
INSERT INTO metadata(key,value)
|
||||||
|
VALUES('legacy_image_catalog_pending',
|
||||||
|
CASE WHEN EXISTS(SELECT 1 FROM image_import_tasks) THEN 1 ELSE 0 END);
|
||||||
|
UPDATE metadata SET value=4
|
||||||
|
WHERE key='schema_version' AND value=3;
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration v4 : `foreign_keys=ON`, `journal_mode=DELETE`,
|
||||||
`synchronous=FULL`, `busy_timeout=5000`. Le mode DELETE convient au propriétaire
|
`synchronous=FULL`, `busy_timeout=5000`. Le mode DELETE convient au propriétaire
|
||||||
unique actuel, évite les fichiers WAL/SHM durables et conserve la synchronisation
|
unique actuel, évite les fichiers WAL/SHM durables et conserve la synchronisation
|
||||||
forte. Le timeout borne l'attente d'un verrou externe à cinq secondes.
|
forte. Le timeout borne l'attente d'un verrou externe à cinq secondes.
|
||||||
|
|
@ -288,7 +354,7 @@ ouvert.
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
**IMPLEMENTED** — SQLite système, schéma v3 et migrations v1→v2→v3, identité
|
**IMPLEMENTED** — SQLite système, schéma v4 et migrations v1→v2→v3→v4, identité
|
||||||
projet, transactions tâche+checkpoint, pagination de reprise et artefacts
|
projet, transactions tâche+checkpoint, pagination de reprise et artefacts
|
||||||
génériques.
|
génériques.
|
||||||
|
|
||||||
|
|
@ -304,9 +370,19 @@ immuables de `import.images` et reconstruction production explicite.
|
||||||
**IMPLEMENTED** — reprise automatique sélective, pagination de 8, ordre par ID,
|
**IMPLEMENTED** — reprise automatique sélective, pagination de 8, ordre par ID,
|
||||||
fenêtre de queue non bloquante et résumé consultable.
|
fenêtre de queue non bloquante et résumé consultable.
|
||||||
|
|
||||||
**NOT_YET_WIRED** — autosave à toutes les transitions, retry UI des sources
|
**IMPLEMENTED** — ScanSets, images logiques, assets SHA-256 et pagination
|
||||||
indisponibles et réconciliation des checkpoints orphelins, ScanSet
|
bornée à 256. Les identités sont des `INTEGER PRIMARY KEY AUTOINCREMENT` SQLite
|
||||||
et catalogue image persistants, Feature Store et Visual Index.
|
allouées sous transaction ; aucun `SELECT MAX()+1` n'est utilisé en
|
||||||
|
fonctionnement normal et une identité validée n'est jamais réutilisée.
|
||||||
|
|
||||||
**PLANNED** — migrations v4+, dépendances d'artefacts, graphe géométrique et
|
La migration v3→v4 ne lit pas `manifest.tsv`. Elle crée le ScanSet legacy et
|
||||||
|
positionne `metadata.legacy_image_catalog_pending=1` dès qu'une ancienne tâche
|
||||||
|
d'import existe. Cet indicateur signifie « données legacy potentiellement non
|
||||||
|
cataloguées », pas « images migrées ».
|
||||||
|
|
||||||
|
**NOT_YET_WIRED** — autosave à toutes les transitions, retry UI des sources
|
||||||
|
indisponibles, migration de la TUI legacy et réconciliation des fichiers
|
||||||
|
orphelins, Feature Store et Visual Index.
|
||||||
|
|
||||||
|
**PLANNED** — migrations v5+, dépendances d'artefacts, graphe géométrique et
|
||||||
reconstruction incrémentale.
|
reconstruction incrémentale.
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@ viewer, puis reprendre ultérieurement exactement où il s'était arrêté.
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| **Définition** | Un *scan set* (ou acquisition) regroupe un ensemble d'images capturées dans un contexte donné : même lieu, même session, même objectif de reconstruction. |
|
| **Définition** | Un *scan set* (ou acquisition) regroupe un ensemble d'images capturées dans un contexte donné : même lieu, même session, même objectif de reconstruction. |
|
||||||
| **Enrichissement progressif** | Un scan set peut être alimenté par vagues successives : images initiales, images de relèvement, images de contrôle. Chaque vague est horodatée et traçable. |
|
| **Enrichissement progressif** | Un scan set peut être alimenté par vagues successives : images initiales, images de relèvement, images de contrôle. Chaque vague est horodatée et traçable. |
|
||||||
| **Identification stable** | Chaque scan set reçoit un identifiant UUID qui ne change jamais, même si le nom lisible est renommé. Les références internes utilisent cet UUID. |
|
| **Identification stable** | Chaque ScanSet reçoit un `scanset_id` SQLite positif qui ne dépend pas de son nom. |
|
||||||
|
|
||||||
**Statut :** PLANNED — le concept existe dans le manifeste projet mais n'est
|
**Statut :** IMPLEMENTED v1 — identité, nom et horodatages persistants. Les
|
||||||
pas encore structuré avec UUID et vague d'import.
|
vagues et relations géométriques entre acquisitions restent planifiées.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -33,14 +33,12 @@ pas encore structuré avec UUID et vague d'import.
|
||||||
|
|
||||||
| Aspect | Description |
|
| Aspect | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| **Identité stable** | Chaque image possède un identifiant interne stable (UUID), distinct du nom de fichier sur disque. Le fichier peut être renommé ou déplacé sans casser les références. |
|
| **Identité stable** | Chaque image possède un `image_id` stable, distinct du nom et de l'asset physique. |
|
||||||
| **Provenance** | Le catalogue enregistre le scan set d'origine, la date d'import, le chemin original et le chemin local. |
|
| **Provenance** | Le catalogue enregistre le ScanSet, la date d'import, le nom et le chemin source, ainsi que la tâche productrice éventuelle. |
|
||||||
| **État de traitement** | Pour chaque image, le catalogue maintient un état : RAW, FEATURES_EXTRACTED, MATCHED, REGISTERED. Cet état est lu par le pipeline pour décider de l'étape suivante. |
|
| **Asset** | Le contenu physique est identifié par `asset_id`, SHA-256, taille et chemin géré. Plusieurs images logiques peuvent partager cet asset. |
|
||||||
|
|
||||||
**Statut :** PARTIELLEMENT IMPLEMENTÉ — le `image_catalog` gère les
|
**Statut :** IMPLEMENTED v1 — catalogue SQLite paginé et assets
|
||||||
métadonnées de base mais pas encore l'état de traitement ni l'UUID. L'import
|
content-addressed. Les états Feature/Matching/Reconstruction restent planifiés.
|
||||||
`import.images` est exécuté par le scheduler générique, copie au plus 32 images
|
|
||||||
par lot et reprend à partir du manifeste publié sans dupliquer une entrée.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -64,8 +62,7 @@ par lot et reprend à partir du manifeste publié sans dupliquer une entrée.
|
||||||
| **Pipeline conceptuel** | Extraction de features globales → construction de l'index → requête par similarité → retour des K plus proches voisins. |
|
| **Pipeline conceptuel** | Extraction de features globales → construction de l'index → requête par similarité → retour des K plus proches voisins. |
|
||||||
| **Proximité temporelle comme signal secondaire** | Lorsque les images portent un horodatage EXIF, la proximité temporelle sert de signal complémentaire au contenu visuel, mais ne remplace jamais l'analyse visuelle. |
|
| **Proximité temporelle comme signal secondaire** | Lorsque les images portent un horodatage EXIF, la proximité temporelle sert de signal complémentaire au contenu visuel, mais ne remplace jamais l'analyse visuelle. |
|
||||||
|
|
||||||
**Statut :** PLANNED — le pipeline d'import利用 déjà la proximité temporelle
|
**Statut :** PLANNED — aucun index visuel ni générateur de paires n'existe.
|
||||||
pour les paires candidats, mais aucun index visuel n'existe.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -97,7 +94,7 @@ candidate generator n'existe pas encore.
|
||||||
| Aspect | Description |
|
| Aspect | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| **Observation 2D → track → point3D** | Un *track* est une chaîne d'observations 2D cohérentes d'un même point 3D à travers plusieurs images. Chaque observation est un keypoint indexé par image. |
|
| **Observation 2D → track → point3D** | Un *track* est une chaîne d'observations 2D cohérentes d'un même point 3D à travers plusieurs images. Chaque observation est un keypoint indexé par image. |
|
||||||
| **Lien avec le catalog** | Les tracks référencent les images par leur UUID interne, pas par nom de fichier. |
|
| **Lien avec le catalogue** | Les tracks référenceront les images par `image_id`, jamais par nom ou chemin. |
|
||||||
| **Persistance** | Les tracks sont persistés entre les sessions de traitement. Un track ne peut être détruit que par une action explicite de l'utilisateur. |
|
| **Persistance** | Les tracks sont persistés entre les sessions de traitement. Un track ne peut être détruit que par une action explicite de l'utilisateur. |
|
||||||
|
|
||||||
**Statut :** PLANNED — aucune implémentation existante.
|
**Statut :** PLANNED — aucune implémentation existante.
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,8 @@ Le Resource Governor est l'unique propriétaire des budgets (RAM, GPU, CPU, IO).
|
||||||
- Pas d'adaptation basée sur le débit (duration_ns non encore utilisé)
|
- Pas d'adaptation basée sur le débit (duration_ns non encore utilisé)
|
||||||
- L'import `import.images` est admis avec 128 Kio fixes, un coût borné par item,
|
- L'import `import.images` est admis avec 128 Kio fixes, un coût borné par item,
|
||||||
un thread CPU, un slot I/O et des lots de 1 à 32. Il enregistre le nombre
|
un thread CPU, un slot I/O et des lots de 1 à 32. Il enregistre le nombre
|
||||||
d'images nouvellement publiées dans le manifeste et la durée réelle du lot.
|
d'images logiques nouvellement enregistrées dans le ScanSet et la durée
|
||||||
|
réelle du lot.
|
||||||
Cela inclut une copie orpheline identique adoptée, même si aucun octet n'est
|
Cela inclut une copie orpheline identique adoptée, même si aucun octet n'est
|
||||||
recopié. `peak_memory_bytes == 0` signifie explicitement « mesure inconnue » :
|
recopié. `peak_memory_bytes == 0` signifie explicitement « mesure inconnue » :
|
||||||
l'échantillon peut conserver taille/durée mais n'alimente jamais l'adaptation
|
l'échantillon peut conserver taille/durée mais n'alimente jamais l'adaptation
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,9 @@ qu'à la terminaison de cette tentative.
|
||||||
**IMPLEMENTED** — snapshot, codec v1 et restauration isolée.
|
**IMPLEMENTED** — snapshot, codec v1 et restauration isolée.
|
||||||
|
|
||||||
**IMPLEMENTED** — l'import `import.images` se sauvegarde à chaque fin de lot et
|
**IMPLEMENTED** — l'import `import.images` se sauvegarde à chaque fin de lot et
|
||||||
se reconstruit explicitement avec un userdata neuf lié au projet rouvert.
|
se reconstruit explicitement avec un userdata neuf lié au projet rouvert. Son
|
||||||
|
intention durable contient `source_path + scanset_id`; le hash/copie et la
|
||||||
|
transaction catalogue restent hors mutex Task et hors mutex DB pendant l'I/O.
|
||||||
|
|
||||||
**IMPLEMENTED** — `project_open()` inventorie par pages de 8, restaure puis
|
**IMPLEMENTED** — `project_open()` inventorie par pages de 8, restaure puis
|
||||||
resoumet automatiquement les tâches production valides. Il retourne après
|
resoumet automatiquement les tâches production valides. Il retourne après
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ fin de l'exécution. Le constructeur métier n'est jamais appelé sous mutex DB.
|
||||||
|
|
||||||
## Persistance et legacy
|
## Persistance et legacy
|
||||||
|
|
||||||
Le checkpoint générique reste en version 1. Project Database v3 conserve le
|
Le checkpoint générique reste en version 1. Project Database v4 conserve le
|
||||||
kind/version ; les lignes migrées depuis v1 restent `NULL/NULL` et sont classées
|
kind/version ; les lignes migrées depuis v1 restent `NULL/NULL` et sont classées
|
||||||
`LEGACY_UNTYPED`. Un kind inconnu ou une version non supportée reste inspectable
|
`LEGACY_UNTYPED`. Un kind inconnu ou une version non supportée reste inspectable
|
||||||
mais inexécutable. Aucun type n'est inventé et aucun code n'est sélectionné par
|
mais inexécutable. Aucun type n'est inventé et aucun code n'est sélectionné par
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ localement : son userdata est détruit, sans callback terminal ni écriture
|
||||||
durable d'une fausse annulation. Une annulation explicitement demandée conserve
|
durable d'une fausse annulation. Une annulation explicitement demandée conserve
|
||||||
le contrat de notification terminale.
|
le contrat de notification terminale.
|
||||||
|
|
||||||
La Project Database v3 peut enregistrer transactionnellement un résumé
|
La Project Database v4 peut enregistrer transactionnellement un résumé
|
||||||
`Lardon3DTaskDurableSnapshot` et la référence de son checkpoint. Elle ne stocke
|
`Lardon3DTaskDurableSnapshot` et la référence de son checkpoint. Elle ne stocke
|
||||||
ni estimation sérialisée complète, ni callback, ni réservation, et ne remplace
|
ni estimation sérialisée complète, ni callback, ni réservation, et ne remplace
|
||||||
pas la validation du fichier checkpoint avant `task_restore()`.
|
pas la validation du fichier checkpoint avant `task_restore()`.
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ Reconstruction Layers (triangulation)
|
||||||
|
|
||||||
Le matching est le pont entre les caractéristiques 2D des images et la structure 3D de la scène.
|
Le matching est le pont entre les caractéristiques 2D des images et la structure 3D de la scène.
|
||||||
|
|
||||||
|
La génération future de paires candidates combinera `image_id`, appartenance
|
||||||
|
au ScanSet, résultats du Visual Index et provenance. Une proximité temporelle
|
||||||
|
pourra servir de signal secondaire ; la proximité dans un dossier et le nom de
|
||||||
|
fichier ne constituent jamais l'identité principale.
|
||||||
|
|
||||||
## Concepts clés
|
## Concepts clés
|
||||||
|
|
||||||
### Étapes du matching
|
### Étapes du matching
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,120 @@
|
||||||
# Scan Sets & Acquisitions
|
# ScanSets et catalogue d'images
|
||||||
|
|
||||||
## Définition
|
|
||||||
|
|
||||||
Un **scan set** (ou ensemble de scans) regroupe l'ensemble des images capturées lors d'une session d'acquisition unique, correspondant à un seul objet ou site reconstruit. Chaque scan set constitue l'unité atomique d'entrée du pipeline de photogrammétrie.
|
|
||||||
|
|
||||||
Une **acquisition** désigne le processus physique de capture des images : positioning des capteurs, paramètres d'exposition, et conditions d'éclairage. Le scan set est le résultat numérique de cette acquisition.
|
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
**PLANNED** — Concept définissant la structure d'entrée, pas encore implémenté comme module distinct. Actuellement, le module `import` et `image_catalog` gèrent les images individuellement sans regroupement en scan sets.
|
**IMPLEMENTED — ScanSet v1 et Image Catalog persistant v1.**
|
||||||
Le chemin source durable de `import.images` ne constitue donc pas encore une
|
|
||||||
identité de ScanSet.
|
|
||||||
|
|
||||||
## Place dans le pipeline
|
Un projet contient zéro ou plusieurs acquisitions logiques appelées
|
||||||
|
`ScanSet`. Un ScanSet vide est valide : il peut être créé avant la capture ou
|
||||||
|
l'import.
|
||||||
|
|
||||||
```
|
## Modèle durable
|
||||||
Acquisition physique
|
|
||||||
↓
|
```text
|
||||||
Scan Set (import groupé)
|
Project
|
||||||
↓
|
└── ScanSet (acquisition logique)
|
||||||
Image Catalog (indexation)
|
└── Image (observation logique)
|
||||||
↓
|
└── Image Asset (contenu physique géré)
|
||||||
Visual Index (indexation visuelle)
|
|
||||||
↓
|
|
||||||
Matching & Tracks
|
|
||||||
↓
|
|
||||||
Reconstruction Layers
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Le scan set est la première structure organisée après l'import brut. Il fournit le contexte de regroupement nécessaire pour les étapes suivantes.
|
- `scanset_id` identifie durablement une acquisition, indépendamment de son
|
||||||
|
nom.
|
||||||
|
- `image_id` identifie durablement une observation dans un ScanSet,
|
||||||
|
indépendamment du nom du fichier et de la tâche d'import.
|
||||||
|
- `asset_id` identifie un contenu physique. Son SHA-256, sa taille et son
|
||||||
|
chemin relatif décrivent le fichier géré par Lardon3D.
|
||||||
|
- La provenance d'une image conserve son nom original, son chemin source, son
|
||||||
|
instant d'import et, s'il existe, le `task_id` importeur.
|
||||||
|
|
||||||
## Concepts clés
|
Un contenu identique n'implique pas une identité logique unique. Dans un même
|
||||||
|
ScanSet, le couple `(scanset_id, asset_id)` est unique : réimporter le même
|
||||||
|
contenu retourne `ALREADY_PRESENT`. Dans deux ScanSets distincts, deux images
|
||||||
|
logiques possèdent deux `image_id`, mais peuvent partager le même `asset_id` et
|
||||||
|
le même fichier physique.
|
||||||
|
|
||||||
### Structure d'un scan set
|
Un ScanSet peut documenter un objet complet, une sous-zone ou une pièce
|
||||||
|
démontée. Aucune pose ni transformation 3D entre ScanSets n'est inventée en v1.
|
||||||
|
|
||||||
| Champ | Type | Description |
|
Ces trois identifiants utilisent `AUTOINCREMENT`. Un ID issu d'une transaction
|
||||||
|-------|------|-------------|
|
validée ne sera jamais réattribué, même après une future suppression physique.
|
||||||
| `id` | `uint64_t` | Identifiant unique du scan set |
|
Cette garantie est nécessaire avant que Feature Store, Visual Index, matches et
|
||||||
| `name` | `char[]` | Nom lisible (ex: "table_statue_01") |
|
tracks ne commencent à les référencer.
|
||||||
| `project_ref` | `uint64_t` | Référence au projet parent |
|
|
||||||
| `image_count` | `uint32_t` | Nombre d'images dans le set |
|
|
||||||
| `acquisition_date` | `int64_t` | Timestamp de la session |
|
|
||||||
| `camera_model` | `char[]` | Modèle de caméra utilisé |
|
|
||||||
| `pixel_size_um` | `double` | Taille de pixel en micromètres |
|
|
||||||
| `focal_length_mm` | `double` | Distance focale nominale |
|
|
||||||
|
|
||||||
### Relations entre images
|
## Stockage physique
|
||||||
|
|
||||||
Au sein d'un scan set, les images entretiennent des relations spatiales :
|
Les assets image sont content-addressed :
|
||||||
|
|
||||||
- **Overlap horizontal** : chevauchement entre images adjacentes d'une même ligne de capture (typiquement 60-80%)
|
```text
|
||||||
- **Overlap vertical** : chevauchement entre lignes de capture successives (typiquement 30-50%)
|
assets/images/<2 premiers hex>/<sha256 hex lowercase>
|
||||||
- **Baseline** : distance physique entre deux positions de capture consécutives
|
```
|
||||||
- **Convergence angle** : angle entre les axes optiques de deux caméras pour un même point
|
|
||||||
|
|
||||||
### Types de scan sets
|
Le hash est un SHA-256 binaire de 32 octets dans SQLite et est encodé par le
|
||||||
|
programme pour construire le chemin. Le nom utilisateur ne participe jamais au
|
||||||
|
chemin de stockage. Le calcul et la copie utilisent un tampon fixe de 64 Kio ;
|
||||||
|
une image entière n'est jamais chargée en mémoire.
|
||||||
|
|
||||||
- **Grid scan** : captures organisées en grille régulière, adapté aux objets de taille moyenne
|
La publication crée un temporaire, le synchronise, publie sans écrasement puis
|
||||||
- **Orbital scan** : captures circulaires autour d'un objet, adapté à la sculpture et aux artefacts
|
synchronise le répertoire. Un asset existant n'est adopté qu'après vérification
|
||||||
- **Linear scan** : captures le long d'un axe linéaire, adapté aux façades et structures longues
|
complète de sa taille et de son SHA-256. La transaction SQLite vient ensuite.
|
||||||
- **Free-form scan** : captures sans contrainte géométrique, nécessite plus d'overlap pour compenser
|
Un échec SQLite peut donc laisser un fichier orphelin, mais jamais une ligne
|
||||||
|
`READY` créée par le chemin métier avant publication.
|
||||||
|
|
||||||
## Relations avec les autres modules
|
## Import et reprise
|
||||||
|
|
||||||
| Module | Relation |
|
`import.images` persiste désormais `source_path + scanset_id`. Le ScanSet est
|
||||||
|--------|----------|
|
immuable pour un `task_id`. L'import parcourt le dossier en streaming et traite
|
||||||
| **Project** | Un scan set appartient à un projet. Le projet contient la structure de répertoires pour les images du set. |
|
des lots bornés. Sa correction ne dépend pas de l'ordre de `readdir()` : la
|
||||||
| **Import** | L'import copie les images dans le projet. Le scan set représente un groupe logique d'images importées. |
|
présence logique est décidée par `(scanset_id, SHA-256)` dans la base.
|
||||||
| **Image Catalog** | Le catalogue indexe les métadonnées de chaque image du scan set. |
|
|
||||||
| **Image View** | Les vues peuvent filtrer ou trier les images par scan set. |
|
|
||||||
| **Resource Governor** | Le gouverneur estime les ressources nécessaires pour traiter un scan set complet. |
|
|
||||||
| **Task** | Le traitement d'un scan set est décomposé en tâches unitaires par le scheduler. |
|
|
||||||
|
|
||||||
## Contraintes de conception
|
Une source externe reste nécessaire tant que la tâche est récupérable. Après
|
||||||
|
`COMPLETED`, le catalogue et l'asset géré ne dépendent plus de sa présence.
|
||||||
|
|
||||||
- Un scan set ne doit jamais être modifié après le début du traitement (immutabilité partielle).
|
Les anciennes tâches v3 sont rattachées par migration à un ScanSet explicite
|
||||||
- L'ajout d'images à un scan set existant doit déclencher un recalcul incrémental, pas un retraitement complet.
|
nommé `Imports antérieurs à ScanSet v1`. Ce rattachement exprime seulement
|
||||||
- La taille maximale d'un scan set est bornée par la RAM disponible : pas plus de N images en mémoire simultanément.
|
l'absence historique de regroupement ; aucune provenance de capture n'est
|
||||||
- Chaque image appartient à exactement un scan set (relation 1:N).
|
inventée.
|
||||||
|
|
||||||
## Terminologie
|
La migration ne transforme pas les lignes de `manifest.tsv` en images v4 : elle
|
||||||
|
n'a ni hash ni transaction catalogue historique permettant de le faire sans
|
||||||
|
rejouer les fichiers. Elle pose donc l'indicateur durable
|
||||||
|
`legacy_image_catalog_pending=1` lorsqu'une ancienne tâche d'import existe.
|
||||||
|
|
||||||
| Terme | Définition |
|
- Si une tâche v3 récupérable retrouve sa source, sa reprise relit les sources,
|
||||||
|-------|------------|
|
publie les assets content-addressed et remplit le catalogue. Les anciennes
|
||||||
| **Scan set** | Groupe d'images d'une même session d'acquisition |
|
copies sous `images/originals` ne sont ni écrasées ni supprimées.
|
||||||
| **Acquisition** | Processus physique de capture |
|
- Si sa source a disparu, la reconstruction échoue proprement et les anciennes
|
||||||
| **Overlap** | Pourcentage de superficie commune entre deux images |
|
données restent uniquement legacy ; aucune provenance ni image v4 n'est
|
||||||
| **Baseline** | Distance entre deux positions de capture |
|
inventée.
|
||||||
| **Capture session** | Période continue d'acquisition d'images |
|
- Un import v3 déjà terminal n'est pas rejoué automatiquement. Ses images du
|
||||||
| **Footprint** | Zone physique couverte par une image au sol |
|
manifeste restent accessibles par la projection legacy mais sont marquées
|
||||||
|
conceptuellement **LEGACY DATA NOT YET CATALOGUED** via l'indicateur DB.
|
||||||
|
|
||||||
|
L'indicateur reste conservateur en v1 et n'est pas effacé automatiquement : une
|
||||||
|
future commande de migration/reconciliation devra vérifier l'intégralité des
|
||||||
|
données historiques avant de le lever.
|
||||||
|
|
||||||
|
## Accès borné
|
||||||
|
|
||||||
|
Les listes de ScanSets et d'images utilisent un curseur par ID et une limite de
|
||||||
|
1 à 256. Aucun `get_all_images()` persistant n'existe. Des milliers d'images ne
|
||||||
|
nécessitent donc pas autant de records simultanément en mémoire.
|
||||||
|
|
||||||
|
## Transition du manifeste
|
||||||
|
|
||||||
|
`images/manifest.tsv` reste pris en charge par l'ancien catalogue en mémoire et
|
||||||
|
les anciennes API d'import. Le chemin production maintient une projection
|
||||||
|
best-effort par hardlinks pour que la TUI existante continue d'afficher les
|
||||||
|
nouvelles images sans seconde copie physique. `project.db + assets/images`
|
||||||
|
reste toutefois la vérité canonique : le manifeste est legacy et diagnostique,
|
||||||
|
et n'est plus une condition de reprise. Deux images de ScanSets différents qui
|
||||||
|
partagent un nom ne peuvent pas toutes deux être représentées dans cette vue
|
||||||
|
legacy. La migration de la TUI vers les pages SQLite reste donc nécessaire.
|
||||||
|
|
||||||
|
## Futures étapes
|
||||||
|
|
||||||
|
**NOT_YET_WIRED** — sélection de ScanSet dans la TUI, migration de l'ancienne
|
||||||
|
vue mémoire, vérification/scrub des assets et réconciliation globale des
|
||||||
|
orphelins.
|
||||||
|
|
||||||
|
**PLANNED** — Feature Store, Visual Index, paires candidates, matching, tracks,
|
||||||
|
SfM, MVS et relations géométriques entre ScanSets.
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ Matching & Tracks (appariement détaillé)
|
||||||
|
|
||||||
L'index visuel se situe entre l'extraction de features et la sélection de paires candidates. Il réduit drastiquement l'espace de recherche pour l'appariement.
|
L'index visuel se situe entre l'extraction de features et la sélection de paires candidates. Il réduit drastiquement l'espace de recherche pour l'appariement.
|
||||||
|
|
||||||
|
Le futur index référencera exclusivement les `image_id` stables du catalogue
|
||||||
|
persistant. Un chemin d'asset ou un nom source n'est ni une identité d'image ni
|
||||||
|
une clé d'index durable. Le partage d'un asset entre deux ScanSets n'empêche pas
|
||||||
|
leurs deux images logiques d'être des observations distinctes.
|
||||||
|
|
||||||
## Concepts clés
|
## Concepts clés
|
||||||
|
|
||||||
### Descripteurs visuels
|
### Descripteurs visuels
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,12 @@ Lardon3D suit une feuille de route ordonnée qui privilégie la stabilité et la
|
||||||
|
|
||||||
### Phase 3 : Persistance
|
### Phase 3 : Persistance
|
||||||
- ✅ Fondation versionnée des checkpoints de tâches
|
- ✅ Fondation versionnée des checkpoints de tâches
|
||||||
- ✅ Project Database v3 (paramètres d'import et allocation durable des task IDs)
|
- ✅ Project Database v4 (tâches, ScanSets, images, assets et migrations)
|
||||||
- ✅ Branchement Project Database au cycle de vie projet et inventaire de reprise
|
- ✅ Branchement Project Database au cycle de vie projet et inventaire de reprise
|
||||||
- ✅ Registry durable des types métier de tâches
|
- ✅ Registry durable des types métier de tâches
|
||||||
- ✅ Premier type métier reconstructible (`import.images`)
|
- ✅ Premier type métier reconstructible (`import.images`)
|
||||||
- ✅ Resoumission automatique contrôlée et bornée des tâches récupérables
|
- ✅ Resoumission automatique contrôlée et bornée des tâches récupérables
|
||||||
- 📋 ScanSet et Image Catalog persistants
|
- ✅ ScanSet v1 et Image Catalog persistant v1
|
||||||
|
|
||||||
### Phase 4 : Pipeline avancé
|
### Phase 4 : Pipeline avancé
|
||||||
- 📋 Feature Store
|
- 📋 Feature Store
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,31 @@
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#include <lardon3d/app_state.h>
|
#include <lardon3d/app_state.h>
|
||||||
|
#include <lardon3d/project_db.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_IMAGE_CATALOG_IMPORTED = 0,
|
||||||
|
LARDON3D_IMAGE_CATALOG_ALREADY_PRESENT,
|
||||||
|
LARDON3D_IMAGE_CATALOG_INVALID_ARGUMENT,
|
||||||
|
LARDON3D_IMAGE_CATALOG_SOURCE_ERROR,
|
||||||
|
LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR,
|
||||||
|
LARDON3D_IMAGE_CATALOG_DB_ERROR
|
||||||
|
} Lardon3DImageCatalogImportResult;
|
||||||
|
|
||||||
|
bool lardon3d_image_catalog_create_scanset(
|
||||||
|
Lardon3DAppState *state, const char *name,
|
||||||
|
Lardon3DProjectDbScanSet *scanset
|
||||||
|
);
|
||||||
|
Lardon3DImageCatalogImportResult lardon3d_image_catalog_import_file(
|
||||||
|
Lardon3DAppState *state, uint64_t scanset_id, const char *source_path,
|
||||||
|
uint64_t producer_task_id, Lardon3DProjectDbImage *image,
|
||||||
|
Lardon3DProjectDbImageAsset *asset
|
||||||
|
);
|
||||||
|
Lardon3DProjectDbResult lardon3d_image_catalog_list(
|
||||||
|
Lardon3DAppState *state, uint64_t scanset_id, uint64_t after_image_id,
|
||||||
|
Lardon3DProjectDbImage *images, Lardon3DProjectDbImageAsset *assets,
|
||||||
|
size_t capacity, size_t *count
|
||||||
|
);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char *filename;
|
char *filename;
|
||||||
|
|
|
||||||
|
|
@ -64,4 +64,15 @@ Lardon3DImportOutcome lardon3d_import_directory_batch(
|
||||||
bool *complete
|
bool *complete
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,13 @@ typedef struct {
|
||||||
|
|
||||||
Lardon3DTask *lardon3d_project_create_image_import_task(
|
Lardon3DTask *lardon3d_project_create_image_import_task(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
|
uint64_t scanset_id,
|
||||||
const char *source_directory,
|
const char *source_directory,
|
||||||
uint64_t *task_id
|
uint64_t *task_id
|
||||||
);
|
);
|
||||||
bool lardon3d_project_enqueue_image_import(
|
bool lardon3d_project_enqueue_image_import(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
|
uint64_t scanset_id,
|
||||||
const char *source_directory,
|
const char *source_directory,
|
||||||
uint64_t *task_id
|
uint64_t *task_id
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,8 @@ Lardon3DProjectTaskCheckpointResult
|
||||||
lardon3d_project_checkpoint_image_import_task(
|
lardon3d_project_checkpoint_image_import_task(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
const Lardon3DTask *task,
|
const Lardon3DTask *task,
|
||||||
const char *source_path
|
const char *source_path,
|
||||||
|
uint64_t scanset_id
|
||||||
);
|
);
|
||||||
Lardon3DProjectDbResult lardon3d_project_list_recoverable(
|
Lardon3DProjectDbResult lardon3d_project_list_recoverable(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,16 @@
|
||||||
#include <lardon3d/task.h>
|
#include <lardon3d/task.h>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 3,
|
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 4,
|
||||||
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
|
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
|
||||||
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
|
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
|
||||||
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
|
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
|
||||||
LARDON3D_PROJECT_DB_ERROR_CAPACITY = 256,
|
LARDON3D_PROJECT_DB_ERROR_CAPACITY = 256,
|
||||||
LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX = 256,
|
LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX = 256,
|
||||||
|
LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX = 256,
|
||||||
|
LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY = 256,
|
||||||
|
LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY = 256,
|
||||||
|
LARDON3D_PROJECT_DB_SHA256_SIZE = 32,
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef struct Lardon3DProjectDb Lardon3DProjectDb;
|
typedef struct Lardon3DProjectDb Lardon3DProjectDb;
|
||||||
|
|
@ -86,8 +90,45 @@ typedef struct {
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint64_t task_id;
|
uint64_t task_id;
|
||||||
char source_path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
char source_path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||||
|
uint64_t scanset_id;
|
||||||
} Lardon3DProjectDbImageImport;
|
} Lardon3DProjectDbImageImport;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint64_t scanset_id;
|
||||||
|
char name[LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY];
|
||||||
|
int64_t created_at;
|
||||||
|
int64_t updated_at;
|
||||||
|
} Lardon3DProjectDbScanSet;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_DB_IMAGE_ASSET_READY = 1
|
||||||
|
} Lardon3DProjectDbImageAssetState;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint64_t asset_id;
|
||||||
|
unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE];
|
||||||
|
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||||
|
uint64_t size_bytes;
|
||||||
|
Lardon3DProjectDbImageAssetState state;
|
||||||
|
int64_t created_at;
|
||||||
|
} Lardon3DProjectDbImageAsset;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint64_t image_id;
|
||||||
|
uint64_t scanset_id;
|
||||||
|
uint64_t asset_id;
|
||||||
|
char original_name[LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY];
|
||||||
|
char source_path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||||
|
bool has_producer_task;
|
||||||
|
uint64_t producer_task_id;
|
||||||
|
int64_t imported_at;
|
||||||
|
} Lardon3DProjectDbImage;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_PROJECT_DB_IMAGE_REGISTERED = 0,
|
||||||
|
LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT
|
||||||
|
} Lardon3DProjectDbImageRegisterStatus;
|
||||||
|
|
||||||
Lardon3DProjectDbResult lardon3d_project_db_open(
|
Lardon3DProjectDbResult lardon3d_project_db_open(
|
||||||
const char *path,
|
const char *path,
|
||||||
Lardon3DProjectDb **database,
|
Lardon3DProjectDb **database,
|
||||||
|
|
@ -98,6 +139,9 @@ bool lardon3d_project_db_last_error(
|
||||||
Lardon3DProjectDb *database,
|
Lardon3DProjectDb *database,
|
||||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]
|
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]
|
||||||
);
|
);
|
||||||
|
Lardon3DProjectDbResult lardon3d_project_db_legacy_catalog_pending(
|
||||||
|
Lardon3DProjectDb *database, bool *pending
|
||||||
|
);
|
||||||
unsigned int lardon3d_project_db_schema_version(Lardon3DProjectDb *database);
|
unsigned int lardon3d_project_db_schema_version(Lardon3DProjectDb *database);
|
||||||
|
|
||||||
Lardon3DProjectDbResult lardon3d_project_db_set_project(
|
Lardon3DProjectDbResult lardon3d_project_db_set_project(
|
||||||
|
|
@ -123,8 +167,41 @@ Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task(
|
||||||
uint32_t task_kind_version,
|
uint32_t task_kind_version,
|
||||||
const Lardon3DProjectDbCheckpoint *checkpoint,
|
const Lardon3DProjectDbCheckpoint *checkpoint,
|
||||||
const char *source_path,
|
const char *source_path,
|
||||||
|
uint64_t scanset_id,
|
||||||
int64_t updated_at
|
int64_t updated_at
|
||||||
);
|
);
|
||||||
|
Lardon3DProjectDbResult lardon3d_project_db_create_scanset(
|
||||||
|
Lardon3DProjectDb *database, const char *name,
|
||||||
|
Lardon3DProjectDbScanSet *scanset
|
||||||
|
);
|
||||||
|
Lardon3DProjectDbResult lardon3d_project_db_load_scanset(
|
||||||
|
Lardon3DProjectDb *database, uint64_t scanset_id,
|
||||||
|
Lardon3DProjectDbScanSet *scanset
|
||||||
|
);
|
||||||
|
Lardon3DProjectDbResult lardon3d_project_db_list_scansets(
|
||||||
|
Lardon3DProjectDb *database, uint64_t after_scanset_id,
|
||||||
|
Lardon3DProjectDbScanSet *scansets, size_t capacity, size_t *count
|
||||||
|
);
|
||||||
|
Lardon3DProjectDbResult lardon3d_project_db_register_image(
|
||||||
|
Lardon3DProjectDb *database, uint64_t scanset_id,
|
||||||
|
const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE],
|
||||||
|
const char *asset_path, uint64_t size_bytes, const char *original_name,
|
||||||
|
const char *source_path, uint64_t producer_task_id, int64_t imported_at,
|
||||||
|
Lardon3DProjectDbImageRegisterStatus *status,
|
||||||
|
Lardon3DProjectDbImage *image
|
||||||
|
);
|
||||||
|
Lardon3DProjectDbResult lardon3d_project_db_load_image(
|
||||||
|
Lardon3DProjectDb *database, uint64_t image_id,
|
||||||
|
Lardon3DProjectDbImage *image, Lardon3DProjectDbImageAsset *asset
|
||||||
|
);
|
||||||
|
Lardon3DProjectDbResult lardon3d_project_db_list_images(
|
||||||
|
Lardon3DProjectDb *database, uint64_t scanset_id, uint64_t after_image_id,
|
||||||
|
Lardon3DProjectDbImage *images, Lardon3DProjectDbImageAsset *assets,
|
||||||
|
size_t capacity, size_t *count
|
||||||
|
);
|
||||||
|
Lardon3DProjectDbResult lardon3d_project_db_count_images(
|
||||||
|
Lardon3DProjectDb *database, uint64_t scanset_id, uint64_t *count
|
||||||
|
);
|
||||||
Lardon3DProjectDbResult lardon3d_project_db_load_image_import(
|
Lardon3DProjectDbResult lardon3d_project_db_load_image_import(
|
||||||
Lardon3DProjectDb *database,
|
Lardon3DProjectDb *database,
|
||||||
uint64_t task_id,
|
uint64_t task_id,
|
||||||
|
|
|
||||||
42
meson.build
42
meson.build
|
|
@ -21,6 +21,7 @@ add_project_arguments(
|
||||||
ncursesw = dependency('ncursesw', required: true)
|
ncursesw = dependency('ncursesw', required: true)
|
||||||
threads = dependency('threads')
|
threads = dependency('threads')
|
||||||
sqlite3 = dependency('sqlite3', required: true)
|
sqlite3 = dependency('sqlite3', required: true)
|
||||||
|
openssl = dependency('openssl', required: true)
|
||||||
|
|
||||||
executable(
|
executable(
|
||||||
'lardon3d',
|
'lardon3d',
|
||||||
|
|
@ -33,6 +34,7 @@ executable(
|
||||||
'src/import.c',
|
'src/import.c',
|
||||||
'src/import_task.c',
|
'src/import_task.c',
|
||||||
'src/image_catalog.c',
|
'src/image_catalog.c',
|
||||||
|
'src/image_catalog_persistent.c',
|
||||||
'src/image_view.c',
|
'src/image_view.c',
|
||||||
'src/project.c',
|
'src/project.c',
|
||||||
'src/project_db.c',
|
'src/project_db.c',
|
||||||
|
|
@ -46,7 +48,7 @@ executable(
|
||||||
'src/hardware_profile.c',
|
'src/hardware_profile.c',
|
||||||
],
|
],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
dependencies: [ncursesw, threads, sqlite3],
|
dependencies: [ncursesw, threads, sqlite3, openssl],
|
||||||
)
|
)
|
||||||
|
|
||||||
import_test = executable(
|
import_test = executable(
|
||||||
|
|
@ -55,8 +57,14 @@ import_test = executable(
|
||||||
'tests/test_import.c',
|
'tests/test_import.c',
|
||||||
'src/app_state.c',
|
'src/app_state.c',
|
||||||
'src/import.c',
|
'src/import.c',
|
||||||
|
'src/image_catalog_persistent.c',
|
||||||
|
'src/project_db.c',
|
||||||
|
'src/task.c',
|
||||||
|
'src/resource_governor.c',
|
||||||
|
'src/resource_snapshot.c',
|
||||||
],
|
],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
|
dependencies: [threads, sqlite3, openssl],
|
||||||
)
|
)
|
||||||
|
|
||||||
test('import', import_test)
|
test('import', import_test)
|
||||||
|
|
@ -78,6 +86,7 @@ import_task_test = executable(
|
||||||
'src/resource_governor.c',
|
'src/resource_governor.c',
|
||||||
'src/resource_snapshot.c',
|
'src/resource_snapshot.c',
|
||||||
'src/image_catalog.c',
|
'src/image_catalog.c',
|
||||||
|
'src/image_catalog_persistent.c',
|
||||||
'src/image_view.c',
|
'src/image_view.c',
|
||||||
],
|
],
|
||||||
c_args: [
|
c_args: [
|
||||||
|
|
@ -86,7 +95,7 @@ import_task_test = executable(
|
||||||
'-DLARDON3D_CHECKPOINT_TESTING',
|
'-DLARDON3D_CHECKPOINT_TESTING',
|
||||||
],
|
],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
dependencies: [threads, sqlite3],
|
dependencies: [threads, sqlite3, openssl],
|
||||||
)
|
)
|
||||||
|
|
||||||
test('import-task', import_task_test, timeout: 30)
|
test('import-task', import_task_test, timeout: 30)
|
||||||
|
|
@ -97,12 +106,36 @@ image_catalog_test = executable(
|
||||||
'tests/test_image_catalog.c',
|
'tests/test_image_catalog.c',
|
||||||
'src/app_state.c',
|
'src/app_state.c',
|
||||||
'src/image_catalog.c',
|
'src/image_catalog.c',
|
||||||
|
'src/image_catalog_persistent.c',
|
||||||
|
'src/project_db.c',
|
||||||
|
'src/task.c',
|
||||||
|
'src/resource_governor.c',
|
||||||
|
'src/resource_snapshot.c',
|
||||||
],
|
],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
|
dependencies: [threads, sqlite3, openssl],
|
||||||
)
|
)
|
||||||
|
|
||||||
test('image-catalog', image_catalog_test, timeout: 30)
|
test('image-catalog', image_catalog_test, timeout: 30)
|
||||||
|
|
||||||
|
persistent_image_catalog_test = executable(
|
||||||
|
'test-persistent-image-catalog',
|
||||||
|
sources: [
|
||||||
|
'tests/test_persistent_image_catalog.c',
|
||||||
|
'src/app_state.c',
|
||||||
|
'src/image_catalog_persistent.c',
|
||||||
|
'src/project_db.c',
|
||||||
|
'src/task.c',
|
||||||
|
'src/resource_governor.c',
|
||||||
|
'src/resource_snapshot.c',
|
||||||
|
],
|
||||||
|
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
|
||||||
|
include_directories: include_directories('include'),
|
||||||
|
dependencies: [threads, sqlite3, openssl],
|
||||||
|
)
|
||||||
|
|
||||||
|
test('persistent-image-catalog', persistent_image_catalog_test, timeout: 60)
|
||||||
|
|
||||||
image_view_test = executable(
|
image_view_test = executable(
|
||||||
'test-image-view',
|
'test-image-view',
|
||||||
sources: [
|
sources: [
|
||||||
|
|
@ -157,7 +190,7 @@ project_db_test = executable(
|
||||||
],
|
],
|
||||||
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
|
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
dependencies: [threads, sqlite3],
|
dependencies: [threads, sqlite3, openssl],
|
||||||
)
|
)
|
||||||
|
|
||||||
test('project-db', project_db_test, timeout: 30)
|
test('project-db', project_db_test, timeout: 30)
|
||||||
|
|
@ -177,6 +210,7 @@ project_test = executable(
|
||||||
'src/import.c',
|
'src/import.c',
|
||||||
'src/import_task.c',
|
'src/import_task.c',
|
||||||
'src/image_catalog.c',
|
'src/image_catalog.c',
|
||||||
|
'src/image_catalog_persistent.c',
|
||||||
'src/image_view.c',
|
'src/image_view.c',
|
||||||
'src/resource_governor.c',
|
'src/resource_governor.c',
|
||||||
'src/resource_snapshot.c',
|
'src/resource_snapshot.c',
|
||||||
|
|
@ -186,7 +220,7 @@ project_test = executable(
|
||||||
'-DLARDON3D_CHECKPOINT_TESTING',
|
'-DLARDON3D_CHECKPOINT_TESTING',
|
||||||
],
|
],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
dependencies: [threads, sqlite3],
|
dependencies: [threads, sqlite3, openssl],
|
||||||
)
|
)
|
||||||
|
|
||||||
test('project', project_test, timeout: 30)
|
test('project', project_test, timeout: 30)
|
||||||
|
|
|
||||||
219
src/image_catalog_persistent.c
Normal file
219
src/image_catalog_persistent.c
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <lardon3d/image_catalog.h>
|
||||||
|
|
||||||
|
enum { COPY_BUFFER_SIZE = 64 * 1024 };
|
||||||
|
|
||||||
|
static bool
|
||||||
|
join_path(char output[PATH_MAX], const char *parent, const char *child)
|
||||||
|
{
|
||||||
|
int written = snprintf(output, PATH_MAX, "%s/%s", parent, child);
|
||||||
|
return written > 0 && (size_t)written < PATH_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
ensure_directory(const char *path)
|
||||||
|
{
|
||||||
|
struct stat information;
|
||||||
|
if (lstat(path, &information) == 0)
|
||||||
|
return S_ISDIR(information.st_mode) && !S_ISLNK(information.st_mode);
|
||||||
|
if (errno != ENOENT) return false;
|
||||||
|
if (mkdir(path, 0755) == 0) return true;
|
||||||
|
return errno == EEXIST && lstat(path, &information) == 0
|
||||||
|
&& S_ISDIR(information.st_mode) && !S_ISLNK(information.st_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
write_all(int descriptor, const unsigned char *data, size_t size)
|
||||||
|
{
|
||||||
|
size_t offset = 0;
|
||||||
|
while (offset < size) {
|
||||||
|
ssize_t written = write(descriptor, data + offset, size - offset);
|
||||||
|
if (written < 0 && errno == EINTR) continue;
|
||||||
|
if (written <= 0) return false;
|
||||||
|
offset += (size_t)written;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
hash_stream(int input, int output,
|
||||||
|
unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE], uint64_t *size)
|
||||||
|
{
|
||||||
|
EVP_MD_CTX *context = EVP_MD_CTX_new();
|
||||||
|
if (!context) return false;
|
||||||
|
bool success = EVP_DigestInit_ex(context, EVP_sha256(), NULL) == 1;
|
||||||
|
unsigned char buffer[COPY_BUFFER_SIZE];
|
||||||
|
uint64_t total = 0;
|
||||||
|
while (success) {
|
||||||
|
ssize_t count = read(input, buffer, sizeof(buffer));
|
||||||
|
if (count < 0 && errno == EINTR) continue;
|
||||||
|
if (count < 0) { success = false; break; }
|
||||||
|
if (count == 0) break;
|
||||||
|
size_t bytes = (size_t)count;
|
||||||
|
if (total > (uint64_t)INT64_MAX - bytes
|
||||||
|
|| EVP_DigestUpdate(context, buffer, bytes) != 1
|
||||||
|
|| (output >= 0 && !write_all(output, buffer, bytes))) {
|
||||||
|
success = false; break;
|
||||||
|
}
|
||||||
|
total += bytes;
|
||||||
|
}
|
||||||
|
unsigned int hash_size = 0;
|
||||||
|
if (!success || EVP_DigestFinal_ex(context, hash, &hash_size) != 1
|
||||||
|
|| hash_size != LARDON3D_PROJECT_DB_SHA256_SIZE) success = false;
|
||||||
|
EVP_MD_CTX_free(context);
|
||||||
|
if (success) *size = total;
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
hash_hex(const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE],
|
||||||
|
char text[65])
|
||||||
|
{
|
||||||
|
static const char digits[] = "0123456789abcdef";
|
||||||
|
for (size_t index = 0; index < LARDON3D_PROJECT_DB_SHA256_SIZE; ++index) {
|
||||||
|
text[index * 2] = digits[hash[index] >> 4];
|
||||||
|
text[index * 2 + 1] = digits[hash[index] & 15U];
|
||||||
|
}
|
||||||
|
text[64] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
same_published_asset(const char *path,
|
||||||
|
const unsigned char expected[LARDON3D_PROJECT_DB_SHA256_SIZE],
|
||||||
|
uint64_t expected_size)
|
||||||
|
{
|
||||||
|
int descriptor = open(path, O_RDONLY | O_NOFOLLOW);
|
||||||
|
if (descriptor < 0) return false;
|
||||||
|
struct stat information;
|
||||||
|
unsigned char actual[LARDON3D_PROJECT_DB_SHA256_SIZE];
|
||||||
|
uint64_t size = 0;
|
||||||
|
bool success = fstat(descriptor, &information) == 0
|
||||||
|
&& S_ISREG(information.st_mode)
|
||||||
|
&& hash_stream(descriptor, -1, actual, &size)
|
||||||
|
&& size == expected_size
|
||||||
|
&& memcmp(actual, expected, sizeof(actual)) == 0;
|
||||||
|
(void)close(descriptor);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sync_directory(const char *path)
|
||||||
|
{
|
||||||
|
int descriptor = open(path, O_RDONLY | O_DIRECTORY);
|
||||||
|
if (descriptor < 0) return false;
|
||||||
|
bool success = fsync(descriptor) == 0;
|
||||||
|
if (close(descriptor) != 0) success = false;
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_image_catalog_create_scanset(Lardon3DAppState *state,
|
||||||
|
const char *name, Lardon3DProjectDbScanSet *scanset)
|
||||||
|
{
|
||||||
|
return state && state->project_loaded && state->project_db
|
||||||
|
&& lardon3d_project_db_create_scanset(state->project_db, name, scanset)
|
||||||
|
== LARDON3D_PROJECT_DB_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DImageCatalogImportResult
|
||||||
|
lardon3d_image_catalog_import_file(Lardon3DAppState *state,
|
||||||
|
uint64_t scanset_id, const char *source_path, uint64_t producer_task_id,
|
||||||
|
Lardon3DProjectDbImage *image, Lardon3DProjectDbImageAsset *asset)
|
||||||
|
{
|
||||||
|
if (!state || !state->project_loaded || !state->project_db
|
||||||
|
|| scanset_id == 0 || !source_path || !source_path[0] || !image || !asset)
|
||||||
|
return LARDON3D_IMAGE_CATALOG_INVALID_ARGUMENT;
|
||||||
|
const char *basename = strrchr(source_path, '/');
|
||||||
|
basename = basename ? basename + 1 : source_path;
|
||||||
|
if (!basename[0] || strchr(basename, '\\') || strchr(basename, '\t')
|
||||||
|
|| strchr(basename, '\r') || strchr(basename, '\n')
|
||||||
|
|| strnlen(basename, LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY)
|
||||||
|
>= LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY)
|
||||||
|
return LARDON3D_IMAGE_CATALOG_INVALID_ARGUMENT;
|
||||||
|
Lardon3DProjectDbScanSet scanset;
|
||||||
|
if (lardon3d_project_db_load_scanset(state->project_db, scanset_id, &scanset)
|
||||||
|
!= LARDON3D_PROJECT_DB_OK) return LARDON3D_IMAGE_CATALOG_INVALID_ARGUMENT;
|
||||||
|
int input = open(source_path, O_RDONLY | O_NOFOLLOW);
|
||||||
|
if (input < 0) return LARDON3D_IMAGE_CATALOG_SOURCE_ERROR;
|
||||||
|
struct stat source_information;
|
||||||
|
if (fstat(input, &source_information) != 0
|
||||||
|
|| !S_ISREG(source_information.st_mode) || source_information.st_size < 0) {
|
||||||
|
(void)close(input); return LARDON3D_IMAGE_CATALOG_SOURCE_ERROR;
|
||||||
|
}
|
||||||
|
char assets[PATH_MAX], images[PATH_MAX], temporary[PATH_MAX];
|
||||||
|
if (!join_path(assets, state->project_path, "assets")
|
||||||
|
|| !ensure_directory(assets) || !sync_directory(state->project_path)
|
||||||
|
|| !join_path(images, assets, "images") || !ensure_directory(images)
|
||||||
|
|| !sync_directory(assets)
|
||||||
|
|| snprintf(temporary, sizeof(temporary), "%s/.asset.tmp.XXXXXX", images) <= 0) {
|
||||||
|
(void)close(input); return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR;
|
||||||
|
}
|
||||||
|
int output = mkstemp(temporary);
|
||||||
|
if (output < 0) { (void)close(input); return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR; }
|
||||||
|
unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE];
|
||||||
|
uint64_t size = 0;
|
||||||
|
bool copied = hash_stream(input, output, hash, &size);
|
||||||
|
if (close(input) != 0) copied = false;
|
||||||
|
if (copied && fsync(output) != 0) copied = false;
|
||||||
|
if (close(output) != 0) copied = false;
|
||||||
|
if (!copied) {
|
||||||
|
(void)unlink(temporary); return LARDON3D_IMAGE_CATALOG_SOURCE_ERROR;
|
||||||
|
}
|
||||||
|
char hex[65], prefix[3], prefix_path[PATH_MAX], final[PATH_MAX];
|
||||||
|
hash_hex(hash, hex); prefix[0] = hex[0]; prefix[1] = hex[1]; prefix[2] = '\0';
|
||||||
|
if (!join_path(prefix_path, images, prefix) || !ensure_directory(prefix_path)
|
||||||
|
|| !sync_directory(images)
|
||||||
|
|| !join_path(final, prefix_path, hex)) {
|
||||||
|
(void)unlink(temporary); return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR;
|
||||||
|
}
|
||||||
|
bool published = false;
|
||||||
|
if (link(temporary, final) == 0) {
|
||||||
|
bool temporary_removed = unlink(temporary) == 0;
|
||||||
|
bool directory_synced = sync_directory(prefix_path);
|
||||||
|
published = temporary_removed && directory_synced;
|
||||||
|
} else if (errno == EEXIST && same_published_asset(final, hash, size)) {
|
||||||
|
published = unlink(temporary) == 0;
|
||||||
|
}
|
||||||
|
if (!published) {
|
||||||
|
(void)unlink(temporary); return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR;
|
||||||
|
}
|
||||||
|
char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||||
|
int relative_size = snprintf(relative, sizeof(relative),
|
||||||
|
"assets/images/%s/%s", prefix, hex);
|
||||||
|
if (relative_size <= 0 || (size_t)relative_size >= sizeof(relative))
|
||||||
|
return LARDON3D_IMAGE_CATALOG_PUBLICATION_ERROR;
|
||||||
|
Lardon3DProjectDbImageRegisterStatus status;
|
||||||
|
int64_t imported_at = (int64_t)time(NULL);
|
||||||
|
if (imported_at < 0 || lardon3d_project_db_register_image(state->project_db,
|
||||||
|
scanset_id, hash, relative, size, basename, source_path,
|
||||||
|
producer_task_id, imported_at, &status, image)
|
||||||
|
!= LARDON3D_PROJECT_DB_OK) return LARDON3D_IMAGE_CATALOG_DB_ERROR;
|
||||||
|
if (lardon3d_project_db_load_image(state->project_db, image->image_id,
|
||||||
|
image, asset) != LARDON3D_PROJECT_DB_OK)
|
||||||
|
return LARDON3D_IMAGE_CATALOG_DB_ERROR;
|
||||||
|
return status == LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT
|
||||||
|
? LARDON3D_IMAGE_CATALOG_ALREADY_PRESENT
|
||||||
|
: LARDON3D_IMAGE_CATALOG_IMPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DProjectDbResult
|
||||||
|
lardon3d_image_catalog_list(Lardon3DAppState *state, uint64_t scanset_id,
|
||||||
|
uint64_t after_image_id, Lardon3DProjectDbImage *images,
|
||||||
|
Lardon3DProjectDbImageAsset *assets, size_t capacity, size_t *count)
|
||||||
|
{
|
||||||
|
if (!state || !state->project_loaded || !state->project_db)
|
||||||
|
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
return lardon3d_project_db_list_images(state->project_db, scanset_id,
|
||||||
|
after_image_id, images, assets, capacity, count);
|
||||||
|
}
|
||||||
104
src/import.c
104
src/import.c
|
|
@ -14,6 +14,7 @@
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <lardon3d/import.h>
|
#include <lardon3d/import.h>
|
||||||
|
#include <lardon3d/image_catalog.h>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
MANIFEST_LINE_CAPACITY = PATH_MAX + 512,
|
MANIFEST_LINE_CAPACITY = PATH_MAX + 512,
|
||||||
|
|
@ -1162,3 +1163,106 @@ lardon3d_import_directory(
|
||||||
NULL
|
NULL
|
||||||
) == LARDON3D_IMPORT_SUCCEEDED;
|
) == 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, "Erreur : dossier source absent ou invalide.");
|
||||||
|
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 annulé à une frontière sûre.");
|
||||||
|
return LARDON3D_IMPORT_CANCELLED;
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
set_status(state, "Erreur pendant un lot d'import.");
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
|
}
|
||||||
|
*complete = !remaining;
|
||||||
|
set_status(state, *complete ? "Import terminé." : "Lot d'import publié.");
|
||||||
|
publish_progress(control, result, result->processed, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_SUCCEEDED;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
#include <lardon3d/import.h>
|
#include <lardon3d/import.h>
|
||||||
#include <lardon3d/import_task.h>
|
#include <lardon3d/import_task.h>
|
||||||
|
#include <lardon3d/image_catalog.h>
|
||||||
#include <lardon3d/project.h>
|
#include <lardon3d/project.h>
|
||||||
#include <lardon3d/task_queue.h>
|
#include <lardon3d/task_queue.h>
|
||||||
|
|
||||||
|
|
@ -23,6 +24,7 @@ enum {
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char project_path[PATH_MAX];
|
char project_path[PATH_MAX];
|
||||||
char source_path[PATH_MAX];
|
char source_path[PATH_MAX];
|
||||||
|
uint64_t scanset_id;
|
||||||
Lardon3DProjectDb *project_db;
|
Lardon3DProjectDb *project_db;
|
||||||
Lardon3DResourceGovernor *governor;
|
Lardon3DResourceGovernor *governor;
|
||||||
} Lardon3DImageImportContext;
|
} Lardon3DImageImportContext;
|
||||||
|
|
@ -124,9 +126,10 @@ run_image_import(Lardon3DTask *task, void *userdata)
|
||||||
bool complete = false;
|
bool complete = false;
|
||||||
struct timespec begin, end;
|
struct timespec begin, end;
|
||||||
(void)clock_gettime(CLOCK_MONOTONIC, &begin);
|
(void)clock_gettime(CLOCK_MONOTONIC, &begin);
|
||||||
Lardon3DImportOutcome outcome = lardon3d_import_directory_batch(
|
Lardon3DImportOutcome outcome = lardon3d_import_directory_batch_to_scanset(
|
||||||
&state, context->source_path, contract.batch_size, &result,
|
&state, context->scanset_id, lardon3d_task_id(task),
|
||||||
&control, &complete);
|
context->source_path, contract.batch_size, &result, &control,
|
||||||
|
&complete);
|
||||||
(void)clock_gettime(CLOCK_MONOTONIC, &end);
|
(void)clock_gettime(CLOCK_MONOTONIC, &end);
|
||||||
if (outcome == LARDON3D_IMPORT_CANCELLED) return false;
|
if (outcome == LARDON3D_IMPORT_CANCELLED) return false;
|
||||||
if (outcome != LARDON3D_IMPORT_SUCCEEDED) {
|
if (outcome != LARDON3D_IMPORT_SUCCEEDED) {
|
||||||
|
|
@ -143,7 +146,7 @@ run_image_import(Lardon3DTask *task, void *userdata)
|
||||||
result.newly_manifested,
|
result.newly_manifested,
|
||||||
elapsed_ns(begin, end), 0);
|
elapsed_ns(begin, end), 0);
|
||||||
if (lardon3d_project_checkpoint_image_import_task(
|
if (lardon3d_project_checkpoint_image_import_task(
|
||||||
&state, task, context->source_path)
|
&state, task, context->source_path, context->scanset_id)
|
||||||
!= LARDON3D_PROJECT_TASK_CHECKPOINT_OK) {
|
!= LARDON3D_PROJECT_TASK_CHECKPOINT_OK) {
|
||||||
return lardon3d_task_fail(task, "Checkpoint import impossible.");
|
return lardon3d_task_fail(task, "Checkpoint import impossible.");
|
||||||
}
|
}
|
||||||
|
|
@ -162,9 +165,10 @@ run_image_import(Lardon3DTask *task, void *userdata)
|
||||||
|
|
||||||
static Lardon3DImageImportContext *
|
static Lardon3DImageImportContext *
|
||||||
create_context(const char *project_path, const char *source_path,
|
create_context(const char *project_path, const char *source_path,
|
||||||
Lardon3DProjectDb *database, Lardon3DResourceGovernor *governor)
|
uint64_t scanset_id, Lardon3DProjectDb *database,
|
||||||
|
Lardon3DResourceGovernor *governor)
|
||||||
{
|
{
|
||||||
if (!project_path || !source_path || !database || !governor) return NULL;
|
if (!project_path || !source_path || scanset_id == 0 || !database || !governor) return NULL;
|
||||||
Lardon3DImageImportContext *context = calloc(1, sizeof(*context));
|
Lardon3DImageImportContext *context = calloc(1, sizeof(*context));
|
||||||
if (!context) return NULL;
|
if (!context) return NULL;
|
||||||
int project_written = snprintf(context->project_path,
|
int project_written = snprintf(context->project_path,
|
||||||
|
|
@ -177,6 +181,7 @@ create_context(const char *project_path, const char *source_path,
|
||||||
}
|
}
|
||||||
context->project_db = database;
|
context->project_db = database;
|
||||||
context->governor = governor;
|
context->governor = governor;
|
||||||
|
context->scanset_id = scanset_id;
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,7 +201,8 @@ lardon3d_image_import_reconstruct(
|
||||||
char source[PATH_MAX];
|
char source[PATH_MAX];
|
||||||
if (!canonical_source(parameters.source_path, source)) return false;
|
if (!canonical_source(parameters.source_path, source)) return false;
|
||||||
Lardon3DImageImportContext *context = create_context(runtime->project_path,
|
Lardon3DImageImportContext *context = create_context(runtime->project_path,
|
||||||
source, runtime->project_db, runtime->resource_governor);
|
source, parameters.scanset_id, runtime->project_db,
|
||||||
|
runtime->resource_governor);
|
||||||
if (!context) return false;
|
if (!context) return false;
|
||||||
*binding = (Lardon3DTaskKindBinding) {
|
*binding = (Lardon3DTaskKindBinding) {
|
||||||
.callback = run_image_import,
|
.callback = run_image_import,
|
||||||
|
|
@ -211,20 +217,24 @@ lardon3d_image_import_reconstruct(
|
||||||
Lardon3DTask *
|
Lardon3DTask *
|
||||||
lardon3d_project_create_image_import_task(
|
lardon3d_project_create_image_import_task(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
|
uint64_t scanset_id,
|
||||||
const char *source_directory,
|
const char *source_directory,
|
||||||
uint64_t *task_id
|
uint64_t *task_id
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (task_id) *task_id = 0;
|
if (task_id) *task_id = 0;
|
||||||
if (!state || !state->project_loaded || !state->project_db
|
if (!state || !state->project_loaded || !state->project_db
|
||||||
|| !state->resource_governor || !task_id) return NULL;
|
|| !state->resource_governor || !task_id || scanset_id == 0) return NULL;
|
||||||
|
Lardon3DProjectDbScanSet scanset;
|
||||||
|
if (lardon3d_project_db_load_scanset(state->project_db, scanset_id, &scanset)
|
||||||
|
!= LARDON3D_PROJECT_DB_OK) return NULL;
|
||||||
char source[PATH_MAX];
|
char source[PATH_MAX];
|
||||||
if (!canonical_source(source_directory, source)) return NULL;
|
if (!canonical_source(source_directory, source)) return NULL;
|
||||||
uint64_t id = 0;
|
uint64_t id = 0;
|
||||||
if (lardon3d_project_db_allocate_task_id(state->project_db, &id)
|
if (lardon3d_project_db_allocate_task_id(state->project_db, &id)
|
||||||
!= LARDON3D_PROJECT_DB_OK) return NULL;
|
!= LARDON3D_PROJECT_DB_OK) return NULL;
|
||||||
Lardon3DImageImportContext *context = create_context(state->project_path,
|
Lardon3DImageImportContext *context = create_context(state->project_path,
|
||||||
source, state->project_db, state->resource_governor);
|
source, scanset_id, state->project_db, state->resource_governor);
|
||||||
if (!context) return NULL;
|
if (!context) return NULL;
|
||||||
const Lardon3DResourceEstimate estimate = {
|
const Lardon3DResourceEstimate estimate = {
|
||||||
.memory_fixed_bytes = IMAGE_IMPORT_FIXED_MEMORY,
|
.memory_fixed_bytes = IMAGE_IMPORT_FIXED_MEMORY,
|
||||||
|
|
@ -241,7 +251,8 @@ lardon3d_project_create_image_import_task(
|
||||||
destroy_context);
|
destroy_context);
|
||||||
if (!task || !lardon3d_task_assign_id(task, id)
|
if (!task || !lardon3d_task_assign_id(task, id)
|
||||||
|| !lardon3d_task_set_finished_callback(task, import_finished, context)
|
|| !lardon3d_task_set_finished_callback(task, import_finished, context)
|
||||||
|| lardon3d_project_checkpoint_image_import_task(state, task, source)
|
|| lardon3d_project_checkpoint_image_import_task(state, task, source,
|
||||||
|
scanset_id)
|
||||||
!= LARDON3D_PROJECT_TASK_CHECKPOINT_OK) {
|
!= LARDON3D_PROJECT_TASK_CHECKPOINT_OK) {
|
||||||
lardon3d_task_destroy(task);
|
lardon3d_task_destroy(task);
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
@ -252,11 +263,11 @@ lardon3d_project_create_image_import_task(
|
||||||
|
|
||||||
bool
|
bool
|
||||||
lardon3d_project_enqueue_image_import(Lardon3DAppState *state,
|
lardon3d_project_enqueue_image_import(Lardon3DAppState *state,
|
||||||
const char *source_directory, uint64_t *task_id)
|
uint64_t scanset_id, const char *source_directory, uint64_t *task_id)
|
||||||
{
|
{
|
||||||
if (!state || !state->task_queue) return false;
|
if (!state || !state->task_queue) return false;
|
||||||
Lardon3DTask *task = lardon3d_project_create_image_import_task(
|
Lardon3DTask *task = lardon3d_project_create_image_import_task(
|
||||||
state, source_directory, task_id);
|
state, scanset_id, source_directory, task_id);
|
||||||
if (!task) return false;
|
if (!task) return false;
|
||||||
if (!lardon3d_task_queue_add(state->task_queue, task, NULL)) {
|
if (!lardon3d_task_queue_add(state->task_queue, task, NULL)) {
|
||||||
lardon3d_task_destroy(task);
|
lardon3d_task_destroy(task);
|
||||||
|
|
@ -273,8 +284,11 @@ lardon3d_import_task_start(Lardon3DImportTask *task, Lardon3DAppState *state,
|
||||||
{
|
{
|
||||||
if (!task || task->task_id != 0 || !state) return false;
|
if (!task || task->task_id != 0 || !state) return false;
|
||||||
task->state = state;
|
task->state = state;
|
||||||
return lardon3d_project_enqueue_image_import(state, source_directory,
|
Lardon3DProjectDbScanSet scanset;
|
||||||
&task->task_id);
|
if (!lardon3d_image_catalog_create_scanset(state, "Import d'images",
|
||||||
|
&scanset)) return false;
|
||||||
|
return lardon3d_project_enqueue_image_import(state, scanset.scanset_id,
|
||||||
|
source_directory, &task->task_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void lardon3d_import_task_request_cancel(Lardon3DImportTask *task)
|
void lardon3d_import_task_request_cancel(Lardon3DImportTask *task)
|
||||||
|
|
|
||||||
|
|
@ -880,7 +880,8 @@ static Lardon3DProjectTaskCheckpointResult
|
||||||
checkpoint_task_internal(
|
checkpoint_task_internal(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
const Lardon3DTask *task,
|
const Lardon3DTask *task,
|
||||||
const char *image_import_source
|
const char *image_import_source,
|
||||||
|
uint64_t image_import_scanset_id
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (!state || !state->project_loaded || !state->project_db) {
|
if (!state || !state->project_loaded || !state->project_db) {
|
||||||
|
|
@ -926,7 +927,8 @@ checkpoint_task_internal(
|
||||||
Lardon3DProjectDbResult recorded = image_import_source
|
Lardon3DProjectDbResult recorded = image_import_source
|
||||||
? lardon3d_project_db_record_image_import_task(
|
? lardon3d_project_db_record_image_import_task(
|
||||||
state->project_db, &snapshot, task_kind, task_kind_version,
|
state->project_db, &snapshot, task_kind, task_kind_version,
|
||||||
&checkpoint, image_import_source, now.tv_sec)
|
&checkpoint, image_import_source, image_import_scanset_id,
|
||||||
|
now.tv_sec)
|
||||||
: lardon3d_project_db_record_task(
|
: lardon3d_project_db_record_task(
|
||||||
state->project_db, &snapshot, task_kind, task_kind_version,
|
state->project_db, &snapshot, task_kind, task_kind_version,
|
||||||
&checkpoint, now.tv_sec);
|
&checkpoint, now.tv_sec);
|
||||||
|
|
@ -947,20 +949,21 @@ lardon3d_project_checkpoint_task(
|
||||||
const Lardon3DTask *task
|
const Lardon3DTask *task
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return checkpoint_task_internal(state, task, NULL);
|
return checkpoint_task_internal(state, task, NULL, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Lardon3DProjectTaskCheckpointResult
|
Lardon3DProjectTaskCheckpointResult
|
||||||
lardon3d_project_checkpoint_image_import_task(
|
lardon3d_project_checkpoint_image_import_task(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
const Lardon3DTask *task,
|
const Lardon3DTask *task,
|
||||||
const char *source_path
|
const char *source_path,
|
||||||
|
uint64_t scanset_id
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (!source_path || !source_path[0]) {
|
if (!source_path || !source_path[0] || scanset_id == 0) {
|
||||||
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
||||||
}
|
}
|
||||||
return checkpoint_task_internal(state, task, source_path);
|
return checkpoint_task_internal(state, task, source_path, scanset_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
|
|
|
||||||
500
src/project_db.c
500
src/project_db.c
|
|
@ -17,10 +17,11 @@ struct Lardon3DProjectDb {
|
||||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char schema_v3[] =
|
static const char schema_v4[] =
|
||||||
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
||||||
"INSERT INTO metadata(key,value) VALUES('schema_version',3);"
|
"INSERT INTO metadata(key,value) VALUES('schema_version',4);"
|
||||||
"INSERT INTO metadata(key,value) VALUES('next_task_id',1);"
|
"INSERT INTO metadata(key,value) VALUES('next_task_id',1);"
|
||||||
|
"INSERT INTO metadata(key,value) VALUES('legacy_image_catalog_pending',0);"
|
||||||
"CREATE TABLE project(singleton INTEGER PRIMARY KEY CHECK(singleton=1),"
|
"CREATE TABLE project(singleton INTEGER PRIMARY KEY CHECK(singleton=1),"
|
||||||
"stable_id TEXT NOT NULL UNIQUE,name TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL);"
|
"stable_id TEXT NOT NULL UNIQUE,name TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL);"
|
||||||
"CREATE TABLE tasks(task_id INTEGER PRIMARY KEY CHECK(task_id>0),name TEXT NOT NULL,"
|
"CREATE TABLE tasks(task_id INTEGER PRIMARY KEY CHECK(task_id>0),name TEXT NOT NULL,"
|
||||||
|
|
@ -40,8 +41,21 @@ static const char schema_v3[] =
|
||||||
"producer_task_id INTEGER REFERENCES tasks(task_id),created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL);"
|
"producer_task_id INTEGER REFERENCES tasks(task_id),created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL);"
|
||||||
"CREATE INDEX artifacts_state_idx ON artifacts(state,artifact_id);"
|
"CREATE INDEX artifacts_state_idx ON artifacts(state,artifact_id);"
|
||||||
"CREATE INDEX artifacts_producer_idx ON artifacts(producer_task_id);"
|
"CREATE INDEX artifacts_producer_idx ON artifacts(producer_task_id);"
|
||||||
|
"CREATE TABLE scansets(scanset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(scanset_id>0),"
|
||||||
|
"name TEXT NOT NULL CHECK(length(name)>0 AND length(name)<256),created_at INTEGER NOT NULL CHECK(created_at>=0),"
|
||||||
|
"updated_at INTEGER NOT NULL CHECK(updated_at>=created_at));"
|
||||||
|
"CREATE TABLE image_assets(asset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(asset_id>0),sha256 BLOB NOT NULL UNIQUE CHECK(length(sha256)=32),"
|
||||||
|
"path TEXT NOT NULL UNIQUE CHECK(length(path)>0 AND length(path)<4096),size_bytes INTEGER NOT NULL CHECK(size_bytes>=0),"
|
||||||
|
"state INTEGER NOT NULL CHECK(state=1),created_at INTEGER NOT NULL CHECK(created_at>=0));"
|
||||||
|
"CREATE TABLE images(image_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(image_id>0),scanset_id INTEGER NOT NULL REFERENCES scansets(scanset_id),"
|
||||||
|
"asset_id INTEGER NOT NULL REFERENCES image_assets(asset_id),original_name TEXT NOT NULL CHECK(length(original_name)>0 AND length(original_name)<256),"
|
||||||
|
"source_path TEXT NOT NULL CHECK(length(source_path)>0 AND length(source_path)<4096),"
|
||||||
|
"producer_task_id INTEGER REFERENCES tasks(task_id),imported_at INTEGER NOT NULL CHECK(imported_at>=0),"
|
||||||
|
"UNIQUE(scanset_id,asset_id));"
|
||||||
|
"CREATE INDEX images_scanset_idx ON images(scanset_id,image_id);"
|
||||||
|
"CREATE INDEX images_producer_idx ON images(producer_task_id,image_id);"
|
||||||
"CREATE TABLE image_import_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
|
"CREATE TABLE image_import_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
|
||||||
"source_path TEXT NOT NULL);";
|
"source_path TEXT NOT NULL,scanset_id INTEGER NOT NULL REFERENCES scansets(scanset_id));";
|
||||||
|
|
||||||
static void
|
static void
|
||||||
copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text)
|
copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text)
|
||||||
|
|
@ -130,12 +144,13 @@ migrate(Lardon3DProjectDb *database, unsigned int from_version)
|
||||||
if (from_version == LARDON3D_PROJECT_DB_SCHEMA_VERSION) {
|
if (from_version == LARDON3D_PROJECT_DB_SCHEMA_VERSION) {
|
||||||
return LARDON3D_PROJECT_DB_OK;
|
return LARDON3D_PROJECT_DB_OK;
|
||||||
}
|
}
|
||||||
if (from_version != 0 && from_version != 1 && from_version != 2) {
|
if (from_version != 0 && from_version != 1 && from_version != 2
|
||||||
|
&& from_version != 3) {
|
||||||
return LARDON3D_PROJECT_DB_CORRUPT;
|
return LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
}
|
}
|
||||||
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
|
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
|
||||||
if (result == LARDON3D_PROJECT_DB_OK && from_version == 0) {
|
if (result == LARDON3D_PROJECT_DB_OK && from_version == 0) {
|
||||||
result = execute(database, schema_v3, "create schema v3");
|
result = execute(database, schema_v4, "create schema v4");
|
||||||
}
|
}
|
||||||
if (result == LARDON3D_PROJECT_DB_OK && from_version == 1) {
|
if (result == LARDON3D_PROJECT_DB_OK && from_version == 1) {
|
||||||
result = execute(database,
|
result = execute(database,
|
||||||
|
|
@ -156,7 +171,8 @@ migrate(Lardon3DProjectDb *database, unsigned int from_version)
|
||||||
"finish schema v2 migration");
|
"finish schema v2 migration");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result == LARDON3D_PROJECT_DB_OK && from_version != 0) {
|
if (result == LARDON3D_PROJECT_DB_OK && from_version != 0
|
||||||
|
&& from_version < 3) {
|
||||||
result = execute(database,
|
result = execute(database,
|
||||||
"CREATE TABLE image_import_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,source_path TEXT NOT NULL);"
|
"CREATE TABLE image_import_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,source_path TEXT NOT NULL);"
|
||||||
"INSERT INTO metadata(key,value) VALUES('next_task_id',(SELECT CASE WHEN COALESCE(MAX(task_id),0)>=9223372036854775807 THEN 0 ELSE COALESCE(MAX(task_id),0)+1 END FROM tasks))",
|
"INSERT INTO metadata(key,value) VALUES('next_task_id',(SELECT CASE WHEN COALESCE(MAX(task_id),0)>=9223372036854775807 THEN 0 ELSE COALESCE(MAX(task_id),0)+1 END FROM tasks))",
|
||||||
|
|
@ -175,6 +191,32 @@ migrate(Lardon3DProjectDb *database, unsigned int from_version)
|
||||||
"finish schema v3 migration");
|
"finish schema v3 migration");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK && from_version != 0) {
|
||||||
|
result = execute(database,
|
||||||
|
"CREATE TABLE scansets(scanset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(scanset_id>0),name TEXT NOT NULL CHECK(length(name)>0 AND length(name)<256),created_at INTEGER NOT NULL CHECK(created_at>=0),updated_at INTEGER NOT NULL CHECK(updated_at>=created_at));"
|
||||||
|
"CREATE TABLE image_assets(asset_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(asset_id>0),sha256 BLOB NOT NULL UNIQUE CHECK(length(sha256)=32),path TEXT NOT NULL UNIQUE CHECK(length(path)>0 AND length(path)<4096),size_bytes INTEGER NOT NULL CHECK(size_bytes>=0),state INTEGER NOT NULL CHECK(state=1),created_at INTEGER NOT NULL CHECK(created_at>=0));"
|
||||||
|
"CREATE TABLE images(image_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(image_id>0),scanset_id INTEGER NOT NULL REFERENCES scansets(scanset_id),asset_id INTEGER NOT NULL REFERENCES image_assets(asset_id),original_name TEXT NOT NULL CHECK(length(original_name)>0 AND length(original_name)<256),source_path TEXT NOT NULL CHECK(length(source_path)>0 AND length(source_path)<4096),producer_task_id INTEGER REFERENCES tasks(task_id),imported_at INTEGER NOT NULL CHECK(imported_at>=0),UNIQUE(scanset_id,asset_id));"
|
||||||
|
"CREATE INDEX images_scanset_idx ON images(scanset_id,image_id);"
|
||||||
|
"CREATE INDEX images_producer_idx ON images(producer_task_id,image_id);"
|
||||||
|
"ALTER TABLE image_import_tasks ADD COLUMN scanset_id INTEGER REFERENCES scansets(scanset_id);"
|
||||||
|
"INSERT INTO scansets(name,created_at,updated_at) SELECT 'Imports antérieurs à ScanSet v1',0,0 WHERE EXISTS(SELECT 1 FROM image_import_tasks);"
|
||||||
|
"UPDATE image_import_tasks SET scanset_id=(SELECT scanset_id FROM scansets WHERE name='Imports antérieurs à ScanSet v1' ORDER BY scanset_id LIMIT 1) WHERE scanset_id IS NULL;"
|
||||||
|
"INSERT INTO metadata(key,value) VALUES('legacy_image_catalog_pending',CASE WHEN EXISTS(SELECT 1 FROM image_import_tasks) THEN 1 ELSE 0 END)",
|
||||||
|
"migrate schema v3 to v4");
|
||||||
|
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||||
|
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4");
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK && forced_failure
|
||||||
|
&& strcmp(forced_failure, "1") == 0) {
|
||||||
|
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
|
||||||
|
"forced migration failure");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
result = execute(database,
|
||||||
|
"UPDATE metadata SET value=4 WHERE key='schema_version' AND value=3",
|
||||||
|
"finish schema v4 migration");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, "COMMIT", "commit migration");
|
if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, "COMMIT", "commit migration");
|
||||||
if (result != LARDON3D_PROJECT_DB_OK) (void)execute(database, "ROLLBACK", "rollback migration");
|
if (result != LARDON3D_PROJECT_DB_OK) (void)execute(database, "ROLLBACK", "rollback migration");
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -237,8 +279,8 @@ lardon3d_project_db_open(const char *path, Lardon3DProjectDb **output, char erro
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) result = read_schema_version(database, &version);
|
if (result == LARDON3D_PROJECT_DB_OK) result = read_schema_version(database, &version);
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) result = migrate(database, version);
|
if (result == LARDON3D_PROJECT_DB_OK) result = migrate(database, version);
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
const char *required[] = {"project", "tasks", "checkpoints", "artifacts", "image_import_tasks"};
|
const char *required[] = {"project", "tasks", "checkpoints", "artifacts", "image_import_tasks", "scansets", "image_assets", "images"};
|
||||||
for (size_t index = 0; index < 5 && result == LARDON3D_PROJECT_DB_OK; ++index) {
|
for (size_t index = 0; index < 8 && result == LARDON3D_PROJECT_DB_OK; ++index) {
|
||||||
if (!table_exists(database->connection, required[index])) {
|
if (!table_exists(database->connection, required[index])) {
|
||||||
copy_error(database->error, "Schéma v1 incomplet.");
|
copy_error(database->error, "Schéma v1 incomplet.");
|
||||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
|
|
@ -274,6 +316,32 @@ lardon3d_project_db_last_error(Lardon3DProjectDb *database, char error[LARDON3D_
|
||||||
}
|
}
|
||||||
unsigned int lardon3d_project_db_schema_version(Lardon3DProjectDb *database) { return database ? LARDON3D_PROJECT_DB_SCHEMA_VERSION : 0U; }
|
unsigned int lardon3d_project_db_schema_version(Lardon3DProjectDb *database) { return database ? LARDON3D_PROJECT_DB_SCHEMA_VERSION : 0U; }
|
||||||
|
|
||||||
|
Lardon3DProjectDbResult
|
||||||
|
lardon3d_project_db_legacy_catalog_pending(Lardon3DProjectDb *database,
|
||||||
|
bool *pending)
|
||||||
|
{
|
||||||
|
if (pending) *pending = false;
|
||||||
|
if (!database || !pending) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
(void)pthread_mutex_lock(&database->mutex);
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
Lardon3DProjectDbResult result = prepare(database,
|
||||||
|
"SELECT value FROM metadata WHERE key='legacy_image_catalog_pending'",
|
||||||
|
&statement);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
int code = sqlite3_step(statement);
|
||||||
|
sqlite3_int64 value = sqlite3_column_int64(statement, 0);
|
||||||
|
if (code != SQLITE_ROW || (value != 0 && value != 1)
|
||||||
|
|| sqlite3_step(statement) != SQLITE_DONE) {
|
||||||
|
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
|
} else {
|
||||||
|
*pending = value == 1;
|
||||||
|
}
|
||||||
|
(void)sqlite3_finalize(statement);
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_unlock(&database->mutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
static Lardon3DProjectDbResult
|
static Lardon3DProjectDbResult
|
||||||
step_done(Lardon3DProjectDb *database, sqlite3_stmt *statement, const char *context)
|
step_done(Lardon3DProjectDb *database, sqlite3_stmt *statement, const char *context)
|
||||||
{
|
{
|
||||||
|
|
@ -374,7 +442,7 @@ static Lardon3DProjectDbResult
|
||||||
record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
|
record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
|
||||||
const char *task_kind, uint32_t task_kind_version,
|
const char *task_kind, uint32_t task_kind_version,
|
||||||
const Lardon3DProjectDbCheckpoint *checkpoint, const char *source_path,
|
const Lardon3DProjectDbCheckpoint *checkpoint, const char *source_path,
|
||||||
int64_t updated_at)
|
uint64_t scanset_id, int64_t updated_at)
|
||||||
{
|
{
|
||||||
bool typed = task_kind != NULL;
|
bool typed = task_kind != NULL;
|
||||||
if (!database || !valid_durable_task(snapshot, updated_at)
|
if (!database || !valid_durable_task(snapshot, updated_at)
|
||||||
|
|
@ -383,6 +451,7 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
|
||||||
|| (!typed && task_kind_version != 0)
|
|| (!typed && task_kind_version != 0)
|
||||||
|| (source_path && !bounded_text(source_path,
|
|| (source_path && !bounded_text(source_path,
|
||||||
LARDON3D_PROJECT_DB_PATH_CAPACITY, false))
|
LARDON3D_PROJECT_DB_PATH_CAPACITY, false))
|
||||||
|
|| (source_path && !valid_task_id(scanset_id))
|
||||||
|| (checkpoint && !valid_checkpoint(checkpoint))) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
|| (checkpoint && !valid_checkpoint(checkpoint))) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
#ifdef LARDON3D_PROJECT_DB_TESTING
|
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||||
const char *forced_busy = getenv("LARDON3D_TEST_PROJECT_DB_BUSY_CHECKPOINT");
|
const char *forced_busy = getenv("LARDON3D_TEST_PROJECT_DB_BUSY_CHECKPOINT");
|
||||||
|
|
@ -440,14 +509,15 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
|
||||||
}
|
}
|
||||||
if (result == LARDON3D_PROJECT_DB_OK && source_path) {
|
if (result == LARDON3D_PROJECT_DB_OK && source_path) {
|
||||||
result = prepare(database,
|
result = prepare(database,
|
||||||
"INSERT INTO image_import_tasks(task_id,source_path) VALUES(?1,?2) "
|
"INSERT INTO image_import_tasks(task_id,source_path,scanset_id) VALUES(?1,?2,?3) "
|
||||||
"ON CONFLICT(task_id) DO UPDATE SET source_path=excluded.source_path "
|
"ON CONFLICT(task_id) DO UPDATE SET source_path=excluded.source_path "
|
||||||
"WHERE image_import_tasks.source_path=excluded.source_path",
|
"WHERE image_import_tasks.source_path=excluded.source_path AND image_import_tasks.scanset_id=excluded.scanset_id",
|
||||||
&statement);
|
&statement);
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)snapshot->id);
|
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)snapshot->id);
|
||||||
(void)sqlite3_bind_text(statement, 2, source_path, -1,
|
(void)sqlite3_bind_text(statement, 2, source_path, -1,
|
||||||
SQLITE_TRANSIENT);
|
SQLITE_TRANSIENT);
|
||||||
|
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)scanset_id);
|
||||||
result = step_done(database, statement, "upsert image import");
|
result = step_done(database, statement, "upsert image import");
|
||||||
if (result == LARDON3D_PROJECT_DB_OK
|
if (result == LARDON3D_PROJECT_DB_OK
|
||||||
&& sqlite3_changes(database->connection) != 1) {
|
&& sqlite3_changes(database->connection) != 1) {
|
||||||
|
|
@ -482,7 +552,7 @@ lardon3d_project_db_record_task(
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return record_task_internal(database, snapshot, task_kind,
|
return record_task_internal(database, snapshot, task_kind,
|
||||||
task_kind_version, checkpoint, NULL, updated_at);
|
task_kind_version, checkpoint, NULL, 0, updated_at);
|
||||||
}
|
}
|
||||||
|
|
||||||
Lardon3DProjectDbResult
|
Lardon3DProjectDbResult
|
||||||
|
|
@ -493,12 +563,13 @@ lardon3d_project_db_record_image_import_task(
|
||||||
uint32_t task_kind_version,
|
uint32_t task_kind_version,
|
||||||
const Lardon3DProjectDbCheckpoint *checkpoint,
|
const Lardon3DProjectDbCheckpoint *checkpoint,
|
||||||
const char *source_path,
|
const char *source_path,
|
||||||
|
uint64_t scanset_id,
|
||||||
int64_t updated_at
|
int64_t updated_at
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (!source_path) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
if (!source_path) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
return record_task_internal(database, snapshot, task_kind,
|
return record_task_internal(database, snapshot, task_kind,
|
||||||
task_kind_version, checkpoint, source_path, updated_at);
|
task_kind_version, checkpoint, source_path, scanset_id, updated_at);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
|
|
@ -650,7 +721,7 @@ lardon3d_project_db_load_image_import(
|
||||||
(void)pthread_mutex_lock(&database->mutex);
|
(void)pthread_mutex_lock(&database->mutex);
|
||||||
sqlite3_stmt *statement = NULL;
|
sqlite3_stmt *statement = NULL;
|
||||||
Lardon3DProjectDbResult result = prepare(database,
|
Lardon3DProjectDbResult result = prepare(database,
|
||||||
"SELECT source_path FROM image_import_tasks WHERE task_id=?1",
|
"SELECT source_path,scanset_id FROM image_import_tasks WHERE task_id=?1",
|
||||||
&statement);
|
&statement);
|
||||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
|
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
|
||||||
|
|
@ -660,7 +731,13 @@ lardon3d_project_db_load_image_import(
|
||||||
parameters->source_path, sizeof(parameters->source_path))) {
|
parameters->source_path, sizeof(parameters->source_path))) {
|
||||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
} else {
|
} else {
|
||||||
|
sqlite3_int64 scanset = sqlite3_column_int64(statement, 1);
|
||||||
|
if (scanset <= 0) {
|
||||||
|
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
|
} else {
|
||||||
parameters->task_id = task_id;
|
parameters->task_id = task_id;
|
||||||
|
parameters->scanset_id = (uint64_t)scanset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(void)sqlite3_finalize(statement);
|
(void)sqlite3_finalize(statement);
|
||||||
}
|
}
|
||||||
|
|
@ -668,6 +745,366 @@ lardon3d_project_db_load_image_import(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
valid_catalog_id(uint64_t value)
|
||||||
|
{
|
||||||
|
return value > 0 && value <= INT64_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
valid_original_name(const char *name)
|
||||||
|
{
|
||||||
|
return bounded_text(name, LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY, false)
|
||||||
|
&& !strchr(name, '/') && !strchr(name, '\\')
|
||||||
|
&& !strchr(name, '\t') && !strchr(name, '\r') && !strchr(name, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
valid_relative_asset_path(const char *path)
|
||||||
|
{
|
||||||
|
return bounded_text(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false)
|
||||||
|
&& path[0] != '/' && !strchr(path, '\\')
|
||||||
|
&& strncmp(path, "../", 3) != 0 && !strstr(path, "/../")
|
||||||
|
&& strcmp(path, "..") != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
canonical_asset_path(
|
||||||
|
const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE],
|
||||||
|
const char *path
|
||||||
|
)
|
||||||
|
{
|
||||||
|
static const char digits[] = "0123456789abcdef";
|
||||||
|
char hex[65];
|
||||||
|
for (size_t index = 0; index < LARDON3D_PROJECT_DB_SHA256_SIZE; ++index) {
|
||||||
|
hex[index * 2] = digits[hash[index] >> 4];
|
||||||
|
hex[index * 2 + 1] = digits[hash[index] & 15U];
|
||||||
|
}
|
||||||
|
hex[64] = '\0';
|
||||||
|
char expected[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||||
|
int written = snprintf(expected, sizeof(expected),
|
||||||
|
"assets/images/%c%c/%s", hex[0], hex[1], hex);
|
||||||
|
return written > 0 && (size_t)written < sizeof(expected)
|
||||||
|
&& strcmp(path, expected) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DProjectDbResult
|
||||||
|
lardon3d_project_db_create_scanset(Lardon3DProjectDb *database,
|
||||||
|
const char *name, Lardon3DProjectDbScanSet *scanset)
|
||||||
|
{
|
||||||
|
if (!database || !scanset
|
||||||
|
|| !bounded_text(name, LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY, false)) {
|
||||||
|
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
int64_t now = (int64_t)time(NULL);
|
||||||
|
if (now < 0) return LARDON3D_PROJECT_DB_IO_ERROR;
|
||||||
|
memset(scanset, 0, sizeof(*scanset));
|
||||||
|
(void)pthread_mutex_lock(&database->mutex);
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
Lardon3DProjectDbResult result = prepare(database,
|
||||||
|
"INSERT INTO scansets(name,created_at,updated_at) VALUES(?1,?2,?2)",
|
||||||
|
&statement);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
(void)sqlite3_bind_text(statement, 1, name, -1, SQLITE_TRANSIENT);
|
||||||
|
(void)sqlite3_bind_int64(statement, 2, now);
|
||||||
|
result = step_done(database, statement, "create scanset");
|
||||||
|
}
|
||||||
|
sqlite3_int64 id = sqlite3_last_insert_rowid(database->connection);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK && id <= 0) {
|
||||||
|
result = LARDON3D_PROJECT_DB_CONSTRAINT;
|
||||||
|
}
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
scanset->scanset_id = (uint64_t)id;
|
||||||
|
scanset->created_at = now;
|
||||||
|
scanset->updated_at = now;
|
||||||
|
(void)snprintf(scanset->name, sizeof(scanset->name), "%s", name);
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_unlock(&database->mutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
read_scanset(sqlite3_stmt *statement, Lardon3DProjectDbScanSet *scanset)
|
||||||
|
{
|
||||||
|
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
|
||||||
|
sqlite3_int64 created = sqlite3_column_int64(statement, 2);
|
||||||
|
sqlite3_int64 updated = sqlite3_column_int64(statement, 3);
|
||||||
|
if (id <= 0 || created < 0 || updated < created
|
||||||
|
|| !copy_column(statement, 1, scanset->name, sizeof(scanset->name))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
scanset->scanset_id = (uint64_t)id;
|
||||||
|
scanset->created_at = created;
|
||||||
|
scanset->updated_at = updated;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DProjectDbResult
|
||||||
|
lardon3d_project_db_load_scanset(Lardon3DProjectDb *database,
|
||||||
|
uint64_t scanset_id, Lardon3DProjectDbScanSet *scanset)
|
||||||
|
{
|
||||||
|
if (!database || !valid_catalog_id(scanset_id) || !scanset)
|
||||||
|
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
memset(scanset, 0, sizeof(*scanset));
|
||||||
|
(void)pthread_mutex_lock(&database->mutex);
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
Lardon3DProjectDbResult result = prepare(database,
|
||||||
|
"SELECT scanset_id,name,created_at,updated_at FROM scansets WHERE scanset_id=?1",
|
||||||
|
&statement);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
|
||||||
|
int code = sqlite3_step(statement);
|
||||||
|
if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND;
|
||||||
|
else if (code != SQLITE_ROW || !read_scanset(statement, scanset))
|
||||||
|
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
|
(void)sqlite3_finalize(statement);
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_unlock(&database->mutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DProjectDbResult
|
||||||
|
lardon3d_project_db_list_scansets(Lardon3DProjectDb *database,
|
||||||
|
uint64_t after_scanset_id, Lardon3DProjectDbScanSet *scansets,
|
||||||
|
size_t capacity, size_t *count)
|
||||||
|
{
|
||||||
|
if (count) *count = 0;
|
||||||
|
if (!database || !scansets || !count || after_scanset_id > INT64_MAX
|
||||||
|
|| capacity == 0 || capacity > LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX)
|
||||||
|
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
(void)pthread_mutex_lock(&database->mutex);
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
Lardon3DProjectDbResult result = prepare(database,
|
||||||
|
"SELECT scanset_id,name,created_at,updated_at FROM scansets WHERE scanset_id>?1 ORDER BY scanset_id LIMIT ?2",
|
||||||
|
&statement);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)after_scanset_id);
|
||||||
|
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)capacity);
|
||||||
|
int code = SQLITE_DONE;
|
||||||
|
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
|
||||||
|
if (!read_scanset(statement, &scansets[*count])) {
|
||||||
|
result = LARDON3D_PROJECT_DB_CORRUPT; break;
|
||||||
|
}
|
||||||
|
++*count;
|
||||||
|
}
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK && *count < capacity
|
||||||
|
&& code != SQLITE_DONE) result = sqlite_result(database, code, "list scansets");
|
||||||
|
(void)sqlite3_finalize(statement);
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_unlock(&database->mutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
read_image(sqlite3_stmt *statement, Lardon3DProjectDbImage *image,
|
||||||
|
Lardon3DProjectDbImageAsset *asset)
|
||||||
|
{
|
||||||
|
sqlite3_int64 image_id = sqlite3_column_int64(statement, 0);
|
||||||
|
sqlite3_int64 scanset_id = sqlite3_column_int64(statement, 1);
|
||||||
|
sqlite3_int64 asset_id = sqlite3_column_int64(statement, 2);
|
||||||
|
sqlite3_int64 size = sqlite3_column_int64(statement, 7);
|
||||||
|
int hash_size = sqlite3_column_bytes(statement, 5);
|
||||||
|
const void *hash = sqlite3_column_blob(statement, 5);
|
||||||
|
if (image_id <= 0 || scanset_id <= 0 || asset_id <= 0 || size < 0
|
||||||
|
|| hash_size != LARDON3D_PROJECT_DB_SHA256_SIZE || !hash
|
||||||
|
|| !copy_column(statement, 3, image->original_name, sizeof(image->original_name))
|
||||||
|
|| !copy_column(statement, 4, image->source_path, sizeof(image->source_path))
|
||||||
|
|| !copy_column(statement, 6, asset->path, sizeof(asset->path))
|
||||||
|
|| !valid_original_name(image->original_name)
|
||||||
|
|| !valid_relative_asset_path(asset->path)) return false;
|
||||||
|
image->image_id = (uint64_t)image_id;
|
||||||
|
image->scanset_id = (uint64_t)scanset_id;
|
||||||
|
image->asset_id = (uint64_t)asset_id;
|
||||||
|
asset->asset_id = (uint64_t)asset_id;
|
||||||
|
memcpy(asset->sha256, hash, LARDON3D_PROJECT_DB_SHA256_SIZE);
|
||||||
|
if (!canonical_asset_path(asset->sha256, asset->path)) return false;
|
||||||
|
asset->size_bytes = (uint64_t)size;
|
||||||
|
asset->state = (Lardon3DProjectDbImageAssetState)sqlite3_column_int(statement, 8);
|
||||||
|
asset->created_at = sqlite3_column_int64(statement, 9);
|
||||||
|
image->has_producer_task = sqlite3_column_type(statement, 10) != SQLITE_NULL;
|
||||||
|
sqlite3_int64 producer = sqlite3_column_int64(statement, 10);
|
||||||
|
if (image->has_producer_task && producer <= 0) return false;
|
||||||
|
image->producer_task_id = image->has_producer_task ? (uint64_t)producer : 0;
|
||||||
|
image->imported_at = sqlite3_column_int64(statement, 11);
|
||||||
|
return asset->state == LARDON3D_DB_IMAGE_ASSET_READY
|
||||||
|
&& asset->created_at >= 0 && image->imported_at >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char image_select[] =
|
||||||
|
"SELECT i.image_id,i.scanset_id,i.asset_id,i.original_name,i.source_path,a.sha256,a.path,a.size_bytes,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 ";
|
||||||
|
|
||||||
|
Lardon3DProjectDbResult
|
||||||
|
lardon3d_project_db_register_image(Lardon3DProjectDb *database,
|
||||||
|
uint64_t scanset_id, const unsigned char sha256[LARDON3D_PROJECT_DB_SHA256_SIZE],
|
||||||
|
const char *asset_path, uint64_t size_bytes, const char *original_name,
|
||||||
|
const char *source_path, uint64_t producer_task_id, int64_t imported_at,
|
||||||
|
Lardon3DProjectDbImageRegisterStatus *status, Lardon3DProjectDbImage *image)
|
||||||
|
{
|
||||||
|
if (!database || !valid_catalog_id(scanset_id) || !sha256
|
||||||
|
|| !valid_relative_asset_path(asset_path)
|
||||||
|
|| !canonical_asset_path(sha256, asset_path) || size_bytes > INT64_MAX
|
||||||
|
|| !valid_original_name(original_name)
|
||||||
|
|| !bounded_text(source_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false)
|
||||||
|
|| (producer_task_id != 0 && !valid_task_id(producer_task_id))
|
||||||
|
|| imported_at < 0 || !status || !image)
|
||||||
|
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||||
|
const char *failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_IMAGE_REGISTER");
|
||||||
|
if (failure && strcmp(failure, "1") == 0) return LARDON3D_PROJECT_DB_BUSY;
|
||||||
|
#endif
|
||||||
|
memset(image, 0, sizeof(*image));
|
||||||
|
*status = LARDON3D_PROJECT_DB_IMAGE_REGISTERED;
|
||||||
|
(void)pthread_mutex_lock(&database->mutex);
|
||||||
|
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin image register");
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) result = prepare(database,
|
||||||
|
"INSERT INTO image_assets(sha256,path,size_bytes,state,created_at) VALUES(?1,?2,?3,1,?4) ON CONFLICT(sha256) DO NOTHING", &statement);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
(void)sqlite3_bind_blob(statement, 1, sha256, LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT);
|
||||||
|
(void)sqlite3_bind_text(statement, 2, asset_path, -1, SQLITE_TRANSIENT);
|
||||||
|
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)size_bytes);
|
||||||
|
(void)sqlite3_bind_int64(statement, 4, imported_at);
|
||||||
|
result = step_done(database, statement, "insert image asset"); statement = NULL;
|
||||||
|
}
|
||||||
|
sqlite3_int64 asset_id = 0;
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) result = prepare(database,
|
||||||
|
"SELECT asset_id,path,size_bytes,state FROM image_assets WHERE sha256=?1", &statement);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
(void)sqlite3_bind_blob(statement, 1, sha256, LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT);
|
||||||
|
int code = sqlite3_step(statement);
|
||||||
|
char stored_path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||||
|
if (code != SQLITE_ROW || !copy_column(statement, 1, stored_path, sizeof(stored_path))
|
||||||
|
|| strcmp(stored_path, asset_path) != 0
|
||||||
|
|| sqlite3_column_int64(statement, 2) != (sqlite3_int64)size_bytes
|
||||||
|
|| sqlite3_column_int(statement, 3) != LARDON3D_DB_IMAGE_ASSET_READY) {
|
||||||
|
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
|
} else asset_id = sqlite3_column_int64(statement, 0);
|
||||||
|
(void)sqlite3_finalize(statement); statement = NULL;
|
||||||
|
}
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) result = prepare(database,
|
||||||
|
"INSERT INTO images(scanset_id,asset_id,original_name,source_path,producer_task_id,imported_at) VALUES(?1,?2,?3,?4,?5,?6) ON CONFLICT(scanset_id,asset_id) DO NOTHING", &statement);
|
||||||
|
int inserted = 0;
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
|
||||||
|
(void)sqlite3_bind_int64(statement, 2, asset_id);
|
||||||
|
(void)sqlite3_bind_text(statement, 3, original_name, -1, SQLITE_TRANSIENT);
|
||||||
|
(void)sqlite3_bind_text(statement, 4, source_path, -1, SQLITE_TRANSIENT);
|
||||||
|
if (producer_task_id) (void)sqlite3_bind_int64(statement, 5, (sqlite3_int64)producer_task_id);
|
||||||
|
else (void)sqlite3_bind_null(statement, 5);
|
||||||
|
(void)sqlite3_bind_int64(statement, 6, imported_at);
|
||||||
|
result = step_done(database, statement, "insert logical image"); statement = NULL;
|
||||||
|
inserted = sqlite3_changes(database->connection);
|
||||||
|
}
|
||||||
|
sqlite3_int64 image_id = 0;
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) result = prepare(database,
|
||||||
|
"SELECT image_id FROM images WHERE scanset_id=?1 AND asset_id=?2", &statement);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
|
||||||
|
(void)sqlite3_bind_int64(statement, 2, asset_id);
|
||||||
|
int code = sqlite3_step(statement);
|
||||||
|
if (code != SQLITE_ROW || (image_id = sqlite3_column_int64(statement, 0)) <= 0)
|
||||||
|
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
|
(void)sqlite3_finalize(statement); statement = NULL;
|
||||||
|
}
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, "COMMIT", "commit image register");
|
||||||
|
if (result != LARDON3D_PROJECT_DB_OK) (void)execute(database, "ROLLBACK", "rollback image register");
|
||||||
|
(void)pthread_mutex_unlock(&database->mutex);
|
||||||
|
if (result != LARDON3D_PROJECT_DB_OK) return result;
|
||||||
|
*status = inserted == 1 ? LARDON3D_PROJECT_DB_IMAGE_REGISTERED
|
||||||
|
: LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT;
|
||||||
|
return lardon3d_project_db_load_image(database, (uint64_t)image_id, image,
|
||||||
|
&(Lardon3DProjectDbImageAsset){0});
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DProjectDbResult
|
||||||
|
lardon3d_project_db_load_image(Lardon3DProjectDb *database, uint64_t image_id,
|
||||||
|
Lardon3DProjectDbImage *image, Lardon3DProjectDbImageAsset *asset)
|
||||||
|
{
|
||||||
|
if (!database || !valid_catalog_id(image_id) || !image || !asset)
|
||||||
|
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
memset(image, 0, sizeof(*image)); memset(asset, 0, sizeof(*asset));
|
||||||
|
(void)pthread_mutex_lock(&database->mutex);
|
||||||
|
char sql[768]; (void)snprintf(sql, sizeof(sql), "%s WHERE i.image_id=?1", image_select);
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
Lardon3DProjectDbResult result = prepare(database, sql, &statement);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)image_id);
|
||||||
|
int code = sqlite3_step(statement);
|
||||||
|
if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND;
|
||||||
|
else if (code != SQLITE_ROW || !read_image(statement, image, asset))
|
||||||
|
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
|
(void)sqlite3_finalize(statement);
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_unlock(&database->mutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DProjectDbResult
|
||||||
|
lardon3d_project_db_list_images(Lardon3DProjectDb *database,
|
||||||
|
uint64_t scanset_id, uint64_t after_image_id, Lardon3DProjectDbImage *images,
|
||||||
|
Lardon3DProjectDbImageAsset *assets, size_t capacity, size_t *count)
|
||||||
|
{
|
||||||
|
if (count) *count = 0;
|
||||||
|
if (!database || !valid_catalog_id(scanset_id) || after_image_id > INT64_MAX
|
||||||
|
|| !images || !assets || !count || capacity == 0
|
||||||
|
|| capacity > LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX)
|
||||||
|
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
Lardon3DProjectDbScanSet scanset;
|
||||||
|
Lardon3DProjectDbResult exists = lardon3d_project_db_load_scanset(
|
||||||
|
database, scanset_id, &scanset);
|
||||||
|
if (exists != LARDON3D_PROJECT_DB_OK) return exists;
|
||||||
|
(void)pthread_mutex_lock(&database->mutex);
|
||||||
|
char sql[896]; (void)snprintf(sql, sizeof(sql),
|
||||||
|
"%s WHERE i.scanset_id=?1 AND i.image_id>?2 ORDER BY i.image_id LIMIT ?3", image_select);
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
Lardon3DProjectDbResult result = prepare(database, sql, &statement);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
|
||||||
|
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_image_id);
|
||||||
|
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
|
||||||
|
int code = SQLITE_DONE;
|
||||||
|
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
|
||||||
|
memset(&images[*count], 0, sizeof(images[*count]));
|
||||||
|
memset(&assets[*count], 0, sizeof(assets[*count]));
|
||||||
|
if (!read_image(statement, &images[*count], &assets[*count])) {
|
||||||
|
result = LARDON3D_PROJECT_DB_CORRUPT; break;
|
||||||
|
}
|
||||||
|
++*count;
|
||||||
|
}
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE)
|
||||||
|
result = sqlite_result(database, code, "list images");
|
||||||
|
(void)sqlite3_finalize(statement);
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_unlock(&database->mutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DProjectDbResult
|
||||||
|
lardon3d_project_db_count_images(Lardon3DProjectDb *database,
|
||||||
|
uint64_t scanset_id, uint64_t *count)
|
||||||
|
{
|
||||||
|
if (count) *count = 0;
|
||||||
|
if (!database || !valid_catalog_id(scanset_id) || !count)
|
||||||
|
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
Lardon3DProjectDbScanSet scanset;
|
||||||
|
Lardon3DProjectDbResult exists = lardon3d_project_db_load_scanset(
|
||||||
|
database, scanset_id, &scanset);
|
||||||
|
if (exists != LARDON3D_PROJECT_DB_OK) return exists;
|
||||||
|
(void)pthread_mutex_lock(&database->mutex);
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
Lardon3DProjectDbResult result = prepare(database,
|
||||||
|
"SELECT count(*) FROM images WHERE scanset_id=?1", &statement);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)scanset_id);
|
||||||
|
int code = sqlite3_step(statement); sqlite3_int64 value = sqlite3_column_int64(statement, 0);
|
||||||
|
if (code != SQLITE_ROW || value < 0) result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
|
else *count = (uint64_t)value;
|
||||||
|
(void)sqlite3_finalize(statement);
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_unlock(&database->mutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
Lardon3DProjectDbResult
|
Lardon3DProjectDbResult
|
||||||
lardon3d_project_db_create_artifact(Lardon3DProjectDb *database, const Lardon3DProjectDbArtifact *artifact)
|
lardon3d_project_db_create_artifact(Lardon3DProjectDb *database, const Lardon3DProjectDbArtifact *artifact)
|
||||||
{
|
{
|
||||||
|
|
@ -732,4 +1169,39 @@ lardon3d_project_db_test_orphan_checkpoint(Lardon3DProjectDb *database)
|
||||||
(void)pthread_mutex_unlock(&database->mutex);
|
(void)pthread_mutex_unlock(&database->mutex);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Lardon3DProjectDbResult
|
||||||
|
lardon3d_project_db_test_delete_catalog_identity(Lardon3DProjectDb *database,
|
||||||
|
uint64_t scanset_id, uint64_t image_id, uint64_t asset_id)
|
||||||
|
{
|
||||||
|
if (!database || !valid_catalog_id(scanset_id)
|
||||||
|
|| !valid_catalog_id(image_id) || !valid_catalog_id(asset_id))
|
||||||
|
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||||
|
(void)pthread_mutex_lock(&database->mutex);
|
||||||
|
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE",
|
||||||
|
"begin catalog identity deletion test");
|
||||||
|
const char *sql[] = {
|
||||||
|
"DELETE FROM images WHERE image_id=?1",
|
||||||
|
"DELETE FROM image_assets WHERE asset_id=?1",
|
||||||
|
"DELETE FROM scansets WHERE scanset_id=?1",
|
||||||
|
};
|
||||||
|
const uint64_t ids[] = {image_id, asset_id, scanset_id};
|
||||||
|
for (size_t index = 0; index < 3 && result == LARDON3D_PROJECT_DB_OK; ++index) {
|
||||||
|
sqlite3_stmt *statement = NULL;
|
||||||
|
result = prepare(database, sql[index], &statement);
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
|
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)ids[index]);
|
||||||
|
result = step_done(database, statement, "delete catalog identity test");
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK
|
||||||
|
&& sqlite3_changes(database->connection) != 1)
|
||||||
|
result = LARDON3D_PROJECT_DB_NOT_FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result == LARDON3D_PROJECT_DB_OK)
|
||||||
|
result = execute(database, "COMMIT", "commit catalog identity deletion test");
|
||||||
|
if (result != LARDON3D_PROJECT_DB_OK)
|
||||||
|
(void)execute(database, "ROLLBACK", "rollback catalog identity deletion test");
|
||||||
|
(void)pthread_mutex_unlock(&database->mutex);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -105,11 +105,14 @@ run_test(void)
|
||||||
Lardon3DAppState state; lardon3d_app_state_init(&state);
|
Lardon3DAppState state; lardon3d_app_state_init(&state);
|
||||||
CHECK(setup_runtime(&state, 4));
|
CHECK(setup_runtime(&state, 4));
|
||||||
CHECK(lardon3d_project_create(&state, "Persistent Import"));
|
CHECK(lardon3d_project_create(&state, "Persistent Import"));
|
||||||
|
Lardon3DProjectDbScanSet scanset;
|
||||||
|
CHECK(lardon3d_image_catalog_create_scanset(&state, "Campagne A", &scanset));
|
||||||
|
|
||||||
CHECK(setenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH", "1", 1) == 0);
|
||||||
CHECK(setenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT", "1", 1) == 0);
|
||||||
uint64_t task_id = 0;
|
uint64_t task_id = 0;
|
||||||
CHECK(lardon3d_project_enqueue_image_import(&state, source, &task_id));
|
CHECK(lardon3d_project_enqueue_image_import(&state, scanset.scanset_id,
|
||||||
|
source, &task_id));
|
||||||
CHECK(task_id > 0 && task_id <= INT64_MAX);
|
CHECK(task_id > 0 && task_id <= INT64_MAX);
|
||||||
Lardon3DTaskSnapshot runtime;
|
Lardon3DTaskSnapshot runtime;
|
||||||
CHECK(wait_for_state(state.task_queue, task_id, TASK_PAUSED, &runtime));
|
CHECK(wait_for_state(state.task_queue, task_id, TASK_PAUSED, &runtime));
|
||||||
|
|
@ -134,7 +137,7 @@ run_test(void)
|
||||||
CHECK(lardon3d_project_db_record_image_import_task(state.project_db,
|
CHECK(lardon3d_project_db_record_image_import_task(state.project_db,
|
||||||
&persisted_snapshot, LARDON3D_IMAGE_IMPORT_TASK_KIND,
|
&persisted_snapshot, LARDON3D_IMAGE_IMPORT_TASK_KIND,
|
||||||
LARDON3D_IMAGE_IMPORT_TASK_KIND_VERSION, &published_not_durable,
|
LARDON3D_IMAGE_IMPORT_TASK_KIND_VERSION, &published_not_durable,
|
||||||
source, 1) == LARDON3D_PROJECT_DB_OK);
|
source, scanset.scanset_id, 1) == LARDON3D_PROJECT_DB_OK);
|
||||||
lardon3d_project_close(&state);
|
lardon3d_project_close(&state);
|
||||||
CHECK(unsetenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH") == 0);
|
CHECK(unsetenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH") == 0);
|
||||||
CHECK(unsetenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT") == 0);
|
CHECK(unsetenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT") == 0);
|
||||||
|
|
@ -154,14 +157,19 @@ run_test(void)
|
||||||
CHECK(lardon3d_project_db_load_image_import(state.project_db, task_id,
|
CHECK(lardon3d_project_db_load_image_import(state.project_db, task_id,
|
||||||
&persisted_parameters) == LARDON3D_PROJECT_DB_OK);
|
&persisted_parameters) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(strcmp(persisted_parameters.source_path, source) == 0);
|
CHECK(strcmp(persisted_parameters.source_path, source) == 0);
|
||||||
|
CHECK(persisted_parameters.scanset_id == scanset.scanset_id);
|
||||||
CHECK(wait_for_state(state.task_queue, task_id, TASK_COMPLETED, &runtime));
|
CHECK(wait_for_state(state.task_queue, task_id, TASK_COMPLETED, &runtime));
|
||||||
CHECK(runtime.progress == 100);
|
CHECK(runtime.progress == 100);
|
||||||
|
|
||||||
char error[256];
|
uint64_t image_count = 0;
|
||||||
Lardon3DImageCatalog *catalog = lardon3d_image_catalog_load(
|
CHECK(lardon3d_project_db_count_images(state.project_db,
|
||||||
&state, error, sizeof(error));
|
scanset.scanset_id, &image_count) == LARDON3D_PROJECT_DB_OK
|
||||||
CHECK(catalog && lardon3d_image_catalog_count(catalog) == 80);
|
&& image_count == 80);
|
||||||
lardon3d_image_catalog_destroy(catalog);
|
char catalog_error[256];
|
||||||
|
Lardon3DImageCatalog *legacy_catalog = lardon3d_image_catalog_load(&state,
|
||||||
|
catalog_error, sizeof(catalog_error));
|
||||||
|
CHECK(legacy_catalog && lardon3d_image_catalog_count(legacy_catalog) == 80);
|
||||||
|
lardon3d_image_catalog_destroy(legacy_catalog);
|
||||||
lardon3d_task_queue_destroy(state.task_queue); state.task_queue = NULL;
|
lardon3d_task_queue_destroy(state.task_queue); state.task_queue = NULL;
|
||||||
lardon3d_project_close(&state);
|
lardon3d_project_close(&state);
|
||||||
|
|
||||||
|
|
@ -208,15 +216,20 @@ test_selective_capacity_one(void)
|
||||||
lardon3d_app_state_init(&state);
|
lardon3d_app_state_init(&state);
|
||||||
CHECK(setup_runtime(&state, 4));
|
CHECK(setup_runtime(&state, 4));
|
||||||
CHECK(lardon3d_project_create(&state, "Selective Recovery"));
|
CHECK(lardon3d_project_create(&state, "Selective Recovery"));
|
||||||
|
Lardon3DProjectDbScanSet scanset;
|
||||||
|
CHECK(lardon3d_image_catalog_create_scanset(&state, "Sélectif", &scanset));
|
||||||
CHECK(setenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_IMPORT_PAUSE_AFTER_BATCH", "1", 1) == 0);
|
||||||
CHECK(setenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT", "1", 1)
|
CHECK(setenv("LARDON3D_TEST_IMPORT_SKIP_FINISHED_CHECKPOINT", "1", 1)
|
||||||
== 0);
|
== 0);
|
||||||
uint64_t missing_id = 0, valid_id = 0, second_valid_id = 0;
|
uint64_t missing_id = 0, valid_id = 0, second_valid_id = 0;
|
||||||
CHECK(lardon3d_project_enqueue_image_import(&state, missing_source,
|
CHECK(lardon3d_project_enqueue_image_import(&state, scanset.scanset_id,
|
||||||
|
missing_source,
|
||||||
&missing_id));
|
&missing_id));
|
||||||
CHECK(lardon3d_project_enqueue_image_import(&state, valid_source,
|
CHECK(lardon3d_project_enqueue_image_import(&state, scanset.scanset_id,
|
||||||
|
valid_source,
|
||||||
&valid_id));
|
&valid_id));
|
||||||
CHECK(lardon3d_project_enqueue_image_import(&state, second_valid_source,
|
CHECK(lardon3d_project_enqueue_image_import(&state, scanset.scanset_id,
|
||||||
|
second_valid_source,
|
||||||
&second_valid_id));
|
&second_valid_id));
|
||||||
Lardon3DTaskSnapshot snapshot;
|
Lardon3DTaskSnapshot snapshot;
|
||||||
CHECK(wait_for_state(state.task_queue, missing_id, TASK_PAUSED, &snapshot));
|
CHECK(wait_for_state(state.task_queue, missing_id, TASK_PAUSED, &snapshot));
|
||||||
|
|
|
||||||
278
tests/test_persistent_image_catalog.c
Normal file
278
tests/test_persistent_image_catalog.c
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <lardon3d/image_catalog.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 output[PATH_MAX], const char *parent, const char *child)
|
||||||
|
{
|
||||||
|
int written = snprintf(output, PATH_MAX, "%s/%s", parent, child);
|
||||||
|
return written > 0 && (size_t)written < PATH_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
write_file(const char *path, const char *content)
|
||||||
|
{
|
||||||
|
int descriptor = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600);
|
||||||
|
if (descriptor < 0) return false;
|
||||||
|
size_t size = strlen(content), offset = 0;
|
||||||
|
while (offset < size) {
|
||||||
|
ssize_t written = write(descriptor, content + offset, size - offset);
|
||||||
|
if (written < 0 && errno == EINTR) continue;
|
||||||
|
if (written <= 0) { (void)close(descriptor); return false; }
|
||||||
|
offset += (size_t)written;
|
||||||
|
}
|
||||||
|
return close(descriptor) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
canonical_asset_path(const char *path)
|
||||||
|
{
|
||||||
|
static const char prefix[] = "assets/images/";
|
||||||
|
if (strncmp(path, prefix, sizeof(prefix) - 1) != 0) return false;
|
||||||
|
const char *short_hash = path + sizeof(prefix) - 1;
|
||||||
|
if (strlen(short_hash) != 2 + 1 + 64 || short_hash[2] != '/') return false;
|
||||||
|
for (size_t index = 0; index < 2; ++index) {
|
||||||
|
if (short_hash[index] != short_hash[index + 3]) return false;
|
||||||
|
}
|
||||||
|
for (size_t index = 0; index < 64; ++index) {
|
||||||
|
char character = short_hash[index + 3];
|
||||||
|
if (!((character >= '0' && character <= '9')
|
||||||
|
|| (character >= 'a' && character <= 'f'))) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
remove_tree(const char *path)
|
||||||
|
{
|
||||||
|
struct stat information;
|
||||||
|
if (lstat(path, &information) != 0) return errno == ENOENT;
|
||||||
|
if (!S_ISDIR(information.st_mode)) return unlink(path) == 0;
|
||||||
|
DIR *directory = opendir(path);
|
||||||
|
if (!directory) return false;
|
||||||
|
bool success = true;
|
||||||
|
for (struct dirent *entry = readdir(directory); entry; entry = readdir(directory)) {
|
||||||
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||||
|
continue;
|
||||||
|
char child[PATH_MAX];
|
||||||
|
if (!join_path(child, path, entry->d_name) || !remove_tree(child)) success = false;
|
||||||
|
}
|
||||||
|
if (closedir(directory) != 0 || rmdir(path) != 0) success = false;
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t
|
||||||
|
count_regular_files(const char *path)
|
||||||
|
{
|
||||||
|
DIR *directory = opendir(path);
|
||||||
|
if (!directory) return 0;
|
||||||
|
size_t count = 0;
|
||||||
|
for (struct dirent *entry = readdir(directory); entry; entry = readdir(directory)) {
|
||||||
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||||
|
continue;
|
||||||
|
char child[PATH_MAX]; struct stat information;
|
||||||
|
if (!join_path(child, path, entry->d_name) || lstat(child, &information) != 0)
|
||||||
|
continue;
|
||||||
|
if (S_ISDIR(information.st_mode)) count += count_regular_files(child);
|
||||||
|
else if (S_ISREG(information.st_mode)) ++count;
|
||||||
|
}
|
||||||
|
(void)closedir(directory);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Lardon3DAppState *state;
|
||||||
|
uint64_t scanset_id;
|
||||||
|
const char *path;
|
||||||
|
Lardon3DImageCatalogImportResult result;
|
||||||
|
uint64_t image_id;
|
||||||
|
} ImportThread;
|
||||||
|
|
||||||
|
static void *
|
||||||
|
import_thread(void *userdata)
|
||||||
|
{
|
||||||
|
ImportThread *thread = userdata;
|
||||||
|
Lardon3DProjectDbImage image;
|
||||||
|
Lardon3DProjectDbImageAsset asset;
|
||||||
|
thread->result = lardon3d_image_catalog_import_file(thread->state,
|
||||||
|
thread->scanset_id, thread->path, 0, &image, &asset);
|
||||||
|
thread->image_id = image.image_id;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
run_test(void)
|
||||||
|
{
|
||||||
|
char root[] = "/tmp/lardon3d-catalog-v1-XXXXXX";
|
||||||
|
CHECK(mkdtemp(root));
|
||||||
|
char database_path[PATH_MAX], source_a[PATH_MAX], source_b[PATH_MAX];
|
||||||
|
CHECK(join_path(database_path, root, "project.db"));
|
||||||
|
CHECK(join_path(source_a, root, "photo001.jpg"));
|
||||||
|
CHECK(join_path(source_b, root, "other-name.jpg"));
|
||||||
|
CHECK(write_file(source_a, "same-image-content"));
|
||||||
|
CHECK(write_file(source_b, "same-image-content"));
|
||||||
|
Lardon3DProjectDb *database = NULL;
|
||||||
|
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||||
|
CHECK(lardon3d_project_db_open(database_path, &database, error)
|
||||||
|
== LARDON3D_PROJECT_DB_OK);
|
||||||
|
Lardon3DAppState state;
|
||||||
|
lardon3d_app_state_init(&state);
|
||||||
|
state.project_loaded = true;
|
||||||
|
state.project_db = database;
|
||||||
|
CHECK(snprintf(state.project_path, sizeof(state.project_path), "%s", root) > 0);
|
||||||
|
|
||||||
|
Lardon3DProjectDbScanSet a, b;
|
||||||
|
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(a.scanset_id != b.scanset_id);
|
||||||
|
Lardon3DProjectDbScanSet scanset_page[2]; size_t scanset_count = 0;
|
||||||
|
CHECK(lardon3d_project_db_list_scansets(database, 0, scanset_page, 1,
|
||||||
|
&scanset_count) == LARDON3D_PROJECT_DB_OK && scanset_count == 1
|
||||||
|
&& scanset_page[0].scanset_id == a.scanset_id);
|
||||||
|
CHECK(lardon3d_project_db_list_scansets(database, a.scanset_id,
|
||||||
|
scanset_page, 2, &scanset_count) == LARDON3D_PROJECT_DB_OK
|
||||||
|
&& scanset_count == 1 && scanset_page[0].scanset_id == b.scanset_id);
|
||||||
|
uint64_t count = 99;
|
||||||
|
CHECK(lardon3d_project_db_count_images(database, a.scanset_id, &count)
|
||||||
|
== LARDON3D_PROJECT_DB_OK && count == 0);
|
||||||
|
unsigned char zero_hash[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0};
|
||||||
|
Lardon3DProjectDbImageRegisterStatus invalid_status;
|
||||||
|
Lardon3DProjectDbImage invalid_image;
|
||||||
|
CHECK(lardon3d_project_db_register_image(database, a.scanset_id, zero_hash,
|
||||||
|
"../../outside", 1, "x.jpg", "/source/x.jpg", 0, 1,
|
||||||
|
&invalid_status, &invalid_image) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||||
|
CHECK(lardon3d_project_db_count_images(database, UINT64_C(999999), &count)
|
||||||
|
== LARDON3D_PROJECT_DB_NOT_FOUND);
|
||||||
|
|
||||||
|
Lardon3DProjectDbImage image_a, duplicate_a, image_b;
|
||||||
|
Lardon3DProjectDbImageAsset asset_a, duplicate_asset, asset_b;
|
||||||
|
CHECK(lardon3d_image_catalog_import_file(&state, a.scanset_id, source_a, 0,
|
||||||
|
&image_a, &asset_a) == LARDON3D_IMAGE_CATALOG_IMPORTED);
|
||||||
|
CHECK(canonical_asset_path(asset_a.path));
|
||||||
|
CHECK(lardon3d_image_catalog_import_file(&state, a.scanset_id, source_b, 0,
|
||||||
|
&duplicate_a, &duplicate_asset) == LARDON3D_IMAGE_CATALOG_ALREADY_PRESENT);
|
||||||
|
CHECK(image_a.image_id == duplicate_a.image_id
|
||||||
|
&& asset_a.asset_id == duplicate_asset.asset_id);
|
||||||
|
CHECK(lardon3d_image_catalog_import_file(&state, b.scanset_id, source_b, 0,
|
||||||
|
&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(strcmp(asset_b.path, asset_a.path) == 0);
|
||||||
|
|
||||||
|
char different_dir[PATH_MAX], same_name[PATH_MAX];
|
||||||
|
CHECK(join_path(different_dir, root, "different") && mkdir(different_dir, 0700) == 0);
|
||||||
|
CHECK(join_path(same_name, different_dir, "photo001.jpg"));
|
||||||
|
CHECK(write_file(same_name, "different-content"));
|
||||||
|
Lardon3DProjectDbImage different_image;
|
||||||
|
Lardon3DProjectDbImageAsset different_asset;
|
||||||
|
CHECK(lardon3d_image_catalog_import_file(&state, a.scanset_id, same_name, 0,
|
||||||
|
&different_image, &different_asset) == LARDON3D_IMAGE_CATALOG_IMPORTED);
|
||||||
|
CHECK(different_image.image_id != image_a.image_id
|
||||||
|
&& different_asset.asset_id != asset_a.asset_id
|
||||||
|
&& canonical_asset_path(different_asset.path)
|
||||||
|
&& strcmp(different_asset.path, asset_a.path) != 0);
|
||||||
|
|
||||||
|
char orphan_source[PATH_MAX], asset_root[PATH_MAX];
|
||||||
|
CHECK(join_path(orphan_source, root, "orphan.jpg"));
|
||||||
|
CHECK(join_path(asset_root, root, "assets/images"));
|
||||||
|
CHECK(write_file(orphan_source, "published-before-db-failure"));
|
||||||
|
size_t files_before = count_regular_files(asset_root);
|
||||||
|
uint64_t logical_before = 0;
|
||||||
|
CHECK(lardon3d_project_db_count_images(database, a.scanset_id,
|
||||||
|
&logical_before) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_IMAGE_REGISTER", "1", 1) == 0);
|
||||||
|
CHECK(lardon3d_image_catalog_import_file(&state, a.scanset_id,
|
||||||
|
orphan_source, 0, &different_image, &different_asset)
|
||||||
|
== LARDON3D_IMAGE_CATALOG_DB_ERROR);
|
||||||
|
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_IMAGE_REGISTER") == 0);
|
||||||
|
CHECK(count_regular_files(asset_root) == files_before + 1);
|
||||||
|
uint64_t logical_after = 0;
|
||||||
|
CHECK(lardon3d_project_db_count_images(database, a.scanset_id,
|
||||||
|
&logical_after) == LARDON3D_PROJECT_DB_OK
|
||||||
|
&& logical_after == logical_before);
|
||||||
|
|
||||||
|
char concurrent_path[PATH_MAX];
|
||||||
|
CHECK(join_path(concurrent_path, root, "concurrent.jpg"));
|
||||||
|
CHECK(write_file(concurrent_path, "concurrent-content"));
|
||||||
|
ImportThread contexts[2] = {
|
||||||
|
{.state = &state, .scanset_id = a.scanset_id, .path = concurrent_path},
|
||||||
|
{.state = &state, .scanset_id = a.scanset_id, .path = concurrent_path},
|
||||||
|
};
|
||||||
|
pthread_t threads[2];
|
||||||
|
CHECK(pthread_create(&threads[0], NULL, import_thread, &contexts[0]) == 0);
|
||||||
|
CHECK(pthread_create(&threads[1], NULL, import_thread, &contexts[1]) == 0);
|
||||||
|
CHECK(pthread_join(threads[0], NULL) == 0 && pthread_join(threads[1], NULL) == 0);
|
||||||
|
CHECK(((contexts[0].result == LARDON3D_IMAGE_CATALOG_IMPORTED
|
||||||
|
&& contexts[1].result == LARDON3D_IMAGE_CATALOG_ALREADY_PRESENT)
|
||||||
|
|| (contexts[1].result == LARDON3D_IMAGE_CATALOG_IMPORTED
|
||||||
|
&& contexts[0].result == LARDON3D_IMAGE_CATALOG_ALREADY_PRESENT))
|
||||||
|
&& contexts[0].image_id == contexts[1].image_id);
|
||||||
|
|
||||||
|
char bulk_directory[PATH_MAX];
|
||||||
|
CHECK(join_path(bulk_directory, root, "bulk") && mkdir(bulk_directory, 0700) == 0);
|
||||||
|
for (uint64_t index = 1; index <= 2048; ++index) {
|
||||||
|
char name[64], source[PATH_MAX], content[64];
|
||||||
|
CHECK(snprintf(name, sizeof(name), "bulk-%llu.jpg",
|
||||||
|
(unsigned long long)index) > 0);
|
||||||
|
CHECK(join_path(source, bulk_directory, name));
|
||||||
|
CHECK(snprintf(content, sizeof(content), "unique-content-%llu",
|
||||||
|
(unsigned long long)index) > 0);
|
||||||
|
CHECK(write_file(source, content));
|
||||||
|
Lardon3DProjectDbImage bulk_image;
|
||||||
|
Lardon3DProjectDbImageAsset bulk_asset;
|
||||||
|
CHECK(lardon3d_image_catalog_import_file(&state, a.scanset_id, source,
|
||||||
|
0, &bulk_image, &bulk_asset) == LARDON3D_IMAGE_CATALOG_IMPORTED);
|
||||||
|
}
|
||||||
|
CHECK(lardon3d_project_db_count_images(database, a.scanset_id, &count)
|
||||||
|
== LARDON3D_PROJECT_DB_OK && count == 2051);
|
||||||
|
Lardon3DProjectDbImage page[8]; Lardon3DProjectDbImageAsset assets[8];
|
||||||
|
size_t page_count = 0; uint64_t cursor = 0, visited = 0;
|
||||||
|
while (lardon3d_image_catalog_list(&state, a.scanset_id, cursor, page,
|
||||||
|
assets, 8, &page_count) == LARDON3D_PROJECT_DB_OK && page_count) {
|
||||||
|
for (size_t index = 0; index < page_count; ++index) {
|
||||||
|
CHECK(page[index].image_id > cursor);
|
||||||
|
cursor = page[index].image_id; ++visited;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK(visited == count);
|
||||||
|
size_t one_count = 0;
|
||||||
|
CHECK(lardon3d_image_catalog_list(&state, a.scanset_id, 0, page, assets,
|
||||||
|
1, &one_count) == LARDON3D_PROJECT_DB_OK && one_count == 1);
|
||||||
|
Lardon3DProjectDbImage maximum_page[LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX];
|
||||||
|
Lardon3DProjectDbImageAsset maximum_assets[LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX];
|
||||||
|
size_t maximum_count = 0;
|
||||||
|
CHECK(lardon3d_image_catalog_list(&state, a.scanset_id, 0, maximum_page,
|
||||||
|
maximum_assets, LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX, &maximum_count)
|
||||||
|
== LARDON3D_PROJECT_DB_OK
|
||||||
|
&& maximum_count == LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX);
|
||||||
|
CHECK(lardon3d_image_catalog_list(&state, a.scanset_id, 0, page, assets,
|
||||||
|
LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX + 1, &page_count)
|
||||||
|
== LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||||
|
CHECK(lardon3d_image_catalog_list(&state, UINT64_C(999999), 0, page,
|
||||||
|
assets, 8, &page_count) == LARDON3D_PROJECT_DB_NOT_FOUND);
|
||||||
|
CHECK(unlink(source_a) == 0 && unlink(source_b) == 0);
|
||||||
|
Lardon3DProjectDbImage loaded; Lardon3DProjectDbImageAsset loaded_asset;
|
||||||
|
CHECK(lardon3d_project_db_load_image(database, image_a.image_id, &loaded,
|
||||||
|
&loaded_asset) == LARDON3D_PROJECT_DB_OK
|
||||||
|
&& loaded.asset_id == asset_a.asset_id);
|
||||||
|
|
||||||
|
lardon3d_project_db_close(database);
|
||||||
|
state.project_db = NULL;
|
||||||
|
CHECK(remove_tree(root));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) { return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; }
|
||||||
|
|
@ -16,6 +16,24 @@
|
||||||
} } while (0)
|
} } while (0)
|
||||||
|
|
||||||
Lardon3DProjectDbResult lardon3d_project_db_test_orphan_checkpoint(Lardon3DProjectDb *database);
|
Lardon3DProjectDbResult lardon3d_project_db_test_orphan_checkpoint(Lardon3DProjectDb *database);
|
||||||
|
Lardon3DProjectDbResult lardon3d_project_db_test_delete_catalog_identity(
|
||||||
|
Lardon3DProjectDb *database, uint64_t scanset_id, uint64_t image_id,
|
||||||
|
uint64_t asset_id);
|
||||||
|
|
||||||
|
static void
|
||||||
|
asset_path_for_hash(const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE],
|
||||||
|
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY])
|
||||||
|
{
|
||||||
|
static const char digits[] = "0123456789abcdef";
|
||||||
|
char hex[65];
|
||||||
|
for (size_t index = 0; index < sizeof(hex) / 2; ++index) {
|
||||||
|
hex[index * 2] = digits[hash[index] >> 4];
|
||||||
|
hex[index * 2 + 1] = digits[hash[index] & 15U];
|
||||||
|
}
|
||||||
|
hex[64] = '\0';
|
||||||
|
(void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY,
|
||||||
|
"assets/images/%c%c/%s", hex[0], hex[1], hex);
|
||||||
|
}
|
||||||
|
|
||||||
static Lardon3DTaskDurableSnapshot
|
static Lardon3DTaskDurableSnapshot
|
||||||
task_snapshot(uint64_t id, Lardon3DTaskState saved)
|
task_snapshot(uint64_t id, Lardon3DTaskState saved)
|
||||||
|
|
@ -67,7 +85,7 @@ create_future_database(const char *path)
|
||||||
sqlite3 *connection = NULL;
|
sqlite3 *connection = NULL;
|
||||||
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
|
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
|
||||||
bool ok = sqlite3_exec(connection, "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
bool ok = sqlite3_exec(connection, "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
||||||
"INSERT INTO metadata VALUES('schema_version',4);", NULL, NULL, NULL) == SQLITE_OK;
|
"INSERT INTO metadata VALUES('schema_version',5);", NULL, NULL, NULL) == SQLITE_OK;
|
||||||
return sqlite3_close(connection) == SQLITE_OK && ok;
|
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,6 +128,21 @@ create_v2_database(const char *path)
|
||||||
return sqlite3_close(connection) == SQLITE_OK && ok;
|
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
create_v3_database(const char *path)
|
||||||
|
{
|
||||||
|
if (!create_v2_database(path)) return false;
|
||||||
|
sqlite3 *connection = NULL;
|
||||||
|
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
|
||||||
|
bool ok = sqlite3_exec(connection,
|
||||||
|
"CREATE TABLE image_import_tasks(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,source_path TEXT NOT NULL);"
|
||||||
|
"INSERT INTO image_import_tasks VALUES(9,'/legacy/source');"
|
||||||
|
"INSERT INTO metadata(key,value) VALUES('next_task_id',10);"
|
||||||
|
"UPDATE metadata SET value=3 WHERE key='schema_version'",
|
||||||
|
NULL, NULL, NULL) == SQLITE_OK;
|
||||||
|
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
query_integer(const char *path, const char *sql, sqlite3_int64 expected)
|
query_integer(const char *path, const char *sql, sqlite3_int64 expected)
|
||||||
{
|
{
|
||||||
|
|
@ -135,7 +168,7 @@ run_test(void)
|
||||||
CHECK(mkdtemp(directory));
|
CHECK(mkdtemp(directory));
|
||||||
char database_path[512], artifact_path[512], future_path[512], corrupt_path[512];
|
char database_path[512], artifact_path[512], future_path[512], corrupt_path[512];
|
||||||
char legacy_path[512], failed_migration_path[512], v2_path[512];
|
char legacy_path[512], failed_migration_path[512], v2_path[512];
|
||||||
char failed_v3_migration_path[512];
|
char failed_v3_migration_path[512], v3_path[512], failed_v4_path[512];
|
||||||
CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0);
|
CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0);
|
||||||
CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin", directory) > 0);
|
CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin", directory) > 0);
|
||||||
CHECK(snprintf(future_path, sizeof(future_path), "%s/future.db", directory) > 0);
|
CHECK(snprintf(future_path, sizeof(future_path), "%s/future.db", directory) > 0);
|
||||||
|
|
@ -145,11 +178,47 @@ run_test(void)
|
||||||
CHECK(snprintf(v2_path, sizeof(v2_path), "%s/v2.db", directory) > 0);
|
CHECK(snprintf(v2_path, sizeof(v2_path), "%s/v2.db", directory) > 0);
|
||||||
CHECK(snprintf(failed_v3_migration_path, sizeof(failed_v3_migration_path),
|
CHECK(snprintf(failed_v3_migration_path, sizeof(failed_v3_migration_path),
|
||||||
"%s/failed-v3-migration.db", directory) > 0);
|
"%s/failed-v3-migration.db", directory) > 0);
|
||||||
|
CHECK(snprintf(v3_path, sizeof(v3_path), "%s/v3.db", directory) > 0);
|
||||||
|
CHECK(snprintf(failed_v4_path, sizeof(failed_v4_path),
|
||||||
|
"%s/failed-v4-migration.db", directory) > 0);
|
||||||
|
|
||||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||||
Lardon3DProjectDb *database = NULL;
|
Lardon3DProjectDb *database = NULL;
|
||||||
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(database && lardon3d_project_db_schema_version(database) == 3);
|
CHECK(database && lardon3d_project_db_schema_version(database) == 4);
|
||||||
|
bool legacy_pending = true;
|
||||||
|
CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending)
|
||||||
|
== LARDON3D_PROJECT_DB_OK && !legacy_pending);
|
||||||
|
|
||||||
|
Lardon3DProjectDbScanSet deleted_scanset;
|
||||||
|
CHECK(lardon3d_project_db_create_scanset(database, "Deleted identity",
|
||||||
|
&deleted_scanset) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
unsigned char first_hash[LARDON3D_PROJECT_DB_SHA256_SIZE] = {1};
|
||||||
|
char first_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||||
|
asset_path_for_hash(first_hash, first_asset_path);
|
||||||
|
Lardon3DProjectDbImageRegisterStatus identity_status;
|
||||||
|
Lardon3DProjectDbImage deleted_image;
|
||||||
|
CHECK(lardon3d_project_db_register_image(database,
|
||||||
|
deleted_scanset.scanset_id, first_hash, first_asset_path, 1,
|
||||||
|
"deleted.jpg", "/source/deleted.jpg", 0, 1, &identity_status,
|
||||||
|
&deleted_image) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
CHECK(lardon3d_project_db_test_delete_catalog_identity(database,
|
||||||
|
deleted_scanset.scanset_id, deleted_image.image_id,
|
||||||
|
deleted_image.asset_id) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
Lardon3DProjectDbScanSet replacement_scanset;
|
||||||
|
CHECK(lardon3d_project_db_create_scanset(database, "Replacement identity",
|
||||||
|
&replacement_scanset) == LARDON3D_PROJECT_DB_OK
|
||||||
|
&& replacement_scanset.scanset_id > deleted_scanset.scanset_id);
|
||||||
|
unsigned char second_hash[LARDON3D_PROJECT_DB_SHA256_SIZE] = {2};
|
||||||
|
char second_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||||
|
asset_path_for_hash(second_hash, second_asset_path);
|
||||||
|
Lardon3DProjectDbImage replacement_image;
|
||||||
|
CHECK(lardon3d_project_db_register_image(database,
|
||||||
|
replacement_scanset.scanset_id, second_hash, second_asset_path, 1,
|
||||||
|
"replacement.jpg", "/source/replacement.jpg", 0, 2,
|
||||||
|
&identity_status, &replacement_image) == LARDON3D_PROJECT_DB_OK
|
||||||
|
&& replacement_image.image_id > deleted_image.image_id
|
||||||
|
&& replacement_image.asset_id > deleted_image.asset_id);
|
||||||
|
|
||||||
Lardon3DProjectDbProject project = {.created_at = 100, .updated_at = 100};
|
Lardon3DProjectDbProject project = {.created_at = 100, .updated_at = 100};
|
||||||
(void)snprintf(project.stable_id, sizeof(project.stable_id), "project-0001");
|
(void)snprintf(project.stable_id, sizeof(project.stable_id), "project-0001");
|
||||||
|
|
@ -174,6 +243,9 @@ run_test(void)
|
||||||
CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, &checkpoint, 200) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, &checkpoint, 200) == LARDON3D_PROJECT_DB_OK);
|
||||||
running.progress = 30; running.sequence_count = 3;
|
running.progress = 30; running.sequence_count = 3;
|
||||||
CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, &checkpoint, 201) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 1, &checkpoint, 201) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
Lardon3DProjectDbScanSet import_scanset;
|
||||||
|
CHECK(lardon3d_project_db_create_scanset(database, "Legacy import",
|
||||||
|
&import_scanset) == LARDON3D_PROJECT_DB_OK);
|
||||||
Lardon3DProjectDbTask task;
|
Lardon3DProjectDbTask task;
|
||||||
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(task.recovery_state == TASK_PENDING && task.progress == 30 && task.sequence_count == 3);
|
CHECK(task.recovery_state == TASK_PENDING && task.progress == 30 && task.sequence_count == 3);
|
||||||
|
|
@ -191,7 +263,8 @@ run_test(void)
|
||||||
CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 0,
|
CHECK(lardon3d_project_db_record_task(database, &running, "test.work", 0,
|
||||||
&checkpoint, 202) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
&checkpoint, 202) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||||
CHECK(lardon3d_project_db_record_image_import_task(database, &running,
|
CHECK(lardon3d_project_db_record_image_import_task(database, &running,
|
||||||
"import.images", 1, &checkpoint, "/tmp/source-a", 202)
|
"import.images", 1, &checkpoint, "/tmp/source-a",
|
||||||
|
import_scanset.scanset_id, 202)
|
||||||
== LARDON3D_PROJECT_DB_CONSTRAINT);
|
== LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||||
|
|
||||||
Lardon3DTaskDurableSnapshot completed = task_snapshot(2, TASK_COMPLETED);
|
Lardon3DTaskDurableSnapshot completed = task_snapshot(2, TASK_COMPLETED);
|
||||||
|
|
@ -206,15 +279,18 @@ run_test(void)
|
||||||
|
|
||||||
Lardon3DTaskDurableSnapshot image_import = task_snapshot(5, TASK_PENDING);
|
Lardon3DTaskDurableSnapshot image_import = task_snapshot(5, TASK_PENDING);
|
||||||
CHECK(lardon3d_project_db_record_image_import_task(database, &image_import,
|
CHECK(lardon3d_project_db_record_image_import_task(database, &image_import,
|
||||||
"import.images", 1, &checkpoint, "/tmp/source-a", 202)
|
"import.images", 1, &checkpoint, "/tmp/source-a",
|
||||||
|
import_scanset.scanset_id, 202)
|
||||||
== LARDON3D_PROJECT_DB_OK);
|
== LARDON3D_PROJECT_DB_OK);
|
||||||
Lardon3DProjectDbImageImport import_parameters;
|
Lardon3DProjectDbImageImport import_parameters;
|
||||||
CHECK(lardon3d_project_db_load_image_import(database, 5,
|
CHECK(lardon3d_project_db_load_image_import(database, 5,
|
||||||
&import_parameters) == LARDON3D_PROJECT_DB_OK);
|
&import_parameters) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(import_parameters.task_id == 5
|
CHECK(import_parameters.task_id == 5
|
||||||
&& strcmp(import_parameters.source_path, "/tmp/source-a") == 0);
|
&& strcmp(import_parameters.source_path, "/tmp/source-a") == 0
|
||||||
|
&& import_parameters.scanset_id == import_scanset.scanset_id);
|
||||||
CHECK(lardon3d_project_db_record_image_import_task(database, &image_import,
|
CHECK(lardon3d_project_db_record_image_import_task(database, &image_import,
|
||||||
"import.images", 1, &checkpoint, "/tmp/source-b", 203)
|
"import.images", 1, &checkpoint, "/tmp/source-b",
|
||||||
|
import_scanset.scanset_id, 203)
|
||||||
== LARDON3D_PROJECT_DB_CONSTRAINT);
|
== LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||||
|
|
||||||
Lardon3DTaskDurableSnapshot rollback_task = task_snapshot(3, TASK_PENDING);
|
Lardon3DTaskDurableSnapshot rollback_task = task_snapshot(3, TASK_PENDING);
|
||||||
|
|
@ -265,7 +341,7 @@ run_test(void)
|
||||||
char too_long[LARDON3D_PROJECT_DB_PATH_CAPACITY + 1]; memset(too_long, 'x', sizeof(too_long)); too_long[sizeof(too_long) - 1] = '\0';
|
char too_long[LARDON3D_PROJECT_DB_PATH_CAPACITY + 1]; memset(too_long, 'x', sizeof(too_long)); too_long[sizeof(too_long) - 1] = '\0';
|
||||||
CHECK(lardon3d_project_db_open(too_long, &database, error) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
CHECK(lardon3d_project_db_open(too_long, &database, error) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||||
lardon3d_project_db_close(contexts[0].database); database = NULL;
|
lardon3d_project_db_close(contexts[0].database); database = NULL;
|
||||||
CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 3));
|
CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 4));
|
||||||
CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1));
|
CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1));
|
||||||
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
|
@ -280,7 +356,7 @@ run_test(void)
|
||||||
|
|
||||||
CHECK(create_v1_database(legacy_path));
|
CHECK(create_v1_database(legacy_path));
|
||||||
CHECK(lardon3d_project_db_open(legacy_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_open(legacy_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(lardon3d_project_db_schema_version(database) == 3);
|
CHECK(lardon3d_project_db_schema_version(database) == 4);
|
||||||
CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK
|
CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK
|
||||||
&& strcmp(loaded_project.stable_id, "legacy-project") == 0);
|
&& strcmp(loaded_project.stable_id, "legacy-project") == 0);
|
||||||
CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK);
|
CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
|
@ -289,7 +365,7 @@ run_test(void)
|
||||||
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact",
|
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact",
|
||||||
&loaded_artifact) == LARDON3D_PROJECT_DB_OK);
|
&loaded_artifact) == LARDON3D_PROJECT_DB_OK);
|
||||||
lardon3d_project_db_close(database); database = NULL;
|
lardon3d_project_db_close(database); database = NULL;
|
||||||
CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 3));
|
CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 4));
|
||||||
|
|
||||||
CHECK(create_v1_database(failed_migration_path));
|
CHECK(create_v1_database(failed_migration_path));
|
||||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0);
|
||||||
|
|
@ -316,7 +392,7 @@ run_test(void)
|
||||||
&loaded_artifact) == LARDON3D_PROJECT_DB_OK);
|
&loaded_artifact) == LARDON3D_PROJECT_DB_OK);
|
||||||
lardon3d_project_db_close(database); database = NULL;
|
lardon3d_project_db_close(database); database = NULL;
|
||||||
CHECK(query_integer(v2_path,
|
CHECK(query_integer(v2_path,
|
||||||
"SELECT value FROM metadata WHERE key='schema_version'", 3));
|
"SELECT value FROM metadata WHERE key='schema_version'", 4));
|
||||||
|
|
||||||
CHECK(create_v2_database(failed_v3_migration_path));
|
CHECK(create_v2_database(failed_v3_migration_path));
|
||||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0);
|
||||||
|
|
@ -328,9 +404,37 @@ run_test(void)
|
||||||
CHECK(query_integer(failed_v3_migration_path,
|
CHECK(query_integer(failed_v3_migration_path,
|
||||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='image_import_tasks'", 0));
|
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='image_import_tasks'", 0));
|
||||||
|
|
||||||
|
CHECK(create_v3_database(v3_path));
|
||||||
|
CHECK(lardon3d_project_db_open(v3_path, &database, error)
|
||||||
|
== LARDON3D_PROJECT_DB_OK);
|
||||||
|
CHECK(lardon3d_project_db_load_image_import(database, 9,
|
||||||
|
&import_parameters) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
CHECK(strcmp(import_parameters.source_path, "/legacy/source") == 0
|
||||||
|
&& import_parameters.scanset_id > 0);
|
||||||
|
CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending)
|
||||||
|
== LARDON3D_PROJECT_DB_OK && legacy_pending);
|
||||||
|
CHECK(lardon3d_project_db_load_task(database, 9, &task)
|
||||||
|
== LARDON3D_PROJECT_DB_OK && task.has_task_kind);
|
||||||
|
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact",
|
||||||
|
&loaded_artifact) == LARDON3D_PROJECT_DB_OK);
|
||||||
|
lardon3d_project_db_close(database); database = NULL;
|
||||||
|
CHECK(query_integer(v3_path,
|
||||||
|
"SELECT value FROM metadata WHERE key='schema_version'", 4));
|
||||||
|
|
||||||
|
CHECK(create_v3_database(failed_v4_path));
|
||||||
|
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0);
|
||||||
|
CHECK(lardon3d_project_db_open(failed_v4_path, &database, error)
|
||||||
|
== LARDON3D_PROJECT_DB_IO_ERROR);
|
||||||
|
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4") == 0);
|
||||||
|
CHECK(query_integer(failed_v4_path,
|
||||||
|
"SELECT value FROM metadata WHERE key='schema_version'", 3));
|
||||||
|
CHECK(query_integer(failed_v4_path,
|
||||||
|
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='scansets'", 0));
|
||||||
|
|
||||||
CHECK(unlink(artifact_path) == 0); CHECK(unlink(database_path) == 0); CHECK(unlink(future_path) == 0); CHECK(unlink(corrupt_path) == 0);
|
CHECK(unlink(artifact_path) == 0); CHECK(unlink(database_path) == 0); CHECK(unlink(future_path) == 0); CHECK(unlink(corrupt_path) == 0);
|
||||||
CHECK(unlink(legacy_path) == 0); CHECK(unlink(failed_migration_path) == 0);
|
CHECK(unlink(legacy_path) == 0); CHECK(unlink(failed_migration_path) == 0);
|
||||||
CHECK(unlink(v2_path) == 0); CHECK(unlink(failed_v3_migration_path) == 0);
|
CHECK(unlink(v2_path) == 0); CHECK(unlink(failed_v3_migration_path) == 0);
|
||||||
|
CHECK(unlink(v3_path) == 0); CHECK(unlink(failed_v4_path) == 0);
|
||||||
CHECK(rmdir(directory) == 0);
|
CHECK(rmdir(directory) == 0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue