feature/generador-instaladores-linux-windows
This commit is contained in:
@@ -12,6 +12,7 @@ producción conviene fijar/known_hosts las claves de cada servidor.
|
||||
|
||||
import ntpath
|
||||
import posixpath
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -22,6 +23,12 @@ from ..utils.logger import app_logger
|
||||
# Timeout de conexión SSH en segundos.
|
||||
SSH_TIMEOUT = 30
|
||||
|
||||
# Verificación de la subida: tras escribir los bytes se confirma el tamaño remoto con
|
||||
# reintentos. Tolera stats flaky (buffering/AV/locking, típico en OpenSSH/Windows) que de
|
||||
# otro modo producirían un FALSO FALLO aunque el archivo sí se entregó.
|
||||
VERIFY_ATTEMPTS = 3
|
||||
VERIFY_DELAY_SECONDS = 1.0
|
||||
|
||||
|
||||
class SFTPCopyError(Exception):
|
||||
"""Error al transferir o limpiar el .bak en el servidor remoto vía SFTP."""
|
||||
@@ -102,12 +109,17 @@ def upload_file_to_folder(local_file: str, cfg: dict, remote_folder: str) -> str
|
||||
raise SFTPCopyError("La carpeta remota destino está vacía")
|
||||
|
||||
remote_sftp = _sftp_path(str(remote_folder).strip(), src.name)
|
||||
expected_size = src.stat().st_size
|
||||
client = _connect(cfg)
|
||||
try:
|
||||
sftp = client.open_sftp()
|
||||
try:
|
||||
app_logger.info(f"Subiendo archivo por SFTP a {cfg['ssh_host']}: {remote_sftp}")
|
||||
sftp.put(str(src), remote_sftp)
|
||||
# confirm=False: la verificación de paramiko hace un stat inmediato que en
|
||||
# OpenSSH/Windows suele fallar por buffering aunque el archivo sí se escribió.
|
||||
# Se verifica el tamaño aparte, con reintentos (ver _verify_remote_size).
|
||||
sftp.put(str(src), remote_sftp, confirm=False)
|
||||
_verify_remote_size(sftp, remote_sftp, expected_size)
|
||||
finally:
|
||||
sftp.close()
|
||||
except SFTPCopyError:
|
||||
@@ -120,16 +132,45 @@ def upload_file_to_folder(local_file: str, cfg: dict, remote_folder: str) -> str
|
||||
return remote_sftp
|
||||
|
||||
|
||||
def _verify_remote_size(sftp, remote_path: str, expected_size: int) -> None:
|
||||
"""Confirma que el archivo remoto tiene el tamaño esperado, con reintentos.
|
||||
|
||||
Un stat transitoriamente fallido o con tamaño aún incompleto (flush/AV en curso) se
|
||||
reintenta; solo tras agotar los intentos se considera fallo genuino de entrega.
|
||||
"""
|
||||
last_error: Optional[str] = None
|
||||
for attempt in range(VERIFY_ATTEMPTS):
|
||||
try:
|
||||
remote_size = sftp.stat(remote_path).st_size
|
||||
if remote_size == expected_size:
|
||||
return
|
||||
last_error = f"tamaño remoto {remote_size} != esperado {expected_size}"
|
||||
except Exception as e: # noqa: BLE001 — se reintenta y, si persiste, se eleva abajo
|
||||
last_error = str(e)
|
||||
if attempt < VERIFY_ATTEMPTS - 1:
|
||||
time.sleep(VERIFY_DELAY_SECONDS)
|
||||
raise SFTPCopyError(
|
||||
f"No se pudo verificar la subida de {remote_path}: {last_error}"
|
||||
)
|
||||
|
||||
|
||||
def upload_zip_parts(local_paths: list[str], cfg: dict, remote_folder: str) -> list[str]:
|
||||
"""
|
||||
Sube uno o más archivos ZIP (incl. multipart) al input_folder del destino.
|
||||
|
||||
Ante un fallo, adjunta a la excepción la lista de partes ya subidas (atributo
|
||||
``uploaded``) para que el llamador pueda limpiar el envío parcial en el destino.
|
||||
|
||||
Returns:
|
||||
Lista de rutas SFTP subidas.
|
||||
"""
|
||||
uploaded: list[str] = []
|
||||
for local_path in local_paths:
|
||||
uploaded.append(upload_file_to_folder(local_path, cfg, remote_folder))
|
||||
try:
|
||||
uploaded.append(upload_file_to_folder(local_path, cfg, remote_folder))
|
||||
except SFTPCopyError as e:
|
||||
e.uploaded = uploaded
|
||||
raise
|
||||
return uploaded
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user