Skip to main content

Script complet de base

#!/usr/bin/env python3 # ============================================================================= # Script : ftp2cab.py # Chemin : /opt/ftp2cab/ftp2cab.py # Description : # Bridge simple entre un dossier FTP et une imprimante CAB en RAW (port 9100). # Le script surveille en boucle le répertoire WATCH_DIR (/srv/ftp/incoming), # détecte les fichiers stables (taille qui ne bouge plus), choisit l’IP de # l’imprimante (par défaut ou via PRINTER_MAP selon le sous-dossier), envoie # le contenu tel quel sur le port 9100, puis déplace le fichier vers _ok/ ou # _error/ selon le résultat. # # Paramètres de configuration (en-tête du fichier) : # - WATCH_DIR (Path) Répertoire surveillé (défaut : /srv/ftp/incoming) # - PRINTER_DEFAULT_IP (str) IP par défaut de l’imprimante CAB # - PRINTER_DEFAULT_PORT (int) Port RAW (défaut : 9100) # - STABLE_WINDOW_SEC (float) Fenêtre d’observation pour stabilité (s) # - RETRY_ON_ERROR (bool) (Réservé) Non utilisé : pas de retry automatique # - PRINTER_MAP (dict) Mapping optionnel "clé de sous-dossier" -> (ip, port) # # Exemples d’utilisation : # - Déposer un fichier dans /srv/ftp/incoming/ : # echo '^XA^FO50,50^ADN,36,20^FDTEST^FS^XZ' > /srv/ftp/incoming/test.prn # → le script l’enverra à PRINTER_DEFAULT_IP:9100 puis le placera dans _ok/ # # - Déposer via un sous-dossier mappé : # mkdir -p /srv/ftp/incoming/squix4 # echo '...' > /srv/ftp/incoming/squix4/etq.lbl # → si PRINTER_MAP["squix4"] = ("192.168.0.121", 9100), envoi vers cette IP. # # Prérequis : # - Python 3.7+ sous Linux # - Un service FTP (ex: vsftpd) écrivant dans WATCH_DIR # - Connectivité réseau vers l’imprimante CAB (port TCP 9100) # - (Recommandé) Lancement via systemd (voir unité ftp2cab.service) # # Auteur : Sylvain SCATTOLINI # Date : 2025-11-07 # Version : 1.1 # ============================================================================= import socket import time import shutil import sys import os from pathlib import Path # ====== CONFIG À ADAPTER ====================================================== WATCH_DIR = Path("/srv/ftp/incoming") # Dossier FTP surveillé PRINTER_DEFAULT_IP = "192.168.1.213" # IP par défaut de l’imprimante CAB PRINTER_DEFAULT_PORT = 9100 # Port RAW de l’imprimante STABLE_WINDOW_SEC = 1.0 # Fenêtre pour juger la stabilité d’un fichier RETRY_ON_ERROR = False # (Réservé) Pas de retry dans cette version # Mapping optionnel par sous-dossier : # /srv/ftp/incoming//fichier.js -> (ip, port) spécifiques PRINTER_MAP = { # "squix4": ("192.168.0.121", 9100), # "eos2": ("192.168.0.122", 9100), } # ============================================================================== # Répertoires de sortie (créés au démarrage si absents) OK_DIR = WATCH_DIR / "_ok" ERR_DIR = WATCH_DIR / "_error" OK_DIR.mkdir(parents=True, exist_ok=True) ERR_DIR.mkdir(parents=True, exist_ok=True) def pick_printer(file_path: Path): """ Détermine l’imprimante (ip, port) à utiliser. - Si le fichier est dans WATCH_DIR//..., et que est dans PRINTER_MAP, on retourne l’(ip, port) associé. - Sinon on retourne (PRINTER_DEFAULT_IP, PRINTER_DEFAULT_PORT). """ try: # Récupère le 1er sous-dossier sous WATCH_DIR key = file_path.relative_to(WATCH_DIR).parts[0] except Exception: # Le fichier est directement sous WATCH_DIR/ (pas de sous-dossier) key = "" return PRINTER_MAP.get(key, (PRINTER_DEFAULT_IP, PRINTER_DEFAULT_PORT)) def is_stable(path: Path, window=STABLE_WINDOW_SEC): """ Vérifie que le fichier ne bouge plus (taille stable sur secondes). - Évite d’ouvrir un fichier encore en cours d’upload. - Renvoie False si le fichier n’existe plus (upload annulé/écrasé). """ if not path.exists(): return False s1 = path.stat().st_size time.sleep(window) if not path.exists(): return False s2 = path.stat().st_size return s1 == s2 and s1 > 0 def send_raw_to_printer(ip: str, port: int, data: bytes, timeout=10): """ Envoie le contenu binaire 'data' tel quel à l’imprimante CAB via TCP RAW. - Ouverture d’une connexion vers (ip, port) - Envoi intégral via sendall() - Fermeture implicite de la socket (context manager) """ with socket.create_connection((ip, port), timeout=timeout) as s: s.sendall(data) def process_file(path: Path): """ Traite un fichier unique : - Ignore les fichiers cachés (commençant par '.') - Attend la stabilité (taille fixe) avant de lire - Choisit l’IP/port de l’imprimante avec pick_printer() - Envoie en RAW 9100 ; si OK déplace vers _ok/, sinon vers _error/ - Journalise sur stdout (OK) ou stderr (ERR) Retourne True si une tentative a été faite (OK ou ERR), False sinon. """ # Ignore les fichiers temporaires masqués (ex. .partial) if path.name.startswith("."): return False # Si le fichier n’est pas encore stable, on le laisse pour un prochain tour if not is_stable(path): return False # Sélection de l’imprimante (mapping sous-dossier ou valeur par défaut) ip, port = pick_printer(path) # Lecture du contenu à transmettre tel quel à l’imprimante data = path.read_bytes() try: # Envoi RAW vers la CAB send_raw_to_printer(ip, port, data) # Déplacement vers le dossier des succès (_ok) dest = OK_DIR / f"{path.name}" shutil.move(str(path), str(dest)) # Log minimal de succès (journalctl captera stdout) print(f"[OK] {path.name} → {ip}:{port}") except Exception as e: # En cas d’erreur d’envoi : on déplace dans _error/ en préfixant par un timestamp dest = ERR_DIR / f"{int(time.time())}_{path.name}" try: shutil.move(str(path), str(dest)) except Exception: # Si on ne peut pas déplacer (fichier disparu, droits…), on ignore l’exception pass # Log minimal d’erreur (journalctl captera stderr) print(f"[ERR] {path.name} → {ip}:{port} : {e}", file=sys.stderr) # Une tentative a été effectuée (OK ou ERR) return True def main_loop(): """ Boucle principale : - S’assure que WATCH_DIR existe. - Parcourt récursivement WATCH_DIR (rglob("*")). - Ne traite que les fichiers (pas les dossiers), en excluant _ok/ et _error/. - Tente process_file() pour chaque entrée ; dort 200 ms entre les passes. """ print(f"Bridge FTP→CAB actif. Surveillance: {WATCH_DIR}") WATCH_DIR.mkdir(parents=True, exist_ok=True) while True: try: # Parcours récursif des entrées (fichiers & sous-dossiers) for entry in list(WATCH_DIR.rglob("*")): # On ne traite que les fichiers, et on ignore les dossiers _ok/_error if entry.is_file() and entry.parent.name not in ("_ok", "_error"): process_file(entry) # Petite pause pour éviter de monopoliser le CPU time.sleep(0.2) except KeyboardInterrupt: # Arrêt propre sur Ctrl+C (utile si lancé manuellement) print("Arrêt demandé.") break if __name__ == "__main__": # Vérification minimale au lancement : le dossier watch doit exister if not WATCH_DIR.exists(): print(f"Dossier introuvable: {WATCH_DIR}", file=sys.stderr) sys.exit(1) # Lancement de la boucle de surveillance main_loop()