From 7afca6ed041b5183d4336f162c60626c4d3bbdc0 Mon Sep 17 00:00:00 2001 From: gilles Date: Sun, 22 Mar 2026 11:42:57 +0100 Subject: [PATCH] aorus --- backend/app/routers/media.py | 37 +++- backend/app/routers/settings.py | 71 ++++++- backend/requirements.txt | 2 + data/jardin.db | Bin 409600 -> 413696 bytes data/jardin.db-shm | Bin 32768 -> 0 bytes data/jardin.db-wal | Bin 24752 -> 0 bytes frontend/src/api/settings.ts | 2 + frontend/src/components/PhotoGallery.vue | 2 +- .../src/components/PhotoIdentifyModal.vue | 2 +- frontend/src/views/AstucesView.vue | 2 +- frontend/src/views/JardinsView.vue | 2 +- frontend/src/views/OutilsView.vue | 2 +- frontend/src/views/PlantesView.vue | 2 +- frontend/src/views/ReglagesView.vue | 198 +++++++++++++++++- 14 files changed, 300 insertions(+), 22 deletions(-) delete mode 100755 data/jardin.db-shm delete mode 100755 data/jardin.db-wal 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 eafb4c96db8c473c8891fc9fb933b9deee960c18..ffe206fae7c00a94b996ec4e411acd2a199c0efc 100755 GIT binary patch delta 6566 zcma)BdvH|c6~B+YdpEmDmLLIQV$6z!JlJsW-FF_`FOrb7goKa)u}v{b)`+GM!=n^C zS?sNK#-v0hE9*EDeiKi^X<2PH!@B0(({ z3~6vjID1%=T(jgp?-^S~(gS6_2Yo;HjrlhF27KMVYkVQ!)xKh%+xs8yyWTU00BP3gv)^%|(M+C}_mjIWA*s9hcEHj>|}|<1*aixD2g!TpHbuOMMl&Y|8*^ zUD>zwM5p62zS40S>u_8~SKNt%!GGlRffcT`uex!T_Q-Wj{zOR(JiF^!)&EV~{M2L;>Elvi!ftxADitSML6-hfggd4Fji8Nmkn zbtNXHyT_ELMfqnHA zQ_?~ClzdozNWM$nDzBH@WkW8L-O>lrS!q~$QCce@X^;4r^kb<;nj?NKzANq)cZ#og zo)wRYgPuCigPxyyMm*o~w5T_E=6fXdg8G_zTzyi#Uma6dtBqIrM`%&?Fq1xA zoC4~AI7GyP;CM979(~3kjm%(nnpI5IgANO^UOkEloDzTS5J#EerySByuO26CKaeYp z5$Th;(g>06&y|LV^a+Pl?@d7)J?;>*77`!JmU424k7i4C?3zvt?{i2E7RDplQjO>G z;VEf;h_DW#`&Ihvs8Zo_6_yE$?+eVTBJ@#H(1r$sd2vb=Hz$_TDR44Js?ltna7cCL z(jRi92F=UyT&Y2Vnk<%6bhLmIdapraeIfT@>csDJq!84xTxpmX{#~w=I`P|FDMm|a z;?Zoe&U#clk}WlO$`nq{63rh(+;IqaS4KW z9=H5k;{BpajtS4$>Mo^vxi^q^F7MvF_B@a0sAq>KsJ^Z4QCF%3?i23Y+>4d-%0A^5 zCE$9^b+4<_rOLOr-8$!DS&jej>pK4|tS@;TkwZe3&R8zOdCoNrh?|*NI$3+zL%l# zYpDrTOuqI`bapja&4s}d6~lVSU+UEHz*x0Cxp8=Lm2p&F=Ko%{=KqqE&gumsxi4T`^mI6+1MccL0nk80o>`@fB#Ph|XWZL#7G z3aPvVy8ZPk92Td{+sqPkrp1a~T#a`;XC7CBpgHr9Rzm$~wxX|B;xI6P0B4i9iZP%X0;oabkJFG)J{aq1 zZL>na0HT^rxCKnL3ZBHg+fRc=DkP;fY8gCS%w#yk3c`4Rr^tOYDpUl)k%79HyF)87Fl{P`VoCbWyOf=ewz>L*3h_(9zt>+?_(EZ4!9NOqRB&1Tllg}4CEtMef_I14 zm3L3xT+dUUu=;292KQ&~F?X4ANV&%KmTOFYQyvt*lrBhH@#6KInA*NJKcM1|SS_mR zN`AT+vEvdeCu=@_Bq3BnqMB&yOl}-XRt%rqKXmem(d0Vwb9mnTYUGjEy-l}fEYU?P ztkoNdIxLImv0D1>u)UAsrgDK3lV^ziy%RiouZ@t*gcylnF} z9Prj|R7%3f4XvEGyT9s_lQTgLtj1A_(-1j3oQXm9?Tk44knSiiT2=3rU$w%nlH`#FXg>Zh}eFL5mK}yeW;8${r(+D`DV@b z!%jjxkDAwDFYxfg6L2PdBA2;(WRfu# z&4TRG)si#|YJ`|_#@?X$IUzv#5%b@4+|HQ^_@-}BVA0w4dU*0Z78yJEQK*CnCIV}h zZPxIli$~WZFB|ReL47h%^i9@?)ASjs*+K=W#*O(uX)AEtmGlAkI zJW?MD)kpL?Jr*@0?0g<=CF2_N#R;k@XaS({k%u&%+!Q;eGTFTjm6GAwUu}Mvlif>! zw%|xHsuNU8paMv$m;W|_*fQV;|Mm3VEOPqbpcz3veTow`tbwlMU{5v!|ojiyPfHUOrt8gc?GCe!S7$ScFLz z!;GsB$Lhk-a3U183!2=0CDX)Abt$Oc+jLRYzhl+u7SruZ-A6=k0WPBGKPLE(;r}Ih j*5A@>wOsrqo%~puy>$nm`i1!CqJAkL!dDKp5K45rg=;8ILeJ?ow;laJGw2g~kKUkH=z0Gf?|r1bSSb8L zOPmhpstQF}qbPhnJ-KBba0o8N<#8yh9ZDAa!aW>N1XDC9j=D)CES&F8iAGUmyjZZD z(n)}ER;+f$6uj^m-*19Mca`9dw}T} z@8ifz1ui3I1&)!-Mz~e0bi&nwmCCynLw+JulBo?WgF#-a_P{zIfIiCV1x(;%4m|3X zsQXL81V`Exm_fWcvk^YFfl)93xBPF`3sJ562NM?yoj1chLo7$}hP8tox6YOF-Ag`gh#UD7Z6C=h`zlEr2 zGdh>VWr?;t%o#Ol%oFAfk1AtsUPlQ{{w90+_bCzPT-a&=7qeA;Z3rdPzGyhs2O65|>g%fmz8aiJ-BF9`cP6frx3z3E$mtPe zCq@Qo5ozCm9ZL052WBpt{0&OJO? z(2JdH|KGt>;zUE>Giy~BaJOvupM50@$FKo}Z-r`9Vb2uV?-AqBeD+^c&S}XgAxxXezMzX~+5+4+7#FZm@S=N&ymJ?f0h$peR;q1tt N>}@#HO4o{g{BNI@aCiU! diff --git a/data/jardin.db-shm b/data/jardin.db-shm deleted file mode 100755 index 258a8b84272b72ad525f8674b953635bf033efc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI*Jq`gu7zW_o^;;@+qEHI0!V&cJEd;tEw&&TR*&Q!WgX3|u>FkEr_Lu$fsYj|YW>L3S zp1YrM8p~PKd+x4fnNyJ cesQQX2@oJafB*pk1PBlyK!5-N0;LqV1G5t&u>b%7 diff --git a/data/jardin.db-wal b/data/jardin.db-wal deleted file mode 100755 index b61c08eafa82e7f9639fcef7946185853a323ee6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24752 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 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