Parcourir la source

first commit

master
Lorenzo Pollutri il y a 1 mois
révision
b6aea1aecb
3 fichiers modifiés avec 790 ajouts et 0 suppressions
  1. +109
    -0
      GUIDA.md
  2. +23
    -0
      config.json
  3. +658
    -0
      file_processor.py

+ 109
- 0
GUIDA.md Voir le fichier

@@ -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

+ 23
- 0
config.json Voir le fichier

@@ -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"
}

+ 658
- 0
file_processor.py Voir le fichier

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

Chargement…
Annuler
Enregistrer