El 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-dotenvLa 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>&1Esto 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.