Sincronización incremental desde SMB con smbprotocol en Linux: autenticación NTLM y control de logs
Etiquetas: smb,smbprotocol,sqlite,sincronización,ntlm,linuxEl problema
Hace poco necesitaba sincronizar un share SMB desde un NAS con mi servidor Linux. La solución obvia sería smbclient o mount -t cifs, pero quería:
- Sincronización incremental (solo archivos nuevos o modificados)
- Detectar archivos eliminados del share
- Controlar la autenticación NTLM directamente desde código
- Silenciar la cantidad obscena de logs que suelta
smbprotocol
La librería smbprotocol de Python resolvía todo esto, pero no está documentado cómo hacerlo bien. Aquí está mi solución.
Setup inicial
Instala las dependencias:
pip install smbprotocol sqlalchemy pydantic python-dotenv
La idea base: mantener una BD SQLite con un registro de todos los ficheros sincronizados (nombre, hash MD5, timestamp). Cada ejecución compara el share actual con la BD y procesa solo cambios.
Silenciar los logs de smbprotocol
Esto es crítico. Sin controlarlo, la librería te llena la consola de mensajes de depuración:
import logging
# Silenciar smbprotocol
logging.getLogger('smbprotocol').setLevel(logging.WARNING)
logging.getLogger('smbprotocol.connection').setLevel(logging.WARNING)
logging.getLogger('smbprotocol.session').setLevel(logging.WARNING)
# Tu logger
logger = logging.getLogger('sync_smb')
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)
Esto reduce los logs a lo razonable. Sin esto, cada operación genera 50 líneas de basura.
Estructura de la BD SQLite
from datetime import datetime
from sqlalchemy import create_engine, Column, String, DateTime, Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class SyncedFile(Base):
__tablename__ = 'synced_files'
filename = Column(String, primary_key=True)
md5_hash = Column(String)
file_size = Column(Integer)
last_modified = Column(DateTime)
sync_timestamp = Column(DateTime, default=datetime.utcnow)
engine = create_engine('sqlite:///smb_sync.db')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
Conexión con NTLM
from smbprotocol.session import Session
from smbprotocol.tree import TreeConnect
import socket
username = "DOMINIO\\usuario" # Formato NETBIOS\usuario
password = "contraseña"
host = "192.168.x.x"
share = "compartido"
# Conexión básica
connection = smbprotocol.connection.Connection(
uuid.uuid4(),
host,
445,
)
connection.connect()
session = Session(connection, username, password)
session.connect()
tree = TreeConnect(session, f"\\\\{host}\\{share}")
tree.connect()
NTLM se negocia automáticamente. No necesitas hacer nada especial, pero asegúrate de usar el formato DOMINIO\usuario correcto.
Sincronización incremental
import hashlib
from pathlib import Path
def get_file_hash(file_data):
"""Calcula MD5 de contenido en bytes"""
return hashlib.md5(file_data).hexdigest()
def sync_smb_share(local_path: Path):
session = Session()
remote_files = {}
# Listar archivos del share
directory = tree.open_file(share, FileAttributes.DIRECTORY, CreateOptions.FILE_DIRECTORY_FILE)
for file_info in directory.query_directory():
if file_info.file_attributes & FileAttributes.DIRECTORY:
continue # Ignorar carpetas por ahora
filename = file_info.file_name
remote_files[filename] = {
'size': file_info.end_of_file,
'modified': file_info.change_time.timestamp()
}
# Leer archivos nuevos o modificados
local_db = session.query(SyncedFile).all()
local_files = {f.filename: f for f in local_db}
for filename, info in remote_files.items():
# Nuevo o modificado
if filename not in local_files or local_files[filename].file_size != info['size']:
logger.info(f"Descargando: {filename}")
file_obj = tree.open_file(filename)
content = b""
for chunk in file_obj:
content += chunk
md5 = get_file_hash(content)
(local_path / filename).write_bytes(content)
# Actualizar BD
sync_record = local_files.get(filename) or SyncedFile()
sync_record.filename = filename
sync_record.md5_hash = md5
sync_record.file_size = info['size']
sync_record.last_modified = datetime.fromtimestamp(info['modified'])
session.merge(sync_record)
session.commit()
# Detectar eliminados
for filename in local_files:
if filename not in remote_files:
logger.warning(f"Archivo eliminado en remoto: {filename}")
(local_path / filename).unlink(missing_ok=True)
session.query(SyncedFile).filter_by(filename=filename).delete()
session.commit()
tree.close()
session.close()
if __name__ == "__main__":
sync_smb_share(Path("/mnt/sync"))
Automatización con cron
0 */4 * * * /usr/bin/python3 /opt/sync_smb/sync.py >> /var/log/smb_sync.log 2>&1
Esto sincroniza cada 4 horas.
Conclusión
Con este setup procesas solo cambios, controlas la autenticación NTLM sin trucos raros, y tienes logs legibles. La BD SQLite es eficiente incluso con miles de archivos.
He usado esto en producción durante meses sin problemas.
Equipamiento recomendado
- Mini PC Intel N100 — Mini PC silencioso y eficiente para servidor doméstico 24/7
- Raspberry Pi 3 B+ — Servidor ligero de bajo consumo para empezar tu homelab
Enlaces de afiliado. Sin coste extra para ti.