commit 8d6ae6291ea9bb2e0b5d8879bf69eb4d9929932c Author: jletienne Date: Mon Oct 20 17:21:55 2025 +0200 Initial commit: Set up FastAPI application for Nextcloud API with configuration, controllers, and utility functions. Added .gitignore, README, and requirements files. Implemented endpoints for file listing, uploading, and health checks. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a8ae7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Fichiers de configuration sensibles +.env + +# Environnement virtuel Python +venv/ +env/ +ENV/ +.venv/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ddb9660 --- /dev/null +++ b/README.md @@ -0,0 +1,274 @@ +# PyNextcloud - API FastAPI pour Nextcloud + +API REST construite avec FastAPI pour interagir avec un serveur Nextcloud et lister les fichiers et dossiers. + +## 📋 Prérequis + +- Python 3.8 ou supérieur +- Un serveur Nextcloud avec accès API +- Identifiants Nextcloud (username et password) + +## 🚀 Installation + +### 1. Créer un environnement virtuel + +```powershell +# Créer l'environnement virtuel +python -m venv venv + +# Activer l'environnement virtuel +.\venv\Scripts\Activate.ps1 +``` + +### 2. Installer les dépendances + +```powershell +pip install -r requirements.txt +``` + +### 3. Configuration + +Créez un fichier `.env` à la racine du projet (vous pouvez copier `env.example`) : + +```powershell +Copy-Item env.example .env +``` + +Modifiez le fichier `.env` avec vos informations Nextcloud : + +```env +NEXTCLOUD_URL=https://votre-serveur-nextcloud.com +NEXTCLOUD_USERNAME=votre_username +NEXTCLOUD_PASSWORD=votre_password + +APP_HOST=0.0.0.0 +APP_PORT=8000 +``` + +## 🎯 Utilisation + +### Démarrer le serveur + +```powershell +# Méthode 1 : Avec uvicorn directement +uvicorn main:app --reload --host 0.0.0.0 --port 8000 + +# Méthode 2 : Avec Python +python main.py +``` + +Le serveur démarre sur `http://localhost:8000` + +### Documentation interactive + +Une fois le serveur démarré, accédez à : +- Documentation Swagger UI : `http://localhost:8000/docs` +- Documentation ReDoc : `http://localhost:8000/redoc` + +## 📡 Endpoints disponibles + +### 1. Route racine +``` +GET / +``` +Retourne un message d'accueil et le lien vers la documentation. + +### 2. Health Check +``` +GET /health +``` +Vérifie l'état de l'API et la connexion à Nextcloud. + +**Réponse exemple :** +```json +{ + "status": "healthy", + "nextcloud_connected": true +} +``` + +### 3. Lister un répertoire +``` +GET /list/{path} +``` +Liste tous les fichiers et dossiers d'un répertoire Nextcloud. + +**Paramètres :** +- `path` : Chemin du répertoire (utilisez `/` pour la racine) + +**Exemples :** +```powershell +# Lister la racine +curl http://localhost:8000/list/ + +# Lister le dossier Documents +curl http://localhost:8000/list/Documents + +# Lister un sous-dossier +curl http://localhost:8000/list/Documents/MonDossier +``` + +**Réponse exemple :** +```json +{ + "path": "/Documents", + "total_items": 5, + "summary": { + "directories": 2, + "files": 3 + }, + "items": [ + { + "name": "rapport.pdf", + "path": "/Documents/rapport.pdf", + "is_dir": false, + "size": 1024567, + "content_type": "application/pdf", + "last_modified": "2025-10-20T10:30:00", + "etag": "abc123" + }, + { + "name": "Images", + "path": "/Documents/Images", + "is_dir": true, + "size": 0, + "content_type": null, + "last_modified": "2025-10-19T15:20:00", + "etag": "def456" + } + ] +} +``` + +### 4. Informations sur un fichier/dossier +``` +GET /info/{path} +``` +Obtient les informations détaillées d'un fichier ou dossier spécifique. + +**Exemples :** +```powershell +curl http://localhost:8000/info/Documents/rapport.pdf +``` + +### 5. Upload un fichier +``` +POST /upload +``` +Upload un fichier vers un dossier Nextcloud. + +**Paramètres (form-data) :** +- `file` : Le fichier à uploader +- `path` : Chemin du dossier de destination (ex: `/Documents`) +- `filename` : Nom du fichier (optionnel, utilise le nom original si non spécifié) + +**Exemples :** +```powershell +# Upload un fichier avec PowerShell +$file = Get-Item "C:\chemin\vers\monfichier.pdf" +$form = @{ + file = $file + path = "/Documents" +} +Invoke-WebRequest -Uri "http://localhost:8000/upload" -Method POST -Form $form + +# Upload avec un nom personnalisé +$form = @{ + file = Get-Item "C:\chemin\vers\monfichier.pdf" + path = "/Documents" + filename = "nouveau_nom.pdf" +} +Invoke-WebRequest -Uri "http://localhost:8000/upload" -Method POST -Form $form +``` + +**Réponse exemple :** +```json +{ + "success": true, + "message": "Fichier uploadé avec succès", + "file": { + "name": "rapport.pdf", + "path": "/Documents/rapport.pdf", + "size": 1024567, + "content_type": "application/pdf" + } +} +``` + +## 🔧 Structure du projet + +``` +PyNextcloud/ +│ +├── main.py # Application FastAPI avec auto-chargement des contrôleurs +├── config.py # Configuration de l'application +├── utils.py # Fonctions utilitaires +├── controllers/ # 📁 Dossier des contrôleurs (auto-chargés) +│ ├── __init__.py +│ ├── root.py # Route racine +│ ├── health.py # Health check +│ ├── list.py # Lister les fichiers/dossiers +│ ├── info.py # Informations d'un fichier/dossier +│ └── upload.py # Upload de fichiers +├── requirements.txt # Dépendances Python +├── env.example # Exemple de fichier de configuration +├── .env # Configuration (à créer, non versionné) +└── README.md # Ce fichier +``` + +### 🎯 Architecture modulaire avec auto-chargement + +Le projet utilise une architecture modulaire où tous les contrôleurs dans le dossier `controllers/` sont **automatiquement chargés** au démarrage. + +**Pour ajouter un nouveau endpoint :** +1. Créez un nouveau fichier dans `controllers/`, par exemple `controllers/mon_endpoint.py` +2. Créez un `router` FastAPI dans ce fichier +3. C'est tout ! Le contrôleur sera automatiquement chargé au démarrage + +**Exemple de nouveau contrôleur :** +```python +# controllers/mon_endpoint.py +from fastapi import APIRouter + +router = APIRouter() + +@router.get("/mon-endpoint") +async def mon_endpoint(): + return {"message": "Mon nouveau endpoint"} +``` + +## 🛠️ Dépendances principales + +- **FastAPI** : Framework web moderne et rapide +- **Uvicorn** : Serveur ASGI +- **nc-py-api** : Client Python pour l'API Nextcloud +- **python-dotenv** : Gestion des variables d'environnement +- **pydantic** : Validation des données + +## ⚠️ Notes importantes + +1. **Sécurité** : Ne committez jamais votre fichier `.env` dans Git. Il contient vos identifiants. +2. **Chemins** : Les chemins doivent être relatifs à la racine de votre espace Nextcloud. +3. **Authentification** : L'API utilise l'authentification basique avec username/password. + +## 🐛 Dépannage + +### Erreur de connexion à Nextcloud +- Vérifiez que l'URL de votre serveur Nextcloud est correcte +- Vérifiez vos identifiants +- Assurez-vous que votre serveur Nextcloud est accessible + +### Erreur 404 sur un répertoire +- Vérifiez que le chemin existe dans votre Nextcloud +- Les chemins sont sensibles à la casse + +### Problèmes d'activation de l'environnement virtuel +Si vous avez une erreur de sécurité PowerShell, exécutez : +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +## 📝 Licence + +Ce projet est libre d'utilisation pour vos besoins personnels et professionnels. + diff --git a/config.py b/config.py new file mode 100644 index 0000000..38fc4f6 --- /dev/null +++ b/config.py @@ -0,0 +1,22 @@ +"""Configuration de l'application""" +from pydantic_settings import BaseSettings, SettingsConfigDict +from dotenv import load_dotenv + +# Charger les variables d'environnement +load_dotenv() + + +class Settings(BaseSettings): + """Configuration de l'application""" + nextcloud_url: str + nextcloud_username: str + nextcloud_password: str + app_host: str = "0.0.0.0" + app_port: int = 8000 + + model_config = SettingsConfigDict(env_file=".env") + + +# Instance globale des paramètres +settings = Settings() + diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100644 index 0000000..1ee7805 --- /dev/null +++ b/controllers/__init__.py @@ -0,0 +1,2 @@ +"""Package des contrôleurs de l'application""" + diff --git a/controllers/debug.py b/controllers/debug.py new file mode 100644 index 0000000..ae72415 --- /dev/null +++ b/controllers/debug.py @@ -0,0 +1,19 @@ +"""Contrôleur de débogage pour lister les routes disponibles""" +from fastapi import APIRouter, Request + +router = APIRouter() + + +@router.get("/debug/routes") +async def list_routes(request: Request): + """Liste toutes les routes disponibles dans l'application""" + routes = [] + for route in request.app.routes: + if hasattr(route, "methods") and hasattr(route, "path"): + routes.append({ + "path": route.path, + "methods": list(route.methods), + "name": route.name + }) + return {"total_routes": len(routes), "routes": routes} + diff --git a/controllers/health.py b/controllers/health.py new file mode 100644 index 0000000..4b7fe63 --- /dev/null +++ b/controllers/health.py @@ -0,0 +1,22 @@ +"""Contrôleur pour vérifier l'état de l'application""" +from fastapi import APIRouter +from utils import get_nextcloud_client + +router = APIRouter() + + +@router.get("/health") +async def health_check(): + """Vérification de l'état de l'API""" + try: + nc = get_nextcloud_client() + # Test de connexion + nc.users.get_list() + return {"status": "healthy", "nextcloud_connected": True} + except Exception as e: + return { + "status": "unhealthy", + "nextcloud_connected": False, + "error": str(e) + } + diff --git a/controllers/info.py b/controllers/info.py new file mode 100644 index 0000000..90225cc --- /dev/null +++ b/controllers/info.py @@ -0,0 +1,56 @@ +"""Contrôleur pour obtenir les informations d'un fichier/dossier""" +from fastapi import APIRouter, HTTPException, Path +from fastapi.responses import JSONResponse +from nc_py_api import NextcloudException +from utils import get_nextcloud_client, format_file_info + +router = APIRouter() + + +@router.get("/info/{path:path}") +async def file_info( + path: str = Path( + ..., + description="Chemin du fichier/dossier", + examples=["Documents/fichier.txt"] + ) +): + """ + Obtient les informations détaillées d'un fichier ou dossier + + Args: + path: Chemin du fichier/dossier + + Returns: + JSON contenant les informations du fichier + """ + try: + nc = get_nextcloud_client() + + # Normaliser le chemin + file_path = "/" + path.strip("/") + + try: + file_info_data = nc.files.by_path(file_path) + except NextcloudException as e: + if "404" in str(e): + raise HTTPException( + status_code=404, + detail=f"Le fichier/dossier '{file_path}' n'existe pas" + ) + else: + raise HTTPException( + status_code=500, + detail=f"Erreur Nextcloud: {str(e)}" + ) + + return JSONResponse(content=format_file_info(file_info_data)) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Erreur interne: {str(e)}" + ) + diff --git a/controllers/list.py b/controllers/list.py new file mode 100644 index 0000000..c838133 --- /dev/null +++ b/controllers/list.py @@ -0,0 +1,65 @@ +"""Contrôleur pour lister les fichiers et dossiers Nextcloud""" +from fastapi import APIRouter, HTTPException, Path +from fastapi.responses import JSONResponse +from nc_py_api import NextcloudException +from utils import get_nextcloud_client, format_file_info + +router = APIRouter() + + +@router.get("/list/{path:path}") +async def list_directory( + path: str = Path( + ..., + description="Chemin du répertoire à lister (utilisez '/' pour la racine)", + examples=["Documents"] + ) +): + """ + Liste tous les fichiers et dossiers d'un répertoire Nextcloud + + Args: + path: Chemin du répertoire (sans le slash initial) + + Returns: + JSON contenant la liste des fichiers et dossiers + """ + try: + # Créer le client Nextcloud + nc = get_nextcloud_client() + + # Normaliser le chemin + if path == "/" or path == "": + directory_path = "/" + else: + # Enlever les slashes de début/fin + directory_path = "/" + path.strip("/") + + # Lister le contenu du répertoire + try: + files_list = nc.files.listdir(path=directory_path) + except NextcloudException as e: + if "404" in str(e): + raise HTTPException( + status_code=404, + detail=f"Le répertoire '{directory_path}' n'existe pas" + ) + else: + raise HTTPException( + status_code=500, + detail=f"Erreur Nextcloud: {str(e)}" + ) + + # Formater les résultats - retourner uniquement la liste des items + items = [format_file_info(item) for item in files_list] + + return JSONResponse(content=items) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Erreur interne: {str(e)}" + ) + diff --git a/controllers/root.py b/controllers/root.py new file mode 100644 index 0000000..ba6f7b5 --- /dev/null +++ b/controllers/root.py @@ -0,0 +1,14 @@ +"""Contrôleur pour la route racine""" +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/") +async def root(): + """Route racine""" + return { + "message": "API Nextcloud - Utilisez /list/{path} pour lister les fichiers", + "documentation": "/docs" + } + diff --git a/controllers/upload.py b/controllers/upload.py new file mode 100644 index 0000000..32adca8 --- /dev/null +++ b/controllers/upload.py @@ -0,0 +1,107 @@ +"""Contrôleur pour uploader des fichiers vers Nextcloud""" +from fastapi import APIRouter, HTTPException, UploadFile, File, Form +from fastapi.responses import JSONResponse +from nc_py_api import NextcloudException +from utils import get_nextcloud_client +from typing import Optional + +router = APIRouter() + + +@router.post("/upload") +async def upload_file( + file: UploadFile = File(..., description="Fichier à uploader"), + path: str = Form(..., description="Chemin du dossier de destination (ex: /Documents)"), + filename: Optional[str] = Form(None, description="Nom du fichier (optionnel, utilise le nom original si non spécifié)") +): + """ + Upload un fichier vers un dossier Nextcloud + + Args: + file: Le fichier à uploader + path: Chemin du dossier de destination + filename: Nom du fichier (optionnel) + + Returns: + JSON contenant les informations du fichier uploadé + """ + try: + # Créer le client Nextcloud + nc = get_nextcloud_client() + + # Déterminer le nom du fichier + final_filename = filename if filename else file.filename + + # Normaliser le chemin de destination + if path == "/" or path == "": + destination_path = f"/{final_filename}" + directory_path = "/" + else: + # Enlever les slashes de début/fin et ajouter le nom du fichier + clean_path = path.strip("/") + destination_path = f"/{clean_path}/{final_filename}" + directory_path = f"/{clean_path}" + + # Vérifier si le dossier existe, sinon le créer + if directory_path != "/": + try: + # Vérifier si le dossier existe + nc.files.by_path(directory_path) + except NextcloudException as e: + if "404" in str(e): + # Le dossier n'existe pas, on le crée + try: + nc.files.mkdir(directory_path) + print(f"📁 Dossier créé : {directory_path}") + except Exception as mkdir_error: + raise HTTPException( + status_code=500, + detail=f"Impossible de créer le dossier '{directory_path}': {str(mkdir_error)}" + ) + + # Lire le contenu du fichier + file_content = await file.read() + + # Uploader le fichier vers Nextcloud + try: + nc.files.upload( + path=destination_path, + content=file_content + ) + except NextcloudException as e: + if "409" in str(e) or "412" in str(e): + raise HTTPException( + status_code=409, + detail=f"Le fichier '{final_filename}' existe déjà" + ) + else: + raise HTTPException( + status_code=500, + detail=f"Erreur Nextcloud: {str(e)}" + ) + + # Retourner les informations du fichier uploadé + result = { + "success": True, + "message": "Fichier uploadé avec succès", + "file": { + "name": final_filename, + "path": destination_path, + "size": len(file_content), + "content_type": file.content_type + } + } + + return JSONResponse(content=result, status_code=201) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Erreur interne: {str(e)}" + ) + finally: + # Fermer le fichier uploadé + await file.close() + diff --git a/env.example b/env.example new file mode 100644 index 0000000..cb5a1c9 --- /dev/null +++ b/env.example @@ -0,0 +1,9 @@ +# Configuration du serveur Nextcloud +NEXTCLOUD_URL=https://votre-serveur-nextcloud.com +NEXTCLOUD_USERNAME=votre_username +NEXTCLOUD_PASSWORD=votre_password + +# Configuration de l'application +APP_HOST=0.0.0.0 +APP_PORT=8000 + diff --git a/main.py b/main.py new file mode 100644 index 0000000..192fa74 --- /dev/null +++ b/main.py @@ -0,0 +1,57 @@ +"""Application FastAPI principale avec auto-chargement des contrôleurs""" +from fastapi import FastAPI +import uvicorn +from pathlib import Path +import importlib +import os +from config import settings + +# Créer l'application FastAPI +app = FastAPI( + title="Nextcloud API", + description="API pour lister les fichiers et dossiers d'un serveur Nextcloud", + version="1.0.0" +) + + +def auto_load_controllers(): + """Charge automatiquement tous les contrôleurs depuis le dossier controllers""" + controllers_dir = Path(__file__).parent / "controllers" + + if not controllers_dir.exists(): + print("⚠️ Le dossier 'controllers' n'existe pas") + return + + # Parcourir tous les fichiers Python dans le dossier controllers + for file_path in controllers_dir.glob("*.py"): + # Ignorer __init__.py + if file_path.stem == "__init__": + continue + + try: + # Importer le module dynamiquement + module_name = f"controllers.{file_path.stem}" + module = importlib.import_module(module_name) + + # Vérifier si le module a un attribut 'router' + if hasattr(module, "router"): + app.include_router(module.router) + print(f"✅ Contrôleur chargé : {file_path.stem}") + else: + print(f"⚠️ Le fichier {file_path.stem}.py n'a pas de 'router'") + + except Exception as e: + print(f"❌ Erreur lors du chargement de {file_path.stem}: {str(e)}") + + +# Auto-charger tous les contrôleurs au démarrage +auto_load_controllers() + + +if __name__ == "__main__": + uvicorn.run( + "main:app", + host=settings.app_host, + port=settings.app_port, + reload=True + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..497a7fc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +fastapi>=0.109.2 +uvicorn[standard]>=0.24.0 +python-dotenv>=1.0.0 +nc-py-api>=0.21.1 +pydantic>=2.5.0 +pydantic-settings>=2.1.0 +python-multipart>=0.0.6 + diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..88c2a8e --- /dev/null +++ b/utils.py @@ -0,0 +1,54 @@ +"""Fonctions utilitaires pour l'application""" +from typing import Dict, Any +from fastapi import HTTPException +from nc_py_api import Nextcloud +from config import settings + + +def get_nextcloud_client() -> Nextcloud: + """Crée et retourne un client Nextcloud""" + try: + nc = Nextcloud( + nextcloud_url=settings.nextcloud_url, + nc_auth_user=settings.nextcloud_username, + nc_auth_pass=settings.nextcloud_password + ) + return nc + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Erreur de connexion à Nextcloud: {str(e)}" + ) + + +def format_file_info(file_info) -> Dict[str, Any]: + """Formate les informations d'un fichier/dossier""" + result = { + "name": getattr(file_info, 'name', ''), + "path": getattr(file_info, 'user_path', getattr(file_info, 'path', '')), + "is_dir": getattr(file_info, 'is_dir', False), + } + + # Attributs optionnels + if hasattr(file_info, 'info'): + info = file_info.info + result["size"] = getattr(info, 'size', 0) + result["content_type"] = getattr(info, 'mimetype', None) + if hasattr(info, 'last_modified'): + result["last_modified"] = info.last_modified.isoformat() if info.last_modified else None + else: + result["last_modified"] = None + result["etag"] = getattr(info, 'etag', None) + else: + result["size"] = getattr(file_info, 'size', 0) + result["content_type"] = getattr(file_info, 'mimetype', getattr(file_info, 'content_type', None)) + if hasattr(file_info, 'last_modified') and file_info.last_modified: + result["last_modified"] = file_info.last_modified.isoformat() + else: + result["last_modified"] = None + result["etag"] = getattr(file_info, 'etag', None) + + result["file_id"] = getattr(file_info, 'file_id', None) + + return result +