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.

This commit is contained in:
jletienne 2025-10-20 17:21:55 +02:00
commit 8d6ae6291e
14 changed files with 754 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@ -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

274
README.md Normal file
View File

@ -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.

22
config.py Normal file
View File

@ -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()

2
controllers/__init__.py Normal file
View File

@ -0,0 +1,2 @@
"""Package des contrôleurs de l'application"""

19
controllers/debug.py Normal file
View File

@ -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}

22
controllers/health.py Normal file
View File

@ -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)
}

56
controllers/info.py Normal file
View File

@ -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)}"
)

65
controllers/list.py Normal file
View File

@ -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)}"
)

14
controllers/root.py Normal file
View File

@ -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"
}

107
controllers/upload.py Normal file
View File

@ -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()

9
env.example Normal file
View File

@ -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

57
main.py Normal file
View File

@ -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
)

8
requirements.txt Normal file
View File

@ -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

54
utils.py Normal file
View File

@ -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