Skip to content

Import & migration

Le dossier migrations/ contient des donnees et scripts d'import one-off (PAS des migrations EF) servant a migrer le contenu legacy (Umbraco 6) vers ce CMS. L'import passe par les endpoints de ImportApiController.

Statut : desactive. ImportApiController est entierement exclu du build (#if false) : ces endpoints n'avaient aucune authentification (route publique ~/umbraco/api/Import/..., dont DeleteArticle), ce qui exposait de la mutation/suppression de contenu non authentifiee en prod. Pour relancer une migration, retirer les directives #if false/#endif et proteger les endpoints par le header X-Webhook-Secret (voir MultimediaApiController).

ImportApiController (desactive)

  • POST CreateArticle : cree un article complet a partir d'un CreateArticleDto (titre, sous-titre, teaser, contenu HTML avec marqueurs de macros, datePublished, tags CSV, image par nom, auteurs CSV, skin, rubrique, categories, localisation JSON, MacroReplacements). Logique :
    1. Resout la racine par skin, cree/trouve le {skin}ArticlesFolder puis le sous-dossier de rubrique (map des codes legacy -> labels FR).
    2. Resout l'image par nom (via Examine), les auteurs (firstname+lastname -> UDIs), les categories (JSON CheckBoxList), la rubrique (section JSON).
    3. Si MacroReplacements fournis, BuildRteWithBlocksValue() convertit les macros legacy en JSON de blocks TinyMCE (UDIs umb://element/..., BlockList imbriques).
    4. Tag automatique des dessins : si categorie "Dessin" + image, media.isDrawing = true.
    5. SaveAndPublish immediat.
  • POST CreateAuthor : cree un auteur sous le dossier global authors.
  • POST DeleteArticle?id= : suppression (cleanup migration/tests).
  • CreateMedia : commente (remplace par import SQL direct).

Dossier migrations/

migrations/
├── doc.md, TODO.md
├── articles/   articles.json (~732 articles legacy), MIGRATION-ARTICLES.md, AUDIT-ARTICLES.md,
│               exportArticlesToJson.sql, import-articles.ps1, import-articles-linfo.ps1, cleanup-broken-articles.ps1
├── authors/    authors.json (~100), exportAuthorToJson.sql, import-author.ps1
└── medias/     medias.json / mediasNew.json, u13_insertMedias.sql, u7_extractMediaToJson.sql, import-media.ps1

Volumes (cf. MIGRATION-ARTICLES.md) : 732 articles (l-info 385, icogne 345, accm 2), ~100 auteurs.

Workflow d'import (articles L-Info)

import-articles-linfo.ps1 lit articles.json, met en cache les metadonnees de pickers via SQL sur la base legacy (vmsqla001), resout les macros (WidgetDocument -> filesBlock, WidgetSlider -> galleryBlock, formulaires/DividedContent -> reconstruction manuelle), construit le CreateArticleDto + MacroReplacements, puis POST vers CreateArticle. Si la base legacy est indisponible, les macros tombent en commentaires HTML (reconstruction manuelle).

Medias

Import via SQL direct (u13_insertMedias.sql) dans les tables Umbraco, plutot que via l'API. mediasNew.json fournit le mapping ancien UDI -> nouveau nom de fichier pour resoudre les macros pendant l'import des articles.

Nouveau systeme Icogne — lecture directe de la DB legacy (2026)

Refonte de l'import (skin icogne d'abord) inspiree du FelixImportController d'intranet-apol-pel : on supprime la chaine export JSON + PowerShell et on lit directement la base legacy en ADO.NET cote serveur. Design complet : docs/superpowers/specs/2026-08-04-import-icogne-legacy-db-design.md.

  • Source legacy : umb_accmpd_prod, Umbraco 7.15 (Starter Kit SKV 3.6.0) — pas un V6 (corriger MIGRATION-ARTICLES.md en consequence). Racine icogne = node 1501 ; type ArticleItem ; contenu versionne (cmsDocument + cmsPropertyData.versionId).
  • Cible : racine icogne node 1233, articles ranges par annee (icogneArticlesFolder : 2023..2026), type icogneArticle. Medias icogne sous node 22247.
  • Reference unique legacyId (Umbraco.Integer) : compositions uSync jetables legacyRefContent (attachee a icogneArticle) et legacyRefMedia (attachee a Image/File). Sert de clef de dedup/idempotence et de pont de resolution des medias. A supprimer apres la migration.
  • Controller : LegacyIcogneImportController (#if DEBUG, jamais en Release ; aucune auth, execution locale). Connexion legacy via config ConnectionStrings:LegacyImportDb (dans appsettings.local.json, git-ignore). Endpoints GET (skip, take, dryRun) :
    • RetagMedia (a lancer en premier) : tamponne legacyId sur les medias cible via migrations/medias/mediasNew.json (jointure Path == umbracoFile).
    • ImportMissingMedia : recree les medias referencies par les articles icogne mais absents de la cible (le snapshot mediasNew.json s'arrete au media legacy id 15598 ; les PDF recents 2024→2026 manquent). Reproduit la hierarchie de dossiers legacy (remontee des parents sous la racine media Icogne legacy 1478 → racine cible 22247, dossiers matches par nom/crees) et pose umbracoFile (path legacy) + legacyId. Les fichiers physiques ne sont pas geres (ils vivent sur le serveur media, meme path /media/xxxx/).
    • RunArticles (replace en plus) : importe les 381 articles icogne, etat de publication conserve (version published si elle existe, sinon newest ; publie si publie, sinon brouillon), routing par annee, dedup/replace par legacyId, conversion du corps. Notifications supprimees (pas de webhook frontend) → recalculer mediaIndex apres via ~/umbraco/api/Multimedia/Reindex.
  • Workflow complet : RetagMediaImportMissingMediaRunArticlesMultimedia/Reindex + reconstruire l'index Examine.
  • Conversion du corps : WidgetDocument -> filesBlock, WidgetSlider -> galleryBlock (resolution des medias par legacyId) ; formulaires laisses en repere HTML <!-- LEGACY_FORM FormGuid=... --> (reconnexion manuelle) ; rubrique/type/geoloc ignores (vides cote icogne).
  • Nettoyage : manuel (backoffice) ; les re-runs passent par replace=true.
  • Pre-requis avant import : importer le schema uSync (creer la propriete legacyId), sinon SetValue("legacyId", ...) echoue. Apres import : reconstruire l'index Examine (tri datePublished:).

Nouveau systeme L-Info — lecture directe de la DB legacy (2026)

Jumeau du systeme icogne pour la skin l-info (meme moteur, memes garde-fous). Design complet : docs/superpowers/specs/2026-08-04-import-linfo-legacy-db-design.md.

  • Source legacy : umb_accmpd_prod (U7.15). Racine l-info = node 1500 ; type ArticleItem. 397 articles (newest=1), dont 386 publies. Media root legacy L-Info = node 1486.
  • Cible : racine l-info node 1234 (linfoSiteRoot) ; conteneur d'articles Articles = node 32176 (parent des dossiers-rubriques) ; media root L-Info = node 4306 ; auteurs l-info sous node 1238. Type linfoArticle.
  • Difference cle vs icogne : rangement par rubrique (pas par annee). articleMagazineRubric (code EN) -> LinfoRubricMap -> label FR, utilise a la fois comme dossier (linfoArticlesFolder sous 32176, trouve par nom sinon cree) et comme valeur section (Dropdown.Flexible, JSON array). Rubrique vide -> article directement sous 32176, sans section.
  • Champs specifiques l-info (absents d'icogne) : section (rubrique), categories (articleMagazineType CSV image/drawing/video/audio -> Genre Photo/Dessin/Vidéo/Audio, CheckBoxList JSON array), noEdition (articleMagazineEditionNumber). Re-tag isDrawing : si Genre contient Dessin et image resolue, media.isDrawing=true.
  • Conversion du corps (delta majeur vs icogne) : le macro dominant l-info est DividedContent (132/397 articles ; WidgetDocument/WidgetSlider marginaux). Conversion DividedContent (macro picker=<DivContent>) -> dividedContentBlock : les enfants DivItem -> BlockList divItems de divItemElement (divItemTitle, divItemContent HTML, divItemImage MediaPicker3 resolu par legacyId). divContentPresentation (int) -> divPresentation (Dropdown, mapping direct int->id : 1 Zones rétractables, 2 Onglets, 3 2 colonnes, 4 3 colonnes, 5 4 colonnes ; 0/inconnu -> vide). Formulaires -> repere HTML. Macro imbrique dans divItemContent (5 cas, tous WidgetDocument) : divItemContent etant un RTE simple (sans blocs), le WidgetDocument est converti en lien(s) HTML inline <a href="/media/...">nom</a> (path legacy = path cible) ; tout autre macro imbrique -> commentaire.
  • Auteurs : authors fusionne deux champs legacy — articleMagazineAuthorsText et articleMagazineAuthorsMultimedia (159 articles, 2e liste distincte : photographes/videastes) — dedup. Resolution scopee (prefere un auteur sous 1238, fallback global).
  • Geolocalisation : articleGeolocations (Archetype legacy, location -> position.datum "lat,lng" + lookup + zoom) -> localisation (Our.Umbraco.GMaps, format {address:{full_address,coordinates:{lat,lng},...},mapconfig:{zoom,maptype,centerCoordinates}}). 348/397 articles ont une geoloc reelle ; les coquilles vides sont ignorees.
  • Legende d'image : articleImageCaption (217) n'a pas de cible article -> stockee dans le description du media image (images non partagees entre articles, pas d'ecrasement d'une description existante).
  • Non mappe : articleImportant (28, flag "a la une" — aucun champ cible), articleDisqusDisplay, articleUrlPrediction, articleStorageDisplayWidgetArea (obsoletes/internes).
  • Controller : LegacyLinfoImportController (#if DEBUG, aucune auth, execution locale), memes endpoints GET qu'icogne (RetagMedia sur 4306, ImportMissingMedia 1486->4306, RunArticles avec skip/take/dryRun/replace). Etat de publication conserve, dedup/replace par legacyId, notifications supprimees -> recalculer mediaIndex via ~/umbraco/api/Multimedia/Reindex.
  • DownloadMediaFiles (rootId def 4306 / skip/take/dryRun/overwrite) : recupere les fichiers physiques absents en local. Parcourt l'arbre media (4306 par defaut, hors dossiers) et, pour chaque media dont le binaire manque (MediaFileManager.FileSystem.FileExists), telecharge {LegacyImport:MediaBaseUrl|https://www.l-info.ch}{/media/...} et l'ecrit via MediaFileManager.FileSystem.AddFile. Idempotent (skip si present, sauf overwrite), 404 -> warning. A la difference des autres endpoints d'import, il n'est PAS #if DEBUG : il vit dans son propre controller MediaDownloadApiController et ne touche que le FileSystem (aucun noeud, aucun NuCache). Deux variantes :
    • POST ~/umbraco/api/MediaDownloadApi/DownloadMediaFiles protege par le secret PublishWebhook:Secret (header X-Webhook-Secret) -> appelable sur staging/prod apres deploiement.
    • GET ~/umbraco/api/MediaDownloadApi/DownloadMediaFilesDev (#if DEBUG + IsDevelopment, sans secret) pour le local. Ensuite synchroniser wwwroot/media vers la VM media si on l'a lance en local. Design : docs/superpowers/specs/2026-08-10-import-linfo-media-download-design.md.
  • Pre-requis schema : composition legacyRefContent attachee a linfoArticle (uSync/v9/ContentTypes/linfoarticle.config, Key 1e9ac0de-0001-0001-0001-000000000001), a importer avant le run. legacyRefMedia couvre deja tous les media types.
  • Workflow complet : purge manuelle des 12 dossiers-rubriques (les 378 articles de l'ancien import PowerShell, sans legacyId) -> RetagMedia -> ImportMissingMedia -> DownloadMediaFiles (dryRun puis complet) -> synchro wwwroot/media -> VM media -> RunArticles (dryRun+take=5 puis complet) -> RunMultiBands (dryRun+take=5 puis complet) -> Multimedia/Reindex + reconstruire l'index Examine.

Phase 2 — MultiBands -> blocks (BlockGrid) (2026-08)

Design : docs/superpowers/specs/2026-08-10-import-linfo-multibands-phase2/ (SDD). Le systeme legacy MultiBands (MultiBandsContainer id doctype 630, MultiBandsItem/bande id 631, BandColumn id 639 — ~213 articles concernes) porte le gros du contenu riche l-info dans bandColumnRichText (galeries WidgetSlider, YouTube/iframes, fichiers WidgetDocument, DividedContent imbriques, formulaires). Converti vers le blocks (BlockGrid) de baseArticle.

  • MultiBandsConverter (src/Web/Services/Import/MultiBandsConverter.cs) : convertisseur pur et unit-teste (aucun acces DB direct — tout est injecte via delegates : contentTypeKeyByAlias, resolveMediaKeys, readPicker, readDivContent, warn). HtmlToBlockGridItems(html) segmente un fragment HTML en items BlockGrid ordonnes : texte -> rteBlock, iframe YouTube -> videoBlock, macro WidgetSlider/WidgetDocument -> galleryBlock/filesBlock, macro DividedContent -> dividedContentBlock (BlockList imbriquee divItemElement, divPresentation = colonnes / zones retractables / onglets, contenu recursif via HtmlToBlockGridItems), macro *Form* -> repere HTML dans un codeBlock, macro inconnue -> codeBlock commentaire. TitleBlock(title) emet un titleBlock seul (titre de bande).
  • Fonds de bande (thèmes)MapBandTheme(theme) mappe un thème legacy (multiBandsTheme, à défaut bandColumnTheme de la 1re colonne) vers une valeur dtl-bg-* : info_sup->backgroundColor dtl-bg-black ; cyan->backgroundColor dtl-bg-cyan ; brun-light->backgroundTexture dtl-bg-brown-light ; pattern->backgroundTexture dtl-bg-brown-texture ; brun->backgroundTexture dtl-bg-brown ; sinon (gray-light, separate, default, vide) -> aucun fond. WrapBandInLayout enveloppe alors tous les blocks d'une bande à thème dans un conteneur oneColumnsGridLayout (area « all » e0fa5134-…) porteur du fond ; une bande sans thème reste à plat (inchangé). Aucun nouveau schéma : oneColumnsGridLayout possède déjà backgroundColor (a6a4240d…) / backgroundTexture (be4aabee…), dont les valeurs sont les classes .dtl-bg-* (rendues par le partial oneColumnsGridLayout.cshtml côté frontend accm-website).
  • Endpoint RunMultiBands (skip/take/dryRun/replace) dans LegacyLinfoImportController : pour chaque article legacy ayant un MultiBandsContainer enfant, lit ses bandes ordonnees (MultiBandsItem, titre multiBandsTitle) et, pour chaque bande, ses colonnes ordonnees (BandColumn.bandColumnRichText ; a defaut, repli sur multiBandsContent de la bande) ; assemble par bande le titre (si present) + blocs de chaque colonne, puis enveloppe la bande dans un oneColumnsGridLayout à fond si elle porte un thème (MapBandTheme/WrapBandInLayout, cf. ci-dessus), et pose le tout dans un seul blocks (BlockGrid) sur l'article cible (retrouve via legacyId, cf. RunArticles). N'ecrase pas un blocks deja rempli sauf replace=true. A executer apres RunArticles (necessite le mapping legacyId -> nodeId). Notifications supprimees comme les autres endpoints -> Multimedia/Reindex a rejouer ensuite.

Contributors

No contributors

Changelog

No recent changes