diff --git a/backend/app/routers/media.py b/backend/app/routers/media.py index 6777597..659cea3 100644 --- a/backend/app/routers/media.py +++ b/backend/app/routers/media.py @@ -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}"} diff --git a/backend/app/routers/settings.py b/backend/app/routers/settings.py index 47b2db3..5344fa2 100644 --- a/backend/app/routers/settings.py +++ b/backend/app/routers/settings.py @@ -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)) diff --git a/backend/requirements.txt b/backend/requirements.txt index 2514d58..1a2311b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/data/jardin.db b/data/jardin.db index eafb4c9..ffe206f 100755 Binary files a/data/jardin.db and b/data/jardin.db differ diff --git a/data/jardin.db-shm b/data/jardin.db-shm deleted file mode 100755 index 258a8b8..0000000 Binary files a/data/jardin.db-shm and /dev/null differ diff --git a/data/jardin.db-wal b/data/jardin.db-wal deleted file mode 100755 index b61c08e..0000000 Binary files a/data/jardin.db-wal and /dev/null differ diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts index 358d35c..d58321e 100644 --- a/frontend/src/api/settings.ts +++ b/frontend/src/api/settings.ts @@ -45,4 +45,6 @@ export const settingsApi = { } return { blob: r.data as Blob, filename } }), + backupSamba: () => + client.post<{ ok: boolean; fichier: string; chemin: string }>('/api/settings/backup/samba').then(r => r.data), } diff --git a/frontend/src/components/PhotoGallery.vue b/frontend/src/components/PhotoGallery.vue index 6d6446c..a078e1c 100644 --- a/frontend/src/components/PhotoGallery.vue +++ b/frontend/src/components/PhotoGallery.vue @@ -4,7 +4,7 @@ {{ medias.length }} photo(s) diff --git a/frontend/src/components/PhotoIdentifyModal.vue b/frontend/src/components/PhotoIdentifyModal.vue index 4e25b57..eac3ce6 100644 --- a/frontend/src/components/PhotoIdentifyModal.vue +++ b/frontend/src/components/PhotoIdentifyModal.vue @@ -16,7 +16,7 @@ Choisir / Photographier