Skip to main content

Script complet de base

Script python :

#!/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/<clé>/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/<clé>/..., et que <clé> 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 <window> 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()