Watch at EaseHistorias. Respuestas. Perspectiva.

Tecnología · GUÍA PRÁCTICA

¿Demasiados archivos en conflicto de Syncthing? Audita cada copia antes de borrar o fusionar

Una auditoría de Python de solo lectura empareja cada copia .sync-conflict documentada con su archivo base probable, calcula ambos SHA-256 y la clasifica como idéntica, diferente o sin base. No borra, renombra ni fusiona nada.

Ilustración editorial original de dos ordenadores con una carpeta pausada entre un archivo base y una copia en conflicto.
Ilustración original: pausa la sincronización y crea un respaldo independiente antes de decidir sobre una copia en conflicto. Watch at Ease

Syncthing conserva una copia en conflicto cuando detecta cambios simultáneos que no puede conciliar con seguridad. La fecha, la hora y el ID de dispositivo del nombre son pistas, pero no demuestran qué contenido debe conservarse. Antes de borrar o renombrar, pausa la carpeta en todos los dispositivos accesibles, deja de editar y crea una copia independiente fuera del árbol sincronizado; después prepara el inventario de solo lectura.

Qué puede y qué no puede decir el nombre del conflicto

La documentación actual muestra nombres como `report.sync-conflict-20261011-102030-DEVICE.docx`. La hora y el sufijo ayudan a identificar la copia, pero Syncthing no entiende el contenido de una hoja de cálculo, base de datos, foto o nota. Una hora más reciente no equivale a contenido correcto; además, los conflictos pueden propagarse a otros dispositivos. Borrar deprisa en un solo equipo no sustituye la revisión del conjunto.

La primera medida segura ocurre fuera de Syncthing: deja de editar, pausa la carpeta en todos los dispositivos accesibles y copia los datos fuera del árbol sincronizado. La auditoría inferior solo lee bytes de archivos; no consulta ni modifica la base de datos de Syncthing.

Usa tres estados en lugar de un botón de borrar

Cada copia se marca como `identical`, `different` o `base-missing`. `identical` significa que el SHA-256 coincide con el del archivo base probable; `different`, que ambos archivos normales existen pero sus bytes difieren; `base-missing`, que el archivo esperado sin sufijo no existe como archivo normal. Son etiquetas de evidencia, no órdenes automáticas.

Incluso una pareja idéntica exige verificar la ruta relativa y conservar una copia. Una pareja diferente necesita comparación con la aplicación que creó el formato. Si falta la base, el conflicto puede ser la única copia restante y nunca debe tratarse como basura.

Ejecuta la auditoría de solo lectura probada

