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:
@@ -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()
|
||||
|
||||
@@ -178,18 +178,34 @@ def envoyer_rapport_spool(nb_envoyes: int, destinataires: list[str], nb_echoues:
|
||||
log.error(f"Erreur envoi rapport spool : {e}")
|
||||
|
||||
|
||||
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_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():
|
||||
"""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)
|
||||
if nb > 0:
|
||||
echoues = total - nb
|
||||
await asyncio.get_event_loop().run_in_executor(
|
||||
None, envoyer_rapport_spool, nb, dests, echoues
|
||||
)
|
||||
"""Alias pour compatibilite — lance la boucle periodique."""
|
||||
await tache_spool_periodique()
|
||||
|
||||
|
||||
FICHIER_EMAILS = RACINE / "data" / "emails_history.json"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user