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
| Environnement | Backend | Bucket | Endpoint |
|---|---|---|---|
| Production | OVH Object Storage S3 | pixee-s3 | s3.gra.io.cloud.ovh.net |
| Staging | MinIO (Docker) | staging-media | minio:9000 (interne) |
| Développement | MinIO (Docker) | dev-media | localhost: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 :
| Variant | Dimensions | Usage |
|---|---|---|
original | Taille d'origine | Archivage, ré-export |
thumbnail | 150×150 px | Listes produits, aperçus |
medium | 300×300 px | Fiche produit web |
large | 800×800 px | Vue 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
| Variable | Exemple | Description |
|---|---|---|
MINIO_ENDPOINT | http://minio:9000 | Endpoint MinIO (vide en production OVH) |
MINIO_ACCESS_KEY | minioadmin | Access key MinIO (dev/staging) |
MINIO_SECRET_KEY | minioadmin | Secret key MinIO (dev/staging) |
MINIO_BUCKET | dev-media | Bucket par défaut MinIO |
AWS_ACCESS_KEY_ID | OVH_ACCESS_KEY | Access key OVH S3 (production) |
AWS_SECRET_ACCESS_KEY | OVH_SECRET_KEY | Secret key OVH S3 (production) |
AWS_S3_BUCKET | pixee-s3 | Bucket OVH S3 (production) |
AWS_REGION | gra | Région OVH (Gravelines) |
STORAGE_SIGNED_URL_TTL | 3600 | TTL des signed URLs en secondes |
STORAGE_MAX_UPLOAD_SIZE_MB | 10 | Taille max upload (Mo) |
STORAGE_ALLOWED_MIME_TYPES | image/jpeg,image/png,image/webp | MIME types autorisés pour les images |
THUMBNAIL_QUALITY_WEBP | 85 | Qualité WebP des thumbnails (0-100) |
TEMP_IMPORTS_RETENTION_DAYS | 7 | Ré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.