# 06 — Stockage objet S3 (Hetzner Object Storage)

---

## Migration MinIO → Hetzner Object Storage

Bonne nouvelle : **aucune modification de code nécessaire.**

Le code Xpeditis utilise déjà le AWS SDK v3 avec `forcePathStyle: true` et un endpoint configurable dans `apps/backend/src/infrastructure/storage/s3-storage.adapter.ts` :

```typescript
// Ce code existant fonctionne avec Hetzner Object Storage
this.s3Client = new S3Client({
  region,
  endpoint,           // ← Changer vers Hetzner
  credentials: { accessKeyId, secretAccessKey },
  forcePathStyle: !!endpoint,   // ← true pour Hetzner (path-style S3)
});
```

Il suffit de **changer 4 variables d'environnement** :

```bash
# AVANT (MinIO local)
AWS_S3_ENDPOINT=http://localhost:9000
AWS_ACCESS_KEY_ID=minioadmin
AWS_SECRET_ACCESS_KEY=minioadmin
AWS_REGION=us-east-1

# APRÈS (Hetzner Object Storage)
AWS_S3_ENDPOINT=https://fsn1.your-objectstorage.com
AWS_ACCESS_KEY_ID=<hetzner_access_key>
AWS_SECRET_ACCESS_KEY=<hetzner_secret_key>
AWS_REGION=eu-central-1
AWS_S3_BUCKET=xpeditis-prod
```

---

## Configuration détaillée

### Variables d'environnement backend (.env.production)

```bash
# S3 / Hetzner Object Storage
AWS_S3_ENDPOINT=https://fsn1.your-objectstorage.com
AWS_ACCESS_KEY_ID=<votre_hetzner_access_key>
AWS_SECRET_ACCESS_KEY=<votre_hetzner_secret_key>
AWS_REGION=eu-central-1
AWS_S3_BUCKET=xpeditis-prod
```

> **Endpoint par région :**
> - Falkenstein : `https://fsn1.your-objectstorage.com`
> - Nuremberg : `https://nbg1.your-objectstorage.com`
> - Helsinki : `https://hel1.your-objectstorage.com`
>
> Utilisez la même région que vos serveurs pour éviter des frais de transfert inter-région.

---

## Tester la connexion depuis le code

```bash
# 1. Build l'image Docker backend avec les vars de test
# 2. Ou tester directement avec AWS CLI

# Test avec AWS CLI (profil configuré dans doc 03)
aws s3 ls s3://xpeditis-prod/ \
  --profile hetzner \
  --endpoint-url https://fsn1.your-objectstorage.com

# Uploader un fichier test
echo "test" | aws s3 cp - s3://xpeditis-prod/test/health.txt \
  --profile hetzner \
  --endpoint-url https://fsn1.your-objectstorage.com

# Générer une URL signée (1h)
aws s3 presign s3://xpeditis-prod/test/health.txt \
  --profile hetzner \
  --endpoint-url https://fsn1.your-objectstorage.com \
  --expires-in 3600

# Nettoyage
aws s3 rm s3://xpeditis-prod/test/health.txt \
  --profile hetzner \
  --endpoint-url https://fsn1.your-objectstorage.com
```

---

## Structure du bucket

Créez les "dossiers" initiaux (S3 utilise des préfixes, pas de vrais dossiers) :

```bash
#!/bin/bash
PROFILE="hetzner"
ENDPOINT="https://fsn1.your-objectstorage.com"
BUCKET="xpeditis-prod"

for PREFIX in documents pdfs exports logos backups/postgres; do
  aws s3api put-object \
    --bucket "$BUCKET" \
    --key "$PREFIX/" \
    --profile "$PROFILE" \
    --endpoint-url "$ENDPOINT" \
    --content-length 0
  echo "✅ Créé: $PREFIX/"
done
```

---

## Lifecycle policies (économies de stockage)

Hetzner Object Storage supporte les lifecycle rules S3 pour archiver automatiquement les anciens fichiers.

```bash
# Créer le fichier de lifecycle
cat > /tmp/lifecycle.json << 'EOF'
{
  "Rules": [
    {
      "ID": "archive-old-pdfs",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "pdfs/"
      },
      "Transitions": [
        {
          "Days": 90,
          "StorageClass": "GLACIER"
        }
      ]
    },
    {
      "ID": "archive-old-exports",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "exports/"
      },
      "Expiration": {
        "Days": 365
      }
    },
    {
      "ID": "cleanup-old-backups",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "backups/"
      },
      "Expiration": {
        "Days": 30
      }
    }
  ]
}
EOF

# Appliquer le lifecycle
aws s3api put-bucket-lifecycle-configuration \
  --bucket xpeditis-prod \
  --lifecycle-configuration file:///tmp/lifecycle.json \
  --profile hetzner \
  --endpoint-url https://fsn1.your-objectstorage.com

# Vérifier
aws s3api get-bucket-lifecycle-configuration \
  --bucket xpeditis-prod \
  --profile hetzner \
  --endpoint-url https://fsn1.your-objectstorage.com
```

---

## CORS (pour upload direct depuis le navigateur)

Si vous implémentez des uploads directs depuis le browser (carrier portal) :

```bash
cat > /tmp/cors.json << 'EOF'
{
  "CORSRules": [
    {
      "AllowedHeaders": ["*"],
      "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
      "AllowedOrigins": [
        "https://app.xpeditis.com",
        "https://xpeditis.com"
      ],
      "ExposeHeaders": ["ETag"],
      "MaxAgeSeconds": 3000
    }
  ]
}
EOF

aws s3api put-bucket-cors \
  --bucket xpeditis-prod \
  --cors-configuration file:///tmp/cors.json \
  --profile hetzner \
  --endpoint-url https://fsn1.your-objectstorage.com
```

---

## Intégration dans le Secret Kubernetes

Le Secret Kubernetes `backend-secrets` contiendra les credentials S3. Voir le doc 09 pour les manifests complets, mais voici la section S3 :

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: backend-secrets
  namespace: xpeditis-prod
type: Opaque
stringData:
  AWS_S3_ENDPOINT: "https://fsn1.your-objectstorage.com"
  AWS_ACCESS_KEY_ID: "<hetzner_access_key>"
  AWS_SECRET_ACCESS_KEY: "<hetzner_secret_key>"
  AWS_REGION: "eu-central-1"
  AWS_S3_BUCKET: "xpeditis-prod"
```

---

## Monitoring du stockage

```bash
# Voir la taille totale du bucket
aws s3 ls s3://xpeditis-prod/ \
  --recursive \
  --human-readable \
  --summarize \
  --profile hetzner \
  --endpoint-url https://fsn1.your-objectstorage.com \
  | tail -3

# Output :
# Total Objects: 1234
# Total Size: 4.5 GiB
```

---

## Tarifs Hetzner Object Storage

| Ressource | Prix |
|---|---|
| Stockage | Inclus dans le pack €4.99/mois (1 TB) |
| Trafic sortant (internet) | Inclus dans le pack (1 TB) |
| Requêtes | Incluses |
| Stockage > 1 TB | €0.0067/TB/heure (~€4.90/TB/mois) |
| Trafic > 1 TB | ~€1/TB |

Pour Xpeditis à 1 000 users (~200 GB de fichiers), le coût est de **€4.99/mois fixe**.
