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 ftplib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
@@ -6,10 +7,13 @@ from pathlib import Path
|
|||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
from urllib.error import URLError
|
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")
|
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):
|
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."""
|
"""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):
|
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 = config_booth.get("url", "").rstrip("/")
|
||||||
url_tunnel = config_booth.get("url_tunnel", "").rstrip("/")
|
url_tunnel = config_booth.get("url_tunnel", "").rstrip("/")
|
||||||
api_key = config_booth.get("api_key", "")
|
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}")
|
log.error(f"Booth erreur HTTP via {tentative_url}")
|
||||||
except (URLError, OSError) as e:
|
except (URLError, OSError) as e:
|
||||||
log.warning(f"Echec envoi booth via {tentative_url} : {e}")
|
log.warning(f"Echec envoi booth via {tentative_url} : {e}")
|
||||||
|
|
||||||
|
_ajouter_booth_spool(str(chemin_photo))
|
||||||
return False
|
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:
|
def recuperer_booth_password() -> dict:
|
||||||
"""Recupere le mot de passe et l'info de la session booth."""
|
"""Recupere le mot de passe et l'info de la session booth."""
|
||||||
config = charger_config()
|
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}")
|
log.error(f"Erreur envoi rapport spool : {e}")
|
||||||
|
|
||||||
|
|
||||||
async def tache_spool_demarrage():
|
RETRY_INTERVAL = 300 # 5 minutes
|
||||||
"""Tente de vider le spool une seule fois au demarrage du serveur."""
|
|
||||||
await asyncio.sleep(10)
|
|
||||||
total = taille_spool()
|
async def tache_spool_periodique():
|
||||||
if total > 0:
|
"""Retente emails + galerie en spool toutes les 5 min."""
|
||||||
log.info(f"Spool: tentative d'envoi au demarrage ({total} en attente)")
|
from backend.destinations import traiter_booth_spool, taille_booth_spool
|
||||||
nb, dests = await asyncio.get_event_loop().run_in_executor(None, traiter_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:
|
if nb > 0:
|
||||||
echoues = total - nb
|
echoues = total_mail - nb
|
||||||
await asyncio.get_event_loop().run_in_executor(
|
await loop.run_in_executor(None, envoyer_rapport_spool, nb, dests, echoues)
|
||||||
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"
|
FICHIER_EMAILS = RACINE / "data" / "emails_history.json"
|
||||||
|
|||||||
@@ -1149,7 +1149,7 @@
|
|||||||
<script src="/js/camera.js?v=23"></script>
|
<script src="/js/camera.js?v=23"></script>
|
||||||
<script src="/js/effects.js?v=4"></script>
|
<script src="/js/effects.js?v=4"></script>
|
||||||
<script src="/js/gallery.js?v=5"></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>
|
<script src="/js/admin.js?v=17"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ async function envoyerEmail() {
|
|||||||
afficherStatut('Envoi en cours...', 'succes');
|
afficherStatut('Envoi en cours...', 'succes');
|
||||||
const resultat = await apiPost('/api/email', { email, photo: photoFinale });
|
const resultat = await apiPost('/api/email', { email, photo: photoFinale });
|
||||||
if (resultat.spool) {
|
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) {
|
} else if (resultat.succes) {
|
||||||
afficherStatut('Email envoyé !', 'succes');
|
afficherStatut('Email envoyé !', 'succes');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user