1.8.1: fase 2 de la auditoría

- Plan.marcar_colisiones: dos movimientos que acabarían en el mismo archivo se
  detectan antes de aplicar, se dejan sin marcar y se explican.
- agregar(): los grupos de archivos también comprueban si el destino ya tiene
  un archivo con ese nombre (40 primeros, para no saturar la red).
- actualizar_inventario: las filas de carpetas se reescriben con su ruta nueva
  en vez de borrarse, y se conserva la marca de basura/proxy heredada, para que
  aplicar por pasos no deje los siguientes con 0 archivos y sin fecha.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-09-16 10:49:36 -05:00
padre b977fe1b3e
commit b492881123
Se han modificado 3 ficheros con 100 adiciones y 16 borrados

Ver fichero

@@ -34,7 +34,7 @@ import actualizador
asistente.O = sys.modules[__name__]
actualizador.O = sys.modules[__name__]
VERSION = "1.8.0"
VERSION = "1.8.1"
CONGELADO = getattr(sys, "frozen", False)
SIN_VENTANA = getattr(subprocess, "CREATE_NO_WINDOW", 0) # ffmpeg/ffprobe/net sin abrir consolas negras # empaquetado como .exe con PyInstaller
@@ -618,7 +618,8 @@ class Plan:
return r
return None
def agregar(self, origen, destino, cubeta, accion, motivo, confianza, archivos=None, n=None, bytes_=None, marcado=None):
def agregar(self, origen, destino, cubeta, accion, motivo, confianza, archivos=None, n=None, bytes_=None,
marcado=None, kw_destinos=None):
destino = os.path.normpath(destino)
if archivos is None and os.path.normpath(origen).lower() == destino.lower():
return
@@ -630,7 +631,19 @@ class Plan:
fila = self.con.execute("SELECT n, bytes FROM carpetas WHERE ruta = ?", (os.path.normpath(origen),)).fetchone()
n, bytes_ = (fila["n"], fila["bytes"]) if fila else (0, 0)
aviso_colision = ""
if archivos is None and accion != "renombrar" and os.path.isfile(destino):
if archivos is not None and accion != "renombrar":
# también para los grupos de archivos: si el destino ya tiene un archivo con ese nombre,
# mover chocaría. Antes solo se comprobaba en las filas de carpeta.
especiales = kw_destinos or {}
# solo los primeros: cada comprobación es una consulta al disco de red, y para avisar
# basta con encontrar uno
choques = [os.path.basename(a[0]) for a in archivos[:40]
if os.path.isfile(especiales.get(a[0]) or os.path.join(destino, os.path.basename(a[0])))]
if choques:
aviso_colision = (f" · OJO: en el destino ya hay archivos con el mismo nombre "
f"(«{choques[0]}»): esos no se moverían")
confianza, marcado = "baja", False
elif archivos is None and accion != "renombrar" and os.path.isfile(destino):
aviso_colision = " · ya existe un archivo con ese nombre en el destino: no se moverá"
confianza, marcado = "baja", False
elif archivos is None and os.path.isdir(destino) and os.path.isdir(origen):
@@ -675,10 +688,46 @@ class Plan:
# primero las filas de archivos sueltos (rutas exactas), luego carpetas de más profunda a menos,
# para que un renombrado de mes no deje rutas viejas en filas que aún no se aplicaron
self.filas.sort(key=lambda f: (0 if f["archivos"] else 1, -len(os.path.normpath(f["origen"]).split(os.sep)), f["origen"].lower()))
self.marcar_colisiones()
for i, f in enumerate(self.filas, 1):
f["id"] = i
return {"raiz": self.raiz, "cliente": self.cliente, "tipo": self.tipo, "filas": self.filas, "avisos": self.avisos}
def marcar_colisiones(self):
"""Dos movimientos del mismo plan que acaban en el mismo archivo. La comprobación de `agregar`
solo mira el disco tal como está ahora, así que no ve lo que van a hacer las demás filas.
Caso típico: «Grabacion mañana» y «Grabacion tarde» del mismo mes van las dos a
00_Grabacion/SONY, y las dos tarjetas traen un C0001.MP4. Antes chocaban al aplicar."""
destinos = {} # archivo destino (en minúsculas) -> primera fila que lo reclama
carpetas = {} # carpeta destino -> primera fila que la reclama (solo renombrados)
for f in self.filas:
if f["archivos"]:
especiales = f.get("destinos_archivo") or {}
for a in f["archivos"]:
d = (especiales.get(a) or os.path.join(f["destino"], os.path.basename(a))).lower()
otra = destinos.get(d)
if otra is not None and otra is not f:
self.avisar_colision(f, otra, os.path.basename(d))
break
destinos.setdefault(d, f)
elif f["accion"] == "renombrar":
d = f["destino"].lower()
otra = carpetas.get(d)
if otra is not None:
self.avisar_colision(f, otra, os.path.basename(f["destino"]))
else:
carpetas[d] = f
def avisar_colision(self, fila, otra, que):
aviso = f" · OJO: «{otra['origen_rel']}» también va a «{fila['destino_rel']}» y las dos traen «{que}»"
if aviso not in fila["motivo"]:
fila["motivo"] += aviso
fila["confianza"] = "baja"
fila["marcado"] = False
self.avisos.append(f"«{fila['origen_rel']}» y «{otra['origen_rel']}» acabarían las dos en "
f"«{fila['destino_rel']}» con archivos del mismo nombre ({que}). Se dejan sin marcar: "
f"cámbiale el destino a una de las dos, o aplícalas por separado.")
def entradas(self, ruta):
try:
with os.scandir(ruta) as it:
@@ -1171,20 +1220,43 @@ def aplicar(raiz, filas):
def actualizar_inventario(movidos):
"""refleja en la base de datos los movimientos hechos, sin re-escanear"""
"""Refleja en la base de datos los movimientos hechos, sin re-escanear.
Las filas de `carpetas` se reescriben con su ruta nueva en vez de borrarse: la interfaz recalcula el
plan tras cada paso SIN volver a escanear, y si el subárbol movido desaparecía del inventario, los
pasos siguientes salían con «0 archivos · 0 B» y sin fecha. La marca de basura/proxy heredada de una
carpeta (por ejemplo lo que cuelga de «Adobe Premiere Pro Auto-Save») también se conserva: antes se
recalculaba desde cero y se perdía."""
con = db()
marcas = [os.sep + p + os.sep for p in R.PAPELERAS]
with con:
for de, a in movidos:
en_papelera = 1 if any(m in a for m in marcas) else 0
con.execute("UPDATE archivos SET ruta = ?, dir = ?, nombre = ?, es_basura = CASE WHEN ? = 1 THEN 1 WHEN rol = 'basura' THEN 1 ELSE 0 END WHERE ruta = ?",
con.execute("UPDATE archivos SET ruta = ?, dir = ?, nombre = ?, "
"es_basura = CASE WHEN ? = 1 OR es_basura = 1 OR rol = 'basura' THEN 1 ELSE 0 END WHERE ruta = ?",
(a, os.path.dirname(a), os.path.basename(a), en_papelera, de))
pref = de + os.sep
for r in con.execute("SELECT ruta FROM archivos WHERE ruta LIKE ? ESCAPE '!'", (esc_like(pref) + "%",)).fetchall():
nueva = a + r["ruta"][len(de):]
con.execute("UPDATE archivos SET ruta = ?, dir = ?, es_basura = CASE WHEN ? = 1 THEN 1 WHEN rol = 'basura' THEN 1 ELSE 0 END WHERE ruta = ?",
con.execute("UPDATE archivos SET ruta = ?, dir = ?, "
"es_basura = CASE WHEN ? = 1 OR es_basura = 1 OR rol = 'basura' THEN 1 ELSE 0 END WHERE ruta = ?",
(nueva, os.path.dirname(nueva), en_papelera, r["ruta"]))
# las carpetas siguen al material: se mueven de ruta, no se borran
viejas = con.execute("SELECT * FROM carpetas WHERE ruta = ? OR ruta LIKE ? ESCAPE '!'",
(de, esc_like(pref) + "%")).fetchall()
con.execute("DELETE FROM carpetas WHERE ruta = ? OR ruta LIKE ? ESCAPE '!'", (de, esc_like(pref) + "%"))
if os.path.isdir(a):
for v in viejas:
nueva = a if v["ruta"] == de else a + v["ruta"][len(de):]
fila = dict(v)
fila["ruta"] = nueva
fila["nombre"] = os.path.basename(nueva)
if en_papelera:
fila["es_basura"] = 1
con.execute("INSERT OR REPLACE INTO carpetas VALUES (?,?,?,?,?,?,?,?,?,?,?)",
(fila["ruta"], fila["raiz"], fila["nombre"], fila["n"], fila["bytes"],
fila["anio_moda"], fila["mes_moda"], fila["frac_moda"], fila["dias_rango"],
fila["es_proxy"], fila["es_basura"]))
con.close()