systeme:documenso:minio
Différences
Ci-dessous, les différences entre deux révisions de la page.
| Les deux révisions précédentesRévision précédenteProchaine révision | Révision précédente | ||
| systeme:documenso:minio [2026/04/11 13:19] – [Exemples de test] techer.charles_educ-valadon-limoges.fr | systeme:documenso:minio [2026/04/11 17:55] (Version actuelle) – [Webhook Python] techer.charles_educ-valadon-limoges.fr | ||
|---|---|---|---|
| Ligne 150: | Ligne 150: | ||
| * main.py – Webhook MinIO → SharePoint | * main.py – Webhook MinIO → SharePoint | ||
| + | |||
| + | <WRAP center round info> | ||
| + | Pour sécuriser le webhook de MinIO vers le Proxy, utilisation d'un secret partagé (token) et vérification dans **main.py** du secret. | ||
| + | </ | ||
| + | |||
| + | <WRAP center round info> | ||
| + | Gérer les fichiers volumineux : | ||
| + | |||
| + | Microsoft Graph refuse : | ||
| + | * les PUT /content simples | ||
| + | * dès que le fichier dépasse ~4–10 Mo (limite variable) | ||
| + | |||
| + | Pour des fichiers > | ||
| + | * upload en session + chunks obligatoire | ||
| + | |||
| + | Principe Graph (officiel) | ||
| + | * Créer une upload session | ||
| + | * Envoyer le fichier par blocs (chunks) | ||
| + | * Graph reconstruit le fichier côté SharePoint | ||
| + | </ | ||
| + | |||
| + | <WRAP center round info> | ||
| + | Gestion de plusieurs dossiers dans une seule équipe SharePoint : | ||
| + | * dossiers automatiquement créés dans Sharepoint | ||
| + | * aucun droit d’écriture manuel côté SharePoint | ||
| + | * traçabilité MinIO → SharePoint | ||
| + | |||
| + | Structure : | ||
| + | < | ||
| + | Documents | ||
| + | ├── administration | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | ├── bts-sio | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | ├── bts-mco | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | </ | ||
| + | |||
| + | Mapping pédagogique MinIO → SharePoint (préfixe MinIO = dossier SharePoint) | ||
| + | ^ MinIO (bucket pedagogie) | ||
| + | |administration/ | ||
| + | |bts-sio/ | ||
| + | |bts-sio/ | ||
| + | </ | ||
| + | |||
| <code python main.py> | <code python main.py> | ||
| - | from flask import Flask, request | + | from flask import Flask, request, abort |
| - | import os | + | import os, requests |
| - | from graph import graph_request | + | |
| - | app = Flask(__name__) | + | from graph import graph_request |
| TENANT_ID = os.getenv(" | TENANT_ID = os.getenv(" | ||
| Ligne 162: | Ligne 212: | ||
| GRAPH_BASE = " | GRAPH_BASE = " | ||
| + | WEBHOOK_SECRET = os.getenv(" | ||
| + | if not WEBHOOK_SECRET: | ||
| + | raise RuntimeError(" | ||
| + | |||
| + | # Détection de la taille du fichier / Upload thresholds | ||
| + | MAX_SIMPLE_UPLOAD = 4 * 1024 * 1024 # 4 MB | ||
| + | CHUNK_SIZE = 10 * 1024 * 1024 # 10 MB | ||
| + | |||
| + | app = Flask(__name__) | ||
| + | |||
| + | |||
| + | # ───────────────────────────── | ||
| + | # Sécurité webhook MinIO | ||
| + | # ───────────────────────────── | ||
| + | def verify_minio_webhook(req): | ||
| + | auth = req.headers.get(" | ||
| + | |||
| + | if not auth or not auth.startswith(" | ||
| + | abort(401, " | ||
| + | |||
| + | token = auth[len(" | ||
| + | |||
| + | if token != WEBHOOK_SECRET: | ||
| + | abort(403, " | ||
| + | | ||
| + | # ───────────────────────────── | ||
| + | # SharePoint drive | ||
| + | # ───────────────────────────── | ||
| def get_drive_id(): | def get_drive_id(): | ||
| site = graph_request( | site = graph_request( | ||
| Ligne 178: | Ligne 256: | ||
| print(f" | print(f" | ||
| + | # ───────────────────────────── | ||
| + | # Upload helpers | ||
| + | # ───────────────────────────── | ||
| + | def create_upload_session(sp_path): | ||
| + | res = graph_request( | ||
| + | " | ||
| + | f" | ||
| + | json={ | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | ) | ||
| + | return res[" | ||
| + | |||
| + | |||
| + | def upload_simple(sp_path, | ||
| + | graph_request( | ||
| + | " | ||
| + | f" | ||
| + | data=data | ||
| + | ) | ||
| + | |||
| + | |||
| + | def upload_chunked(sp_path, | ||
| + | upload_url = create_upload_session(sp_path) | ||
| + | size = os.path.getsize(file_path) | ||
| + | |||
| + | with open(file_path, | ||
| + | start = 0 | ||
| + | while start < size: | ||
| + | chunk = f.read(CHUNK_SIZE) | ||
| + | end = start + len(chunk) - 1 | ||
| + | |||
| + | headers = { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | r = requests.put(upload_url, | ||
| + | r.raise_for_status() | ||
| + | start += len(chunk) | ||
| + | |||
| + | |||
| + | # ───────────────────────────── | ||
| + | # Event handlers | ||
| + | # ───────────────────────────── | ||
| + | def upload_object(key): | ||
| + | sp_path = map_mapping_path(key) | ||
| + | |||
| + | if not sp_path: | ||
| + | print(f" | ||
| + | return | ||
| + | |||
| + | print(f" | ||
| + | |||
| + | # Télécharger depuis MinIO → /tmp (à implémenter) | ||
| + | file_path = f"/ | ||
| + | |||
| + | size = os.path.getsize(file_path) | ||
| + | |||
| + | if size <= MAX_SIMPLE_UPLOAD: | ||
| + | with open(file_path, | ||
| + | upload_simple(key, | ||
| + | else: | ||
| + | upload_chunked(key, | ||
| + | |||
| + | os.remove(file_path) | ||
| + | |||
| + | |||
| + | def delete_object(key): | ||
| + | sp_path = map_mapping_path(key) | ||
| + | |||
| + | if not sp_path: | ||
| + | return | ||
| + | |||
| + | print(f" | ||
| + | |||
| + | graph_request( | ||
| + | " | ||
| + | f" | ||
| + | ) | ||
| + | |||
| + | # ───────────────────────────── | ||
| + | # Mappping | ||
| + | # ───────────────────────────── | ||
| + | def map_mapping_path(key: | ||
| + | """ | ||
| + | Transforme une clé MinIO en chemin SharePoint autorisé. | ||
| + | Retourne None si le chemin n'est pas declare. | ||
| + | """ | ||
| + | |||
| + | key = key.lstrip("/" | ||
| + | |||
| + | ALLOWED_PREFIXES = [ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | ] | ||
| + | |||
| + | for prefix in ALLOWED_PREFIXES: | ||
| + | if key.startswith(prefix): | ||
| + | return key | ||
| + | |||
| + | # Tout le reste est ignoré | ||
| + | return None | ||
| + | |||
| + | |||
| + | |||
| + | # ───────────────────────────── | ||
| + | # Webhook endpoint | ||
| + | # ───────────────────────────── | ||
| @app.route("/ | @app.route("/ | ||
| def s3_event(): | def s3_event(): | ||
| - | event = request.json | + | |
| + | | ||
| record = event[" | record = event[" | ||
| event_name = record[" | event_name = record[" | ||
| Ligne 191: | Ligne 382: | ||
| return "", | return "", | ||
| - | |||
| - | def upload_object(key): | ||
| - | print(f" | ||
| - | url = f" | ||
| - | graph_request(" | ||
| - | |||
| - | def delete_object(key): | ||
| - | print(f" | ||
| - | url = f" | ||
| - | graph_request(" | ||
| if __name__ == " | if __name__ == " | ||
| Ligne 251: | Ligne 432: | ||
| MINIO_ROOT_USER=admin | MINIO_ROOT_USER=admin | ||
| MINIO_ROOT_PASSWORD=motdepasse | MINIO_ROOT_PASSWORD=motdepasse | ||
| - | |||
| - | MINIO_NOTIFY_WEBHOOK_ENABLE: | ||
| TENANT_ID=< | TENANT_ID=< | ||
| Ligne 262: | Ligne 441: | ||
| AUTHORITY=https:// | AUTHORITY=https:// | ||
| GRAPH_SCOPE=https:// | GRAPH_SCOPE=https:// | ||
| + | |||
| + | WEBHOOK_SECRET=" | ||
| DOCUMENT_LIBRARY=" | DOCUMENT_LIBRARY=" | ||
| Ligne 391: | Ligne 572: | ||
| === Créer le webhook === | === Créer le webhook === | ||
| - | |||
| <WRAP center round info> | <WRAP center round info> | ||
| Ligne 398: | Ligne 578: | ||
| Si le proxy est sur un serveur distinct et non dans Docker, il faut alors utiliser le nom DNS du server qui exécute le proxy. | Si le proxy est sur un serveur distinct et non dans Docker, il faut alors utiliser le nom DNS du server qui exécute le proxy. | ||
| </ | </ | ||
| + | |||
| + | Pour sécuriser le webhook MinIO vers le Proxy, utilisation d'un header statique contenant un token pour empêcher toute requête non légitime d’appeler /s3event. | ||
| + | |||
| + | |||
| + | * Configurer le webhook avec le token : | ||
| < | < | ||
| mc admin config set minio notify_webhook: | mc admin config set minio notify_webhook: | ||
| - | endpoint=" | + | endpoint=" |
| - | + | auth_token=" | |
| => résultat attendu : | => résultat attendu : | ||
| Successfully applied new settings. | Successfully applied new settings. | ||
| Ligne 446: | Ligne 632: | ||
| Commentaires : | Commentaires : | ||
| - | * le 1er notify_webhool est le wrbhook | + | * le 1er notify_webhool est le webhook |
| - | * Le webhook nommé **sp** est bien configuré avec le endpoint **http:// | + | * Le webhook nommé **sharepoint** est bien configuré avec le endpoint **http:// |
| * afficher la configuration d'un webhook | * afficher la configuration d'un webhook | ||
| Ligne 462: | Ligne 648: | ||
| < | < | ||
| mc admin config reset minio notify_webhook: | mc admin config reset minio notify_webhook: | ||
| + | |||
| + | => doit renvoyer | ||
| + | ' | ||
| + | Please restart your server with `mc admin service restart minio`. | ||
| </ | </ | ||
| + | |||
| + | * créer le bucket | ||
| + | |||
| + | < | ||
| + | mc mb minio/lycee | ||
| + | |||
| + | => résultat attendu | ||
| + | Bucket created successfully `minio/ | ||
| + | </ | ||
| + | |||
| + | * vérification | ||
| + | |||
| + | < | ||
| + | mc ls minio | ||
| + | </ | ||
| + | |||
| * Lier le bucket aux événements afin que MinIO envoie les données | * Lier le bucket aux événements afin que MinIO envoie les données | ||
| + | |||
| + | <WRAP center round info> | ||
| + | Filtrage côté MinIO (RECOMMANDÉ) : le proxy ne reçoit QUE ce qui est attendu | ||
| + | </ | ||
| + | |||
| < | < | ||
| - | mc event add minio/test arn: | + | mc event add minio/lycee arn: |
| + | | ||
| + | --prefix administration/ | ||
| + | --prefix bts-sio/ \ | ||
| + | --prefix bts-mco/ | ||
| + | |||
| => doit renvoyer | => doit renvoyer | ||
| Ligne 539: | Ligne 756: | ||
| * Fichier supprimé dans SharePoint | * Fichier supprimé dans SharePoint | ||
| - | ===== Sécuriser le webhook MinIO → Proxy ===== | + | ===== Gérer les fichiers volumineux (Upload Microsoft Graph en chunks > |
| + | Microsoft Graph refuse : | ||
| + | * les PUT /content simples | ||
| + | * dès que le fichier dépasse ~4–10 Mo (limite variable) | ||
| + | * Pour des fichiers > | ||
| - | ================== | + | Principe Graph (officiel) |
| - | < | + | * Créer une upload session |
| - | name: documenso-production | + | * Envoyer le fichier par blocs (chunks) |
| + | * Graph reconstruit le fichier côté SharePoint | ||
| - | services: | + | ==== Détection de la taille ==== |
| - | database: | + | |
| - | image: postgres: | + | |
| - | environment: | + | |
| - | - POSTGRES_USER=${POSTGRES_USER:? | + | |
| - | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:? | + | |
| - | - POSTGRES_DB=${POSTGRES_DB:? | + | |
| - | healthcheck: | + | |
| - | test: [' | + | |
| - | interval: 10s | + | |
| - | timeout: 5s | + | |
| - | retries: 5 | + | |
| - | volumes: | + | |
| - | - ./ | + | |
| - | # --------------------------------------------------------------------------- | + | <code python> |
| - | # | + | import os |
| - | # --------------------------------------------------------------------------- | + | |
| - | minio: | + | |
| - | image: minio/ | + | |
| - | container_name: | + | |
| - | restart: unless-stopped | + | |
| - | command: server /data --console-address ": | + | |
| - | environment: | + | |
| - | MINIO_ROOT_USER: | + | |
| - | MINIO_ROOT_PASSWORD: | + | |
| - | ports: | + | |
| - | - " | + | |
| - | - " | + | |
| - | volumes: | + | |
| - | - ./ | + | |
| - | # --------------------------------------------------------------------------- | + | MAX_SIMPLE_UPLOAD = 4 * 1024 * 1024 # |
| - | # | + | |
| - | # --------------------------------------------------------------------------- | + | |
| - | documenso: | + | |
| - | image: documenso/ | + | |
| - | depends_on: | + | |
| - | database: | + | |
| - | condition: service_healthy | + | |
| - | minio: | + | |
| - | condition: service_started | + | |
| - | environment: | + | |
| - | - PORT=${PORT: | + | |
| - | - NEXTAUTH_SECRET=${NEXTAUTH_SECRET:? | + | |
| - | - NEXT_PRIVATE_ENCRYPTION_KEY=${NEXT_PRIVATE_ENCRYPTION_KEY:? | + | |
| - | - NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY:? | + | |
| - | - NEXT_PRIVATE_GOOGLE_CLIENT_ID=${NEXT_PRIVATE_GOOGLE_CLIENT_ID} | + | |
| - | - NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=${NEXT_PRIVATE_GOOGLE_CLIENT_SECRET} | + | |
| - | - NEXT_PUBLIC_WEBAPP_URL=${NEXT_PUBLIC_WEBAPP_URL:? | + | |
| - | - NEXT_PRIVATE_INTERNAL_WEBAPP_URL=${NEXT_PRIVATE_INTERNAL_WEBAPP_URL: | + | |
| - | - NEXT_PRIVATE_DATABASE_URL=${NEXT_PRIVATE_DATABASE_URL:? | + | |
| - | - NEXT_PRIVATE_DIRECT_DATABASE_URL=${NEXT_PRIVATE_DIRECT_DATABASE_URL: | + | |
| - | # ----------- CONFIG STOCKAGE S3 (MINIO) --------------- | + | size = os.path.getsize(file_path) |
| - | - NEXT_PUBLIC_UPLOAD_TRANSPORT=s3 | + | |
| - | - NEXT_PRIVATE_UPLOAD_ENDPOINT=http:// | + | |
| - | - NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE=true | + | |
| - | - NEXT_PRIVATE_UPLOAD_REGION=us-east-1 | + | |
| - | - NEXT_PRIVATE_UPLOAD_BUCKET=documenso | + | |
| - | - NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=${MINIO_ROOT_USER: | + | |
| - | - NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD: | + | |
| - | # ----------- SMTP & autres --------------- | + | if size <= MAX_SIMPLE_UPLOAD: |
| - | - NEXT_PRIVATE_SMTP_TRANSPORT=${NEXT_PRIVATE_SMTP_TRANSPORT:?err} | + | |
| - | - NEXT_PRIVATE_SMTP_HOST=${NEXT_PRIVATE_SMTP_HOST} | + | else: |
| - | - NEXT_PRIVATE_SMTP_PORT=${NEXT_PRIVATE_SMTP_PORT} | + | |
| - | - NEXT_PRIVATE_SMTP_USERNAME=${NEXT_PRIVATE_SMTP_USERNAME} | + | </ |
| - | - NEXT_PRIVATE_SMTP_PASSWORD=${NEXT_PRIVATE_SMTP_PASSWORD} | + | |
| - | - NEXT_PRIVATE_SMTP_APIKEY_USER=${NEXT_PRIVATE_SMTP_APIKEY_USER} | + | |
| - | - NEXT_PRIVATE_SMTP_APIKEY=${NEXT_PRIVATE_SMTP_APIKEY} | + | |
| - | - NEXT_PRIVATE_SMTP_SECURE=${NEXT_PRIVATE_SMTP_SECURE} | + | |
| - | - NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS=${NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS} | + | |
| - | - NEXT_PRIVATE_SMTP_FROM_NAME=${NEXT_PRIVATE_SMTP_FROM_NAME:?err} | + | |
| - | - NEXT_PRIVATE_SMTP_FROM_ADDRESS=${NEXT_PRIVATE_SMTP_FROM_ADDRESS:? | + | |
| - | - NEXT_PRIVATE_SMTP_SERVICE=${NEXT_PRIVATE_SMTP_SERVICE} | + | |
| - | - NEXT_PRIVATE_RESEND_API_KEY=${NEXT_PRIVATE_RESEND_API_KEY} | + | * Création de la session d’upload |
| - | - NEXT_PRIVATE_MAILCHANNELS_API_KEY=${NEXT_PRIVATE_MAILCHANNELS_API_KEY} | + | |
| - | - NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=${NEXT_PRIVATE_MAILCHANNELS_ENDPOINT} | + | |
| - | - NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN=${NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN} | + | |
| - | - NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR=${NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR} | + | |
| - | - NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY=${NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY} | + | |
| - | - NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=${NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT} | + | |
| - | - NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY} | + | |
| - | - NEXT_PUBLIC_DISABLE_SIGNUP=${NEXT_PUBLIC_DISABLE_SIGNUP} | + | |
| - | - NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=${NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS} | + | |
| - | - NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=${NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: | + | <code python> |
| - | - NEXT_PRIVATE_SIGNING_PASSPHRASE=${NEXT_PRIVATE_SIGNING_PASSPHRASE} | + | def create_upload_session(drive_id, |
| - | - NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=${NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS} | + | return graph_request( |
| - | ports: | + | " |
| - | - ${PORT: | + | f"{GRAPH_BASE}/drives/{drive_id}/root:/ |
| - | | + | json={ |
| - | | + | " |
| + | " | ||
| + | } | ||
| + | | ||
| + | | ||
| + | </code> | ||
| - | | + | |
| - | # RCLONE — synchronisation BIDIRECTIONNELLE MinIO ↔ SharePoint | + | |
| - | # --------------------------------------------------------------------------- | + | |
| - | rclone-sync: | + | |
| - | image: rclone/ | + | |
| - | container_name: | + | |
| - | restart: unless-stopped | + | |
| - | depends_on: | + | |
| - | - minio | + | |
| - | environment: | + | |
| - | RCLONE_CONFIG: | + | |
| - | volumes: | + | |
| - | - ./ | + | |
| - | - ./ | + | |
| - | entrypoint: ["/ | + | |
| - | command: > | + | |
| - | " | + | |
| - | while true; do | + | |
| - | echo ' | + | |
| - | rclone bisync s3minio: | + | |
| - | --conf=/ | + | |
| - | --verbose \ | + | |
| - | --remove-empty-dirs \ | + | |
| - | --log-file=/ | + | |
| - | echo ' | + | |
| - | sleep 60; | + | |
| - | done | + | |
| - | " | + | |
| - | volumes: | + | < |
| - | | + | import requests |
| + | def upload_chunked(sp_path, | ||
| + | upload_url = create_upload_session(DRIVE_ID, | ||
| - | </code> | + | chunk_size = 10 * 1024 * 1024 # 10 MB |
| + | size = os.path.getsize(local_file) | ||
| + | |||
| + | with open(local_file, | ||
| + | start = 0 | ||
| + | while start < size: | ||
| + | data = f.read(chunk_size) | ||
| + | end = start + len(data) - 1 | ||
| + | |||
| + | headers = { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | r = requests.put(upload_url, | ||
| + | r.raise_for_status() | ||
| + | |||
| + | start += chunk_size | ||
| + | </ | ||
systeme/documenso/minio.1775906376.txt.gz · Dernière modification : 2026/04/11 13:19 de techer.charles_educ-valadon-limoges.fr
