Mailix / web_routes.py
ernestmindres's picture
Update web_routes.py
d1829d1 verified
# web_routes.py (Version API Pure - Logique Métier Conservée)
from decorators import api_session_required
import shutil
# Suppression de render_template, redirect, url_for, flash
from flask import Blueprint, request, jsonify, send_from_directory, current_app, session
from werkzeug.utils import secure_filename
import os
import uuid
import json
from auth_backend import get_plan_details
from baserow_storage import get_health_status, update_user_deployment_data, get_deployment_by_id, save_new_repository, delete_deployment_by_id, save_new_repository, get_all_user_repositories # NOUVELLES FONCTIONS
import huggingface_storage
from huggingface_storage import (
create_empty_repo_folder_on_hf,
list_repo_files_from_hf # NOUVELLES FONCTIONS
)
from decorators import login_required
from datetime import datetime # NOUVEL IMPORT
# NOUVEAU : Fonction pour vérifier les extensions autorisées pour le déploiement statique
def allowed_static_file(filename):
"""Vérifie si le fichier a une extension autorisée (HTML, CSS, JS, Images)."""
# Conserver cette logique pour la validation des fichiers TÉLÉVERSÉS vers l'API
STATIC_ALLOWED_EXTENSIONS = {'html', 'htm', 'css', 'js', 'jpg', 'jpeg', 'png', 'gif', 'svg'}
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in STATIC_ALLOWED_EXTENSIONS
# Création du Blueprint 'web_bp'
web_bp = Blueprint('web_bp', __name__)
# ----------------------------------------------------------------------
# ROUTES D'AFFICHAGE STATIQUE/DE PAGES (SUPPRIMÉES)
# Toutes les routes suivantes ont été supprimées :
# @web_bp.route("/")
# @web_bp.route("/dashboard")
# @web_bp.route("/pricing")
# @web_bp.route("/docs")
# @web_bp.route("/contact")
# @web_bp.route("/serve_static/<deploy_id>/<path:filename>") # Cette route ne sert plus si le frontend est statique
# ----------------------------------------------------------------------
# ENDPOINTS API DE GESTION DES PROJETS
# ----------------------------------------------------------------------
# NOTE: Le déploiement implique toujours le téléversement d'un fichier.
@web_bp.route("/api/deploy", methods=['POST'])
@login_required
def upload_file():
"""Endpoint API pour le téléversement et le déploiement d'un nouveau projet."""
user_id = session.get('user_id')
# Les autres variables (plan_details, etc.) restent gérées par les fonctions backend
# 1. Vérifications et gestion du fichier (logique inchangée pour le téléversement de fichiers)
if 'file' not in request.files:
return jsonify({"success": False, "message": "Aucun fichier n'a été envoyé."}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"success": False, "message": "Nom de fichier vide."}), 400
if not allowed_static_file(file.filename):
return jsonify({"success": False, "message": f"Extension non autorisée: {file.filename.rsplit('.', 1)[1].lower()}."}), 400
deploy_id = str(uuid.uuid4())
temp_folder_path = os.path.join(current_app.config['TEMP_UPLOAD_FOLDER'], deploy_id)
os.makedirs(temp_folder_path, exist_ok=True)
# On utilise le nom original pour la sauvegarde dans le dossier temporaire
secured_filename = secure_filename(file.filename)
file_path = os.path.join(temp_folder_path, secured_filename)
file.save(file_path)
index_html_present = secured_filename.lower() in ('index.html', 'index.htm')
success_hf = False
try:
# --- ÉTAPE 2: Téléversement vers Hugging Face ---
success_hf, message_hf, full_repo_path = huggingface_storage.upload_project_folder_to_hf(
local_folder_path=temp_folder_path,
deploy_id=deploy_id
)
if not success_hf:
return jsonify({"success": False, "message": f"Échec du déploiement Hugging Face: {message_hf}"}), 500
# --- ÉTAPE 3: Enregistrement dans la base de données ---
success_db, message_db, deployment_row_id = save_new_deployment(
user_id=user_id,
deploy_id=deploy_id,
index_file_present=index_html_present,
full_repo_path=full_repo_path
)
if not success_db:
# Tenter de nettoyer Hugging Face si l'enregistrement BDD échoue
huggingface_storage.delete_project_folder_from_hf(deploy_id)
current_app.logger.error(f"Échec de l'enregistrement du projet: {message_db}. Annulation du déploiement.")
return jsonify({"success": False, "message": f"Échec BDD: {message_db}. Déploiement annulé."}), 500
# --- ÉTAPE 4: Succès et génération de l'URL permanente ---
# Générer l'URL de déploiement FINAL.
# C'est l'URL HUGGING FACE DIRECTE qui sera servie par le FRONTEND Vercel.
# ATTENTION: Cette URL doit pointer vers la ressource statique. Si vous utilisez
# le stockage HF comme CDN, l'URL sera différente.
# Je laisse la logique de construction d'URL, mais elle doit être adaptée à la manière
# dont Vercel ou un CDN servirait le fichier (ici, je fais un lien symbolique vers
# l'ancienne route, qui renvoyait le fichier).
# ÉTANT DONNÉ LA SÉPARATION, CETTE LOGIQUE D'URL DOIT ÊTRE MIS À JOUR POUR POINTER VERS L'EMPLACEMENT HF/CDN.
# Pour l'instant, je retourne l'ID de déploiement, le frontend peut construire l'URL.
deployment_url = f"[À DÉTERMINER - URL STATIQUE DU FICHIER DANS LE REPO HF/{deploy_id}/{secured_filename}]"
current_app.logger.info(f"Déploiement permanent réussi pour {deploy_id}. URL: {deployment_url}")
return jsonify({
"success": True,
"message": "Déploiement permanent réussi ! Votre site est sauvegardé sur Hugging Face.",
"deploy_id": deploy_id,
"url": deployment_url,
"index_present": index_html_present
}), 200
except Exception as e:
current_app.logger.error(f"Erreur inattendue lors du déploiement HF: {e}")
# Tenter le nettoyage si l'erreur s'est produite après le téléversement HF
if success_hf:
huggingface_storage.delete_project_folder_from_hf(deploy_id)
current_app.logger.warning(f"Annulation du déploiement {deploy_id} suite à une erreur inattendue.")
return jsonify({"success": False, "message": f"Erreur interne du serveur lors du déploiement: {str(e)}"}), 500
finally:
# Nettoyage du dossier temporaire local
if os.path.exists(temp_folder_path):
shutil.rmtree(temp_folder_path)
@web_bp.route("/api/project/<string:deploy_id>", methods=['GET'])
@login_required
def get_project_details(deploy_id):
"""Endpoint API pour récupérer les détails d'un projet spécifique."""
# 1. Vérification de l'appartenance du projet (Sécurité)
user_id = session.get('user_id')
project_data = get_deployment_by_id(deploy_id)
if not project_data or project_data.get('user_id') != user_id:
return jsonify({"success": False, "message": "Projet non trouvé ou accès non autorisé."}), 404
# 2. Retourner les données
return jsonify({
"success": True,
"project": {
"deploy_id": project_data.get('deploy_id'),
"last_deployed": project_data.get('last_deployed'),
"repo_path": project_data.get('full_repo_path'),
"url": project_data.get('url_deploiement_permanente') # Assurez-vous que cette clé existe
}
}), 200
@web_bp.route("/api/project/<string:deploy_id>", methods=['DELETE'])
@login_required
def delete_project_api(deploy_id):
"""Endpoint API pour supprimer un projet."""
user_id = session.get('user_id')
project_data = get_deployment_by_id(deploy_id)
# 1. Vérification de l'appartenance du projet
if not project_data or project_data.get('user_id') != user_id:
return jsonify({"success": False, "message": "Projet non trouvé ou accès non autorisé."}), 404
try:
# 2. Suppression de Hugging Face
success_hf, message_hf = huggingface_storage.delete_project_folder_from_hf(deploy_id)
if not success_hf:
# On ne considère pas comme une erreur critique si la BDD est la cible principale
current_app.logger.warning(f"Échec suppression HF pour {deploy_id}: {message_hf}")
# 3. Suppression de la Base de Données
success_db, message_db = delete_deployment_by_id(deploy_id)
if not success_db:
# L'utilisateur doit être averti que la BDD n'a pas été mise à jour
return jsonify({"success": False, "message": f"Échec de la suppression dans la base de données: {message_db}"}), 500
# 4. Succès
return jsonify({"success": True, "message": "Projet supprimé avec succès."}), 200
except Exception as e:
current_app.logger.error(f"Erreur inattendue lors de la suppression du projet {deploy_id}: {e}")
return jsonify({"success": False, "message": f"Erreur interne du serveur lors de la suppression: {str(e)}"}), 500
# ----------------------------------------------------------------------
# NOUVELLES ROUTES POUR LA GESTION DES DÉPÔTS (GitForge)
# ----------------------------------------------------------------------
@web_bp.route('/api/create_repository', methods=['POST'])
@api_session_required # ⬅️ CHANGEMENT CLÉ : Utiliser le décorateur API basé sur la session
def create_repository(client_user): # ⬅️ CHANGEMENT CLÉ : Accepter l'objet utilisateur injecté
"""
Point API pour créer un nouveau dépôt utilisateur.
L'utilisateur est authentifié et fourni par le décorateur @api_session_required via la session.
"""
# 1. Récupération de l'ID utilisateur à partir de l'objet injecté
# L'authentification (vérification de la session) est gérée par le décorateur.
user_id = client_user['uuid']
data = request.get_json()
# L'ancienne vérification 'if not user_id' est désormais gérée par le décorateur.
if not data or 'repo-name' not in data or 'visibility' not in data:
return jsonify({"success": False, "message": "Nom et visibilité du dépôt requis."}), 400
repo_name = data.get('repo-name').strip()
# Créer un slug simple pour le chemin HF et l'URL
# secure_filename doit être importé de werkzeug.utils
repo_slug = secure_filename(repo_name).lower().replace('-', '_')
if not repo_slug:
return jsonify({"success": False, "message": "Nom de dépôt invalide."}), 400
# Validation basique des données
visibility = data.get('visibility')
if visibility not in ['public', 'private']:
return jsonify({"success": False, "message": "Visibilité invalide."}), 400
# 1. Vérification des limites de plan (À implémenter plus tard si besoin)
# limit = get_plan_limit(client_user.get('user_plan'))
# 2. Vérification de l'existence du slug (pour s'assurer que l'utilisateur ne crée pas de doublon)
existing_repos = get_all_user_repositories(user_id)
if any(repo['repo_slug'] == repo_slug for repo in existing_repos):
return jsonify({"success": False, "message": f"Un dépôt avec le nom '{repo_name}' existe déjà."}), 409
# 3. Préparation des données du nouveau dépôt
repo_id = str(uuid.uuid4()) # ID unique pour le dépôt
new_repo_data = {
"repo_id": repo_id,
"repo_name": repo_name,
"repo_slug": repo_slug,
"user_id": user_id, # ⬅️ ID utilisateur récupéré et correctement utilisé
"visibility": visibility,
"license": data.get('license', 'None'),
"description": data.get('description', ''),
"created_at": datetime.now().isoformat()
}
try:
# 4. Enregistrement dans Baserow (Base de Données)
success_db, message_db = save_new_repository(user_id, new_repo_data)
if not success_db:
return jsonify({"success": False, "message": f"Échec de l'enregistrement du dépôt: {message_db}"}), 500
# 5. Création du dossier initial sur Hugging Face (Stockage de fichiers)
success_hf, message_hf = create_empty_repo_folder_on_hf(repo_slug, str(user_id))
if not success_hf:
# On log l'erreur et on retourne quand même un succès BDD, mais un avertissement
current_app.logger.error(f"Échec de l'initialisation HF pour {repo_slug}: {message_hf}")
return jsonify({
"success": True,
"message": f"Dépôt créé dans la BDD. Avertissement: Échec de l'initialisation du dossier sur Hugging Face ({message_hf}).",
"repo_slug": repo_slug
}), 201
# 6. Succès
return jsonify({
"success": True,
"message": "Dépôt créé avec succès.",
"repo_slug": repo_slug
}), 201
except Exception as e:
# current_app doit être importé de flask
current_app.logger.error(f"Erreur inattendue lors de la création du dépôt: {e}")
return jsonify({"success": False, "message": "Erreur serveur inattendue."}), 500
@web_bp.route('/api/repository_files/<repo_slug>', methods=['GET'])
@login_required
def get_repository_files(repo_slug):
"""
Point API pour retourner tous les fichiers du dépôt spécifié par son slug,
en listant le contenu du Dataset Hugging Face.
"""
user_id = session.get('user_id')
if not user_id:
return jsonify({"success": False, "message": "Session utilisateur requise."}), 401
# 1. Vérification rapide de l'existence du dépôt dans la BDD
existing_repos = get_all_user_repositories(user_id)
repo_info = next((repo for repo in existing_repos if repo['repo_slug'] == repo_slug), None)
if not repo_info:
return jsonify({"success": False, "message": "Dépôt non trouvé ou accès non autorisé."}), 404
# 2. Récupération des fichiers depuis Hugging Face
file_list, message = list_repo_files_from_hf(repo_slug, str(user_id))
if file_list is not None:
# Succès (même si la liste est vide)
return jsonify({
"success": True,
"message": message,
"repo_info": repo_info,
"files": file_list
}), 200
else:
# Échec de la récupération (ex: erreur de connexion HF)
return jsonify({"success": False, "message": message}), 500