Script richiesto da Paladino per il trasferimento e filtro automatico delle timbrature dal PC 10.0.200.218 a una cartella condivisa.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

659 line
24 KiB

  1. #!/usr/bin/env python3
  2. import os
  3. import sys
  4. import json
  5. import platform
  6. import subprocess
  7. from datetime import datetime
  8. import tempfile
  9. import shutil
  10. import hashlib
  11. import argparse
  12. def load_config(config_file="config.json"):
  13. """Carica la configurazione dal file JSON."""
  14. try:
  15. with open(config_file, 'r') as f:
  16. return json.load(f)
  17. except FileNotFoundError:
  18. raise Exception(f"File di configurazione '{config_file}' non trovato")
  19. except json.JSONDecodeError:
  20. raise Exception(f"Errore nel parsing del file di configurazione '{config_file}'")
  21. def get_date_string():
  22. """Restituisce la data odierna nel formato giorno+mese+anno."""
  23. now = datetime.now()
  24. return f"{now.day:02d}{now.month:02d}{now.year}"
  25. def get_previous_day_string():
  26. """Restituisce la data di ieri nel formato giorno+mese+anno."""
  27. from datetime import timedelta
  28. yesterday = datetime.now() - timedelta(days=1)
  29. return f"{yesterday.day:02d}{yesterday.month:02d}{yesterday.year}"
  30. def parse_date_argument(date_str):
  31. """
  32. Converte una data in formato GG-MM-AAAA nel formato GGMMAAAA.
  33. Args:
  34. date_str: stringa nel formato "GG-MM-AAAA" (es: "01-01-2026")
  35. Returns:
  36. stringa nel formato "GGMMAAAA" (es: "01012026")
  37. Raises:
  38. ValueError: se il formato non è valido
  39. """
  40. try:
  41. date_obj = datetime.strptime(date_str, "%d-%m-%Y")
  42. return f"{date_obj.day:02d}{date_obj.month:02d}{date_obj.year}"
  43. except ValueError:
  44. raise ValueError(f"Formato data non valido: {date_str}. Usa il formato GG-MM-AAAA (es: 01-01-2026)")
  45. def calculate_content_hash(content):
  46. """Calcola l'hash MD5 del contenuto."""
  47. return hashlib.md5(content.encode('utf-8')).hexdigest()
  48. def get_state_filename(date_string):
  49. """Restituisce il nome del file di stato per una data specifica."""
  50. return f".state_{date_string}.json"
  51. def load_state(date_string):
  52. """
  53. Carica lo stato dell'ultima elaborazione per una data specifica.
  54. Returns:
  55. dict con 'last_hash' e 'last_check_time', oppure None se non esiste
  56. """
  57. state_file = get_state_filename(date_string)
  58. if not os.path.exists(state_file):
  59. return None
  60. try:
  61. with open(state_file, 'r') as f:
  62. return json.load(f)
  63. except:
  64. return None
  65. def save_state(date_string, content_hash):
  66. """Salva lo stato dell'elaborazione corrente."""
  67. state_file = get_state_filename(date_string)
  68. state = {
  69. 'last_hash': content_hash,
  70. 'last_check_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  71. 'date': date_string
  72. }
  73. try:
  74. with open(state_file, 'w') as f:
  75. json.dump(state, f, indent=4)
  76. return True
  77. except Exception as e:
  78. print(f"ATTENZIONE: Impossibile salvare lo stato: {e}")
  79. return False
  80. def cleanup_old_state_files():
  81. """Elimina i file di stato di date precedenti."""
  82. today = get_date_string()
  83. for filename in os.listdir('.'):
  84. if filename.startswith('.state_') and filename.endswith('.json'):
  85. # Estrai la data dal nome del file
  86. try:
  87. state_date = filename.replace('.state_', '').replace('.json', '')
  88. if state_date != today:
  89. os.remove(filename)
  90. print(f"File di stato obsoleto eliminato: {filename}")
  91. except:
  92. pass
  93. def process_single_date(config, date_string, force=False, date_label="", skip_state=False):
  94. """
  95. Processa un singolo file per una data specifica.
  96. Args:
  97. config: configurazione caricata
  98. date_string: stringa data in formato GGMMAAAA
  99. force: forza elaborazione anche se non modificato
  100. date_label: etichetta descrittiva (es: "oggi", "ieri")
  101. skip_state: se True, non usa né salva i file di stato (per modalità daily)
  102. Returns:
  103. (success, skipped) - (True/False, True se saltato perché non modificato)
  104. """
  105. input_filename = f"{date_string}.txt"
  106. output_filename = f"{date_string}.txt"
  107. if date_label:
  108. print(f"\n--- Elaborazione file di {date_label}: {input_filename} ---")
  109. else:
  110. print(f"\n--- Elaborazione file: {input_filename} ---")
  111. # Leggi file di input dal PC remoto
  112. print(f"Lettura file da {config['input']['host']}...")
  113. try:
  114. input_content = read_remote_file(config['input'], input_filename)
  115. print(f"File letto con successo ({len(input_content)} caratteri)")
  116. except FileNotFoundError:
  117. print(f"File {input_filename} non trovato (normale se non esiste ancora)")
  118. return (True, True) # Non è un errore, semplicemente il file non esiste
  119. except Exception as e:
  120. error_msg = f"Impossibile leggere il file di input {input_filename}: {str(e)}"
  121. print(f"ERRORE: {error_msg}")
  122. write_error_file(error_msg, "input")
  123. return (False, False)
  124. # Calcola hash del contenuto
  125. content_hash = calculate_content_hash(input_content)
  126. # Verifica se il file è cambiato rispetto all'ultima elaborazione (solo se non skip_state)
  127. if not skip_state and not force:
  128. previous_state = load_state(date_string)
  129. if previous_state and previous_state.get('last_hash') == content_hash:
  130. print(f"File non modificato rispetto all'ultimo controllo ({previous_state.get('last_check_time')})")
  131. print("Elaborazione saltata.")
  132. return (True, True) # Success ma skipped
  133. if previous_state:
  134. print(f"File modificato rispetto all'ultimo controllo ({previous_state.get('last_check_time')})")
  135. else:
  136. print("Prima elaborazione per questo file")
  137. elif skip_state:
  138. print("Modalità DAILY: elaborazione senza controllo stato")
  139. else:
  140. print("Modalità --force: elaborazione forzata")
  141. # Processa il contenuto
  142. print("Elaborazione stringhe che iniziano con '01003'...")
  143. filtered_lines = process_file(input_content)
  144. print(f"Trovate {len(filtered_lines)} stringhe che iniziano con '01003'")
  145. # Crea file di output (anche se vuoto)
  146. output_content = '\n'.join(filtered_lines)
  147. if filtered_lines:
  148. output_content += '\n' # Aggiungi newline finale
  149. # Verifica se l'output deve essere locale o remoto
  150. use_local = config['output'].get('locale', 'no').lower() in ['yes', 'si', 'true', '1']
  151. if use_local:
  152. # Output locale
  153. print("Modalità output LOCALE")
  154. local_path = config['output'].get('local_path', './output')
  155. try:
  156. output_file_path = write_local_file(config['output'], output_content, output_filename)
  157. print(f"File {output_filename} scritto in: {output_file_path}")
  158. if len(filtered_lines) == 0:
  159. print("NOTA: File output vuoto (nessuna stringa '01003')")
  160. # Salva lo stato corrente (solo se non skip_state)
  161. if not skip_state:
  162. if save_state(date_string, content_hash):
  163. print(f"Stato salvato")
  164. else:
  165. print("Stato non salvato (modalità DAILY)")
  166. except Exception as e:
  167. error_msg = f"Impossibile scrivere il file di output locale: {str(e)}"
  168. print(f"ERRORE: {error_msg}")
  169. write_error_file(error_msg, "output")
  170. return (False, False)
  171. else:
  172. # Output remoto (comportamento originale)
  173. print("Modalità output REMOTO")
  174. # Scrivi su file temporaneo locale
  175. temp_output = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt')
  176. try:
  177. temp_output.write(output_content)
  178. temp_output.close()
  179. # Copia il file sul PC remoto di output
  180. print(f"Scrittura file su {config['output']['host']}...")
  181. try:
  182. copy_via_smb(config['output'], temp_output.name, output_filename)
  183. print(f"File {output_filename} creato con successo")
  184. if len(filtered_lines) == 0:
  185. print("NOTA: File output vuoto (nessuna stringa '01003')")
  186. # Salva lo stato corrente (solo se non skip_state)
  187. if not skip_state:
  188. if save_state(date_string, content_hash):
  189. print(f"Stato salvato")
  190. else:
  191. print("Stato non salvato (modalità DAILY)")
  192. except Exception as e:
  193. error_msg = f"Impossibile scrivere il file di output: {str(e)}"
  194. print(f"ERRORE: {error_msg}")
  195. write_error_file(error_msg, "output")
  196. return (False, False)
  197. finally:
  198. # Elimina file temporaneo
  199. try:
  200. os.unlink(temp_output.name)
  201. except:
  202. pass
  203. print("Elaborazione completata")
  204. return (True, False) # Success e non skipped
  205. def write_error_file(error_message, error_type="input"):
  206. """Scrive un file di errore con il numero del giorno e la descrizione."""
  207. day = datetime.now().day
  208. error_filename = f"error{day:02d}.txt"
  209. # Usa il percorso errori dalla configurazione (se disponibile) o default
  210. try:
  211. # Cerca di leggere error_path dalla configurazione
  212. if hasattr(write_error_file, 'error_path'):
  213. error_dir = write_error_file.error_path
  214. else:
  215. error_dir = "./errors" # Default
  216. # Crea la directory se non esiste
  217. os.makedirs(error_dir, exist_ok=True)
  218. error_filepath = os.path.join(error_dir, error_filename)
  219. with open(error_filepath, 'w') as f:
  220. f.write(f"Errore di {error_type}\n")
  221. f.write(f"Data: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}\n")
  222. f.write(f"Descrizione: {error_message}\n")
  223. print(f"File di errore creato: {error_filepath}")
  224. except Exception as e:
  225. # Fallback: prova a scrivere nella directory corrente
  226. try:
  227. with open(error_filename, 'w') as f:
  228. f.write(f"Errore di {error_type}\n")
  229. f.write(f"Data: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}\n")
  230. f.write(f"Descrizione: {error_message}\n")
  231. print(f"ATTENZIONE: File di errore creato nella directory corrente: {error_filename}")
  232. print(f"(Impossibile usare directory configurata: {e})")
  233. except Exception as e2:
  234. print(f"ERRORE CRITICO: Impossibile creare file di errore: {e2}")
  235. def mount_windows_share(config, temp_dir):
  236. """Monta una condivisione Windows usando net use (Windows) o mount (Linux)."""
  237. is_windows = platform.system() == "Windows"
  238. # Costruisci il percorso UNC
  239. unc_path = f"\\\\{config['host']}\\{config['share']}"
  240. if is_windows:
  241. # Windows: usa net use
  242. mount_point = unc_path
  243. cmd = ["net", "use", unc_path]
  244. if config['password']:
  245. cmd.append(config['password'])
  246. if config['username']:
  247. cmd.append(f"/user:{config['domain']}\\{config['username']}" if config['domain'] else f"/user:{config['username']}")
  248. else:
  249. # Linux: usa mount.cifs
  250. mount_point = os.path.join(temp_dir, "mount_" + config['host'].replace('.', '_'))
  251. os.makedirs(mount_point, exist_ok=True)
  252. creds = f"username={config['username']},password={config['password']}"
  253. if config['domain']:
  254. creds += f",domain={config['domain']}"
  255. cmd = ["mount", "-t", "cifs", f"//{config['host']}/{config['share']}",
  256. mount_point, "-o", creds]
  257. try:
  258. result = subprocess.run(cmd, capture_output=True, text=True, check=True)
  259. return mount_point
  260. except subprocess.CalledProcessError as e:
  261. raise Exception(f"Impossibile montare {unc_path}: {e.stderr}")
  262. def unmount_share(mount_point):
  263. """Smonta una condivisione."""
  264. is_windows = platform.system() == "Windows"
  265. try:
  266. if is_windows:
  267. subprocess.run(["net", "use", mount_point, "/delete"],
  268. capture_output=True, check=False)
  269. else:
  270. subprocess.run(["umount", mount_point],
  271. capture_output=True, check=False)
  272. except Exception:
  273. pass # Ignora errori di smontaggio
  274. def copy_via_smb(config, local_file, remote_filename):
  275. """Copia un file locale su una condivisione remota usando smbclient."""
  276. is_windows = platform.system() == "Windows"
  277. if is_windows:
  278. # Su Windows usa net use + copy
  279. temp_dir = tempfile.mkdtemp()
  280. try:
  281. mount_point = mount_windows_share(config, temp_dir)
  282. remote_path = os.path.join(mount_point, config['path'], remote_filename)
  283. # Crea la directory se non esiste
  284. remote_dir = os.path.join(mount_point, config['path'])
  285. os.makedirs(remote_dir, exist_ok=True)
  286. # Copia il file
  287. shutil.copy2(local_file, remote_path)
  288. unmount_share(mount_point)
  289. finally:
  290. shutil.rmtree(temp_dir, ignore_errors=True)
  291. else:
  292. # Su Linux usa smbclient
  293. unc_path = f"//{config['host']}/{config['share']}/{config['path']}/{remote_filename}"
  294. cmd = [
  295. "smbclient",
  296. f"//{config['host']}/{config['share']}",
  297. "-U", f"{config['domain']}\\{config['username']}" if config['domain'] else config['username'],
  298. "-c", f"cd {config['path']}; put {local_file} {remote_filename}"
  299. ]
  300. env = os.environ.copy()
  301. if config['password']:
  302. env['PASSWD'] = config['password']
  303. cmd.insert(2, config['password'])
  304. try:
  305. result = subprocess.run(cmd, capture_output=True, text=True, env=env)
  306. if result.returncode != 0:
  307. raise Exception(f"Errore smbclient: {result.stderr}")
  308. except FileNotFoundError:
  309. raise Exception("smbclient non trovato. Installare samba-client o cifs-utils")
  310. def write_local_file(config, content, filename):
  311. """
  312. Scrive il file di output in locale invece che su una condivisione remota.
  313. Args:
  314. config: configurazione output (deve contenere 'local_path')
  315. content: contenuto da scrivere
  316. filename: nome del file
  317. Returns:
  318. path completo del file creato
  319. """
  320. local_path = config.get('local_path', './output')
  321. # Crea la directory se non esiste
  322. os.makedirs(local_path, exist_ok=True)
  323. # Percorso completo del file
  324. output_file = os.path.join(local_path, filename)
  325. # Scrivi il file
  326. with open(output_file, 'w') as f:
  327. f.write(content)
  328. return output_file
  329. def read_remote_file(config, filename):
  330. """Legge un file da una condivisione remota."""
  331. is_windows = platform.system() == "Windows"
  332. temp_dir = tempfile.mkdtemp()
  333. try:
  334. if is_windows:
  335. # Windows: monta e leggi
  336. mount_point = mount_windows_share(config, temp_dir)
  337. file_path = os.path.join(mount_point, config['path'], filename)
  338. if not os.path.exists(file_path):
  339. unmount_share(mount_point)
  340. raise FileNotFoundError(f"File {filename} non trovato su {config['host']}")
  341. with open(file_path, 'r') as f:
  342. content = f.read()
  343. unmount_share(mount_point)
  344. return content
  345. else:
  346. # Linux: usa smbclient per scaricare
  347. local_temp = os.path.join(temp_dir, filename)
  348. cmd = [
  349. "smbclient",
  350. f"//{config['host']}/{config['share']}",
  351. "-U", f"{config['domain']}\\{config['username']}" if config['domain'] else config['username'],
  352. "-c", f"cd {config['path']}; get {filename} {local_temp}"
  353. ]
  354. if config['password']:
  355. cmd.insert(2, config['password'])
  356. result = subprocess.run(cmd, capture_output=True, text=True)
  357. if result.returncode != 0 or not os.path.exists(local_temp):
  358. raise FileNotFoundError(f"File {filename} non trovato su {config['host']}")
  359. with open(local_temp, 'r') as f:
  360. content = f.read()
  361. return content
  362. finally:
  363. shutil.rmtree(temp_dir, ignore_errors=True)
  364. def process_file(input_content):
  365. """Processa il contenuto del file di input e restituisce le righe filtrate."""
  366. filtered_lines = []
  367. for line in input_content.splitlines():
  368. line = line.strip()
  369. if line.startswith('01003'):
  370. filtered_lines.append(line)
  371. return filtered_lines
  372. def main():
  373. """Funzione principale."""
  374. # Default per modalità daily
  375. DEFAULT_DAILY_HOUR = 3
  376. # Parse argomenti da riga di comando
  377. parser = argparse.ArgumentParser(
  378. description='Processa file remoti filtrando stringhe che iniziano con 01003',
  379. formatter_class=argparse.RawDescriptionHelpFormatter,
  380. epilog="""
  381. Esempi:
  382. python file_processor.py # Usa modalità da config (hourly/daily)
  383. python file_processor.py 01-01-2026 # Processa data specifica
  384. python file_processor.py --force # Forza elaborazione
  385. """
  386. )
  387. parser.add_argument(
  388. 'date',
  389. nargs='?',
  390. help='Data nel formato GG-MM-AAAA (opzionale, default: oggi)'
  391. )
  392. parser.add_argument(
  393. '--force',
  394. action='store_true',
  395. help='Forza l\'elaborazione anche se il file non è cambiato'
  396. )
  397. parser.add_argument(
  398. '--config',
  399. default='config.json',
  400. help='Percorso del file di configurazione (default: config.json)'
  401. )
  402. parser.add_argument(
  403. '--no-previous-day-check',
  404. action='store_true',
  405. help='Non controllare il file del giorno precedente (solo modalità hourly)'
  406. )
  407. args = parser.parse_args()
  408. print("=== Avvio elaborazione ===")
  409. print(f"Ora corrente: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}")
  410. try:
  411. # Carica configurazione
  412. print("\nCaricamento configurazione...")
  413. config = load_config(args.config)
  414. # Imposta il percorso errori dalla configurazione
  415. error_path = config.get('error_path', './errors')
  416. write_error_file.error_path = error_path
  417. print(f"Percorso errori: {error_path}")
  418. # Leggi modalità di esecuzione
  419. execution_mode = config.get('execution_mode', 'hourly').lower()
  420. daily_hour = config.get('daily_execution_hour', DEFAULT_DAILY_HOUR)
  421. print(f"Modalità esecuzione: {execution_mode.upper()}")
  422. if execution_mode == 'daily':
  423. print(f"Ora configurata per esecuzione daily: {daily_hour}:00")
  424. # Determina la data da processare
  425. if args.date:
  426. # Data specifica fornita dall'utente - ignora la modalità
  427. try:
  428. date_string = parse_date_argument(args.date)
  429. print(f"Data specificata: {args.date} -> {date_string}")
  430. except ValueError as e:
  431. print(f"ERRORE: {e}")
  432. return 1
  433. print("\nModalità: DATA SPECIFICA (ignora execution_mode)")
  434. # Processa solo la data specificata con stato (comportamento standard)
  435. success, skipped = process_single_date(config, date_string, args.force, skip_state=False)
  436. if success:
  437. print("\n=== Elaborazione completata con successo ===")
  438. return 0
  439. else:
  440. return 1
  441. # Nessuna data specifica: usa la modalità configurata
  442. if execution_mode == 'daily':
  443. # ============================================================
  444. # MODALITÀ DAILY: Processa SOLO il giorno precedente
  445. # ============================================================
  446. print("\n" + "="*60)
  447. print("MODALITÀ DAILY: Consolidamento giorno precedente")
  448. print("="*60)
  449. yesterday_string = get_previous_day_string()
  450. # Processa solo ieri, senza file di stato
  451. success, skipped = process_single_date(
  452. config,
  453. yesterday_string,
  454. force=True, # In daily forziamo sempre (no controllo modifiche)
  455. date_label="IERI (consolidamento finale)",
  456. skip_state=True # Non usa file di stato
  457. )
  458. if success:
  459. if not skipped:
  460. print("\n=== Consolidamento giornaliero completato con successo ===")
  461. else:
  462. print("\n=== File del giorno precedente non trovato ===")
  463. return 0
  464. else:
  465. print("\n=== Consolidamento giornaliero completato con errori ===")
  466. return 1
  467. elif execution_mode == 'hourly':
  468. # ============================================================
  469. # MODALITÀ HOURLY: Comportamento originale (ieri + oggi)
  470. # ============================================================
  471. today_string = get_date_string()
  472. yesterday_string = get_previous_day_string()
  473. print(f"Data odierna: {datetime.now().strftime('%d/%m/%Y')}")
  474. # Pulizia file di stato obsoleti (solo in modalità automatica)
  475. cleanup_old_state_files()
  476. overall_success = True
  477. # FASE 1: Controlla il file di IERI per consolidare eventuali modifiche tardive
  478. if not args.no_previous_day_check:
  479. print("\n" + "="*60)
  480. print("FASE 1: Consolidamento file del giorno precedente")
  481. print("="*60)
  482. success, skipped = process_single_date(
  483. config,
  484. yesterday_string,
  485. args.force,
  486. "IERI",
  487. skip_state=False # Usa file di stato in hourly
  488. )
  489. if not success:
  490. overall_success = False
  491. print("ATTENZIONE: Errore nel consolidamento del file di ieri")
  492. elif skipped:
  493. print("File di ieri già aggiornato o non esistente")
  494. # FASE 2: Processa il file di OGGI
  495. print("\n" + "="*60)
  496. print("FASE 2: Elaborazione file del giorno corrente")
  497. print("="*60)
  498. success, skipped = process_single_date(
  499. config,
  500. today_string,
  501. args.force,
  502. "OGGI",
  503. skip_state=False # Usa file di stato in hourly
  504. )
  505. if not success:
  506. overall_success = False
  507. if overall_success:
  508. print("\n=== Elaborazione completata con successo ===")
  509. return 0
  510. else:
  511. print("\n=== Elaborazione completata con errori ===")
  512. return 1
  513. else:
  514. # Modalità non riconosciuta
  515. print(f"\nERRORE: Modalità '{execution_mode}' non valida. Usa 'daily' o 'hourly'.")
  516. return 1
  517. except Exception as e:
  518. error_msg = f"Errore generale: {str(e)}"
  519. print(f"ERRORE: {error_msg}")
  520. write_error_file(error_msg, "input")
  521. return 1
  522. if __name__ == "__main__":
  523. sys.exit(main())