This commit is contained in:
2026-03-22 11:42:57 +01:00
parent 2043a1b8b5
commit 7afca6ed04
14 changed files with 300 additions and 22 deletions

View File

@@ -10,6 +10,7 @@ from sqlmodel import Session, select
from app.config import UPLOAD_DIR
from app.database import get_session
from app.models.media import Attachment, Media
from app.models.settings import UserSettings
class MediaPatch(BaseModel):
@@ -102,6 +103,20 @@ def _canonicalize_rows(rows: List[Media], session: Session) -> None:
session.commit()
try:
import pillow_heif
pillow_heif.register_heif_opener()
except ImportError:
pass
def _is_heic(content_type: str, filename: str) -> bool:
if content_type.lower() in ("image/heic", "image/heif"):
return True
fn = (filename or "").lower()
return fn.endswith(".heic") or fn.endswith(".heif")
def _save_webp(data: bytes, max_px: int) -> str:
try:
from PIL import Image
@@ -122,12 +137,28 @@ def _save_webp(data: bytes, max_px: int) -> str:
@router.post("/upload")
async def upload_file(file: UploadFile = File(...)):
async def upload_file(
file: UploadFile = File(...),
session: Session = Depends(get_session),
):
os.makedirs(UPLOAD_DIR, exist_ok=True)
data = await file.read()
ct = file.content_type or ""
if ct.startswith("image/"):
name = _save_webp(data, 1200)
# Lire la largeur max configurée (défaut 1200, 0 = taille originale)
setting = session.exec(select(UserSettings).where(UserSettings.cle == "image_max_width")).first()
max_px = 1200
if setting:
try:
max_px = int(setting.valeur)
except (ValueError, TypeError):
pass
if max_px <= 0:
max_px = 99999
heic = _is_heic(ct, file.filename or "")
if heic or ct.startswith("image/"):
name = _save_webp(data, max_px)
thumb = _save_webp(data, 300)
return {"url": f"/uploads/{name}", "thumbnail_url": f"/uploads/{thumb}"}

View File

@@ -8,7 +8,7 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from starlette.background import BackgroundTask
from sqlmodel import Session, select
@@ -235,8 +235,8 @@ def get_debug_system_stats() -> dict[str, Any]:
}
@router.get("/settings/backup/download")
def download_backup_zip() -> FileResponse:
def _create_backup_zip() -> tuple[Path, str]:
"""Crée l'archive ZIP de sauvegarde. Retourne (chemin_tmp, nom_fichier)."""
now = datetime.now(timezone.utc)
ts = now.strftime("%Y%m%d_%H%M%S")
db_path = _resolve_sqlite_db_path()
@@ -247,17 +247,12 @@ def download_backup_zip() -> FileResponse:
os.close(fd)
tmp_zip = Path(tmp_zip_path)
stats = {
"database_files": 0,
"upload_files": 0,
"text_files": 0,
}
stats = {"database_files": 0, "upload_files": 0, "text_files": 0}
with zipfile.ZipFile(tmp_zip, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as zipf:
if db_path and db_path.is_file():
zipf.write(db_path, arcname=f"db/{db_path.name}")
stats["database_files"] = 1
stats["upload_files"] = _zip_directory(zipf, uploads_dir, "uploads")
stats["text_files"] = _zip_data_text_files(zipf, data_root, db_path, uploads_dir)
@@ -274,10 +269,66 @@ def download_backup_zip() -> FileResponse:
}
zipf.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2))
download_name = f"jardin_backup_{ts}.zip"
return tmp_zip, f"jardin_backup_{ts}.zip"
@router.get("/settings/backup/download")
def download_backup_zip() -> FileResponse:
tmp_zip, download_name = _create_backup_zip()
return FileResponse(
path=str(tmp_zip),
media_type="application/zip",
filename=download_name,
background=BackgroundTask(_safe_remove, str(tmp_zip)),
)
@router.post("/settings/backup/samba")
def backup_to_samba(session: Session = Depends(get_session)) -> dict[str, Any]:
"""Envoie une sauvegarde ZIP vers un partage Samba/CIFS."""
def _get(key: str, default: str = "") -> str:
row = session.exec(select(UserSettings).where(UserSettings.cle == key)).first()
return row.valeur if row else default
server = _get("samba_serveur").strip()
share = _get("samba_partage").strip()
username = _get("samba_utilisateur").strip()
password = _get("samba_motdepasse")
subfolder = _get("samba_sous_dossier").strip().strip("/\\")
if not server or not share:
raise HTTPException(400, "Configuration Samba incomplète : serveur et partage requis.")
try:
import smbclient # type: ignore
except ImportError:
raise HTTPException(500, "Module smbprotocol non installé dans l'environnement.")
tmp_zip, filename = _create_backup_zip()
try:
smbclient.register_session(server, username=username or None, password=password or None)
remote_dir = f"\\\\{server}\\{share}"
if subfolder:
remote_dir = f"{remote_dir}\\{subfolder}"
try:
smbclient.makedirs(remote_dir, exist_ok=True)
except Exception:
pass
remote_path = f"{remote_dir}\\{filename}"
with open(tmp_zip, "rb") as local_f:
data = local_f.read()
with smbclient.open_file(remote_path, mode="wb") as smb_f:
smb_f.write(data)
return {"ok": True, "fichier": filename, "chemin": remote_path}
except HTTPException:
raise
except Exception as exc:
raise HTTPException(500, f"Erreur Samba : {exc}") from exc
finally:
_safe_remove(str(tmp_zip))

View File

@@ -6,6 +6,8 @@ aiofiles==24.1.0
pytest==8.3.3
httpx==0.28.0
Pillow==11.1.0
pillow-heif==0.21.0
smbprotocol==1.15.0
skyfield==1.49
pytz==2025.1
numpy==2.2.3