Guarda lo siguiente como `syncthing_conflict_audit.py`. Solo usa la biblioteca estándar de Python, omite enlaces simbólicos, no sigue directorios enlazados y puede escribir el manifiesto fuera del árbol sincronizado. ```python #!/usr/bin/env python3 """Inventory Syncthing conflict copies without changing any files. The tool pairs filenames of the form ``name.sync-conflict-YYYYMMDD-HHMMSS-DEVICE.ext`` with their likely base file, hashes both when possible, and emits a CSV or JSON manifest. """ from __future__ import annotations import argparse import csv import hashlib import json import os import re import sys from dataclasses import asdict, dataclass from pathlib import Path from typing import Iterable, TextIO CONFLICT_RE = re.compile( r"^(?P<stem>.+)\.sync-conflict-" r"(?P<date>\d{8})-(?P<time>\d{6})-" r"(?P<device>[^./\\]+)(?P<suffix>\.[^/\\]+)?$" ) @dataclass(frozen=True) class AuditRow: conflict_path: str base_path: str status: str conflict_sha256: str base_sha256: str conflict_bytes: int base_bytes: int | None conflict_timestamp: str modified_by: str def parse_conflict_name(name: str) -> tuple[str, str, str] | None: """Return (base filename, compact timestamp, device ID), if recognized.""" match = CONFLICT_RE.match(name) if not match: return None suffix = match.group("suffix") or "" base_name = f"{match.group('stem')}{suffix}" timestamp = f"{match.group('date')}T{match.group('time')}" return base_name, timestamp, match.group("device") def sha256_file(path: Path, chunk_size: int = 1024 * 1024) -> str: digest = hashlib.sha256() with path.open("rb") as handle: while chunk := handle.read(chunk_size): digest.update(chunk) return digest.hexdigest() def find_conflicts(root: Path) -> Iterable[Path]: """Yield regular conflict files in deterministic order; do not follow links.""" found: list[Path] = [] for directory, dirnames, filenames in os.walk(root, followlinks=False): dirnames.sort() for filename in sorted(filenames): candidate = Path(directory, filename) if parse_conflict_name(filename) and candidate.is_file() and not candidate.is_symlink(): found.append(candidate) yield from sorted(found) def audit(root: Path) -> list[AuditRow]: root = root.resolve() rows: list[AuditRow] = [] for conflict in find_conflicts(root): parsed = parse_conflict_name(conflict.name) assert parsed is not None base_name, timestamp, device = parsed base = conflict.with_name(base_name) conflict_hash = sha256_file(conflict) conflict_size = conflict.stat().st_size if base.is_file() and not base.is_symlink(): base_hash = sha256_file(base) base_size: int | None = base.stat().st_size status = "identical" if conflict_hash == base_hash else "different" else: base_hash = "" base_size = None status = "base-missing" rows.append( AuditRow( conflict_path=str(conflict.relative_to(root)), base_path=str(base.relative_to(root)), status=status, conflict_sha256=conflict_hash, base_sha256=base_hash, conflict_bytes=conflict_size, base_bytes=base_size, conflict_timestamp=timestamp, modified_by=device, ) ) return rows def write_json(rows: list[AuditRow], output: TextIO) -> None: json.dump([asdict(row) for row in rows], output, ensure_ascii=False, indent=2) output.write("\n") def write_csv(rows: list[AuditRow], output: TextIO) -> None: fieldnames = list(AuditRow.__dataclass_fields__) writer = csv.DictWriter(output, fieldnames=fieldnames) writer.writeheader() writer.writerows(asdict(row) for row in rows) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Create a read-only manifest of Syncthing .sync-conflict files." ) parser.add_argument("root", type=Path, help="Folder to scan recursively") parser.add_argument("--format", choices=("csv", "json"), default="csv") parser.add_argument( "--output", type=Path, help="Manifest path; omit to write to standard output", ) return parser def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) if not args.root.is_dir(): print(f"error: not a directory: {args.root}", file=sys.stderr) return 2 rows = audit(args.root) output: TextIO if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) output = args.output.open("w", encoding="utf-8", newline="") else: output = sys.stdout try: if args.format == "json": write_json(rows, output) else: write_csv(rows, output) finally: if args.output: output.close() print( f"Scanned {args.root}: {len(rows)} conflict file(s); no files changed.", file=sys.stderr, ) return 0 if __name__ == "__main__": raise SystemExit(main()) ```

En PowerShell o una terminal ejecuta `python syncthing_conflict_audit.py "/ruta/carpeta" --output "/lugar-seguro/syncthing-conflicts.csv"`. Para JSON añade `--format json`. No coloques el manifiesto dentro de la carpeta analizada porque se convertiría en otro cambio sincronizado.

Lee el manifiesto sin sacar conclusiones excesivas

Ordena por `status` y revisa primero `base-missing`. Copia esos conflictos al respaldo independiente antes de abrirlos. Para `different`, compara base y conflicto en la aplicación de origen; el texto admite una herramienta diff, mientras documentos de oficina, catálogos fotográficos, bases de notas y archivos comprimidos requieren inspección o exportación específica. Conserva ambos hasta verificar el resultado.

En `identical`, un SHA-256 igual solo demuestra que los bytes eran iguales al auditar. No autoriza el borrado. Confirma rutas, cierra aplicaciones que puedan reescribir y resuelve lotes pequeños con la sincronización aún pausada.

El sufijo y la hora son pistas, no ganadores

El nombre documentado identifica cuándo creó Syncthing la copia y qué cambio de dispositivo se movió a ese nombre. Desajustes de reloj, formas de guardado y copias manuales posteriores pueden volver engañosa la hora. Separa la comparación de contenido de los metadatos y revisa también el historial interno si el formato lo ofrece.

Si los conflictos reaparecen, investiga ediciones simultáneas, aplicaciones que reescriben archivos abiertos, relojes de dispositivos y acceso concurrente a archivos tipo base de datos. Corregir el desencadenante importa más que borrar repetidamente el síntoma.

Usa versionado en el futuro, con una limitación

El versionado de Syncthing se configura por carpeta y dispositivo. La documentación oficial dice que archiva cambios recibidos de otros dispositivos, pero no cambios locales realizados en el mismo equipo. Añade una capa de recuperación, pero no es una copia de seguridad completa ni sustituye el historial de la aplicación o un respaldo independiente.

Tras resolver los archivos y corregir la causa, reactiva un dispositivo, confirma el conjunto esperado y reincorpora los demás gradualmente. Guarda el manifiesto con el respaldo como registro de rutas y hashes revisados.

Ilustración editorial original de dos archivos cuyos hashes llegan a un manifiesto de tres filas mientras la papelera permanece cerrada.
Ilustración original: la auditoría registra estados idéntico, diferente y sin base, pero no borra archivos. Watch at Ease