Products Manager APP

Technical

Media Storage

Media Storage

Products Manager gère ~922 000 objets en production sur un stockage S3-compatible. L'architecture repose sur une abstraction boto3/aiobotocore qui permet d'utiliser OVH S3 en production et MinIO en développement/staging sans modifier une ligne de code applicatif.


Architecture

Production vs Développement

EnvironnementBackendBucketEndpoint
ProductionOVH Object Storage S3pixee-s3s3.gra.io.cloud.ovh.net
StagingMinIO (Docker)staging-mediaminio:9000 (interne)
DéveloppementMinIO (Docker)dev-medialocalhost:9000

Abstraction boto3/aiobotocore

Le code applicatif interagit exclusivement avec l'interface S3 standard. Le choix du backend (OVH vs MinIO) se fait uniquement via les variables d'environnement :

# storage/s3_client.py
import aiobotocore.session

async def get_s3_client():
    session = aiobotocore.session.get_session()
    return session.create_client(
        "s3",
        endpoint_url=settings.MINIO_ENDPOINT or None,  # None = AWS/OVH natif
        aws_access_key_id=settings.AWS_ACCESS_KEY_ID or settings.MINIO_ACCESS_KEY,
        aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY or settings.MINIO_SECRET_KEY,
        region_name=settings.AWS_REGION or "gra",
    )

En développement, MINIO_ENDPOINT=http://localhost:9000 redirige tous les appels vers MinIO. En production, la variable est vide et boto3 route vers OVH S3 via AWS_REGION=gra.


Types de Médias Stockés

Images Produit

Format principal : JPEG et WebP (conversion automatique). Chaque image source génère 4 variants :

VariantDimensionsUsage
originalTaille d'origineArchivage, ré-export
thumbnail150×150 pxListes produits, aperçus
medium300×300 pxFiche produit web
large800×800 pxVue détaillée, zoom

La génération des variants est déléguée à Celery (queue media) immédiatement après l'upload de l'image originale.

Documents Fournisseurs

  • Fiches techniques PDF
  • Tarifs Excel (.xlsx, .xls)
  • Listes produits CSV
  • Documents contractuels PDF

Accès par signed URL uniquement (jamais en accès public).

Vidéos Produit

  • MP4 hébergés en propre (taille max configurable, défaut 200MB)
  • Liens externes Icecat (URL stockée en DB, pas de fichier local)

Fichiers Générés

  • Exports catalogue (CSV, Excel, JSON) — générés par Celery, disponibles 24h
  • Rapports analytics (PDF, Excel)
  • Archives ZIP de médias

Imports Temporaires

Fichiers uploadés pour import (CSV, Excel, XML) — stockés en imports/temp/, nettoyés automatiquement après traitement par le job Celery cleanup_temp_imports (planifié quotidiennement).


Organisation des Préfixes

pixee-s3/
├── products/
│   └── {product_id}/
│       ├── images/
│       │   ├── original/{hash}.jpg
│       │   ├── thumbnail/{hash}_150x150.webp
│       │   ├── medium/{hash}_300x300.webp
│       │   └── large/{hash}_800x800.webp
│       └── documents/
│           └── {filename}
├── exports/
│   └── {job_id}/
│       └── export_{timestamp}.csv
├── imports/
│   └── temp/
│       └── {job_id}/
│           └── {original_filename}
├── icecat/
│   └── {icecat_id}/
│       └── images/
│           └── {hash}.jpg
└── reports/
    └── {tenant_id}/
        └── {date}/
            └── {report_name}.xlsx

UUID Sharding (Scalabilité)

Pour les catalogues très larges, un sharding par les 2 premiers caractères de l'UUID est supporté en alternative :

products/{product_id[0:2]}/{product_id}/images/original/{hash}.jpg
# Exemple : products/3f/3f8a2c1d-.../images/original/abc123.jpg

Ce pattern crée 256 sous-répertoires (00-ff) et évite les limitations de listing S3 sur les buckets à >1M objets.


Sécurité

ACL par Type de Média

# Images produit : accès public (CDN-friendly)
S3_ACL_PRODUCT_IMAGES = "public-read"

# Documents fournisseurs : privés, signed URLs
S3_ACL_DOCUMENTS = "private"

# Exports générés : privés, signed URLs
S3_ACL_EXPORTS = "private"

# Imports temporaires : privés
S3_ACL_IMPORTS_TEMP = "private"

Signed URLs

Les ressources privées ne sont jamais exposées directement. L'API génère une URL signée à la demande :

async def generate_signed_url(
    object_key: str,
    ttl_seconds: int = settings.STORAGE_SIGNED_URL_TTL,  # défaut 3600s
    bucket: str = settings.MINIO_BUCKET,
) -> str:
    async with get_s3_client() as s3:
        url = await s3.generate_presigned_url(
            "get_object",
            Params={"Bucket": bucket, "Key": object_key},
            ExpiresIn=ttl_seconds,
        )
    return url

TTL recommandés par usage :

  • Téléchargement document fournisseur : 1h (défaut)
  • Export CSV à télécharger : 24h
  • Aperçu inline document : 15min

