z230
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
# Název: janssenpc_file_send.py
|
||||
# Verze: 2.4
|
||||
# Datum: 2026-06-05
|
||||
# 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).
|
||||
|
||||
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"
|
||||
|
||||
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 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 prejmenuj(directory: Path) -> 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:
|
||||
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:
|
||||
try:
|
||||
encrypted = _FERNET.encrypt(f.read_bytes())
|
||||
enc_name = f.name + ".enc"
|
||||
resp = requests.post(
|
||||
UPLOAD_URL,
|
||||
headers={"Authorization": f"Bearer {TOKEN}"},
|
||||
files={"file": (enc_name, encrypted, "application/octet-stream")},
|
||||
timeout=120,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
status = resp.json().get('status', '?').upper()
|
||||
log(f" {status:10} | {f.name} (zašifrováno)")
|
||||
move_to_trash(f)
|
||||
log(f" PŘESUNUTO | {f.name} -> Trash")
|
||||
except Exception as e:
|
||||
log(f" CHYBA | {f.name} | {e}")
|
||||
|
||||
log("=== Hotovo ===")
|
||||
@@ -0,0 +1,76 @@
|
||||
# janssenpc_file_send v2.5
|
||||
|
||||
Odesílací skript na JNJ počítači: přejmenuje soubory ve `##JNJPrenos`, zašifruje
|
||||
(Fernet/AES-128) a odešle na `https://msgs.buzalka.cz/upload-file`, pak přesune
|
||||
do `Trash`. Detekci nových souborů zajišťuje `janssenpc_file_watch.py`.
|
||||
Stahování ze serveru řeší samostatný `janssenpc_file_receive` (ručně) — sem nepatří.
|
||||
|
||||
## Spuštění
|
||||
Spouští se automaticky přes `janssenpc_file_watch.py` (watchdog). Ručně:
|
||||
```
|
||||
C:\Users\vbuzalka\OneDrive - JNJ\##JNJPrenos\Python\python.exe "...\janssenpc_file_send.py"
|
||||
```
|
||||
|
||||
## Co je nového v v2.5 — diagnostika JNJ filtru
|
||||
|
||||
Důvod: v logu z 2026-06-12 byl poslední soubor označen jako `UPLOADED` + `PŘESUNUTO`,
|
||||
ale **doma fakticky nepřišel**. Podezření na korporátní filtr (Zscaler/SiteMinder),
|
||||
který už dříve sabotoval downloady (`403 + ?_sm_nck=1`). v2.4 logoval jen
|
||||
`resp.json()['status']`, takže podvrženou/přesměrovanou odpověď nebylo poznat.
|
||||
|
||||
### 1. Funkce `upload()` — podrobný log každého POSTu
|
||||
Loguje:
|
||||
- přesné jméno v multipartu `repr(enc_name)` (odhalí problémové znaky/diakritiku),
|
||||
- velikost zašifrovaného payloadu i původního souboru,
|
||||
- **HTTP kód** a dobu trvání,
|
||||
- **finální URL** po redirectech (`resp.url`),
|
||||
- celý **řetěz redirectů** (`resp.history` + `Location`),
|
||||
- hlavičky `Content-Type`, `Server`, `Content-Length`,
|
||||
- **tělo odpovědi** (oříznuté na 1500 B).
|
||||
|
||||
### 2. Detekce stop filtru → `⚠ PODEZŘENÍ NA FILTR`
|
||||
Zaloguje varování, pokud:
|
||||
- `_sm_nck` ve finální URL (SiteMinder/Zscaler replay),
|
||||
- finální URL je na cizím hostu (přesměrováno),
|
||||
- `Content-Type` není `application/json`,
|
||||
- tělo vypadá jako HTML (block page),
|
||||
- hlavička `Server` není `uvicorn` ani `nginx`.
|
||||
|
||||
> **Pozn.:** Před uvicornem běží SWAG reverse proxy, takže `Server: nginx` je
|
||||
> NORMÁLNÍ a poplach se na něj nehlásí. Spolehlivý důkaz pravé odpovědi serveru je
|
||||
> přítomnost `dropbox_path` v JSON těle (to generuje jen `app.py`).
|
||||
|
||||
## Ověřeno v provozu (2026-06-12)
|
||||
První ostrý běh v2.5 prokázal, že přenosový řetězec klient → server → Dropbox
|
||||
funguje korektně: `HTTP 200`, finální URL bez redirectu/`_sm_nck`, pravé JSON tělo
|
||||
s `dropbox_path`, soubor fyzicky dorazil do `U:\Dropbox\!!!Days\Downloads Z230\`
|
||||
(velikost bajt po bajtu sedí). Dřívější podezření na JNJ filtr se NEPOTVRDILO —
|
||||
šlo o zpožděný Dropbox sync na domácí straně.
|
||||
|
||||
### 3. Bezpečné mazání — Trash JEN při ověřeném úspěchu
|
||||
Soubor se přesune do `Trash` **pouze** když:
|
||||
`HTTP 200` **a** `Content-Type: application/json` **a** pole `status` ∈
|
||||
`{OK, UPLOADED, SAVED, RECEIVED}`.
|
||||
|
||||
Jinak se loguje `PONECHÁNO` a soubor zůstává ve složce — aby falešný „úspěch"
|
||||
podvržený filtrem nesmazal nepřenesený soubor. (Pozn.: `OK_STATUSES` případně sladit
|
||||
s tím, co skutečně vrací `app.py` na `/upload-file` — ověřit z těla v logu.)
|
||||
|
||||
## Jak diagnostikovat
|
||||
1. Nasadit v2.5 na JNJ stroj místo v2.4 (jako `janssenpc_file_send.py`).
|
||||
2. Spustit přenos, otevřít `file_send.log` u sekce `>> UPLOAD start`.
|
||||
3. Porovnat: pokud je tam `⚠ PODEZŘENÍ NA FILTR` nebo `status` mimo množinu →
|
||||
chyba je na cestě (filtr / endpoint), ne v serveru. Pokud je odpověď čistá JSON
|
||||
`uvicorn` 200 a soubor přesto doma chybí → problém je až na serveru
|
||||
(`/upload-file` dešifrování / zápis do Dropboxu) — řešit v `app.py`.
|
||||
|
||||
## Vazby
|
||||
- Watcher: `janssenpc_file_watch.py` (v1.1).
|
||||
- Server: `app.py` ≥ v2.3, endpoint `POST /upload-file` (dešifruje `.enc` → Dropbox
|
||||
`/!!!Days/Downloads Z230/`).
|
||||
- Protějšek pro download: `janssenpc_file_receive_v1.2`.
|
||||
|
||||
## Historie
|
||||
- v2.5 (2026-06-12): podrobný logging uploadu + detekce filtru + Trash jen při ověřeném úspěchu
|
||||
- v2.4 (2026-06-05): Fernet šifrování uploadu, endpoint `/upload-file`
|
||||
- v2.2 (2026-06-02): odesílání bez šifrování přes `/upload-dropbox`
|
||||
@@ -0,0 +1,398 @@
|
||||
# Název: janssenpc_file_send.py
|
||||
# Verze: 2.5
|
||||
# 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.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): '_sm_nck' v URL,
|
||||
# redirect na cizí host, Content-Type != application/json, HTML tělo,
|
||||
# hlavička Server nepatřící uvicornu.
|
||||
# - 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ě). Jinak
|
||||
# zůstává ve složce a loguje se PODEZŘENÍ — aby se „falešný úspěch"
|
||||
# (podvržená odpověď filtru) nemazal.
|
||||
|
||||
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
|
||||
|
||||
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 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 "<html" in body.lower() or "<!doctype" in body.lower():
|
||||
suspicious.append("tělo vypadá jako HTML (block page filtru)")
|
||||
# Pozn.: před uvicornem běží SWAG reverse proxy → 'Server: nginx' je OČEKÁVANÉ,
|
||||
# nehlásíme jako podezření. Spolehlivý signál pravosti je dropbox_path v JSON těle.
|
||||
if server_hdr and not any(s in server_hdr.lower() for s in ("uvicorn", "nginx")):
|
||||
suspicious.append(f"hlavička Server není uvicorn/nginx ({server_hdr!r})")
|
||||
if suspicious:
|
||||
for s in suspicious:
|
||||
log(f" ⚠ PODEZŘENÍ NA FILTR: {s}")
|
||||
|
||||
# --- Ověření skutečného úspěchu ---
|
||||
if resp.status_code != 200:
|
||||
log(f" NEÚSPĚCH | {f.name} | HTTP {resp.status_code} — soubor PONECHÁN")
|
||||
return False
|
||||
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
log(f" NEÚSPĚCH | {f.name} | odpověď není JSON ({e}) — soubor PONECHÁN")
|
||||
return False
|
||||
|
||||
status = str(data.get("status", "?")).upper()
|
||||
if status in OK_STATUSES:
|
||||
log(f" UPLOADED | {f.name} (zašifrováno, status={status})")
|
||||
return True
|
||||
else:
|
||||
log(f" NEÚSPĚCH | {f.name} | server status={status!r} mimo {OK_STATUSES} — soubor PONECHÁN")
|
||||
return False
|
||||
|
||||
|
||||
def prejmenuj(directory: Path) -> 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:
|
||||
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:
|
||||
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 ===")
|
||||
@@ -0,0 +1,59 @@
|
||||
# janssenpc_file_send v2.6
|
||||
|
||||
Odesílací skript na JNJ počítači: přejmenuje soubory ve `##JNJPrenos`, zašifruje
|
||||
(Fernet/AES-128) a odešle na `https://msgs.buzalka.cz/upload-file`, pak přesune
|
||||
do `Trash`. Detekci nových souborů zajišťuje `janssenpc_file_watch.py` (v1.2).
|
||||
Stahování ze serveru řeší samostatný `janssenpc_file_receive` (ručně) — sem nepatří.
|
||||
|
||||
## Spuštění
|
||||
Spouští se automaticky přes `janssenpc_file_watch.py` (watchdog). Ručně:
|
||||
```
|
||||
C:\Users\vbuzalka\OneDrive - JNJ\##JNJPrenos\Python\python.exe "...\janssenpc_file_send.py"
|
||||
```
|
||||
|
||||
## Co je nového v v2.6 — ošetření nedotaženého stahování z Chrome
|
||||
|
||||
Problém: Chrome stahuje report 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. Watchdog vystřelí hned na `on_created`, takže dřívější verze
|
||||
začaly zpracovávat / odesílat i napůl stažený soubor (riziko: upload rozdělaného
|
||||
`.crdownload`, nebo načtení neúplného CSV/XLSX).
|
||||
|
||||
### `is_ready(f)` — gate před zpracováním i odesláním
|
||||
Soubor projde, jen když:
|
||||
1. **nemá dočasnou příponu** (`.crdownload` / `.tmp` / `.part`),
|
||||
2. **velikost je stabilní** — `STABILITY_REQUIRED` (3) kontrol po sobě stejná a > 0,
|
||||
poll po `STABILITY_INTERVAL` (1 s), max `STABILITY_TIMEOUT` (120 s),
|
||||
3. **jde otevřít na zápis** (`r+b`) — zamčený soubor (Chrome ještě zapisuje) hodí
|
||||
`PermissionError` → přeskočí se.
|
||||
|
||||
Nepřipravený soubor se v daném běhu jen přeskočí (zaloguje důvod) a zpracuje ho
|
||||
příští běh / další událost watcheru.
|
||||
|
||||
Volá se na DVOU místech: na začátku `prejmenuj()` (ať pandas nečte neúplný soubor)
|
||||
a znovu v odesílací fázi (pojistka před uploadem).
|
||||
|
||||
## Spolupráce s watcherem (v1.2)
|
||||
- Watcher **ignoruje** dočasné přípony (nespouští send na `.crdownload`).
|
||||
- Watcher **debouncuje** sérii událostí (vytvoření + přejmenování + zápis) do jednoho
|
||||
spuštění (`DEBOUNCE_SECONDS = 5`).
|
||||
- Vlastní „dotažení" řeší až `is_ready` v file_send — watcher jen omezuje zbytečná spuštění.
|
||||
|
||||
## Zděděno z v2.5 — podrobný logging uploadu
|
||||
Funkce `upload()` loguje HTTP kód, finální URL, redirecty, hlavičky a tělo odpovědi;
|
||||
detekuje stopy korporátního filtru (`_sm_nck`, cizí host, ne-JSON, HTML, neznámý Server).
|
||||
`Server: nginx` je OČEKÁVANÉ (SWAG reverse proxy). Soubor se přesune do `Trash` jen
|
||||
při ověřeném úspěchu (`HTTP 200` + `application/json` + `status` ∈
|
||||
`{OK, UPLOADED, SAVED, RECEIVED}`); jinak `PONECHÁNO`.
|
||||
|
||||
## Vazby
|
||||
- Watcher: `janssenpc_file_watch_v1.2.py`.
|
||||
- Server: `app.py` ≥ v2.3, endpoint `POST /upload-file` (vrací
|
||||
`{"status":"uploaded","file":...,"dropbox_path":...}`).
|
||||
- Protějšek pro download: `janssenpc_file_receive_v1.2`.
|
||||
|
||||
## Historie
|
||||
- v2.6 (2026-06-12): is_ready gate (stabilní velikost + zámek + ignor .crdownload) proti nedotaženému stahování
|
||||
- v2.5 (2026-06-12): podrobný logging uploadu + detekce filtru + Trash jen při ověřeném úspěchu (ověřeno: přenos OK, filtr nevinen)
|
||||
- v2.4 (2026-06-05): Fernet šifrování uploadu, endpoint `/upload-file`
|
||||
- v2.2 (2026-06-02): odesílání bez šifrování přes `/upload-dropbox`
|
||||
@@ -0,0 +1,467 @@
|
||||
# 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 "<html" in body.lower() or "<!doctype" in body.lower():
|
||||
suspicious.append("tělo vypadá jako HTML (block page filtru)")
|
||||
# Pozn.: před uvicornem běží SWAG reverse proxy → 'Server: nginx' je OČEKÁVANÉ,
|
||||
# nehlásíme jako podezření. Spolehlivý signál pravosti je dropbox_path v JSON těle.
|
||||
if server_hdr and not any(s in server_hdr.lower() for s in ("uvicorn", "nginx")):
|
||||
suspicious.append(f"hlavička Server není uvicorn/nginx ({server_hdr!r})")
|
||||
if suspicious:
|
||||
for s in suspicious:
|
||||
log(f" ⚠ PODEZŘENÍ NA FILTR: {s}")
|
||||
|
||||
# --- Ověření skutečného úspěchu ---
|
||||
if resp.status_code != 200:
|
||||
log(f" NEÚSPĚCH | {f.name} | HTTP {resp.status_code} — soubor PONECHÁN")
|
||||
return False
|
||||
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
log(f" NEÚSPĚCH | {f.name} | odpověď není JSON ({e}) — soubor PONECHÁN")
|
||||
return False
|
||||
|
||||
status = str(data.get("status", "?")).upper()
|
||||
if status in OK_STATUSES:
|
||||
log(f" UPLOADED | {f.name} (zašifrováno, status={status})")
|
||||
return True
|
||||
else:
|
||||
log(f" NEÚSPĚCH | {f.name} | server status={status!r} mimo {OK_STATUSES} — soubor PONECHÁN")
|
||||
return False
|
||||
|
||||
|
||||
def prejmenuj(directory: Path) -> 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 ===")
|
||||
@@ -0,0 +1,89 @@
|
||||
# Název: janssenpc_file_watch.py
|
||||
# Verze: 1.2
|
||||
# Datum: 2026-06-12
|
||||
# Popis: Démon hlídající složku ##JNJPrenos (watchdog). Při objevení nového souboru
|
||||
# spustí janssenpc_file_send.py, který zajistí přejmenování, upload a přesun do Trash.
|
||||
#
|
||||
# Změna v1.2 (NEDOTAŽENÉ STAHOVÁNÍ Z CHROME):
|
||||
# - Ignoruje dočasné přípony (.crdownload/.tmp/.part) — Chrome stahuje do
|
||||
# *.crdownload a teprve po dokončení přejmenuje na finální jméno. Spouštět
|
||||
# send na dočasný soubor nemá smysl (vyřeší to až událost přejmenování).
|
||||
# - Debounce: shlukne rychlou sérii událostí (vytvoření + přejmenování + zápis)
|
||||
# do JEDNOHO spuštění send. Mezi spuštěními drží DEBOUNCE_SECONDS odstup.
|
||||
# Vlastní kontrolu „dotažení" (stabilní velikost + zámek) dělá až file_send
|
||||
# (funkce is_ready) — watch jen omezuje zbytečná spuštění.
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
|
||||
SOURCE_DIR = Path(r"C:\Users\vbuzalka\OneDrive - JNJ\##JNJPrenos")
|
||||
SEND_SCRIPT = Path(__file__).parent / "janssenpc_file_send.py"
|
||||
|
||||
IGNORE_SUFFIXES = {".crdownload", ".tmp", ".part"} # probíhající stahování
|
||||
DEBOUNCE_SECONDS = 5.0 # série událostí v tomto okně spustí send jen jednou
|
||||
|
||||
|
||||
def run_send():
|
||||
subprocess.run([sys.executable, str(SEND_SCRIPT)], check=False)
|
||||
|
||||
|
||||
class DebouncedRunner:
|
||||
"""Shlukne rychlou sérii událostí do jednoho spuštění send."""
|
||||
def __init__(self, delay: float):
|
||||
self.delay = delay
|
||||
self._timer = None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def trigger(self):
|
||||
with self._lock:
|
||||
if self._timer is not None:
|
||||
self._timer.cancel()
|
||||
self._timer = threading.Timer(self.delay, run_send)
|
||||
self._timer.daemon = True
|
||||
self._timer.start()
|
||||
|
||||
|
||||
_runner = DebouncedRunner(DEBOUNCE_SECONDS)
|
||||
|
||||
|
||||
def _is_temp(path: str) -> bool:
|
||||
return Path(path).suffix.lower() in IGNORE_SUFFIXES
|
||||
|
||||
|
||||
class NewFileHandler(FileSystemEventHandler):
|
||||
def on_created(self, event):
|
||||
if event.is_directory or _is_temp(event.src_path):
|
||||
return
|
||||
_runner.trigger()
|
||||
|
||||
def on_moved(self, event):
|
||||
if event.is_directory:
|
||||
return
|
||||
# Cíl přejmenování je rozhodující (Chrome: *.crdownload -> finální jméno)
|
||||
dest = getattr(event, "dest_path", "") or event.src_path
|
||||
if _is_temp(dest):
|
||||
return
|
||||
_runner.trigger()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Při startu zpracuj soubory, které už tam jsou (kromě dočasných)
|
||||
if any(f for f in SOURCE_DIR.iterdir() if f.is_file() and f.suffix.lower() not in IGNORE_SUFFIXES):
|
||||
run_send()
|
||||
|
||||
observer = Observer()
|
||||
observer.schedule(NewFileHandler(), str(SOURCE_DIR), recursive=False)
|
||||
observer.start()
|
||||
print(f"Hlídám: {SOURCE_DIR}")
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
observer.stop()
|
||||
observer.join()
|
||||
Reference in New Issue
Block a user