From d42337225182b5387dde78c020cda17e0d2e68c6 Mon Sep 17 00:00:00 2001 From: Ladebeze66 Date: Tue, 28 Apr 2026 12:22:24 +0200 Subject: [PATCH] extract_picture --- .gitignore | 3 + app/components/ContentSection.tsx | 3 +- docs-site-interne/06-strapi-extraction.md | 12 +- package-lock.json | 673 +++++++++++++++--- package.json | 7 +- .../media-sync/01-fetch-inventory.js | 139 ++++ strapi_extraction/media-sync/02-download.js | 108 +++ .../media-sync/03-convert-webp.js | 133 ++++ .../media-sync/04-upload-replace.js | 270 +++++++ strapi_extraction/media-sync/README.md | 58 ++ strapi_extraction/media-sync/config.js | 78 ++ .../media-sync/lib/collect-media.js | 41 ++ 12 files changed, 1417 insertions(+), 108 deletions(-) create mode 100644 strapi_extraction/media-sync/01-fetch-inventory.js create mode 100644 strapi_extraction/media-sync/02-download.js create mode 100644 strapi_extraction/media-sync/03-convert-webp.js create mode 100644 strapi_extraction/media-sync/04-upload-replace.js create mode 100644 strapi_extraction/media-sync/README.md create mode 100644 strapi_extraction/media-sync/config.js create mode 100644 strapi_extraction/media-sync/lib/collect-media.js diff --git a/.gitignore b/.gitignore index be7cacb..5012480 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ llm-api/*.pyc # Legacy RAG index (ChromaDB) — obsolete depuis bascule graph+BM25 /chroma-index/ +# Téléchargements + WebP générés par strapi_extraction/media-sync/ (poids important) +strapi_extraction/extract/media-sync-work/ + diff --git a/app/components/ContentSection.tsx b/app/components/ContentSection.tsx index fb2a531..a0a0e94 100644 --- a/app/components/ContentSection.tsx +++ b/app/components/ContentSection.tsx @@ -6,6 +6,7 @@ import { fetchData } from "../utils/fetchData"; import { getApiUrl } from "../utils/getApiUrl"; import Carousel from "./Carousel"; import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; interface ImageData { url: string; @@ -211,7 +212,7 @@ export default function ContentSection({ prose-li:marker:text-primary prose-hr:border-0 prose-hr:w-16 prose-hr:mx-auto prose-hr:bg-primary/30 prose-hr:h-0.5 prose-hr:rounded-full prose-hr:my-6" > - {richText} + {richText} )} diff --git a/docs-site-interne/06-strapi-extraction.md b/docs-site-interne/06-strapi-extraction.md index f3cb9da..eb1131c 100644 --- a/docs-site-interne/06-strapi-extraction.md +++ b/docs-site-interne/06-strapi-extraction.md @@ -1,6 +1,6 @@ # Outils `strapi_extraction/` -**Dernière mise à jour :** 2026-04-22 +**Dernière mise à jour :** 2026-04-28 Dossier de **scripts Node + Python** pour extraire, nettoyer et convertir les données issues de l'API Strapi en base de connaissance chatbot (hors runtime @@ -84,8 +84,18 @@ comme source de vérité sans comparer au CMS. Ces points seront corrigés en même temps que l'enrichissement du vault (glossaire + homepage Strapi → notes `40-Glossaire/` et `30-Parcours/`). +## Sync médias WebP (hors pipeline GrasBot) + +Dossier **`strapi_extraction/media-sync/`** — inventaire des fichiers image liés aux +content-types (`projects`, `competences`, `homepages`, `realisation-ias`, `glossaires`), +téléchargement classé par rubrique, conversion WebP (sharp), puis ré-upload optionnel. + +Documentation : voir `strapi_extraction/media-sync/README.md`. +Sortie lourde (ignorée Git) : `strapi_extraction/extract/media-sync-work/`. + ## Liens complémentaires - Vault + retrieval : [`08-vault-obsidian-retrieval.md`](./08-vault-obsidian-retrieval.md) - API LLM : [`04-api-llm-et-chatbot.md`](./04-api-llm-et-chatbot.md) - Schémas Strapi : [`03-cms-strapi.md`](./03-cms-strapi.md) +- Performances images (audit) : [`09-performances-images.md`](./09-performances-images.md) diff --git a/package-lock.json b/package-lock.json index c3bf24f..a6e9f48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "postcss": "^8", + "sharp": "^0.33.5", "tailwindcss": "^3.4.1", "typescript": "^5" } @@ -47,9 +48,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "license": "MIT", "optional": true, "dependencies": { @@ -57,9 +58,9 @@ } }, "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, "engines": { @@ -67,12 +68,13 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -85,16 +87,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -107,16 +110,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.0.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -127,12 +131,13 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -143,12 +148,13 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "cpu": [ "arm" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -159,12 +165,13 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -207,12 +214,13 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "cpu": [ "s390x" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -223,12 +231,13 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -239,12 +248,13 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -255,12 +265,13 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -271,12 +282,13 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -289,16 +301,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.0.5" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -311,7 +324,7 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, "node_modules/@img/sharp-linux-ppc64": { @@ -359,12 +372,13 @@ } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -377,16 +391,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -399,16 +414,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -421,16 +437,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -443,20 +460,21 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@emnapi/runtime": "^1.2.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -485,12 +503,13 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -504,12 +523,13 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1164,6 +1184,20 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1182,6 +1216,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -1276,8 +1321,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -1860,6 +1905,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3058,6 +3110,367 @@ } } }, + "node_modules/next/node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/next/node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/next/node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -3086,6 +3499,51 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/next/node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3713,8 +4171,8 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "devOptional": true, "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, @@ -3723,16 +4181,16 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -3741,30 +4199,25 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" } }, "node_modules/shebang-command": { @@ -3872,6 +4325,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index a78d7fd..b7e2f9c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,11 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "media:inventory": "node strapi_extraction/media-sync/01-fetch-inventory.js", + "media:download": "node strapi_extraction/media-sync/02-download.js", + "media:webp": "node strapi_extraction/media-sync/03-convert-webp.js", + "media:upload": "node strapi_extraction/media-sync/04-upload-replace.js" }, "dependencies": { "@strapi/blocks-react-renderer": "^1.0.1", @@ -31,6 +35,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "postcss": "^8", + "sharp": "^0.33.5", "tailwindcss": "^3.4.1", "typescript": "^5" } diff --git a/strapi_extraction/media-sync/01-fetch-inventory.js b/strapi_extraction/media-sync/01-fetch-inventory.js new file mode 100644 index 0000000..7831519 --- /dev/null +++ b/strapi_extraction/media-sync/01-fetch-inventory.js @@ -0,0 +1,139 @@ +/** + * 01 — Appelle l’API Strapi (lecture publique) et produit media-inventory.json + * (liste de tous les fichiers image liés aux content-types configurés). + * + * Usage : node strapi_extraction/media-sync/01-fetch-inventory.js + */ +const fs = require("fs"); +const path = require("path"); +const { + API_BASE, + WORK_ROOT, + FILE_INVENTORY, + COLLECTIONS, + PAGE_SIZE, +} = require("./config"); +const { normalizeField, safeSlug } = require("./lib/collect-media"); + +function unwrapEntry(entry) { + if (!entry) return null; + if (entry.attributes) { + return { + ...entry.attributes, + id: entry.id, + documentId: entry.documentId, + }; + } + return entry; +} + +async function fetchJson(url) { + const r = await fetch(url); + if (!r.ok) { + const txt = await r.text(); + throw new Error(`HTTP ${r.status} ${url}\n${txt.slice(0, 500)}`); + } + return r.json(); +} + +async function fetchAllEntries(plural) { + const out = []; + let page = 1; + /* eslint-disable no-constant-condition */ + while (true) { + const params = new URLSearchParams(); + params.set("pagination[page]", String(page)); + params.set("pagination[pageSize]", String(PAGE_SIZE)); + /** + * Strapi v5 : populate[picture]=* peut provoquer « Invalid key related » selon versions. + * populate=* hydrate les relations premier niveau (y compris les médias). + */ + params.set("populate", "*"); + + const url = `${API_BASE}/${plural}?${params}`; + const json = await fetchJson(url); + const rows = Array.isArray(json.data) ? json.data : []; + for (const row of rows) { + out.push(unwrapEntry(row)); + } + const pageCount = json.meta?.pagination?.pageCount ?? 1; + if (page >= pageCount || rows.length === 0) break; + page += 1; + await new Promise((r) => setTimeout(r, 200)); + } + return out; +} + +async function main() { + console.log("🔍 STRAPI_URL (origine) →", require("./config").STRAPI_URL); + console.log("🔍 API_BASE →", API_BASE); + + if (!fs.existsSync(WORK_ROOT)) { + fs.mkdirSync(WORK_ROOT, { recursive: true }); + } + + const inventory = { + generatedAt: new Date().toISOString(), + apiBase: API_BASE, + files: [], + }; + + /** @type {typeof inventory.files} */ + const records = []; + + for (const col of COLLECTIONS) { + console.log(`\n📂 ${col.plural} (${col.section})…`); + let entries; + try { + entries = await fetchAllEntries(col.plural); + } catch (e) { + console.error(` ⚠️ Endpoint indisponible ou erreur : ${e.message}`); + continue; + } + + console.log(` ${entries.length} entrée(s)`); + + for (const entry of entries) { + if (!entry) continue; + const slug = safeSlug(entry); + + for (const field of col.fields) { + const items = normalizeField(entry[field.name], field.multiple); + for (const { file, index } of items) { + const base = path.basename(file.url.split("?")[0] || "file"); + records.push({ + collectionPlural: col.plural, + collectionSingular: col.singular, + strapiRef: col.ref, + section: col.section, + entrySlug: slug, + entryId: entry.id, + entryDocumentId: entry.documentId ?? null, + fieldName: field.name, + fieldMultiple: field.multiple, + fieldIndex: index, + fileId: file.id, + fileDocumentId: file.documentId ?? null, + filename: file.name || base, + url: file.url, + mime: file.mime, + size: file.size, + ext: file.ext, + /** chemin relatif WORK_ROOT/downloaded/... rempli par 02 */ + relativeDownloadPath: null, + }); + } + } + } + } + + inventory.files = records.slice().sort((a, b) => a.fileId - b.fileId); + + fs.writeFileSync(FILE_INVENTORY, JSON.stringify(inventory, null, 2), "utf8"); + console.log(`\n✅ Inventaire : ${inventory.files.length} fichier(s) → ${FILE_INVENTORY}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/strapi_extraction/media-sync/02-download.js b/strapi_extraction/media-sync/02-download.js new file mode 100644 index 0000000..c66f74a --- /dev/null +++ b/strapi_extraction/media-sync/02-download.js @@ -0,0 +1,108 @@ +/** + * 02 — Télécharge chaque fichier unique de l’inventaire vers + * extract/media-sync-work/downloaded/{section}/{slug}/... + * et met à jour relativeDownloadPath dans media-inventory.json + * + * Usage : node strapi_extraction/media-sync/02-download.js + */ +const fs = require("fs"); +const path = require("path"); +const { STRAPI_URL, FILE_INVENTORY, WORK_ROOT, DIR_DOWNLOADED } = require("./config"); + +function safeFilePart(name) { + return String(name || "file") + .replace(/[<>:"/\\|?*\x00-\x1f]/g, "_") + .slice(0, 180); +} + +function absoluteUrl(relativeOrAbsolute) { + if (relativeOrAbsolute.startsWith("http://") || relativeOrAbsolute.startsWith("https://")) { + return relativeOrAbsolute; + } + return `${STRAPI_URL}${relativeOrAbsolute.startsWith("/") ? "" : "/"}${relativeOrAbsolute}`; +} + +async function downloadBuffer(url) { + const r = await fetch(url); + if (!r.ok) { + throw new Error(`HTTP ${r.status} ${url}`); + } + const buf = Buffer.from(await r.arrayBuffer()); + return buf; +} + +async function main() { + if (!fs.existsSync(FILE_INVENTORY)) { + console.error("Manque l’inventaire. Lance d’abord : 01-fetch-inventory.js"); + process.exit(1); + } + + const raw = fs.readFileSync(FILE_INVENTORY, "utf8"); + const inventory = JSON.parse(raw); + const files = inventory.files; + if (!Array.isArray(files) || files.length === 0) { + console.log("Inventaire vide — rien à télécharger."); + return; + } + + if (!fs.existsSync(DIR_DOWNLOADED)) { + fs.mkdirSync(DIR_DOWNLOADED, { recursive: true }); + } + + const byId = new Map(); + let ok = 0; + let skipped = 0; + let fail = 0; + + for (const row of files) { + const id = row.fileId; + if (byId.has(id)) { + row.relativeDownloadPath = byId.get(id); + skipped++; + continue; + } + + const rel = path.join( + row.section, + row.entrySlug, + `${id}_${safeFilePart(row.filename)}` + ); + const abs = path.join(DIR_DOWNLOADED, rel); + + try { + const url = absoluteUrl(row.url); + const buf = await downloadBuffer(url); + fs.mkdirSync(path.dirname(abs), { recursive: true }); + fs.writeFileSync(abs, buf); + row.relativeDownloadPath = rel.replace(/\\/g, "/"); + byId.set(id, row.relativeDownloadPath); + ok++; + console.log(`✅ ${rel} (${(buf.length / 1024).toFixed(1)} KB)`); + } catch (e) { + console.error(`❌ fileId=${id} : ${e.message}`); + row.relativeDownloadPath = null; + row.downloadError = String(e.message); + fail++; + } + } + + inventory.downloadedAt = new Date().toISOString(); + inventory.stats = { + uniqueFiles: byId.size, + rowsTotal: files.length, + downloadedOk: ok, + dedupSkipped: skipped, + failed: fail, + }; + + fs.writeFileSync(FILE_INVENTORY, JSON.stringify(inventory, null, 2), "utf8"); + console.log(`\n📁 Base téléchargements : ${DIR_DOWNLOADED}`); + console.log( + `Résumé : ${ok} téléchargement(s), ${skipped} lignes réutilisent un fichier déjà pris, ${fail} échec(s).` + ); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/strapi_extraction/media-sync/03-convert-webp.js b/strapi_extraction/media-sync/03-convert-webp.js new file mode 100644 index 0000000..194d9ee --- /dev/null +++ b/strapi_extraction/media-sync/03-convert-webp.js @@ -0,0 +1,133 @@ +/** + * 03 — Convertit les téléchargements en WebP (sharp) sous + * extract/media-sync-work/webp/{section}/{slug}/{fileId}_{stem}.webp + * Les SVG sont copiés en .svg (sans conversion raster). + * + * Usage : node strapi_extraction/media-sync/03-convert-webp.js + */ +const fs = require("fs"); +const path = require("path"); +const sharp = require("sharp"); +const { FILE_INVENTORY, DIR_DOWNLOADED, DIR_WEBP, WORK_ROOT } = require("./config"); + +const MAX_EDGE = 2560; +const WEBP_QUALITY = 82; + +function stemFromFilename(filename) { + const b = path.basename(filename); + const i = b.lastIndexOf("."); + return i <= 0 ? b : b.slice(0, i); +} + +async function toWebpRaster(srcPath, destPath) { + const meta = await sharp(srcPath).metadata(); + const w = meta.width || 0; + const h = meta.height || 0; + let pipeline = sharp(srcPath).rotate(); + if (w > MAX_EDGE || h > MAX_EDGE) { + pipeline = pipeline.resize(MAX_EDGE, MAX_EDGE, { + fit: "inside", + withoutEnlargement: true, + }); + } + fs.mkdirSync(path.dirname(destPath), { recursive: true }); + await pipeline.webp({ quality: WEBP_QUALITY, effort: 4 }).toFile(destPath); +} + +async function main() { + if (!fs.existsSync(FILE_INVENTORY)) { + console.error("Manque media-inventory.json — lance 01 puis 02."); + process.exit(1); + } + + const inventory = JSON.parse(fs.readFileSync(FILE_INVENTORY, "utf8")); + const files = inventory.files; + if (!Array.isArray(files) || !files.length) { + console.log("Rien à convertir."); + return; + } + + if (!fs.existsSync(WORK_ROOT)) fs.mkdirSync(WORK_ROOT, { recursive: true }); + if (!fs.existsSync(DIR_WEBP)) fs.mkdirSync(DIR_WEBP, { recursive: true }); + + const byId = new Map(); + let ok = 0; + let err = 0; + + for (const row of files) { + const id = row.fileId; + if (!row.relativeDownloadPath) { + row.relativeWebpPath = null; + continue; + } + if (byId.has(id)) { + row.relativeWebpPath = byId.get(id); + continue; + } + + const src = path.join(DIR_DOWNLOADED, row.relativeDownloadPath); + if (!fs.existsSync(src)) { + row.relativeWebpPath = null; + row.convertError = "fichier téléchargé manquant"; + err++; + continue; + } + + const ext = path.extname(src).toLowerCase(); + const baseStem = `${id}_${stemFromFilename(row.filename)}`; + const relDir = path.join(row.section, row.entrySlug); + let relOutFile; + let absOut; + + try { + if (ext === ".svg") { + relOutFile = path.join(relDir, `${baseStem}.svg`).replace(/\\/g, "/"); + absOut = path.join(DIR_WEBP, relOutFile); + fs.mkdirSync(path.dirname(absOut), { recursive: true }); + fs.copyFileSync(src, absOut); + console.log(`svg-copy → ${relOutFile}`); + } else if (ext === ".webp") { + relOutFile = path.join(relDir, `${baseStem}.webp`).replace(/\\/g, "/"); + absOut = path.join(DIR_WEBP, relOutFile); + fs.mkdirSync(path.dirname(absOut), { recursive: true }); + fs.copyFileSync(src, absOut); + console.log(`webp-copy → ${relOutFile}`); + } else if ([".png", ".jpg", ".jpeg", ".tif", ".tiff"].includes(ext)) { + relOutFile = path.join(relDir, `${baseStem}.webp`).replace(/\\/g, "/"); + absOut = path.join(DIR_WEBP, relOutFile); + await toWebpRaster(src, absOut); + console.log(`webp-transform → ${relOutFile}`); + } else { + relOutFile = path.join(relDir, path.basename(src)).replace(/\\/g, "/"); + absOut = path.join(DIR_WEBP, relOutFile); + fs.mkdirSync(path.dirname(absOut), { recursive: true }); + fs.copyFileSync(src, absOut); + console.log(`raw-copy → ${relOutFile}`); + } + + row.relativeWebpPath = relOutFile; + byId.set(id, relOutFile); + ok++; + } catch (e) { + row.relativeWebpPath = null; + row.convertError = String(e.message); + err++; + console.error(`❌ fileId ${id}: ${e.message}`); + } + } + + inventory.convertedAt = new Date().toISOString(); + inventory.convertSettings = { + maxEdgePx: MAX_EDGE, + webpQuality: WEBP_QUALITY, + }; + + fs.writeFileSync(FILE_INVENTORY, JSON.stringify(inventory, null, 2), "utf8"); + console.log(`\n✅ Fichiers uniques traités : ${ok}, erreurs : ${err}`); + console.log(`📁 ${DIR_WEBP}`); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/strapi_extraction/media-sync/04-upload-replace.js b/strapi_extraction/media-sync/04-upload-replace.js new file mode 100644 index 0000000..cdbcd25 --- /dev/null +++ b/strapi_extraction/media-sync/04-upload-replace.js @@ -0,0 +1,270 @@ +/** + * 04 — Ré-upload les WebP générés vers Strapi et remplace les entrées média dans + * les fiches concernées (API authentifiée). + * + * Requiert dans l’env : STRAPI_API_TOKEN (JWT Strapi avec droits upload + mise à jour + * des collection types utilisés ; typiquement un jeton Full access en local). + * + * Par défaut : **dry-run** (aucune mutation). + * Mutation réelle : node strapi_extraction/media-sync/04-upload-replace.js --execute + * + * DANGER : sauvegarder au préalable votre base Strapi / médias. Tester sur une + * copie locale (Strapi localhost + même base si possible). + */ +const fs = require("fs"); +const path = require("path"); +require("dotenv").config({ + path: path.join(__dirname, "../../cmsbackend/.env"), +}); +require("dotenv").config({ + path: path.join(__dirname, "../../.env.local"), +}); + +const { + FILE_INVENTORY, + DIR_WEBP, + API_BASE, +} = require("./config"); + +const TOKEN = process.env.STRAPI_API_TOKEN; +const EXECUTE = process.argv.includes("--execute"); + +function mimeForFile(filePath) { + const ext = path.extname(filePath).toLowerCase(); + const map = { + ".webp": "image/webp", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + }; + return map[ext] || "application/octet-stream"; +} + +/** Compte combien de lignes d’inventaire pointent le même fileId */ +function countRefs(files) { + const c = {}; + for (const r of files) { + c[r.fileId] = (c[r.fileId] || 0) + 1; + } + return c; +} + +/** + * Nom réservé multipart (ByteString) : les noms avec caractères hors U+00–U+FF + * (ex. U+2194 flèches dans des noms générés) provoquent l’erreur Node + * « Cannot convert argument to a ByteString » sur form.append(..., filename). + */ +function asciiUploadName(filePath, fileId) { + const ext = path.extname(filePath).toLowerCase(); + const extOk = /^\.(webp|svg|png|jpe?g|gif|avif)$/.test(ext) ? ext : ".webp"; + return `upload-${fileId}${extOk}`; +} + +async function postUpload(filePath, fileId) { + const buf = fs.readFileSync(filePath); + const asciiName = asciiUploadName(filePath, fileId); + const blob = new Blob([buf], { type: mimeForFile(filePath) }); + const body = new FormData(); + body.append("files", blob, asciiName); + + const res = await fetch(`${API_BASE}/upload`, { + method: "POST", + headers: TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}, + body, + }); + if (!res.ok) { + const t = await res.text(); + throw new Error(`POST /upload ${res.status} ${t.slice(0, 800)}`); + } + const json = await res.json(); + const arr = Array.isArray(json) ? json : json?.data; + if (!Array.isArray(arr) || !arr[0]) { + throw new Error(`Réponse upload inattendue : ${JSON.stringify(json).slice(0, 400)}`); + } + return arr[0]; +} + +function unwrapEntry(raw) { + if (!raw) return null; + if (raw.data) return unwrapEntry(raw.data); + if (raw.attributes) { + return { + ...raw.attributes, + id: raw.id, + documentId: raw.documentId, + }; + } + return raw; +} + +async function getEntryPlural(plural, documentId) { + const qs = new URLSearchParams({ populate: "*" }); + const url = `${API_BASE}/${plural}/${documentId}?${qs}`; + const res = await fetch(url, { + headers: TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}, + }); + if (!res.ok) { + const t = await res.text(); + throw new Error(`GET ${url} → ${res.status} ${t.slice(0, 400)}`); + } + const json = await res.json(); + return unwrapEntry(json); +} + +function getMediaIdsFromField(entry, fieldName, multiple) { + const v = entry[fieldName]; + if (multiple) { + const arr = Array.isArray(v) ? v : []; + return arr.map((x) => (typeof x === "object" && x !== null ? x.id : x)).filter(Boolean); + } + if (!v) return []; + if (typeof v === "object" && v.id) return [v.id]; + if (typeof v === "number") return [v]; + return []; +} + +async function putEntry(plural, documentId, payloadData) { + const url = `${API_BASE}/${plural}/${documentId}`; + const res = await fetch(url, { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...(TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}), + }, + body: JSON.stringify({ data: payloadData }), + }); + if (!res.ok) { + const t = await res.text(); + throw new Error(`PUT ${url} → ${res.status} ${t.slice(0, 800)}`); + } + return res.json(); +} + +async function deleteFile(fileId) { + const url = `${API_BASE}/upload/files/${fileId}`; + const res = await fetch(url, { + method: "DELETE", + headers: TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}, + }); + if (!res.ok) { + const t = await res.text(); + throw new Error(`DELETE ${url} → ${res.status} ${t.slice(0, 400)}`); + } +} + +async function main() { + if (!fs.existsSync(FILE_INVENTORY)) { + console.error("Manque media-inventory.json"); + process.exit(1); + } + + if (!TOKEN) { + console.error( + "STRAPI_API_TOKEN manquant. Crée un jeton API dans Strapi (Settings → API Tokens), puis exporte :\n" + + " Windows PowerShell : $env:STRAPI_API_TOKEN=\"…\"\n" + + " ou ajoute STRAPI_API_TOKEN dans cmsbackend/.env (non commité)." + ); + process.exit(1); + } + + const inventory = JSON.parse(fs.readFileSync(FILE_INVENTORY, "utf8")); + const files = inventory.files || []; + const refCount = countRefs(files); + + const candidates = files.filter( + (r) => + r.relativeWebpPath && + r.relativeWebpPath.endsWith(".webp") && + r.entryDocumentId + ); + + console.log( + `Lignes inventaire : ${files.length} — candidats WebP remplaçables (avec documentId) : ${candidates.length}` + ); + console.log(`Mode : ${EXECUTE ? "EXECUTE (mutations réelles)" : "DRY-RUN (aucune mutation)"}\n`); + + if (!EXECUTE) { + console.log( + "Exemple d’actions qui seraient effectuées avec --execute :\n" + + " 1. POST /api/upload pour chaque fichier .webp sous webp/\n" + + " 2. PUT /api/:collection/:documentId avec le champ média mis à jour (ids)\n" + + " 3. DELETE /api/upload/files/:oldFileId uniquement si l’ancien id n’a qu’une référence dans l’inventaire\n" + ); + for (let i = 0; i < Math.min(5, candidates.length); i++) { + const r = candidates[i]; + console.log( + ` • fileId=${r.fileId} → ${r.relativeWebpPath} → entrée ${r.collectionPlural} documentId=${r.entryDocumentId} champ=${r.fieldName}[${r.fieldIndex}]` + ); + } + console.log("\nAjoute --execute pour réellement téléverser (après avoir testé la sauvegarde)."); + return; + } + + /** Par sécurité, traite fichier par fichier ; re-fetch après chaque succès évite désalignement indices */ + for (const row of candidates) { + const src = path.join(DIR_WEBP, row.relativeWebpPath.replace(/\//g, path.sep)); + if (!fs.existsSync(src)) { + console.warn(`SKIP fileId=${row.fileId} : fichier absent ${src}`); + continue; + } + + const docId = row.entryDocumentId; + const plural = row.collectionPlural; + const field = row.fieldName; + const idx = row.fieldIndex; + const oldId = row.fileId; + + try { + const uploaded = await postUpload(src, row.fileId); + const newId = uploaded.id; + + const entry = await getEntryPlural(plural, docId); + if (!entry) throw new Error("entrée introuvable"); + + const multiple = row.fieldMultiple; + const currentIds = getMediaIdsFromField(entry, field, multiple); + + if (multiple) { + if (idx < 0 || idx >= currentIds.length) { + throw new Error(`index ${idx} hors limites (ids actuels : ${currentIds.join(",")})`); + } + if (currentIds[idx] !== oldId) { + throw new Error( + `id à l’index ${idx} est ${currentIds[idx]}, attendu ${oldId} — arrêt pour éviter corruption` + ); + } + const next = currentIds.slice(); + next[idx] = newId; + await putEntry(plural, docId, { [field]: next }); + } else { + const cur = currentIds[0]; + if (cur != null && cur !== oldId) { + throw new Error(`champ simple : attendu ancien id ${oldId}, vu ${cur}`); + } + await putEntry(plural, docId, { [field]: newId }); + } + + console.log(`OK upload+remplace : ${oldId} → ${newId} (${plural}/${docId})`); + + const canDeleteOld = refCount[oldId] === 1; + if (canDeleteOld && oldId !== newId) { + try { + await deleteFile(oldId); + console.log(` ancien fichier Strapi ${oldId} supprimé`); + } catch (de) { + console.warn(` suppression ancien échouée (non bloquant) : ${de.message}`); + } + } + } catch (e) { + console.error(`ÉCHEC fileId=${row.fileId} : ${e.message}`); + } + } + + console.log("\nTerminé. Recharge les fiches dans l’admin et vide le cache navigateur."); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/strapi_extraction/media-sync/README.md b/strapi_extraction/media-sync/README.md new file mode 100644 index 0000000..1a00eef --- /dev/null +++ b/strapi_extraction/media-sync/README.md @@ -0,0 +1,58 @@ +# Pipeline médias Strapi → dossiers par section → WebP → ré-upload optionnel + +**Répertoire de travail (lourd)** : `strapi_extraction/extract/media-sync-work/` — ignoré par Git (`/.gitignore`). + +## Prérequis + +- Node 18+ +- Dépendance `sharp` installée depuis la racine (`npm install` déjà fait si le dépôt à jour). + +## Variables optionnelles + +| Variable | Rôle | +|----------|------| +| `STRAPI_URL` | Origine Strapi sans `/api` (défaut depuis `NEXT_PUBLIC_API_URL` ou prod). | +| `STRAPI_API_TOKEN` | **Seulement** pour `04-upload-replace.js --execute`. Jeton créé dans Strapi → Settings → API Tokens (droits lecture + Upload + mise à jour des CT concernés). Ne pas committer ce jeton. | + +Chemins `.env.local` à la racine et `cmsbackend/.env` sont chargés par les scripts (`config.js` ou `04`). + +## Chaîne normale + +```powershell +# depuis la racine du repo J:\my-next-site + +npm run media:inventory # 01 — liste tous les médias utilisés → media-inventory.json + +npm run media:download # 02 — téléchargement physique par section sous downloaded/ + +npm run media:webp # 03 — conversion/copies sous webp/ + +npm run media:upload # 04 — dry-run uniquement (aucune mutation) + +# Mutation CMS (**attention**) après lecture ci‑dessous : + +$env:STRAPI_API_TOKEN="" +node strapi_extraction/media-sync/04-upload-replace.js --execute +``` + +### Organisation des dossiers + +- **`downloaded/{section}/{slug-du-contenu}/{fileId}_{nom-fichier}`** + Sections : `portfolio`, `competences`, `home`, `realisation-ias`, `glossaire`. +- **`webp/...`** même structure, avec `.webp` (ou `.svg` copié en clair). + +Les **fichiers média uniques** sont dédupliqués par `fileId` : une seule fois sur disque, plusieurs lignes dans l’inventaire peuvent référencer le même téléchargement. + +### Erreur « Cannot convert argument to a ByteString » (upload) + +Les noms de fichiers sur disque peuvent contenir des caractères Unicode (tirets fins, flèches dans des noms AI, etc.). Le `FormData` HTTP n’accepte qu’un nom de fichier **ASCII** dans l’en-tête multipart ; le script **`04-upload-replace.js`** renomme donc en interne chaque envoi en `upload-{fileId}.webp` (voir `asciiUploadName` dans le fichier). Relance `--execute` après mise à jour du script. + +### Avant le `--execute` sur la prod + +1. **Tester d’abord** contre une instance Strapi locale (ex. importer la base vers `cmsbackend`, `npm run develop`, puis `$env:STRAPI_URL="http://localhost:1337"` et un jeton local). +2. **Sauvegarder** la base et `/public/uploads`. +3. Lire le préambule dans `04-upload-replace.js` ; le script vérifie que l’ancien id correspond encore avant remplacement pour limiter les corruptions. + +## Si l’étape ré-upload vous suffit après conversion manuelle + +Vous pouvez aussi **importer uniquement les WebP** depuis l’admin Strapi puis réassigner les champs à la main ; ce dépôt ne vous oblige pas à utiliser `04`. diff --git a/strapi_extraction/media-sync/config.js b/strapi_extraction/media-sync/config.js new file mode 100644 index 0000000..c4e5968 --- /dev/null +++ b/strapi_extraction/media-sync/config.js @@ -0,0 +1,78 @@ +/** + * Configuration partagée — sync médias Strapi (téléchargement / WebP / ré-upload). + * Variables d'environnement (optionnelles) : + * STRAPI_URL — origine sans /api (ex. https://api.fernandgrascalvet.com ou http://localhost:1337) + * STRAPI_API_TOKEN — jeton API Strapi (requis pour 04-upload-replace.js --execute) + */ +require("dotenv").config({ path: require("path").join(__dirname, "../../.env.local") }); +require("dotenv").config({ path: require("path").join(__dirname, "../../cmsbackend/.env") }); + +const path = require("path"); + +const STRAPI_URL = + process.env.STRAPI_URL || + process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || + "https://api.fernandgrascalvet.com"; + +const API_BASE = `${STRAPI_URL}/api`; + +/** Sortie : mirrors extract/ existant (gitignored lourd) */ +const WORK_ROOT = path.join(__dirname, "../extract/media-sync-work"); +const DIR_DOWNLOADED = path.join(WORK_ROOT, "downloaded"); +const DIR_WEBP = path.join(WORK_ROOT, "webp"); +const FILE_INVENTORY = path.join(WORK_ROOT, "media-inventory.json"); + +/** + * Content-types avec champs média — aligné sur cmsbackend/src/api (schema.json par type). + * section = sous-dossier humain (portfolio, competences, …) + */ +const COLLECTIONS = [ + { + plural: "projects", + singular: "project", + ref: "api::project.project", + section: "portfolio", + fields: [{ name: "picture", multiple: true }], + }, + { + plural: "competences", + singular: "competence", + ref: "api::competence.competence", + section: "competences", + fields: [{ name: "picture", multiple: true }], + }, + { + plural: "homepages", + singular: "homepage", + ref: "api::homepage.homepage", + section: "home", + fields: [{ name: "photo", multiple: false }], + }, + { + plural: "realisation-ias", + singular: "realisation-ia", + ref: "api::realisation-ia.realisation-ia", + section: "realisation-ias", + fields: [{ name: "picture", multiple: true }], + }, + { + plural: "glossaires", + singular: "glossaire", + ref: "api::glossaire.glossaire", + section: "glossaire", + fields: [{ name: "images", multiple: true }], + }, +]; + +const PAGE_SIZE = 100; + +module.exports = { + STRAPI_URL, + API_BASE, + WORK_ROOT, + DIR_DOWNLOADED, + DIR_WEBP, + FILE_INVENTORY, + COLLECTIONS, + PAGE_SIZE, +}; diff --git a/strapi_extraction/media-sync/lib/collect-media.js b/strapi_extraction/media-sync/lib/collect-media.js new file mode 100644 index 0000000..c7ba6f0 --- /dev/null +++ b/strapi_extraction/media-sync/lib/collect-media.js @@ -0,0 +1,41 @@ +/** + * Helpers — extraire les fichiers média des réponses Strapi v5 (structure plate). + */ + +function isUploadedImage(obj) { + if (!obj || typeof obj !== "object" || typeof obj.url !== "string") return false; + if (!obj.url.includes("/uploads/")) return false; + if (obj.mime && !obj.mime.startsWith("image/")) return false; + return true; +} + +/** Liste { file, index } pour un champ média Strapi */ +function normalizeField(fieldVal, multiple) { + if (!fieldVal) return []; + if (multiple) { + const arr = Array.isArray(fieldVal) ? fieldVal : [fieldVal]; + return arr + .filter(isUploadedImage) + .map((file, index) => ({ file, index })); + } + return isUploadedImage(fieldVal) + ? [{ file: fieldVal, index: 0 }] + : []; +} + +function safeSlug(entry) { + const s = + entry.slug ?? + entry.documentId ?? + (entry.id != null ? String(entry.id) : "unknown"); + return String(s) + .replace(/[^\wÀ-ÖØ-öø-ÿ.-]+/gu, "_") + .slice(0, 96) + .replace(/^_|_$/g, "") || "entry"; +} + +module.exports = { + normalizeField, + safeSlug, + isUploadedImage, +};