Chiffrement

  • At rest : AES-256 côté OVH (géré transparemment par le provider)
  • In transit : TLS 1.2+ enforced sur tous les endpoints OVH S3 et MinIO (HTTPS)
  • Credentials : clés d'accès S3 chiffrées en base de données via Fernet (clé dérivée de SECRET_KEY)
# Chiffrement des credentials tiers en DB
from cryptography.fernet import Fernet

def encrypt_credential(value: str) -> str:
    f = Fernet(settings.FERNET_KEY)
    return f.encrypt(value.encode()).decode()

def decrypt_credential(encrypted: str) -> str:
    f = Fernet(settings.FERNET_KEY)
    return f.decrypt(encrypted.encode()).decode()

Upload Flow

Upload via l'API FastAPI

Client (multipart/form-data)
       |
       v
FastAPI /api/v1/products/{id}/images (POST)
       |
       ├─ Validation MIME type (image/jpeg, image/png, image/webp, image/gif)
       ├─ Validation taille (max configurable, défaut 10MB)
       ├─ Génération file_hash (SHA-256 pour déduplication)
       |
       v
aiobotocore → OVH S3 (upload objet original)
       |
       v
INSERT media_files (db_media) ← métadonnées + object_key
       |
       v
Celery task: generate_thumbnails.delay(media_file_id)
       |
       v
Réponse API : {media_file_id, storage_url, status: "processing"}

Génération des Thumbnails (Asynchrone)

@celery_app.task(queue="media", rate_limit="200/m")
async def generate_thumbnails(media_file_id: str):
    media_file = await get_media_file(media_file_id)

    # Téléchargement de l'original depuis S3
    original_bytes = await download_from_s3(media_file.object_key)
    img = Image.open(BytesIO(original_bytes))

    variants = [
        ("thumbnail", (150, 150)),
        ("medium", (300, 300)),
        ("large", (800, 800)),
    ]

    for size_name, dimensions in variants:
        resized = img.copy()
        resized.thumbnail(dimensions, Image.LANCZOS)

        # Conversion WebP pour les variants
        buffer = BytesIO()
        resized.save(buffer, format="WEBP", quality=85)

        variant_key = build_variant_key(media_file, size_name)
        await upload_to_s3(variant_key, buffer.getvalue(), "image/webp")

        await save_thumbnail_record(media_file_id, size_name, dimensions, variant_key)

    await update_processing_status(media_file_id, "completed")

Déduplication par Hash

Avant chaque upload, le SHA-256 du fichier est calculé et vérifié en base. Si un objet identique existe déjà, l'upload est court-circuité et l'enregistrement existant est réutilisé :

file_hash = hashlib.sha256(file_content).hexdigest()
existing = await db.execute(
    select(MediaFile).where(MediaFile.file_hash == file_hash)
)
if existing.scalar_one_or_none():
    return existing  # Réutilisation sans upload S3

Tables de Base de Données (db_media)

Les métadonnées de tous les objets stockés sont persistées dans db_media :

  • media_files — métadonnées fichier (hash, MIME, dimensions, object_key, statut processing)
  • media_thumbnails — variants générés par taille (small, medium, large)
  • media_metadata — données EXIF et métadonnées techniques (appareil photo, GPS, etc.)
  • media_processing_jobs — suivi des tâches de traitement background

La colonne object_key est la référence canonique vers l'objet S3. Les URLs publiques et signed URLs sont générées à la demande, jamais stockées en dur (elles expirent).


MinIO en Développement

MinIO s'exécute en conteneur Docker et expose la même API S3. L'interface d'administration est disponible sur http://localhost:9001.

Pour la configuration complète de MinIO en environnement local (création des buckets, politique d'accès, credentials) : Intégration MinIO → /docs/integrations/minio


Variables d'Environnement

VariableExempleDescription
MINIO_ENDPOINThttp://minio:9000Endpoint MinIO (vide en production OVH)
MINIO_ACCESS_KEYminioadminAccess key MinIO (dev/staging)
MINIO_SECRET_KEYminioadminSecret key MinIO (dev/staging)
MINIO_BUCKETdev-mediaBucket par défaut MinIO
AWS_ACCESS_KEY_IDOVH_ACCESS_KEYAccess key OVH S3 (production)
AWS_SECRET_ACCESS_KEYOVH_SECRET_KEYSecret key OVH S3 (production)
AWS_S3_BUCKETpixee-s3Bucket OVH S3 (production)
AWS_REGIONgraRégion OVH (Gravelines)
STORAGE_SIGNED_URL_TTL3600TTL des signed URLs en secondes
STORAGE_MAX_UPLOAD_SIZE_MB10Taille max upload (Mo)
STORAGE_ALLOWED_MIME_TYPESimage/jpeg,image/png,image/webpMIME types autorisés pour les images
THUMBNAIL_QUALITY_WEBP85Qualité WebP des thumbnails (0-100)
TEMP_IMPORTS_RETENTION_DAYS7Rétention fichiers imports temporaires

Ressources

Volume en Production

~922 000 objets en production sur OVH S3 (bucket pixee-s3). La déduplication par hash SHA-256 évite les doublons et réduit les coûts de stockage d'environ 15-20% sur les catalogues multi-fournisseurs contenant les mêmes images produit.