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()