Skip to content

feat: stockage des médias en PostgreSQL (alternative au S3)#458

Open
chaibax wants to merge 11 commits intonumerique-gouv:mainfrom
chaibax:feature/db-storage-backend
Open

feat: stockage des médias en PostgreSQL (alternative au S3)#458
chaibax wants to merge 11 commits intonumerique-gouv:mainfrom
chaibax:feature/db-storage-backend

Conversation

@chaibax
Copy link
Copy Markdown
Collaborator

@chaibax chaibax commented Mar 17, 2026

Contexte

Sur les plateformes PaaS (Scalingo, Heroku, Clever Cloud) ou les déploiements Docker, le système de fichiers est souvent éphémère : chaque redéploiement efface les fichiers uploadés. Cette PR ajoute une alternative au S3 : stocker les médias directement dans PostgreSQL.

Changements

  • Nouvelle app db_storage : modèle StoredFile (BinaryField), backend Django Storage complet, vue de service des fichiers avec cache headers
  • Commande migrate_s3_to_db : transfert des fichiers S3 vers la DB + mise à jour des URLs S3 dans les révisions Wagtail et les champs RichText
  • Commande migrate_files_to_db : migration du filesystem local vers la DB
  • Documentation : guide complet dans docs/db-storage.md
  • 17 tests unitaires couvrant le storage, les vues et la migration S3

Activation

SF_USE_DB_STORAGE=1

Priorité des backends

Priorité Backend Variable
1 S3 S3_HOST
2 PostgreSQL SF_USE_DB_STORAGE=1
3 FileSystem (défaut)

Test réel effectué

  1. Connexion au bucket S3 OVH sf-no-code (187 fichiers, 5.4 MB)
  2. Création d'une page Wagtail avec image servie depuis S3
  3. Exécution de migrate_s3_to_db → 189 fichiers transférés
  4. Bascule vers DB storage → image affichée depuis /db-storage/serve/

Limitations documentées

  • Adapté aux sites avec volume modéré de médias (< 500 Mo)
  • Pas de CDN (cache navigateur via Cache-Control: max-age=3600)
  • Augmente la taille des backups PostgreSQL

Test plan

  • python manage.py test db_storage — 17/17 tests passent
  • Test réel avec bucket S3 OVH → migration → vérification affichage
  • Test de non-régression global
  • Upload d'image via l'admin Wagtail en mode DB storage
  • Vérification des renditions d'images (redimensionnement)

🤖 Generated with Claude Code

chaibax and others added 5 commits March 5, 2026 12:20
Add a new `db_storage` app that provides a custom Django Storage backend
storing files directly in PostgreSQL. This offers a persistent storage
alternative for PaaS deployments (Scalingo, Heroku, etc.) where the
filesystem is ephemeral and S3 is not available.

Activated via SF_USE_DB_STORAGE=1. Priority: S3 > DB Storage > FileSystem.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds `migrate_s3_to_db` management command that:
- Downloads all files from S3 bucket and stores them as StoredFile entries
- Updates hardcoded S3 URLs in Wagtail Revision content_json
- Scans URLField/CharField and RichTextField on all models for S3 URLs
- Supports --dry-run, --skip-files, and --skip-urls options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add comprehensive documentation for the PostgreSQL media storage feature
including activation guide, S3 migration steps, and architecture overview.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix line length violations (>119 chars) in test_migrate_s3.py by
extracting S3 env dict to module constant. Remove unused imports
and fix black formatting in migrate_s3_to_db.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread config/settings.py Outdated
"forms",
"wagtail_honeypot",
"dashboard",
"db_storage",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ce serait mieux je pense d'ajouter l'app uniquement si SF_USE_DB_STORAGE est à True

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrigé dans 5afda59 : l'app est maintenant ajoutée conditionnellement via if SF_USE_DB_STORAGE: INSTALLED_APPS.insert(-1, "db_storage"), comme c'est fait pour SF_USE_WHITENOISE.

Comment thread config/urls.py Outdated
path("sitemap.xml", sitemap, name="xml_sitemap"),
path(settings.WAGTAILADMIN_PATH, include(wagtailadmin_urls)),
path("documents/", include(wagtaildocs_urls)),
path("db-storage/", include("db_storage.urls")),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pareil ici, que l'ajout de l'URL soit dépendant de la variable d'environnement

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrigé dans le même commit : la route est conditionnée par if settings.SF_USE_DB_STORAGE, comme PROCONNECT_ACTIVATED pour les URLs oidc.

L'app db_storage et sa route /db-storage/ ne sont ajoutées que si
SF_USE_DB_STORAGE=True, suivant le même pattern que SF_USE_WHITENOISE
et PROCONNECT_ACTIVATED.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread config/urls.py Outdated
TemplateView.as_view(template_name="robots.txt", content_type="text/plain"),
),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
] + static(settings.MEDIA_URL, document_root=getattr(settings, "MEDIA_ROOT", ""))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A priori, pas besoin de changer ici car MEDIA_ROOT n'est utile que dans le cas d'utilisation d'un file system storage et il est déjà défini dans la conditionnelle à son utilisation dans config/settings.py

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merci, j'ai enlevé

