# Migration des images Base64 historiques

## Périmètre et état audité

La commande `app:migrate-ckeditor-base64` lit la base SQLite résolue depuis la
connexion Doctrine (`DATABASE_URL`). Le chemin et l'environnement sont affichés.
Elle refuse une base absente, en mémoire ou un autre moteur. Le dry-run ouvre
SQLite avec `mode=ro` et `PRAGMA query_only=ON` ; il ne crée aucun fichier image.

Champs contrôlés (table/champ SQL explicites dans `Base64ImageMigration::CONTENTS`) :

- Projet, Offre, Ressource : `description`, `shortDescription` ;
- Page : `content` ;
- Pôle : `description`, `shortDescription`, `offresDescription`,
  `equipeDescription`, `projetsDescription`, `ressourcesDescription` ;
- Client : `content` ;
- Enjeu : `shortDescription`.

L'inventaire préalable de toutes les colonnes textuelles de la base locale n'a
trouvé aucune autre colonne contenant une Data URI. Les réglages CKEditor et
elFinder restent inchangés.

Audit local du 18 septembre 2026 : 19 enregistrements / 19 champs / 70 occurrences /
70 binaires uniques. Seules les descriptions sont concernées : 16 Projets
(IDs 1, 3, 4, 6, 8, 9, 10, 18, 20, 22, 23, 24, 26, 28, 34, 35), Offre 46,
Ressources 7 et 13. MIME déclarés et détectés concordants : 62 JPEG, 8 PNG.
Aucun doublon, SVG/Base64 ou Data URI dans srcset, href ou style identifié.

- Base64 : 77 252 000 octets (73,673 MiB).
- Data URI complètes : 77 253 602 octets.
- Binaires : 57 938 933 octets (55,255 MiB).
- Réduction nette prévue des champs HTML : 77 247 063 octets (73,669 MiB).
- 70 nouveaux fichiers : 59 Projet, 1 Offre, 10 Ressource ; aucun fichier réutilisé.
- Base avant : 107 978 752 octets (102,977 MiB), dont 27,391 MiB de pages libres.

Ces valeurs décrivent la base locale, pas la production. Le dry-run est à refaire
sur chaque environnement.

## Utilisation

```sh
php bin/console app:migrate-ckeditor-base64
```

Option facultative pour exporter les offsets, hashes, statuts et chemins, sans les
blobs ni le HTML, dans un nouveau fichier hors du projet :

```sh
php bin/console app:migrate-ckeditor-base64 --report=/chemin/hors-projet/plan-base64.json
```

Toute erreur bloque l'ensemble du futur apply. Le rapport distingue occurrences,
binaires uniques, doublons, fichiers par destination, nouveaux fichiers et réemplois.
Un MIME non pris en charge, un Base64 invalide, un src ambigu, un autre attribut
contenant une Data URI, un chemin symbolique ou une collision de fichier sont bloquants.

L'écriture nécessite explicitement `--apply`. Aucune confirmation interactive
supplémentaire n'est imposée, ce qui permet une exécution contrôlée non interactive.
L'avertissement de backup est affiché avant toute mutation.

## Fichiers et HTML

Noms : SHA-256 complet du binaire et extension déduite de son MIME réel (`jpg`,
`png`, `webp`, `gif`). Les binaires restent identiques, sans compression ni conversion.
`image/jpg` est accepté comme alias explicite de `image/jpeg` ; SVG est refusé.
Le décodage strict est suivi de fileinfo et getimagesizefromstring.

Destinations : `/images/{projets,offres,ressources}/contenus/<hash>.<extension>`.
Pour les autres familles : `pages/contenus`, `poles/contenus`, `clients/contenus` ;
les Enjeux utilisent `poles/contenus`. Ces chemins sont relatifs à `public/` et ne
contiennent jamais de domaine. Les répertoires sont créés seulement lors d'un apply.

La déduplication physique se fait par répertoire. Une même image Projet + Offre
produit deux fichiers portant le même hash, un dans chaque famille. Ce choix
évite de faire dépendre un contenu de la médiathèque d'une autre famille.
Le rapport distingue donc binaires uniques et fichiers physiques.

Le repérage lexical respecte les guillemets, les attributs sans guillemets,
les commentaires et les contenus bruts script/style/textarea/title. Seuls les
intervalles correspondant aux valeurs de `img[src]` sont remplacés, de droite à
gauche. Les attributs, leur ordre, la casse, les entités HTML et la structure
restent inchangés ; aucun DOM n'est resérialisé. Les Data URI dans les commentaires
ou du texte script ne sont pas traitées comme des images actives.

## Atomicité, erreurs et reprise

1. Analyse globale en lecture seule et validation de toutes les destinations.
2. Verrou exclusif de migration dans `var/ckeditor-base64-migrations/`.
3. Relecture des sources avec comparaison des hashes HTML du plan.
4. Préparation de fichiers temporaires dans leurs répertoires cibles, validation
   des tailles et hashes, journal des inodes et chemins hors de `public/`.
5. Transaction SQLite et nouveau contrôle de tous les contenus.
6. Installation atomique sans écrasement par liens physiques depuis le staging.
   Le même système de fichiers est garanti ; l'absence de support des hard links
   provoque un échec contrôlé, sans réécriture des contenus.
7. Vérification des fichiers finaux, modification des seuls champs HTML, commit.
8. Nettoyage du staging ; conservation du journal JSON d'exécution.

Un échec avant commit entraîne rollback SQL et suppression des seuls fichiers
créés par cette exécution, après contrôle d'inode et de hash. Un fichier réutilisé
n'est jamais supprimé. Une édition concurrente du contenu invalide le plan.
Un résultat de commit ou de rollback incertain conserve les fichiers et produit
un statut `inspection-required` : ne pas relancer aveuglément.

Une transaction SQL et un système de fichiers ne forment pas une transaction
unique. En cas de SIGKILL, panne système ou coupure d'alimentation, le code de
nettoyage peut ne pas s'exécuter. SQLite assure sa récupération transactionnelle,
mais des fichiers/stagings peuvent subsister. Le journal permet la réconciliation :

- arrêter les autres écritures et vérifier `PRAGMA integrity_check` ;
- comparer chaque champ aux hashes `originalHash` et `rewrittenHash` du journal ;
- si les champs sont tous réécrits, conserver et vérifier les fichiers cibles ;
- si les champs sont tous originaux, ne retirer que les fichiers nouvellement
  installés identifiés par inode, device et hash dans le journal, après vérification
  qu'aucun autre contenu ne les référence ;
- si l'état est ambigu, conserver les fichiers et restaurer la sauvegarde validée.

Ne jamais supprimer en masse les fichiers hashés : certains peuvent préexister.
Après succès, une nouvelle exécution n'a plus d'images actives à migrer et ne crée
ni journal ni fichier supplémentaire.

## Sauvegarde et futur passage préproduction / production

Aucune sauvegarde de production ni migration réelle n'a été réalisée dans cette passe.
Les tests apply utilisent uniquement leurs bases et répertoires temporaires.

Chemin local effectivement résolu :
`/Users/denischarrier/Dev/sensipode-site/var/database_sensipode_v2.db`.
Depuis la racine du projet, la sauvegarde SQLite cohérente recommandée est :

```sh
sqlite3 var/database_sensipode_v2.db ".backup '/chemin/hors-web/sensipode-avant-base64.db'"
sqlite3 /chemin/hors-web/sensipode-avant-base64.db 'PRAGMA integrity_check;'
```

Remplacer le répertoire de destination par un répertoire existant protégé et
choisir un nom de backup encore inutilisé. En préproduction/production, utiliser
**le chemin affiché par le dry-run de cet environnement**, pas supposer qu'il est
identique au chemin local. `.backup` est préférable à une copie brute d'une base
ouverte, notamment si SQLite fonctionne en mode WAL.

Procédure future, d'abord en préproduction sur une copie représentative :

1. Déployer la commande et ses classes, reconstruire le cache Symfony de
   l'environnement ; vérifier fileinfo, PDO SQLite, permissions et espace libre.
2. Suspendre les modifications éditoriales, faire le dry-run et examiner le plan.
3. Sauvegarder la base avec `.backup`, vérifier la sauvegarde et sa restauration.
4. Prévoir l'espace des images, des journaux SQL et du backup ; lancer volontairement :

```sh
php bin/console app:migrate-ckeditor-base64 --env=prod --no-debug --apply
```

5. Vérifier les journaux, les fichiers et les pages concernées manuellement.
6. Refaire le dry-run : zéro occurrence active attendue. Vérifier l'intégrité SQLite.
7. Répéter en production après validation préproduction et nouveau backup propre
   à la production. Ne pas copier une base locale sur la production.

Pour revenir en arrière après un succès : arrêter les écritures et restaurer la
sauvegarde SQLite validée avec une procédure de restauration cohérente (y compris
la gestion de WAL/SHM). Les fichiers nouveaux peuvent être conservés provisoirement ;
leur suppression éventuelle doit suivre le journal et vérifier les références.

## Taille logique et VACUUM séparé

La réduction attendue du contenu HTML est de 73,669 MiB. La taille physique ne
baissera normalement pas d'autant au commit. La taille logique exacte après
migration ne peut être certifiée avant l'apply ; relever ensuite :

```sh
sqlite3 var/database_sensipode_v2.db 'PRAGMA page_size; PRAGMA page_count; PRAGMA freelist_count;'
```

L'occupation estimée des pages utilisées est `(page_count - freelist_count) * page_size`.
La répartition des pages et les index empêchent de déduire la taille finale exacte
en soustrayant simplement la taille des chaînes Base64 au fichier SQLite.

Seulement après validation des pages, sauvegarde vérifiée, absence de writers et
espace disque suffisant pour la copie temporaire (prévoir jusqu'à deux fois la taille
de la base en espace disponible pour VACUUM), on pourra lancer séparément :

```sh
sqlite3 var/database_sensipode_v2.db 'VACUUM;'
sqlite3 var/database_sensipode_v2.db 'PRAGMA integrity_check;'
```

Aucun VACUUM n'est appelé par la commande et aucun n'a été exécuté pendant ce chantier.

Références SQLite : [CLI et `.backup`](https://www.sqlite.org/cli.html),
[VACUUM et espace temporaire](https://www.sqlite.org/lang_vacuum.html).
