1818from dataclasses import dataclass , field
1919from typing import Optional , List , Dict , Any
2020
21+ # === FACE PHOTO CACHE ===
22+ # Cache local de fotos faciais — salva ao fazer upload, busca quando download falha.
23+ # Compartilhado entre todos os terminais (indexado por employeeNo).
24+ class _FaceCache :
25+ """Cache local de fotos JPEG indexado por employeeNo."""
26+ _instance = None
27+
28+ def __new__ (cls ):
29+ if cls ._instance is None :
30+ cls ._instance = super ().__new__ (cls )
31+ cls ._instance ._init_dir ()
32+ return cls ._instance
33+
34+ def _init_dir (self ):
35+ import sys as _s
36+ if getattr (_s , 'frozen' , False ):
37+ base = os .path .dirname (_s .executable )
38+ else :
39+ base = os .path .join (os .path .dirname (os .path .abspath (__file__ )), ".." )
40+ self .cache_dir = os .path .join (base , "face_cache" )
41+ os .makedirs (self .cache_dir , exist_ok = True )
42+
43+ def save (self , employee_no : str , jpeg_bytes : bytes ):
44+ """Salva foto no cache."""
45+ if not employee_no or not jpeg_bytes or len (jpeg_bytes ) < 100 :
46+ return
47+ try :
48+ path = os .path .join (self .cache_dir , f"{ employee_no } .jpg" )
49+ with open (path , "wb" ) as f :
50+ f .write (jpeg_bytes )
51+ except Exception :
52+ pass
53+
54+ def get (self , employee_no : str ):
55+ """Retorna bytes JPEG do cache ou None."""
56+ try :
57+ path = os .path .join (self .cache_dir , f"{ employee_no } .jpg" )
58+ if os .path .isfile (path ):
59+ with open (path , "rb" ) as f :
60+ data = f .read ()
61+ if data and len (data ) > 100 :
62+ return data
63+ except Exception :
64+ pass
65+ return None
66+
67+ def exists (self , employee_no : str ) -> bool :
68+ path = os .path .join (self .cache_dir , f"{ employee_no } .jpg" )
69+ return os .path .isfile (path )
70+
71+
2172# Debug logger para arquivo - compativel com PyInstaller .exe
2273_debug_logger = logging .getLogger ("isapi_debug" )
2374_debug_logger .setLevel (logging .DEBUG )
@@ -1695,6 +1746,7 @@ def add_face(self, employee_no, face_data):
16951746 r = self .post ("/ISAPI/Intelligent/FDLib/FaceDataRecord" , data = body ,
16961747 json_format = True , content_type = f"multipart/form-data; boundary={ boundary } " )
16971748 if r .get ("ok" ):
1749+ _FaceCache ().save (employee_no , face_data )
16981750 return r
16991751 # Fallback: FDSetUp (upsert) para modelos que preferem PUT
17001752 _debug_logger .info (f"[add_face] FaceDataRecord falhou ({ r .get ('error' ,'' )} ), tentando FDSetUp..." )
@@ -1707,13 +1759,22 @@ def add_face(self, employee_no, face_data):
17071759 r2 = self .put ("/ISAPI/Intelligent/FDLib/FDSetUp?format=json" , data = body2 ,
17081760 content_type = f"multipart/form-data; boundary={ boundary } " )
17091761 if r2 .get ("ok" ):
1762+ _FaceCache ().save (employee_no , face_data )
17101763 return r2
17111764 except Exception :
17121765 pass
17131766 return r # Retornar erro original
17141767
17151768 def get_face (self , employee_no , log_callback = None ):
1716- """Busca imagem facial de um funcionario. Retorna bytes da imagem ou None.
1769+ """Busca imagem facial. Verifica cache local primeiro, depois ISAPI.
1770+ Salva no cache automaticamente quando encontra."""
1771+ result = self ._get_face_impl (employee_no , log_callback )
1772+ if result and len (result ) > 100 :
1773+ _FaceCache ().save (employee_no , result )
1774+ return result
1775+
1776+ def _get_face_impl (self , employee_no , log_callback = None ):
1777+ """Implementação interna do get_face — busca via ISAPI com múltiplas fases.
17171778 Descobre automaticamente o tipo de biblioteca facial do terminal.
17181779 log_callback(msg) — se fornecido, envia mensagens de progresso para a UI."""
17191780 from urllib .parse import urlparse
@@ -1763,6 +1824,12 @@ def _safe_get(endpoint):
17631824 except Exception as e :
17641825 return {"ok" : False , "error" : str (e ), "status_code" : 0 }
17651826
1827+ # === CACHE LOCAL: busca instantânea antes de qualquer request ===
1828+ cached = _FaceCache ().get (employee_no )
1829+ if cached :
1830+ _log (f"Cache hit! { len (cached )// 1024 } KB" )
1831+ return cached
1832+
17661833 # Aquecer sessão Digest Auth
17671834 _log ("Autenticando sessão..." )
17681835 try :
@@ -2558,6 +2625,10 @@ def _src_wait_ready():
25582625
25592626 log (f"Export concluido: { ok_count } baixadas, { fail_count } falhas "
25602627 f"de { len (face_records )} registros" )
2628+ # Salvar todas as fotos exportadas no cache local
2629+ cache = _FaceCache ()
2630+ for emp_no , jpeg in faces .items ():
2631+ cache .save (emp_no , jpeg )
25612632 return faces
25622633
25632634 def delete_faces (self , employee_nos : list , callback = None ) -> Dict [str , Any ]:
0 commit comments