Retry periodique email + spool galerie en cas de coupure internet

- Spool galerie booth : si upload echoue, photo stockee dans
  data/booth_spool.json et retentee toutes les 5 min
- Email spool : retry periodique (5 min) au lieu de juste au demarrage
- Les deux spools traites dans la meme boucle async
- Message frontend spool : "sera envoyee des que possible"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 10:05:21 +02:00
parent 5c774babbb
commit d21d87347d
4 changed files with 119 additions and 15 deletions

View File

@@ -1,4 +1,5 @@
import ftplib
import json
import logging
import shutil
import threading
@@ -6,10 +7,13 @@ from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import URLError
from backend.config import charger_config, mettre_a_jour_config
from backend.config import charger_config, mettre_a_jour_config, RACINE
log = logging.getLogger("photobooth.destinations")
FICHIER_BOOTH_SPOOL = RACINE / "data" / "booth_spool.json"
_booth_spool_lock = threading.Lock()
def distribuer_photo(chemin_photo: Path, imprimee: bool = False, copies: int = 1, format_papier: str | None = None):
"""Copie la photo vers toutes les destinations activees."""
@@ -118,7 +122,7 @@ def _upload_booth(url: str, api_key: str, event_id: str, chemin_photo: Path) ->
def envoyer_booth(chemin_photo: Path, config_booth: dict):
"""Envoie une photo vers la galerie live booth."""
"""Envoie une photo vers la galerie live booth. En cas d'echec, met en spool."""
url = config_booth.get("url", "").rstrip("/")
url_tunnel = config_booth.get("url_tunnel", "").rstrip("/")
api_key = config_booth.get("api_key", "")
@@ -137,9 +141,93 @@ def envoyer_booth(chemin_photo: Path, config_booth: dict):
log.error(f"Booth erreur HTTP via {tentative_url}")
except (URLError, OSError) as e:
log.warning(f"Echec envoi booth via {tentative_url} : {e}")
_ajouter_booth_spool(str(chemin_photo))
return False
# --- Spool galerie booth ---
def _ajouter_booth_spool(chemin: str):
with _booth_spool_lock:
spool = _charger_booth_spool()
if chemin not in spool:
spool.append(chemin)
_sauver_booth_spool(spool)
log.info(f"Photo mise en spool galerie ({len(spool)} en attente)")
def _charger_booth_spool() -> list:
if not FICHIER_BOOTH_SPOOL.exists():
return []
try:
with open(FICHIER_BOOTH_SPOOL, "r") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return []
def _sauver_booth_spool(spool: list):
try:
with open(FICHIER_BOOTH_SPOOL, "w") as f:
json.dump(spool, f)
except OSError:
pass
def taille_booth_spool() -> int:
with _booth_spool_lock:
return len(_charger_booth_spool())
def traiter_booth_spool() -> int:
"""Retente l'envoi des photos en spool. Retourne le nombre envoyees."""
with _booth_spool_lock:
spool = _charger_booth_spool()
if not spool:
return 0
config = charger_config()
booth = config.get("booth", {})
if not booth.get("actif", False):
return 0
envoyes = 0
restants = []
for chemin_str in spool:
chemin = Path(chemin_str)
if not chemin.exists():
log.warning(f"Spool booth: photo introuvable {chemin}, ignoree")
continue
url = booth.get("url", "").rstrip("/")
url_tunnel = booth.get("url_tunnel", "").rstrip("/")
api_key = booth.get("api_key", "")
event_id = config.get("evenement", {}).get("event_id") or booth.get("event_id", "default")
ok = False
for tentative_url in [u for u in (url_tunnel, url) if u]:
try:
if _upload_booth(tentative_url, api_key, event_id, chemin):
log.info(f"Spool booth: {chemin.name} envoyee via {tentative_url}")
ok = True
break
except (URLError, OSError):
pass
if ok:
envoyes += 1
else:
restants.append(chemin_str)
break
if envoyes > 0 or len(restants) < len(spool):
idx = envoyes + len(restants)
restants.extend(spool[idx:])
with _booth_spool_lock:
_sauver_booth_spool(restants)
log.info(f"Spool booth: {envoyes} envoyee(s), {len(restants)} en attente")
return envoyes
def recuperer_booth_password() -> dict:
"""Recupere le mot de passe et l'info de la session booth."""
config = charger_config()

View File

@@ -178,18 +178,34 @@ def envoyer_rapport_spool(nb_envoyes: int, destinataires: list[str], nb_echoues:
log.error(f"Erreur envoi rapport spool : {e}")
async def tache_spool_demarrage():
"""Tente de vider le spool une seule fois au demarrage du serveur."""
await asyncio.sleep(10)
total = taille_spool()
if total > 0:
log.info(f"Spool: tentative d'envoi au demarrage ({total} en attente)")
nb, dests = await asyncio.get_event_loop().run_in_executor(None, traiter_spool)
RETRY_INTERVAL = 300 # 5 minutes
async def tache_spool_periodique():
"""Retente emails + galerie en spool toutes les 5 min."""
from backend.destinations import traiter_booth_spool, taille_booth_spool
await asyncio.sleep(15)
while True:
loop = asyncio.get_event_loop()
# Emails
total_mail = taille_spool()
if total_mail > 0:
log.info(f"Spool email: retry ({total_mail} en attente)")
nb, dests = await loop.run_in_executor(None, traiter_spool)
if nb > 0:
echoues = total - nb
await asyncio.get_event_loop().run_in_executor(
None, envoyer_rapport_spool, nb, dests, echoues
)
echoues = total_mail - nb
await loop.run_in_executor(None, envoyer_rapport_spool, nb, dests, echoues)
# Galerie booth
total_booth = taille_booth_spool()
if total_booth > 0:
log.info(f"Spool galerie: retry ({total_booth} en attente)")
await loop.run_in_executor(None, traiter_booth_spool)
await asyncio.sleep(RETRY_INTERVAL)
async def tache_spool_demarrage():
"""Alias pour compatibilite — lance la boucle periodique."""
await tache_spool_periodique()
FICHIER_EMAILS = RACINE / "data" / "emails_history.json"

View File

@@ -1149,7 +1149,7 @@
<script src="/js/camera.js?v=23"></script>
<script src="/js/effects.js?v=4"></script>
<script src="/js/gallery.js?v=5"></script>
<script src="/js/share.js?v=9"></script>
<script src="/js/share.js?v=10"></script>
<script src="/js/admin.js?v=17"></script>
</body>
</html>

View File

@@ -185,7 +185,7 @@ async function envoyerEmail() {
afficherStatut('Envoi en cours...', 'succes');
const resultat = await apiPost('/api/email', { email, photo: photoFinale });
if (resultat.spool) {
afficherStatut('Vous recevrez votre photo par email dans la semaine', 'succes');
afficherStatut('Votre photo sera envoyee par email des que possible', 'succes');
} else if (resultat.succes) {
afficherStatut('Email envoyé !', 'succes');
} else {