Skip to content

Commit 2acdc1b

Browse files
Glauber Varjãoclaude
andcommitted
feat: cache local de fotos faciais — resolve download em terminais sem suporte
Novo sistema _FaceCache (singleton, face_cache/{employeeNo}.jpg): - add_face: salva cópia JPEG no cache ao enviar foto ao terminal - get_face: busca no cache ANTES de qualquer request ISAPI (instantâneo) - get_face: salva no cache quando download ISAPI tem sucesso - _export_all_faces: salva todas as fotos exportadas no cache Cache compartilhado entre terminais pelo employeeNo. Resolve DS-K1T672MX: foto enviada via qualquer terminal fica disponível. Bump v4.2.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 16fbdef commit 2acdc1b

5 files changed

Lines changed: 79 additions & 8 deletions

File tree

build_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
BUILD_TIMESTAMP = "2026-04-01 19:34"
1+
BUILD_TIMESTAMP = "2026-04-01 21:22"

core/isapi_client.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,57 @@
1818
from dataclasses import dataclass, field
1919
from 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]:

file_version_info.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# UTF-8
22
VSVersionInfo(
33
ffi=FixedFileInfo(
4-
filevers=(4, 1, 9, 0),
5-
prodvers=(4, 1, 9, 0),
4+
filevers=(4, 2, 0, 0),
5+
prodvers=(4, 2, 0, 0),
66
mask=0x3f,
77
flags=0x0,
88
OS=0x40004,
@@ -18,12 +18,12 @@ VSVersionInfo(
1818
[
1919
StringStruct(u'CompanyName', u'Protector Sistemas'),
2020
StringStruct(u'FileDescription', u'Protector ISAPI Manager'),
21-
StringStruct(u'FileVersion', u'4.1.9'),
21+
StringStruct(u'FileVersion', u'4.2.0'),
2222
StringStruct(u'InternalName', u'Protector_ISAPI_Manager'),
2323
StringStruct(u'LegalCopyright', u'© 2026 Protector Sistemas'),
2424
StringStruct(u'OriginalFilename', u'Protector_ISAPI_Manager.exe'),
2525
StringStruct(u'ProductName', u'Protector ISAPI Manager'),
26-
StringStruct(u'ProductVersion', u'4.1.9'),
26+
StringStruct(u'ProductVersion', u'4.2.0'),
2727
]
2828
)
2929
]

installer.iss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
; ============================================================================
1515

1616
#define MyAppName "Protector ISAPI Manager"
17-
#define MyAppVersion "4.1.9"
17+
#define MyAppVersion "4.2.0"
1818
#define MyAppPublisher "Protector Sistemas"
1919
#define MyAppURL "https://github.com/ProtectorAnalytics/protector-isapi-manager"
2020
#define MyAppExeName "Protector_ISAPI_Manager.exe"

version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# MAJOR = mudança que quebra compatibilidade
1111
# MINOR = funcionalidades novas (retrocompatíveis)
1212
# PATCH = correções de bugs
13-
VERSION = "4.1.9"
13+
VERSION = "4.2.0"
1414

1515
# Status: "stable", "beta", "dev", "rc1", "rc2"...
1616
VERSION_STATUS = "stable"

0 commit comments

Comments
 (0)