Outils pour utilisateurs

Outils du site


systeme:documenso:minio

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentesRévision précédente
Prochaine révision
Révision précédente
systeme:documenso:minio [2026/04/11 13:26] – [Configuration MinIO] techer.charles_educ-valadon-limoges.frsysteme: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.  Voir plus loin pour la configuration de la signature dans le Webhook.  
 +</WRAP>
 +
 +<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 > 250 Mo (vidéos, archives, sauvegardes)
 +  * 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>
 +
 +<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 : 
 +<code>
 +Documents
 +├── administration
 +│   ├── gestion
 +│   ├── scolaires
 +│   └── administration
 +├── bts-sio
 +│   ├── stages
 +│   ├── ccf
 +│   └── livrets
 +├── bts-mco
 +│   ├── stages
 +│   ├── ccf
 +│   └── livrets
 +</code>
 +
 +Mapping pédagogique MinIO → SharePoint (préfixe MinIO = dossier SharePoint)
 +^ MinIO (bucket pedagogie)  ^  SharePoint (Documents)  ^
 +|administration/gestion/|Documents/administration/gestion/|
 +|bts-sio/|Documents/bts-sio/|
 +|bts-sio/stages/|Documents/bts-sio/stages/|
 +</WRAP>
 +
  
 <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") TENANT_ID = os.getenv("TENANT_ID")