chaibax and others added 3 commits March 18, 2026 13:39
- Add noqa: F405 for INSTALLED_APPS references from star import
- Revert getattr(settings, "MEDIA_ROOT", "") to settings.MEDIA_ROOT
  since Django provides a default empty string in global_settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add warning that DB storage is not recommended beyond 1 GB of media
  in settings.py, .env.example, and docs/db-storage.md
- Add migrate_db_to_s3 management command to transfer stored files
  from PostgreSQL back to an S3 bucket (with --dry-run support and
  skip-if-exists logic)
- Add 5 tests for the new command

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread config/settings.py Outdated
# Allow storing media files in PostgreSQL instead of the filesystem (disabled by default)
# Useful for PaaS deployments with ephemeral filesystems (Scalingo, Heroku, etc.)
# /!\ Not recommended beyond 1 GB of media — prefer S3 for larger volumes.
# Priority: S3 > DB Storage > FileSystem
Copy link
Copy Markdown
Collaborator

@Ash-Crow Ash-Crow Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'aurais dit le contraire.

Suggested change
# Priority: S3 > DB Storage > FileSystem
# Priority: FileSystem > S3 > DB Storage

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je suis d'accord, je n'avais pas vu

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remplacé "Priority" par "Selection order: S3_HOST wins if set, then SF_USE_DB_STORAGE, then filesystem (default)" — distingue la précédence dans le code de la recommandation d'usage. Corrigé dans dddfba2.

Comment thread docs/db-storage.md Outdated
|----------|---------|----------------------|-------------|
| 1 | **S3** (Object Storage) | `S3_HOST` | Production avec stockage S3 compatible |
| 2 | **PostgreSQL** (DB Storage) | `SF_USE_DB_STORAGE=1` | PaaS sans S3, Docker, Plesk |
| 3 | **Système de fichiers** | _(par défaut)_ | Développement local |
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idem ci-dessus.

Dans les cas d'usage je rajouterais Hébergement sur VPS.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tableau inversé (filesystem en premier) et ajout de VPS + hébergement dédié dans les cas d'usage. Corrigé dans dddfba2.

Replace misleading "Priority" wording with "Selection order" to
distinguish code precedence from recommendation. Add VPS to
filesystem use cases in documentation table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread config/settings_test.py
Comment on lines +5 to +6
if "db_storage" not in INSTALLED_APPS: # noqa: F405
INSTALLED_APPS.insert(-1, "db_storage") # noqa: F405
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pourquoi ajouter des noqa ?

Comment thread config/urls.py

if settings.SF_USE_DB_STORAGE:
urlpatterns += [
path("db-storage/", include("db_storage.urls")),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c'est généralement un anti pattern de servir les médias depuis une vue django, c'est assez bien expliqué dans la doc de whitenoise https://whitenoise.readthedocs.io/en/stable/django.html#serving-media-files

Ici ca va rendre compliqué de scaler car chaque chargement d'image va être géré par django et occuper le worker prévu pour servir des pages

@@ -0,0 +1,43 @@
# Generated by Django 6.0.3 on 2026-03-05 11:15
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ca me semble overkill de créer une app django dédiée à ca, ca pourrait vivre dans config je pense pour ne pas gener les réutilisations (actuelles ou futures) de sites conformes sous forme d'app django

Comment thread db_storage/views.py
Comment on lines +14 to +16
name = request.GET.get("name")
if not name:
return HttpResponseNotFound("File not found.")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ici je pense qu'on aurait intérêt à mieux utiliser le routeur django avec une valeur accessible directement en argument de la fonction : https://docs.djangoproject.com/fr/6.0/topics/http/urls/#example

Copy link
Copy Markdown
Contributor

@fabienheureux fabienheureux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je me suis permis une review car j'ai vu le sujet discuté sur Mattermost, ca me semble assez cavalier comme approche vu que des sites avec plusieurs centaines de pages risquent d'avoir de nombreuses images stockées, l'approche serait intéressante niveau simplicité d'hébergement pour quelques landing pages mais pas tellement pour des sites pouvant stocker plusieurs milliers d'images, car elle risque de présenter rapidement des problèmes de scalabilité

@chaibax
Copy link
Copy Markdown
Collaborator Author

chaibax commented Mar 30, 2026

Merci pour ta review @fabienheureux . Je suis d'accord avec toi sur la scalabilité.

C'est bien pour quelques landing pages que nous faisons ça. 30 petits sites (images/trafics etc) qui doivent migrer rapidement, et pour qui prendre un S3 est très compliqué administrativement .

Wagtail 3+ renamed Revision.content_json (TextField) to
Revision.content (JSONField). The migration script now serializes
the JSON content to string for URL search/replace, then parses
back. Adds test covering revision URL updates.

Fixes numerique-gouv#482 (FieldError on Scalingo deployment)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v3.1 scheduled for v.3.1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants