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:
12
README.md
12
README.md
@@ -332,6 +332,18 @@ diálogo en un PowerShell aparte para no bloquear el servidor y devuelve la ruta
|
||||
- Interfaz: los botones ya no vuelven a su etiqueta vieja pisando lo que la acción acaba de calcular, y las
|
||||
rutas de Windows se recortan bien (soltar un archivo registraba el archivo como carpeta de cliente).
|
||||
|
||||
1.8.1, fase 2 de la auditoría:
|
||||
- **Dos movimientos que acabarían en el mismo archivo se detectan antes de aplicar** (`Plan.marcar_colisiones`).
|
||||
Caso real: dos carpetas de grabación genéricas del mismo mes van las dos a `00_Grabacion/SONY` y las dos
|
||||
traen un `C0001.MP4`. Ahora salen sin marcar, en rojo, con un aviso que nombra las dos carpetas y el archivo
|
||||
que choca. También se comprueba si el destino ya tiene un archivo con ese nombre en el disco (antes solo se
|
||||
miraba en las filas de carpeta, no en los grupos de archivos); se revisan los 40 primeros de cada grupo para
|
||||
no llenar la red de consultas.
|
||||
- **Aplicar por pasos ya no degrada el inventario.** `actualizar_inventario` reescribe las filas de `carpetas`
|
||||
con su ruta nueva en vez de borrarlas, y conserva la marca de basura/proxy heredada. Antes, como la interfaz
|
||||
recalcula el plan sin volver a escanear, los pasos siguientes salían con «0 archivos · 0 B» y sin fecha.
|
||||
Comprobado: tras aplicar el paso de fechas, la fila del paso 3 conserva sus 3 archivos y su peso.
|
||||
|
||||
Probado el 9 sep 2026 sobre una copia de ensayo: plan → aplicar → deshacer → volver a aplicar deja el árbol
|
||||
exacto; conversión ffmpeg verificada y reemplazo con original a `_Para borrar`. El puente de Resolve conectó
|
||||
con Resolve Studio 21.1 pero no pudo abrir proyecto porque Resolve estaba en el Gestor de proyectos:
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"nombre": "OrganiLAB",
|
||||
"version": "1.8.0",
|
||||
"builtAt": "2026-09-16T15:38:23.000Z",
|
||||
"archivo": "OrganizadorLabstream-portatil-1.8.0.zip",
|
||||
"size": 198682459,
|
||||
"sha256": "7975c51317de5c63aa8271a11434d183c783893a234718d24151264638fee751",
|
||||
"version": "1.8.1",
|
||||
"builtAt": "2026-09-16T15:47:26.000Z",
|
||||
"archivo": "OrganizadorLabstream-portatil-1.8.1.zip",
|
||||
"size": 198686206,
|
||||
"sha256": "f001ba76c553d379f73d7928314d2f187788a8060e3730978b5a67174f2e660f",
|
||||
"app": {
|
||||
"archivo": "OrganizadorLabstream-app-1.8.0.zip",
|
||||
"size": 14862710,
|
||||
"sha256": "e8ab00045256795e182563241c7f62a9582776d6f7effc866d3eab2013b7eb8b",
|
||||
"url": "/api/herramientas/OrganizadorLabstream-app-1.8.0.zip"
|
||||
"archivo": "OrganizadorLabstream-app-1.8.1.zip",
|
||||
"size": 14866457,
|
||||
"sha256": "b3ec0d410e06102e928ed459bfa0a0f1c6e7713c6a807b18357fc7ef9fca49c7",
|
||||
"url": "/api/herramientas/OrganizadorLabstream-app-1.8.1.zip"
|
||||
},
|
||||
"notes": "",
|
||||
"firma": {
|
||||
"alg": "rsa2048-pkcs1v15-sha256",
|
||||
"valor": "41a83ab9e9918fe7d16ca9559a7524e1c88685223196c3c85b2f32ea0f71da819d8ae3b099c68fbbabc3e276782a92dcf8d9ea42facaf3ae594f3062297cec7770e2dc885f15ba083dc28e7fd5422ed4abd11ead1ebae7ef7c69a34018c88d8ac5e4bcb290c19ee789261001663234b7ae9d3372a7951947dbfe367cebb3a323f0688a8a177f07cc8827e8d293b8ac014a4f4348cabead577aa02d41d64a6f96af50c2cc70de6b809a3a4bda2dc13d6ee07c64924e19d02d5100600cdc9be5a273a121757a111c5ff20229f090306a39494e280679913d347843ba5193ab36d4f12b992f352e578815e1e535fc542f032ade94d16922bab8fc8f63c85bc760ba"
|
||||
"valor": "b20e039a9f2a3234ab82a41f69e46583128e671b16ae5ea0bf73356ec9bbc34289e8418737c81d459c20a2cc7c0819c74a066680a83d0f9b88e4a5f5c3db0ff7a3c2d3376ccee53755e0600a063ee951f63048e1cadc5afeaba6e4895a450791e247dc888e3e3a510a36268c7ae4111c20521d8eb41677b0e482de857bbcb165963c0e8d208c076c19e6e75766967eddd62d5a2293217f88e54986c7b4062a75ee3fc2fe153680ca875e9a85d074b2845fe480b9bb32d9f6cfceecbb850bbfc11c81caf81ded6bb7063b88861263ba8d392911355cbeb4f772058754f26823b577885585ae0de6635515a325cfe4404d905ef38a4ff9b0100bd2024195833b92"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
Referencia en una nueva incidencia
Block a user