Ligne 162: Ligne 212:
 GRAPH_BASE = "https://graph.microsoft.com/v1.0" GRAPH_BASE = "https://graph.microsoft.com/v1.0"
  
 +WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET")
 +if not WEBHOOK_SECRET:
 +    raise RuntimeError("WEBHOOK_SECRET not set")
 +
 +# 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("Authorization")
 +
 +    if not auth or not auth.startswith("Bearer "):
 +        abort(401, "Missing Authorization header")
 +
 +    token = auth[len("Bearer "):].strip()
 +
 +    if token != WEBHOOK_SECRET:
 +        abort(403, "Invalid MinIO webhook token")
 +        
 +# ─────────────────────────────
 +# SharePoint drive
 +# ─────────────────────────────
 def get_drive_id(): def get_drive_id():
     site = graph_request(     site = graph_request(
Ligne 178: Ligne 256:
 print(f"Drive ID initialized: {DRIVE_ID}") print(f"Drive ID initialized: {DRIVE_ID}")
  
 +# ─────────────────────────────
 +# Upload helpers
 +# ─────────────────────────────
 +def create_upload_session(sp_path):
 +    res = graph_request(
 +        "POST",
 +        f"{GRAPH_BASE}/drives/{DRIVE_ID}/root:/{sp_path}:/createUploadSession",
 +        json={
 +            "item": {
 +                "@microsoft.graph.conflictBehavior": "replace"
 +            }
 +        }
 +    )
 +    return res["uploadUrl"]
 +
 +
 +def upload_simple(sp_path, data):
 +    graph_request(
 +        "PUT",
 +        f"{GRAPH_BASE}/drives/{DRIVE_ID}/root:/{sp_path}:/content",
 +        data=data
 +    )
 +
 +
 +def upload_chunked(sp_path, file_path):
 +    upload_url = create_upload_session(sp_path)
 +    size = os.path.getsize(file_path)
 +
 +    with open(file_path, "rb") as f:
 +        start = 0
 +        while start < size:
 +            chunk = f.read(CHUNK_SIZE)
 +            end = start + len(chunk) - 1
 +
 +            headers = {
 +                "Content-Length": str(len(chunk)),
 +                "Content-Range": f"bytes {start}-{end}/{size}"
 +            }
 +
 +            r = requests.put(upload_url, headers=headers, data=chunk)
 +            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"Ignored (non declare): {key}", flush=True)
 +        return
 +
 +    print(f"Upload autortise: {sp_path}", flush=True)
 +
 +    # Télécharger depuis MinIO → /tmp (à implémenter)
 +    file_path = f"/tmp/{os.path.basename(key)}"
 +
 +    size = os.path.getsize(file_path)
 +
 +    if size <= MAX_SIMPLE_UPLOAD:
 +        with open(file_path, "rb") as f:
 +            upload_simple(key, f.read())
 +    else:
 +        upload_chunked(key, file_path)
 +
 +    os.remove(file_path)
 +
 +
 +def delete_object(key):
 +    sp_path = map_mapping_path(key)
 +
 +    if not sp_path:
 +        return
 +
 +    print(f"Delete pédagogique: {sp_path}", flush=True)
 +
 +    graph_request(
 +        "DELETE",
 +        f"{GRAPH_BASE}/drives/{DRIVE_ID}/root:/{key}"
 +    )
 + 
 +# ─────────────────────────────
 +# Mappping
 +# ─────────────────────────────
 +def map_mapping_path(key: str) -> str | None:
 +    """
 +    Transforme une clé MinIO en chemin SharePoint autorisé.
 +    Retourne None si le chemin n'est pas declare.
 +    """
 +
 +    key = key.lstrip("/")
 +
 +    ALLOWED_PREFIXES = [
 +        "administration/",
 +        "bts-sio/",
 +        "bts-mco/"
 +    ]
 +
 +    for prefix in ALLOWED_PREFIXES:
 +        if key.startswith(prefix):
 +            return key
 +
 +    # Tout le reste est ignoré
 +    return None
 +
 +
 +
 +# ─────────────────────────────
 +# Webhook endpoint
 +# ─────────────────────────────
 @app.route("/s3event", methods=["POST"]) @app.route("/s3event", methods=["POST"])
 def s3_event(): def s3_event():
-    event = request.json+    verify_minio_webhook(request) 
 +    event = request.get_json()
     record = event["Records"][0]     record = event["Records"][0]
     event_name = record["eventName"]     event_name = record["eventName"]
Ligne 191: Ligne 382:
  
     return "", 204     return "", 204
- 
-def upload_object(key): 
-    print(f"Uploading {key} to SharePoint") 
-    url = f"{GRAPH_BASE}/drives/{DRIVE_ID}/root:/{key}:/content" 
-    graph_request("PUT", url) 
- 
-def delete_object(key): 
-    print(f"Deleting {key} from SharePoint") 
-    url = f"{GRAPH_BASE}/drives/{DRIVE_ID}/root:/{key}" 
-    graph_request("DELETE", url) 
  
 if __name__ == "__main__": if __name__ == "__main__":
Ligne 251: Ligne 432:
 MINIO_ROOT_USER=admin MINIO_ROOT_USER=admin
 MINIO_ROOT_PASSWORD=motdepasse MINIO_ROOT_PASSWORD=motdepasse
- 
-MINIO_NOTIFY_WEBHOOK_ENABLE: "on" 
  
 TENANT_ID=<ID du tenant> TENANT_ID=<ID du tenant>
Ligne 262: Ligne 441:
 AUTHORITY=https://login.microsoftonline.com/<ID du tenant> AUTHORITY=https://login.microsoftonline.com/<ID du tenant>
 GRAPH_SCOPE=https://graph.microsoft.com/.default GRAPH_SCOPE=https://graph.microsoft.com/.default
 +
 +WEBHOOK_SECRET="super-secret-minio"
  
 DOCUMENT_LIBRARY="Documents" DOCUMENT_LIBRARY="Documents"
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.
 </WRAP> </WRAP>
 +
 +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 :
  
 <code> <code>
 mc admin config set minio notify_webhook:sharepoint \ mc admin config set minio notify_webhook:sharepoint \
-  endpoint="http://sharepoint-proxy:8080/s3event"  +  endpoint="http://sharepoint-proxy:8080/s3event" \ 
- +  auth_token="$WEBHOOK_SECRET
 => 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 global et est désactivé. +  * le 1er notify_webhool est le webhook global et est désactivé. 
-  * Le webhook nommé **sp** est bien configuré avec le endpoint **http://sharepoint-proxy:8080/s3event**.+  * Le webhook nommé **sharepoint** est bien configuré avec le endpoint **http://sharepoint-proxy:8080/s3event**.
  
   * afficher la configuration d'un webhook   * afficher la configuration d'un webhook
Ligne 462: Ligne 648:
 <code> <code>
 mc admin config reset minio notify_webhook:sharepoint mc admin config reset minio notify_webhook:sharepoint
 +
 +=> doit renvoyer
 +'notify_webhook:sharepoint' is successfully reset.
 +Please restart your server with `mc admin service restart minio`.
 </code> </code>
 +
 +  * créer le bucket
 +
 +<code>
 +mc mb minio/lycee
 +
 +=> résultat attendu
 +Bucket created successfully `minio/lycee`.
 +</code>
 +
 +  * vérification
 +
 +<code>
 +mc ls minio
 +</code>
 +
  
   *  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
 +</WRAP>
 +
  
 <code> <code>
-mc event add minio/test arn:minio:sqs::sharepoint:webhook --event put,delete+mc event add minio/lycee arn:minio:sqs::sharepoint:webhook 
 +  --event put,delete 
 +  --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 > 250 Mo) ===== 
 +Microsoft Graph refuse : 
 +  * les PUT /content simples 
 +  * dès que le fichier dépasse ~4–10 Mo (limite variable) 
 +  * Pour des fichiers > 250 Mo (vidéos, archives, sauvegardes) upload en session + chunks obligatoire
  
