Mailix / huggingface_storage.py
ernestmindres's picture
Update huggingface_storage.py
7cc6465 verified
# huggingface_storage.py
import os
import shutil
import logging
from huggingface_hub import HfApi, HfFileSystem
from huggingface_hub.utils import HfHubHTTPError # <-- LIGNE CORRIGÉE
from typing import Optional, Dict
logger = logging.getLogger(__name__)
# --- Configuration Hugging Face ---
# Le token sera automatiquement chargé par HfApi si la variable d'environnement HF_TOKEN est définie.
# Repo ID du dataset (Ex: 'mon_utilisateur/mon_dataset_sites_web')
HF_DATASET_REPO_ID = os.environ.get("HF_DATASET_REPO_ID")
api = HfApi()
fs = HfFileSystem()
def upload_project_folder_to_hf(
local_folder_path: str,
deploy_id: str,
repo_id: Optional[str] = None
) -> tuple[bool, str, Optional[str]]:
"""
Téléverse un dossier local (le projet utilisateur) vers un dossier spécifique
dans le Dataset Hugging Face, en une seule commit, en utilisant deploy_id comme path_in_repo.
:param local_folder_path: Chemin absolu du dossier local à téléverser (ex: temp_uploads/abc-123).
:param deploy_id: L'ID unique du projet (utilisé comme nom de dossier dans le repo HF).
:param repo_id: L'ID complet du repo HF (si non fourni, utilise HF_DATASET_REPO_ID).
:return: (Succès: bool, Message: str, Chemin complet du repo: Optional[str])
"""
repo_id = repo_id if repo_id else HF_DATASET_REPO_ID
if not repo_id:
return False, "Erreur de configuration: Repository Hugging Face non défini (HF_DATASET_REPO_ID manquant).", None
try:
if not os.path.isdir(local_folder_path):
return False, f"Erreur: Le dossier local pour le déploiement ID '{deploy_id}' est introuvable.", None
# path_in_repo est le chemin où les fichiers seront stockés dans le repo (le dossier du projet)
hf_folder_path = f"{deploy_id}"
logger.info(f"Début du téléversement de {local_folder_path} vers HF repo: {repo_id}/{hf_folder_path}")
api.upload_folder(
folder_path=local_folder_path,
repo_id=repo_id,
path_in_repo=hf_folder_path,
repo_type="dataset",
commit_message=f"Ajout du site web statique pour le projet ID: {deploy_id}",
delete_old_files=True
)
full_repo_path = f"https://huggingface.co/datasets/{repo_id}/tree/main/{deploy_id}"
logger.info(f"Téléversement HF réussi pour le projet {deploy_id}.")
return True, "Téléversement vers Hugging Face réussi.", full_repo_path
except HfHubHTTPError as e:
error_message = f"Erreur API Hugging Face (vérifiez votre jeton d'API et les permissions): {e.response.text}"
logger.error(error_message)
return False, error_message, None
except Exception as e:
logger.error(f"Erreur lors du téléchargement HF pour {deploy_id}: {e}")
return False, f"Échec du téléchargement Hugging Face: {str(e)}", None
def get_project_files_from_hf(
deploy_id: str,
local_target_path: str,
repo_id: Optional[str] = None
) -> tuple[bool, str]:
"""
Télécharge le dossier d'un projet depuis Hugging Face vers un dossier local.
"""
repo_id = repo_id if repo_id else HF_DATASET_REPO_ID
if not repo_id:
return False, "Erreur de configuration: Repository Hugging Face non défini."
try:
# Créer le dossier cible local
if os.path.exists(local_target_path):
shutil.rmtree(local_target_path)
os.makedirs(local_target_path, exist_ok=True)
# Chemin complet vers le dossier du projet dans le dépôt Hugging Face
hf_folder_path = f"datasets/{repo_id}/{deploy_id}"
if not fs.isdir(hf_folder_path):
return False, f"Projet non trouvé sur Hugging Face: {deploy_id}"
# Télécharger récursivement le dossier du projet
# fs.get(chemin_distant, chemin_local)
fs.get(hf_folder_path, local_target_path, recursive=True)
logger.info(f"Téléchargement réussi du projet {deploy_id} vers {local_target_path}.")
return True, "Dossier de projet téléchargé avec succès."
except Exception as e:
logger.error(f"Erreur lors du téléchargement HF pour {deploy_id}: {e}")
return False, f"Échec du téléchargement Hugging Face: {str(e)}"
def delete_project_folder_from_hf(
deploy_id: str,
repo_id: Optional[str] = None
) -> tuple[bool, str]:
"""
Supprime un dossier (projet utilisateur) complet du Dataset Hugging Face.
"""
repo_id = repo_id if repo_id else HF_DATASET_REPO_ID
if not repo_id:
return False, "Erreur de configuration: Repository Hugging Face non défini."
try:
# delete_folder crée une seule commit pour la suppression du dossier.
commit_url = api.delete_folder(
path_in_repo=deploy_id,
repo_id=repo_id,
repo_type="dataset",
commit_message=f"Suppression du projet utilisateur ID: {deploy_id}"
)
logger.info(f"Suppression réussie du projet {deploy_id}. URL de commit: {commit_url}")
return True, "Dossier de projet supprimé avec succès de Hugging Face."
except Exception as e:
logger.error(f"Erreur inconnue lors de la suppression HF pour {deploy_id}: {e}")
# Si le dossier est déjà supprimé, HfHubHTTPError 404 est levée par delete_folder
if "404" in str(e):
return True, "Dossier non trouvé sur Hugging Face (déjà supprimé ou ID invalide)."
return False, f"Échec de la suppression Hugging Face: {str(e)}"
def create_empty_repo_folder_on_hf(
repo_slug: str,
user_id: str,
repo_id: Optional[str] = None
) -> tuple[bool, str]:
"""
Crée une structure de dossier vide (représentant le dépôt) sur le Dataset Hugging Face.
Ceci est souvent implémenté en créant un fichier .gitkeep ou README.md initial.
Nous allons créer un fichier README.md minimal.
"""
repo_id = repo_id if repo_id else HF_DATASET_REPO_ID
if not repo_id:
return False, "Erreur de configuration: Repository Hugging Face non défini."
readme_content = f"# Dépôt {repo_slug}\n\nBienvenue sur votre dépôt GitForge, hébergé sur Mailix.\n\nCommencez par ajouter ou créer des fichiers."
path_in_repo = f"{user_id}/{repo_slug}/README.md"
try:
# Utilisation de HfApi.upload_file pour créer un fichier unique, ce qui crée le dossier
commit_url = api.upload_file(
path_or_fileobj=readme_content.encode('utf-8'),
path_in_repo=path_in_repo,
repo_id=repo_id,
repo_type="dataset",
commit_message=f"Initialisation du dépôt {repo_slug}"
)
logger.info(f"Dépôt vide créé sur HF pour {repo_slug}. Commit: {commit_url}")
return True, "Dépôt initialisé avec succès sur Hugging Face."
except Exception as e:
logger.error(f"Échec de l'initialisation du dépôt HF pour {repo_slug}: {e}")
return False, f"Échec de l'initialisation Hugging Face: {str(e)}"
def list_repo_files_from_hf(
repo_slug: str,
user_id: str,
repo_id: Optional[str] = None
) -> tuple[list[Dict], str]:
"""
Liste tous les fichiers et dossiers dans le chemin spécifique (user_id/repo_slug)
du Dataset Hugging Face.
"""
repo_id = repo_id if repo_id else HF_DATASET_REPO_ID
if not repo_id:
return [], "Erreur de configuration: Repository Hugging Face non défini."
# Le chemin dans le repo est 'user_id/repo_slug'
path_to_repo = f"{user_id}/{repo_slug}"
try:
# Utiliser HfFileSystem pour lister le contenu du dossier
items = fs.ls(f"datasets/{repo_id}/{path_to_repo}", detail=True)
file_list = []
for item in items:
# item est un dictionnaire avec 'name', 'type', 'size', 'last_modified', etc.
# On ignore le nom du dossier racine pour l'affichage utilisateur
file_name = item['name'].split('/')[-1]
# Ignorer les dossiers et les fichiers système cachés (.DS_Store, .gitattributes, etc.)
if item['type'] == 'directory' or file_name.startswith('.'):
# Pour une future étape, on pourrait inclure les dossiers
continue
file_list.append({
"name": file_name,
"path": item['name'], # Chemin complet pour référence interne
"size": item.get('size', 0),
"type": "file", # Dans cette version, nous listons seulement les fichiers
"last_modified": item.get('last_modified', 'N/A')
})
logger.info(f"Liste des fichiers récupérée pour {path_to_repo} ({len(file_list)} fichiers).")
return file_list, "Liste des fichiers récupérée avec succès."
except HfHubHTTPError as e:
if "404" in str(e):
# Le dossier n'existe pas encore (dépôt vide sans initialisation)
return [], f"Le dépôt '{repo_slug}' n'a pas encore de fichiers sur Hugging Face (404)."
logger.error(f"Erreur HTTP HF lors de la liste des fichiers pour {path_to_repo}: {e}")
return [], f"Erreur Hugging Face: {str(e)}"
except Exception as e:
logger.error(f"Erreur inconnue lors de la liste des fichiers HF pour {path_to_repo}: {e}")
return [], f"Erreur inconnue lors de l'accès à Hugging Face: {str(e)}"