commit b6aea1aecb5513bbb1cea67c2aa9de03dc11c794 Author: pollutri Date: Tue Feb 10 16:56:48 2026 +0100 first commit diff --git a/GUIDA.md b/GUIDA.md new file mode 100644 index 0000000..eb22c1f --- /dev/null +++ b/GUIDA.md @@ -0,0 +1,109 @@ +# Guida Rapida + +## Utilizzo Base + +```bash +# Usa modalità da config (hourly o daily) +python file_processor.py + +# Processa una data specifica (ignora execution_mode) +python file_processor.py 15-03-2025 + +# Forza elaborazione +python file_processor.py --force + +# Config alternativo +python file_processor.py --config altro_config.json + +# Salta controllo giorno precedente (solo hourly) +python file_processor.py --no-previous-day-check +``` + +## Configurazione (config.json) + +```json +{ + "execution_mode": "hourly", + "daily_execution_hour": 3, + "input": { + "host": "192.168.1.100", + "share": "SharedFolder", + "path": "input", + "username": "user", + "password": "pass", + "domain": "" + }, + "output": { + "locale": "no", + "local_path": "./output", + "host": "192.168.1.200", + "share": "OutputFolder", + "path": "output", + "username": "user", + "password": "pass", + "domain": "" + }, + "error_path": "./errors" +} +``` + +**Execution mode:** +- `"hourly"` → Controlli multipli durante il giorno (ieri + oggi) +- `"daily"` → Una sola esecuzione al giorno (solo ieri, consolidamento finale) + +**Daily execution hour:** Ora configurata per modalità daily (default: 3) +**Output locale:** `"locale": "yes"` → scrive in `local_path` +**Output remoto:** `"locale": "no"` → scrive su PC remoto +**Percorso errori:** `"error_path": "./errors"` → directory per file di errore + +## Modalità di Esecuzione + +### HOURLY (Controlli Multipli) +- Ogni ora controlla ieri + oggi +- Usa file di stato (`.state_GGMMAAAA.json`) +- Skip se file non modificato +- FASE 1: consolida ieri, FASE 2: processa oggi + +**Cron configurazione:** +```bash +# Ogni ora +0 * * * * cd /path/to/script && python3 file_processor.py >> /var/log/file_processor.log 2>&1 +``` + +### DAILY (Consolidamento Giornaliero) +- Una sola esecuzione al giorno +- Processa SOLO il giorno precedente (consolidamento finale) +- NON usa file di stato +- Ignora il file di oggi (verrà processato domani) +- NON crea output per oggi (rimarrebbe vuoto) + +**Cron configurazione:** +```bash +# Alle 03:00 ogni giorno (o all'ora configurata in daily_execution_hour) +0 3 * * * cd /path/to/script && python3 file_processor.py >> /var/log/file_processor.log 2>&1 +``` + +## Funzionalità + +- Legge file dal PC remoto (nome: `GGMMAAAA.txt`) +- Filtra righe che iniziano con `01003` +- Scrive output (locale o remoto) +- **HOURLY:** Controlla modifiche con hash, skip se non modificato +- **DAILY:** Processa sempre il giorno precedente, ignora oggi + +## Consolidamento Giorno Precedente + +**HOURLY:** Ogni esecuzione controlla PRIMA il file di ieri, poi quello di oggi +**DAILY:** Processa SOLO il file di ieri (consolidamento finale alle 03:00) + +## Gestione Errori + +- File input non trovato → crea `errorGG.txt` in `error_path` (tipo: input) +- Errore scrittura output → crea `errorGG.txt` in `error_path` (tipo: output) +- Nessuna stringa 01003 → crea file output vuoto (normale) +- Directory errori creata automaticamente se non esiste + +## Requisiti + +**Linux:** `sudo apt-get install smbclient` +**Windows:** Solo Python 3.x diff --git a/config.json b/config.json new file mode 100644 index 0000000..24c2915 --- /dev/null +++ b/config.json @@ -0,0 +1,23 @@ +{ + "execution_mode": "hourly", + "daily_execution_hour": 3, + "input": { + "host": "10.0.200.218", + "share": "giornaliere", + "path": "", + "username": "ced", + "password": "Furtebor!7853", + "domain": "" + }, + "output": { + "locale": "yes", + "local_path": "./output", + "host": "192.168.1.200", + "share": "", + "path": "", + "username": "", + "password": "", + "domain": "" + }, + "error_path": "./errors" +} diff --git a/file_processor.py b/file_processor.py new file mode 100644 index 0000000..1cc40ab --- /dev/null +++ b/file_processor.py @@ -0,0 +1,658 @@ +#!/usr/bin/env python3 + +import os +import sys +import json +import platform +import subprocess +from datetime import datetime +import tempfile +import shutil +import hashlib +import argparse + + +def load_config(config_file="config.json"): + """Carica la configurazione dal file JSON.""" + try: + with open(config_file, 'r') as f: + return json.load(f) + except FileNotFoundError: + raise Exception(f"File di configurazione '{config_file}' non trovato") + except json.JSONDecodeError: + raise Exception(f"Errore nel parsing del file di configurazione '{config_file}'") + + +def get_date_string(): + """Restituisce la data odierna nel formato giorno+mese+anno.""" + now = datetime.now() + return f"{now.day:02d}{now.month:02d}{now.year}" + + +def get_previous_day_string(): + """Restituisce la data di ieri nel formato giorno+mese+anno.""" + from datetime import timedelta + yesterday = datetime.now() - timedelta(days=1) + return f"{yesterday.day:02d}{yesterday.month:02d}{yesterday.year}" + + +def parse_date_argument(date_str): + """ + Converte una data in formato GG-MM-AAAA nel formato GGMMAAAA. + + Args: + date_str: stringa nel formato "GG-MM-AAAA" (es: "01-01-2026") + + Returns: + stringa nel formato "GGMMAAAA" (es: "01012026") + + Raises: + ValueError: se il formato non è valido + """ + try: + date_obj = datetime.strptime(date_str, "%d-%m-%Y") + return f"{date_obj.day:02d}{date_obj.month:02d}{date_obj.year}" + except ValueError: + raise ValueError(f"Formato data non valido: {date_str}. Usa il formato GG-MM-AAAA (es: 01-01-2026)") + + +def calculate_content_hash(content): + """Calcola l'hash MD5 del contenuto.""" + return hashlib.md5(content.encode('utf-8')).hexdigest() + + +def get_state_filename(date_string): + """Restituisce il nome del file di stato per una data specifica.""" + return f".state_{date_string}.json" + + +def load_state(date_string): + """ + Carica lo stato dell'ultima elaborazione per una data specifica. + + Returns: + dict con 'last_hash' e 'last_check_time', oppure None se non esiste + """ + state_file = get_state_filename(date_string) + + if not os.path.exists(state_file): + return None + + try: + with open(state_file, 'r') as f: + return json.load(f) + except: + return None + + +def save_state(date_string, content_hash): + """Salva lo stato dell'elaborazione corrente.""" + state_file = get_state_filename(date_string) + + state = { + 'last_hash': content_hash, + 'last_check_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'date': date_string + } + + try: + with open(state_file, 'w') as f: + json.dump(state, f, indent=4) + return True + except Exception as e: + print(f"ATTENZIONE: Impossibile salvare lo stato: {e}") + return False + + +def cleanup_old_state_files(): + """Elimina i file di stato di date precedenti.""" + today = get_date_string() + + for filename in os.listdir('.'): + if filename.startswith('.state_') and filename.endswith('.json'): + # Estrai la data dal nome del file + try: + state_date = filename.replace('.state_', '').replace('.json', '') + if state_date != today: + os.remove(filename) + print(f"File di stato obsoleto eliminato: {filename}") + except: + pass + + +def process_single_date(config, date_string, force=False, date_label="", skip_state=False): + """ + Processa un singolo file per una data specifica. + + Args: + config: configurazione caricata + date_string: stringa data in formato GGMMAAAA + force: forza elaborazione anche se non modificato + date_label: etichetta descrittiva (es: "oggi", "ieri") + skip_state: se True, non usa né salva i file di stato (per modalità daily) + + Returns: + (success, skipped) - (True/False, True se saltato perché non modificato) + """ + input_filename = f"{date_string}.txt" + output_filename = f"{date_string}.txt" + + if date_label: + print(f"\n--- Elaborazione file di {date_label}: {input_filename} ---") + else: + print(f"\n--- Elaborazione file: {input_filename} ---") + + # Leggi file di input dal PC remoto + print(f"Lettura file da {config['input']['host']}...") + try: + input_content = read_remote_file(config['input'], input_filename) + print(f"File letto con successo ({len(input_content)} caratteri)") + except FileNotFoundError: + print(f"File {input_filename} non trovato (normale se non esiste ancora)") + return (True, True) # Non è un errore, semplicemente il file non esiste + except Exception as e: + error_msg = f"Impossibile leggere il file di input {input_filename}: {str(e)}" + print(f"ERRORE: {error_msg}") + write_error_file(error_msg, "input") + return (False, False) + + # Calcola hash del contenuto + content_hash = calculate_content_hash(input_content) + + # Verifica se il file è cambiato rispetto all'ultima elaborazione (solo se non skip_state) + if not skip_state and not force: + previous_state = load_state(date_string) + + if previous_state and previous_state.get('last_hash') == content_hash: + print(f"File non modificato rispetto all'ultimo controllo ({previous_state.get('last_check_time')})") + print("Elaborazione saltata.") + return (True, True) # Success ma skipped + + if previous_state: + print(f"File modificato rispetto all'ultimo controllo ({previous_state.get('last_check_time')})") + else: + print("Prima elaborazione per questo file") + elif skip_state: + print("Modalità DAILY: elaborazione senza controllo stato") + else: + print("Modalità --force: elaborazione forzata") + + # Processa il contenuto + print("Elaborazione stringhe che iniziano con '01003'...") + filtered_lines = process_file(input_content) + print(f"Trovate {len(filtered_lines)} stringhe che iniziano con '01003'") + + # Crea file di output (anche se vuoto) + output_content = '\n'.join(filtered_lines) + if filtered_lines: + output_content += '\n' # Aggiungi newline finale + + # Verifica se l'output deve essere locale o remoto + use_local = config['output'].get('locale', 'no').lower() in ['yes', 'si', 'true', '1'] + + if use_local: + # Output locale + print("Modalità output LOCALE") + local_path = config['output'].get('local_path', './output') + + try: + output_file_path = write_local_file(config['output'], output_content, output_filename) + print(f"File {output_filename} scritto in: {output_file_path}") + + if len(filtered_lines) == 0: + print("NOTA: File output vuoto (nessuna stringa '01003')") + + # Salva lo stato corrente (solo se non skip_state) + if not skip_state: + if save_state(date_string, content_hash): + print(f"Stato salvato") + else: + print("Stato non salvato (modalità DAILY)") + + except Exception as e: + error_msg = f"Impossibile scrivere il file di output locale: {str(e)}" + print(f"ERRORE: {error_msg}") + write_error_file(error_msg, "output") + return (False, False) + else: + # Output remoto (comportamento originale) + print("Modalità output REMOTO") + + # Scrivi su file temporaneo locale + temp_output = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') + try: + temp_output.write(output_content) + temp_output.close() + + # Copia il file sul PC remoto di output + print(f"Scrittura file su {config['output']['host']}...") + try: + copy_via_smb(config['output'], temp_output.name, output_filename) + print(f"File {output_filename} creato con successo") + + if len(filtered_lines) == 0: + print("NOTA: File output vuoto (nessuna stringa '01003')") + + # Salva lo stato corrente (solo se non skip_state) + if not skip_state: + if save_state(date_string, content_hash): + print(f"Stato salvato") + else: + print("Stato non salvato (modalità DAILY)") + + except Exception as e: + error_msg = f"Impossibile scrivere il file di output: {str(e)}" + print(f"ERRORE: {error_msg}") + write_error_file(error_msg, "output") + return (False, False) + + finally: + # Elimina file temporaneo + try: + os.unlink(temp_output.name) + except: + pass + + print("Elaborazione completata") + return (True, False) # Success e non skipped + + +def write_error_file(error_message, error_type="input"): + """Scrive un file di errore con il numero del giorno e la descrizione.""" + day = datetime.now().day + error_filename = f"error{day:02d}.txt" + + # Usa il percorso errori dalla configurazione (se disponibile) o default + try: + # Cerca di leggere error_path dalla configurazione + if hasattr(write_error_file, 'error_path'): + error_dir = write_error_file.error_path + else: + error_dir = "./errors" # Default + + # Crea la directory se non esiste + os.makedirs(error_dir, exist_ok=True) + + error_filepath = os.path.join(error_dir, error_filename) + + with open(error_filepath, 'w') as f: + f.write(f"Errore di {error_type}\n") + f.write(f"Data: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}\n") + f.write(f"Descrizione: {error_message}\n") + print(f"File di errore creato: {error_filepath}") + except Exception as e: + # Fallback: prova a scrivere nella directory corrente + try: + with open(error_filename, 'w') as f: + f.write(f"Errore di {error_type}\n") + f.write(f"Data: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}\n") + f.write(f"Descrizione: {error_message}\n") + print(f"ATTENZIONE: File di errore creato nella directory corrente: {error_filename}") + print(f"(Impossibile usare directory configurata: {e})") + except Exception as e2: + print(f"ERRORE CRITICO: Impossibile creare file di errore: {e2}") + + +def mount_windows_share(config, temp_dir): + """Monta una condivisione Windows usando net use (Windows) o mount (Linux).""" + is_windows = platform.system() == "Windows" + + # Costruisci il percorso UNC + unc_path = f"\\\\{config['host']}\\{config['share']}" + + if is_windows: + # Windows: usa net use + mount_point = unc_path + cmd = ["net", "use", unc_path] + + if config['password']: + cmd.append(config['password']) + if config['username']: + cmd.append(f"/user:{config['domain']}\\{config['username']}" if config['domain'] else f"/user:{config['username']}") + + else: + # Linux: usa mount.cifs + mount_point = os.path.join(temp_dir, "mount_" + config['host'].replace('.', '_')) + os.makedirs(mount_point, exist_ok=True) + + creds = f"username={config['username']},password={config['password']}" + if config['domain']: + creds += f",domain={config['domain']}" + + cmd = ["mount", "-t", "cifs", f"//{config['host']}/{config['share']}", + mount_point, "-o", creds] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return mount_point + except subprocess.CalledProcessError as e: + raise Exception(f"Impossibile montare {unc_path}: {e.stderr}") + + +def unmount_share(mount_point): + """Smonta una condivisione.""" + is_windows = platform.system() == "Windows" + + try: + if is_windows: + subprocess.run(["net", "use", mount_point, "/delete"], + capture_output=True, check=False) + else: + subprocess.run(["umount", mount_point], + capture_output=True, check=False) + except Exception: + pass # Ignora errori di smontaggio + + +def copy_via_smb(config, local_file, remote_filename): + """Copia un file locale su una condivisione remota usando smbclient.""" + is_windows = platform.system() == "Windows" + + if is_windows: + # Su Windows usa net use + copy + temp_dir = tempfile.mkdtemp() + try: + mount_point = mount_windows_share(config, temp_dir) + remote_path = os.path.join(mount_point, config['path'], remote_filename) + + # Crea la directory se non esiste + remote_dir = os.path.join(mount_point, config['path']) + os.makedirs(remote_dir, exist_ok=True) + + # Copia il file + shutil.copy2(local_file, remote_path) + + unmount_share(mount_point) + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + else: + # Su Linux usa smbclient + unc_path = f"//{config['host']}/{config['share']}/{config['path']}/{remote_filename}" + + cmd = [ + "smbclient", + f"//{config['host']}/{config['share']}", + "-U", f"{config['domain']}\\{config['username']}" if config['domain'] else config['username'], + "-c", f"cd {config['path']}; put {local_file} {remote_filename}" + ] + + env = os.environ.copy() + if config['password']: + env['PASSWD'] = config['password'] + cmd.insert(2, config['password']) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, env=env) + if result.returncode != 0: + raise Exception(f"Errore smbclient: {result.stderr}") + except FileNotFoundError: + raise Exception("smbclient non trovato. Installare samba-client o cifs-utils") + + +def write_local_file(config, content, filename): + """ + Scrive il file di output in locale invece che su una condivisione remota. + + Args: + config: configurazione output (deve contenere 'local_path') + content: contenuto da scrivere + filename: nome del file + + Returns: + path completo del file creato + """ + local_path = config.get('local_path', './output') + + # Crea la directory se non esiste + os.makedirs(local_path, exist_ok=True) + + # Percorso completo del file + output_file = os.path.join(local_path, filename) + + # Scrivi il file + with open(output_file, 'w') as f: + f.write(content) + + return output_file + + +def read_remote_file(config, filename): + """Legge un file da una condivisione remota.""" + is_windows = platform.system() == "Windows" + temp_dir = tempfile.mkdtemp() + + try: + if is_windows: + # Windows: monta e leggi + mount_point = mount_windows_share(config, temp_dir) + file_path = os.path.join(mount_point, config['path'], filename) + + if not os.path.exists(file_path): + unmount_share(mount_point) + raise FileNotFoundError(f"File {filename} non trovato su {config['host']}") + + with open(file_path, 'r') as f: + content = f.read() + + unmount_share(mount_point) + return content + + else: + # Linux: usa smbclient per scaricare + local_temp = os.path.join(temp_dir, filename) + + cmd = [ + "smbclient", + f"//{config['host']}/{config['share']}", + "-U", f"{config['domain']}\\{config['username']}" if config['domain'] else config['username'], + "-c", f"cd {config['path']}; get {filename} {local_temp}" + ] + + if config['password']: + cmd.insert(2, config['password']) + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode != 0 or not os.path.exists(local_temp): + raise FileNotFoundError(f"File {filename} non trovato su {config['host']}") + + with open(local_temp, 'r') as f: + content = f.read() + + return content + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + +def process_file(input_content): + """Processa il contenuto del file di input e restituisce le righe filtrate.""" + filtered_lines = [] + + for line in input_content.splitlines(): + line = line.strip() + if line.startswith('01003'): + filtered_lines.append(line) + + return filtered_lines + + +def main(): + """Funzione principale.""" + # Default per modalità daily + DEFAULT_DAILY_HOUR = 3 + + # Parse argomenti da riga di comando + parser = argparse.ArgumentParser( + description='Processa file remoti filtrando stringhe che iniziano con 01003', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Esempi: + python file_processor.py # Usa modalità da config (hourly/daily) + python file_processor.py 01-01-2026 # Processa data specifica + python file_processor.py --force # Forza elaborazione + """ + ) + parser.add_argument( + 'date', + nargs='?', + help='Data nel formato GG-MM-AAAA (opzionale, default: oggi)' + ) + parser.add_argument( + '--force', + action='store_true', + help='Forza l\'elaborazione anche se il file non è cambiato' + ) + parser.add_argument( + '--config', + default='config.json', + help='Percorso del file di configurazione (default: config.json)' + ) + parser.add_argument( + '--no-previous-day-check', + action='store_true', + help='Non controllare il file del giorno precedente (solo modalità hourly)' + ) + + args = parser.parse_args() + + print("=== Avvio elaborazione ===") + print(f"Ora corrente: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}") + + try: + # Carica configurazione + print("\nCaricamento configurazione...") + config = load_config(args.config) + + # Imposta il percorso errori dalla configurazione + error_path = config.get('error_path', './errors') + write_error_file.error_path = error_path + print(f"Percorso errori: {error_path}") + + # Leggi modalità di esecuzione + execution_mode = config.get('execution_mode', 'hourly').lower() + daily_hour = config.get('daily_execution_hour', DEFAULT_DAILY_HOUR) + + print(f"Modalità esecuzione: {execution_mode.upper()}") + if execution_mode == 'daily': + print(f"Ora configurata per esecuzione daily: {daily_hour}:00") + + # Determina la data da processare + if args.date: + # Data specifica fornita dall'utente - ignora la modalità + try: + date_string = parse_date_argument(args.date) + print(f"Data specificata: {args.date} -> {date_string}") + except ValueError as e: + print(f"ERRORE: {e}") + return 1 + + print("\nModalità: DATA SPECIFICA (ignora execution_mode)") + # Processa solo la data specificata con stato (comportamento standard) + success, skipped = process_single_date(config, date_string, args.force, skip_state=False) + + if success: + print("\n=== Elaborazione completata con successo ===") + return 0 + else: + return 1 + + # Nessuna data specifica: usa la modalità configurata + if execution_mode == 'daily': + # ============================================================ + # MODALITÀ DAILY: Processa SOLO il giorno precedente + # ============================================================ + print("\n" + "="*60) + print("MODALITÀ DAILY: Consolidamento giorno precedente") + print("="*60) + + yesterday_string = get_previous_day_string() + + # Processa solo ieri, senza file di stato + success, skipped = process_single_date( + config, + yesterday_string, + force=True, # In daily forziamo sempre (no controllo modifiche) + date_label="IERI (consolidamento finale)", + skip_state=True # Non usa file di stato + ) + + if success: + if not skipped: + print("\n=== Consolidamento giornaliero completato con successo ===") + else: + print("\n=== File del giorno precedente non trovato ===") + return 0 + else: + print("\n=== Consolidamento giornaliero completato con errori ===") + return 1 + + elif execution_mode == 'hourly': + # ============================================================ + # MODALITÀ HOURLY: Comportamento originale (ieri + oggi) + # ============================================================ + today_string = get_date_string() + yesterday_string = get_previous_day_string() + + print(f"Data odierna: {datetime.now().strftime('%d/%m/%Y')}") + + # Pulizia file di stato obsoleti (solo in modalità automatica) + cleanup_old_state_files() + + overall_success = True + + # FASE 1: Controlla il file di IERI per consolidare eventuali modifiche tardive + if not args.no_previous_day_check: + print("\n" + "="*60) + print("FASE 1: Consolidamento file del giorno precedente") + print("="*60) + + success, skipped = process_single_date( + config, + yesterday_string, + args.force, + "IERI", + skip_state=False # Usa file di stato in hourly + ) + + if not success: + overall_success = False + print("ATTENZIONE: Errore nel consolidamento del file di ieri") + elif skipped: + print("File di ieri già aggiornato o non esistente") + + # FASE 2: Processa il file di OGGI + print("\n" + "="*60) + print("FASE 2: Elaborazione file del giorno corrente") + print("="*60) + + success, skipped = process_single_date( + config, + today_string, + args.force, + "OGGI", + skip_state=False # Usa file di stato in hourly + ) + + if not success: + overall_success = False + + if overall_success: + print("\n=== Elaborazione completata con successo ===") + return 0 + else: + print("\n=== Elaborazione completata con errori ===") + return 1 + else: + # Modalità non riconosciuta + print(f"\nERRORE: Modalità '{execution_mode}' non valida. Usa 'daily' o 'hourly'.") + return 1 + + except Exception as e: + error_msg = f"Errore generale: {str(e)}" + print(f"ERRORE: {error_msg}") + write_error_file(error_msg, "input") + return 1 + + +if __name__ == "__main__": + sys.exit(main())