-Utilisation d'une signature HMAC (RECOMMANDÉEpour empêcher toute requête non légitime d’appeler /s3event : +Principe Graph (officiel
-  * signature du webhook par MinIO, +  * Créer une upload session 
-  * vérification de la signature par le proxy.+  * Envoyer le fichier par blocs (chunks) 
 +  * Graph reconstruit le fichier côté SharePoint
  
-==== Configuration MinIO ====+==== Détection de la taille ====
  
-  * Créer une clé secrète partagée :+<code python> 
 +import os
  
-<code> +MAX_SIMPLE_UPLOAD 4 * 1024 * 1024  # 4 MB
-export WEBHOOK_SECRET="super-secret-minio" +
-</code>+
  
-  * Configurer le webhook avec HMAC :+size = os.path.getsize(file_path)
  
-<code> +if size <= MAX_SIMPLE_UPLOAD
-mc admin config set minio notify_webhook:sharepoint \ +    upload_simple(path, file_path) 
-  endpoint="http://sharepoint-proxy:8080/s3event" \ +else: 
-  auth_token="$WEBHOOK_SECRET" +    upload_chunked(path, file_path)
- +
-mc admin service restart minio+
 </code> </code>
  
-  * vérification côté proxy (Flask)+  * Création de la session d’upload
  
-Dans main.py 
 <code python> <code python>
-import hmachashlibos +def create_upload_session(drive_idsp_path): 
-from flask import abort+    return graph_request( 
 +        "POST"
 +        f"{GRAPH_BASE}/drives/{drive_id}/root:/{sp_path}:/createUploadSession", 
 +        json={ 
 +            "item":
 +                "@microsoft.graph.conflictBehavior": "replace" 
 +            } 
 +        } 
 +    )["uploadUrl"
 +</code>
  
-SHARED_SECRET = os.getenv("WEBHOOK_SECRET").encode()+  * Upload par chunks (robuste et reprenable)
  
-def verify_hmac(request): +<code> 
-    signature = request.headers.get("X-Minio-Signature"+import requests
-    if not signature: +
-        abort(401)+
  
-    body = request.data +def upload_chunked(sp_path, local_file): 
-    expected hmac.new( +    upload_url create_upload_session(DRIVE_IDsp_path)
-        SHARED_SECRETbody, hashlib.sha256 +
-    ).hexdigest()+
  
-    if not hmac.compare_digest(signatureexpected): +    chunk_size = 10 * 1024 * 1024  # 10 MB 
-        abort(403+    size = os.path.getsize(local_file) 
-</code>+ 
 +    with open(local_file"rb"as f
 +        start = 0 
 +        while start < size: 
 +            data = f.read(chunk_size
 +            end = start + len(data) - 1
  
-  * dans la route+            headers = { 
 +                "Content-Length": str(len(data)), 
 +                "Content-Range": f"bytes {start}-{end}/{size}" 
 +            } 
 + 
 +            r = requests.put(upload_url, headers=headers, data=data) 
 +            r.raise_for_status() 
 + 
 +            start += chunk_size
  
-<code> 
-@app.route("/s3event", methods=["POST"]) 
-def s3_event(): 
-    verify_hmac(request) 
-    ... 
 </code> </code>
systeme/documenso/minio.1775906781.txt.gz · Dernière modification : 2026/04/11 13:26 de techer.charles_educ-valadon-limoges.fr