# Název: janssenpc_file_send.py # Verze: 2.6 # Datum: 2026-06-12 # Popis: Přejmenuje soubory ve složce ##JNJPrenos, zašifruje (Fernet/AES-128), # odešle na msgs.buzalka.cz a přesune do podsložky Trash. # Detekci nových souborů k odeslání zajišťuje FileWatch na JNJ počítači. # (Stahování souborů ze serveru řeší samostatný skript janssenpc_file_receive, # spouštěný ručně ad hoc — do file_send nepatří.) # Loguje průběh do file_send.log vedle skriptu. # Podporuje: PANORAMA Site Contacts (xlsx), Panorama Dashboard (xlsx), # Site Visit Report (xlsx), Follow-Up Letter (xlsx), # Clario MayoScore (csv), Clario MayoDiary (csv), # Clario Data Corrections / DCRs (csv). # # Změna v2.6 (NEDOTAŽENÉ STAHOVÁNÍ Z CHROME): # Chrome stahuje do dočasného souboru *.crdownload a teprve po dokončení # ho přejmenuje na finální jméno; u některých typů zapisuje rovnou do # finálního jména. Dřív watch+send začaly zpracovávat/odesílat i napůl # stažený soubor. Nově: # - IGNORE_SUFFIXES: dočasné přípony (.crdownload/.tmp/.part) se přeskakují. # - is_ready(): před přejmenováním i před odesláním se čeká, až je velikost # STABILNÍ (nemění se po STABILITY_REQUIRED kontrol) a soubor jde otevřít # na zápis (r+b → zamčený soubor hodí PermissionError). Nestabilní/zamčený # soubor se v tomto běhu přeskočí (zpracuje ho další běh / další událost). # # Změna v2.5 (PODROBNÝ LOGGING UPLOADU — diagnostika JNJ filtru): # - upload() loguje celý průběh: přesné jméno v multipartu (repr), velikost # payloadu, HTTP kód, dobu, FINÁLNÍ URL po redirectech, řetěz redirectů, # hlavičky Server/Content-Type/Content-Length, a tělo odpovědi (oříznuté). # - Detekuje stopy korporátního filtru (Zscaler/SiteMinder); 'Server: nginx' # je OČEKÁVANÉ (SWAG reverse proxy), nehlásí se. # - Soubor se přesune do Trash JEN při OVĚŘENÉM úspěchu serveru # (HTTP 200 + application/json + status v očekávané množině). import os import time import shutil import base64 import hashlib import requests import pandas as pd from pathlib import Path from datetime import datetime from cryptography.fernet import Fernet TOKEN = "13e1bb01-9fd5-44a8-8ce9-4ee27133d340" UPLOAD_URL = "https://msgs.buzalka.cz/upload-file" _FERNET = Fernet(base64.urlsafe_b64encode(hashlib.sha256(TOKEN.encode()).digest())) SOURCE_DIR = Path(r"C:\Users\vbuzalka\OneDrive - JNJ\##JNJPrenos") TRASH_DIR = SOURCE_DIR / "Trash" LOG_FILE = Path(__file__).parent / "file_send.log" # Hodnoty pole "status", které server vrací při skutečném přijetí souboru. OK_STATUSES = {"OK", "UPLOADED", "SAVED", "RECEIVED"} # Maximální délka těla odpovědi, kterou zapíšeme do logu. BODY_LOG_LIMIT = 1500 # --- Readiness (nedotažené stahování) --- IGNORE_SUFFIXES = {".crdownload", ".tmp", ".part"} # dočasné soubory prohlížeče/OS STABILITY_INTERVAL = 1.0 # vteřin mezi kontrolami velikosti STABILITY_REQUIRED = 3 # kolikrát po sobě musí být velikost stejná STABILITY_TIMEOUT = 120 # max vteřin čekání na stabilizaci MAYO_DIARY_COLUMNS = [ 'Protocol', 'Country', 'Site', 'PI Name', 'Subject ID', 'Report Date', 'Report Start Date/Time', 'Report End Date/Time', 'Stool Frequency', 'Form Number', 'Role', 'Original Source', ] MAYO_SCORE_COLUMNS = [ 'Protocol', 'Study Population', 'Country', 'Site', 'Principal Investigator', 'Participant ID', 'Baseline Stool Frequency', 'Visit', 'Visit Date', 'Endoscopy Completed?', 'Central Endoscopy Score', 'Local Endoscopy Score', 'Partial Mayo Score', 'Full Mayo Score', ] DCR_ECOA_COLUMNS = [ 'Protocol', 'Data Correction ID', 'Description', 'Query History', ] DCR_ECG_COLUMNS = [ 'Protocol', 'Data Correction ID', 'Site ID', 'PI_NAME', 'Subject Number', 'Query History', ] PANORAMA_COLUMNS = [ 'Part', 'Source', 'Sector', 'TA', 'Protocol ID', 'Interventional', 'Region', 'Country Name', 'Institution Name', 'Site City', 'Site Zip/Postal Code', 'Site Address', 'MSID', 'Site ID', 'Site Status', 'SM Full Name', 'PI Name', 'St F Subj Enr Act', 'ID', 'Category', 'Type', 'Priority', 'Severity', 'Description', 'Brief Description - Subject ID', 'Comments', 'Created By', 'Create Date', 'Last Modified Date', 'Start Date', 'Due Date', 'End Date', 'Status', 'Days Outstanding', 'Action Taken', 'Escalated To', 'Visit Report Status', 'Visit Report Approved', 'Visit Report Type', 'Visit Report Status End Date', 'Active', 'Association', 'Deviation', 'Deviation Closed Date', 'Reason For Exclusion' ] def log(msg: str): ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S') line = f"[{ts}] {msg}" print(line) with LOG_FILE.open("a", encoding="utf-8") as lf: lf.write(line + "\n") def is_ready(f: Path) -> bool: """Je soubor dotažený a připravený ke zpracování? - Dočasné přípony (.crdownload/.tmp/.part) → přeskočit. - Velikost musí být STABILNÍ (STABILITY_REQUIRED kontrol po sobě) a > 0. - Soubor musí jít otevřít na zápis (r+b) — jinak ho ještě někdo drží (Chrome zapisuje). Vrací False, pokud soubor není připravený (zpracuje ho příští běh / událost).""" suffix = f.suffix.lower() if suffix in IGNORE_SUFFIXES: log(f" Přeskočeno (probíhá stahování): {f.name}") return False last_size = -1 stable = 0 waited = 0.0 while waited <= STABILITY_TIMEOUT: try: size = f.stat().st_size except FileNotFoundError: # soubor mezitím zmizel (Chrome přejmenoval .crdownload → finální) log(f" Přeskočeno (soubor zmizel během kontroly): {f.name}") return False if size > 0 and size == last_size: stable += 1 if stable >= STABILITY_REQUIRED: break else: stable = 0 last_size = size time.sleep(STABILITY_INTERVAL) waited += STABILITY_INTERVAL else: log(f" Přeskočeno (velikost se za {STABILITY_TIMEOUT}s neustálila): {f.name}") return False # Zámek: zapisuje-li do souboru ještě jiný proces, r+b selže (Windows). try: with f.open("r+b"): pass except PermissionError: log(f" Přeskočeno (soubor je zamčený jiným procesem): {f.name}") return False except FileNotFoundError: log(f" Přeskočeno (soubor zmizel během kontroly zámku): {f.name}") return False return True def move_to_trash(f: Path): TRASH_DIR.mkdir(exist_ok=True) dest = TRASH_DIR / f.name if dest.exists(): ts = datetime.now().strftime('%Y%m%d_%H%M%S') dest = TRASH_DIR / f"{f.stem}_{ts}{f.suffix}" shutil.move(str(f), dest) def get_timestamp(file_path: str) -> str: return datetime.fromtimestamp(os.path.getmtime(file_path)).strftime('%Y-%m-%d_%H-%M-%S') def upload(f: Path) -> bool: """Zašifruje a odešle soubor. Vrací True JEN při ověřeném úspěchu serveru. Podrobně loguje odpověď kvůli diagnostice korporátního filtru (Zscaler/SiteMinder).""" enc_name = f.name + ".enc" try: encrypted = _FERNET.encrypt(f.read_bytes()) except Exception as e: log(f" CHYBA (šifrování) | {f.name} | {e}") return False log(f" >> UPLOAD start | {f.name}") log(f" URL pole 'file' (multipart filename) = {enc_name!r}") log(f" payload (zašifrováno) = {len(encrypted)} B | původní soubor = {f.stat().st_size} B") log(f" POST {UPLOAD_URL}") try: t0 = time.time() resp = requests.post( UPLOAD_URL, headers={"Authorization": f"Bearer {TOKEN}"}, files={"file": (enc_name, encrypted, "application/octet-stream")}, timeout=120, allow_redirects=True, ) dt = time.time() - t0 except Exception as e: log(f" CHYBA (POST selhal) | {f.name} | {type(e).__name__}: {e}") return False # --- Podrobný rozbor odpovědi --- final_url = str(resp.url) ctype = resp.headers.get("Content-Type", "") server_hdr = resp.headers.get("Server", "") clen = resp.headers.get("Content-Length", "") body = resp.text or "" log(f" <- HTTP {resp.status_code} | {dt:.2f}s") log(f" finální URL : {final_url}") log(f" Content-Type: {ctype!r}") log(f" Server : {server_hdr!r} | Content-Length: {clen!r}") # Řetěz redirectů (filtr typicky přesměrovává) if resp.history: for h in resp.history: loc = h.headers.get("Location", "") log(f" REDIRECT {h.status_code} -> {loc}") # Tělo odpovědi (oříznuté) body_snip = body[:BODY_LOG_LIMIT].replace("\n", "\\n") log(f" tělo[{len(body)}B]: {body_snip}") # --- Detekce stop korporátního filtru --- suspicious = [] if "_sm_nck" in final_url: suspicious.append("'_sm_nck' ve finální URL (SiteMinder/Zscaler replay)") if "msgs.buzalka.cz" not in final_url: suspicious.append("finální URL je na CIZÍM hostu (přesměrováno filtrem)") if "application/json" not in ctype.lower(): suspicious.append(f"Content-Type není JSON ({ctype!r})") if " None: log(f"--- Přejmenování, adresář: {directory} ---") files = [f for f in directory.iterdir() if f.is_file()] log(f" Nalezeno souborů: {len(files)} — {[f.name for f in files]}") for f in files: # Přeskoč nedotažené / zamčené soubory (Chrome ještě stahuje) if not is_ready(f): continue filename = f.name file_path = str(f) # 0a. CLARIO MAYO DIARY (CSV) if 'MAYO-DIARY' in filename and filename.endswith('.csv'): log(f" Detekován MayoDiary: {filename}") try: df = pd.read_csv(file_path) missing = set(MAYO_DIARY_COLUMNS) - set(df.columns) if not missing: protocols = df['Protocol'].dropna().unique() log(f" Protocol: {list(protocols)}") if len(protocols) > 0: study = str(protocols[0]).strip() new_name = f"{get_timestamp(file_path)} {study} Clario MayoDiary.csv" f.rename(directory / new_name) log(f" ÚSPĚCH: -> '{new_name}'") else: log(f" VAROVÁNÍ: Sloupec Protocol je prázdný.") else: log(f" PŘESKOČENO: Chybí sloupce: {missing}") except Exception as e: log(f" CHYBA: {e}") continue # 0b. CLARIO MAYO SCORE (CSV) if 'Custom.MayoScoreReport' in filename and filename.endswith('.csv'): log(f" Detekován MayoScore: {filename}") try: df = pd.read_csv(file_path) missing = set(MAYO_SCORE_COLUMNS) - set(df.columns) if not missing: protocols = df['Protocol'].dropna().unique() log(f" Protocol: {list(protocols)}") if len(protocols) > 0: study = str(protocols[0]).strip() new_name = f"{get_timestamp(file_path)} {study} Clario MayoScore.csv" f.rename(directory / new_name) log(f" ÚSPĚCH: -> '{new_name}'") else: log(f" VAROVÁNÍ: Sloupec Protocol je prázdný.") else: log(f" PŘESKOČENO: Chybí sloupce: {missing}") except Exception as e: log(f" CHYBA: {e}") continue # 0c. CLARIO DATA CORRECTIONS (CSV) — ECG nebo eCOA if filename.endswith('.csv'): try: df = pd.read_csv(file_path, nrows=2) cols = set(df.columns) log(f" CSV sloupce ({filename}): {sorted(cols)}") missing_ecg = set(DCR_ECG_COLUMNS) - cols missing_ecoa = set(DCR_ECOA_COLUMNS) - cols log(f" Chybí pro ECG: {missing_ecg or '—'}") log(f" Chybí pro eCOA: {missing_ecoa or '—'}") if not missing_ecg: label = "Clario ECG DCRs" elif not missing_ecoa: label = "Clario eCOA DCRs" else: log(f" Neznámý CSV typ — bude odeslán bez přejmenování: {filename}") # nepokračujeme continue — soubor projde dál k odeslání label = None if label: log(f" Detekován {label}: {filename}") protocols = df['Protocol'].dropna().unique() log(f" Protocol: {list(protocols)}") if len(protocols) > 0: study = str(protocols[0]).strip() new_name = f"{get_timestamp(file_path)} {study} {label}.csv" f.rename(directory / new_name) log(f" ÚSPĚCH přejmenování: -> '{new_name}'") else: log(f" VAROVÁNÍ: Sloupec Protocol je prázdný — odesílám pod původním názvem.") except Exception as e: log(f" CHYBA při zpracování CSV {filename}: {e}") continue # Ostatní — jen xlsx if not filename.endswith('.xlsx'): log(f" Přeskočeno (neznámý typ): {filename}") continue # 1a. PANORAMA SITE CONTACTS (XLSX) — soubor pojmenovaný "PANORAMA Dashboard" if 'PANORAMA Dashboard' in filename: log(f" Detekován PANORAMA Site Contacts: {filename}") try: with pd.ExcelFile(file_path) as xl: sheet_names = xl.sheet_names if 'Site Contacts' in sheet_names: df_a1 = xl.parse('Site Contacts', nrows=1, header=None) a1 = str(df_a1.iloc[0, 0]) if not df_a1.empty else '' else: a1 = None # soubor je nyní zavřen — přejmenování proběhne bez chyby if a1 is None: log(f" PŘESKOČENO: List 'Site Contacts' nenalezen.") elif 'Title: Site Contacts' in a1: new_name = f"{get_timestamp(file_path)} PANORAMA Site Contacts.xlsx" f.rename(directory / new_name) log(f" ÚSPĚCH: -> '{new_name}'") else: log(f" PŘESKOČENO: A1 neodpovídá vzoru ({a1[:50]})") except Exception as e: log(f" CHYBA: {e}") continue # 1. PANORAMA DASHBOARD (XLSX) if 'Panorama Dashboard' in filename: log(f" Detekován Panorama: {filename}") try: df = pd.read_excel(file_path, skiprows=5) missing = set(PANORAMA_COLUMNS) - set(df.columns) if not missing: ids = df['Protocol ID'].dropna().unique() log(f" Protocol ID: {list(ids)}") if len(ids) > 0: study = str(ids[0]).strip() new_name = f"{get_timestamp(file_path)} {study} Panorama Deviations and Issues.xlsx" f.rename(directory / new_name) log(f" ÚSPĚCH: -> '{new_name}'") else: log(f" VAROVÁNÍ: Protocol ID je prázdný.") else: log(f" PŘESKOČENO: Chybí sloupce: {missing}") except Exception as e: log(f" CHYBA: {e}") continue # 2. SITE VISIT REPORT A FOLLOW-UP LETTER (XLSX) try: df_a1 = pd.read_excel(file_path, nrows=1, header=None) if not df_a1.empty: a1 = str(df_a1.iloc[0, 0]) log(f" A1: {a1[:80]}") is_site_visit = "Title: Site Visit Report Details" in a1 is_follow_up = "Title: Follow-Up Letter Details" in a1 if is_site_visit or is_follow_up: suffix = "Site Visit Details.xlsx" if is_site_visit else "FUL details.xlsx" log(f" Detekován {'Site Visit' if is_site_visit else 'Follow-Up Letter'}: {filename}") df = pd.read_excel(file_path, skiprows=5) if 'Protocol ID' in df.columns: ids = df['Protocol ID'].dropna().unique() log(f" Protocol ID: {list(ids)}") if len(ids) > 0: study = str(ids[0]).strip() new_name = f"{get_timestamp(file_path)} {study} {suffix}" f.rename(directory / new_name) log(f" ÚSPĚCH: -> '{new_name}'") else: log(f" VAROVÁNÍ: Protocol ID je prázdný.") else: log(f" PŘESKOČENO: Chybí sloupec Protocol ID.") else: log(f" Přeskočeno (neznámý xlsx obsah): {filename}") except Exception as e: log(f" CHYBA: {e}") log("--- Přejmenování dokončeno ---") # === HLAVNÍ LOGIKA === log("=== Spuštění ===") log(f"Zdrojový adresář: {SOURCE_DIR} (existuje: {SOURCE_DIR.exists()})") # 1. Přejmenuj prejmenuj(SOURCE_DIR) # 2. Počkej 10 vteřin log("Čekám 10 vteřin...") time.sleep(10) # 3. Odešli soubory files = [f for f in SOURCE_DIR.iterdir() if f.is_file()] log(f"Souborů k odeslání: {len(files)}") for f in files: log(f" Nalezen: {f.name}") if not files: log("Žádné soubory k odeslání.") else: for f in files: # Pojistka: neodesílej nedotažený / zamčený / dočasný soubor if not is_ready(f): continue ok = upload(f) if ok: move_to_trash(f) log(f" PŘESUNUTO | {f.name} -> Trash") else: log(f" PONECHÁNO | {f.name} (NEodesláno ověřeně — zůstává ve složce)") log("=== Hotovo ===")