Spaces:
Build error
Build error
| # 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. | |
| 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) | |
| 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 | |
| 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) | |
| # ---------------------------------------------------------------------- | |
| # ⬅️ 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 | |
| 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 |