Le problème#
J’avais récemment besoin de synchroniser un partage SMB depuis un NAS avec mon serveur Linux. La solution évidente serait smbclient ou mount -t cifs, mais je voulais :
- Synchronisation incrémentale (uniquement les fichiers nouveaux ou modifiés)
- Détecter les fichiers supprimés du partage
- Contrôler l’authentification NTLM directement depuis le code
- Réduire la quantité obscène de logs que génère
smbprotocol
La librairie smbprotocol de Python résolvait tout cela, mais il n’y a pas de documentation sur comment bien le faire. Voici ma solution.
Configuration initiale#
Installe les dépendances :
pip install smbprotocol sqlalchemy pydantic python-dotenvL’idée de base : maintenir une base de données SQLite avec un enregistrement de tous les fichiers synchronisés (nom, hash MD5, timestamp). À chaque exécution, on compare le partage actuel avec la base de données et on traite uniquement les changements.
Réduire les logs de smbprotocol#
C’est critique. Sans le contrôler, la librairie remplit ta console de messages de débogage :
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)Cela réduit les logs à un niveau raisonnable. Sans cela, chaque opération génère 50 lignes de charabia.
Structure de la base de données 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)Connexion avec 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 est négocié automatiquement. Tu n’as rien de spécial à faire, mais assure-toi d’utiliser le format DOMINIO\usuario correct.
Synchronisation incrémentale#
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"))Automatisation avec cron#
0 */4 * * * /usr/bin/python3 /opt/sync_smb/sync.py >> /var/log/smb_sync.log 2>&1Cela synchronise toutes les 4 heures.
Conclusion#
Avec cette configuration, tu traites uniquement les changements, tu contrôles l’authentification NTLM sans trucs louches, et tu as des logs lisibles. La base de données SQLite est efficace même avec des milliers de fichiers.
J’ai utilisé ce setup en production pendant des mois sans problèmes.
Équipement recommandé#
- Mini PC Intel N100 — Mini PC silencieux et efficace pour serveur personnel 24/7
- Raspberry Pi 3 B+ — Serveur léger à faible consommation pour débuter ton homelab
Liens d’affiliation. Aucun coût supplémentaire pour toi.