feature/generador-instaladores-linux-windows
This commit is contained in:
@@ -28,6 +28,8 @@ class Job:
|
||||
extract_ms: Optional[int]
|
||||
restore_ms: Optional[int]
|
||||
filelist_ms: Optional[int]
|
||||
# Fecha en que la retención borró del disco el archivo de este job (None = aún en disco).
|
||||
purged_at: Optional[str] = None
|
||||
|
||||
|
||||
class JobRepository:
|
||||
@@ -185,14 +187,109 @@ class JobRepository:
|
||||
"""
|
||||
db.execute("DELETE FROM jobs WHERE job_id = ?", (job_id,))
|
||||
|
||||
@staticmethod
|
||||
def get_obsolete_completed_by_node(days: int = 2) -> List[Job]:
|
||||
"""Jobs 'completed' obsoletos por nodo, candidatos a que se borre su archivo del disco.
|
||||
|
||||
Para cada node_name (no NULL) toma su restauración más reciente (MAX(finished_at))
|
||||
como referencia y devuelve los jobs completados de ese nodo con finished_at anterior a
|
||||
(referencia − days). Como la referencia es el máximo, la comparación estricta '<' NUNCA
|
||||
incluye la restauración más reciente; los nodos con una sola restauración quedan fuera.
|
||||
Se excluyen node_name NULL y los ya purgados (purged_at no nulo).
|
||||
|
||||
Args:
|
||||
days: días de antigüedad respecto al más reciente de cada nodo.
|
||||
|
||||
Returns:
|
||||
Lista de jobs obsoletos ordenada por nodo y fecha ascendente.
|
||||
"""
|
||||
modifier = f"-{int(days)} days"
|
||||
rows = db.fetchall(
|
||||
"""
|
||||
WITH refs AS (
|
||||
SELECT node_name, MAX(finished_at) AS ref_finished_at
|
||||
FROM jobs
|
||||
WHERE status = ? AND finished_at IS NOT NULL AND node_name IS NOT NULL
|
||||
GROUP BY node_name
|
||||
)
|
||||
SELECT j.* FROM jobs j
|
||||
JOIN refs r ON r.node_name = j.node_name
|
||||
WHERE j.status = ?
|
||||
AND j.finished_at IS NOT NULL
|
||||
AND j.purged_at IS NULL
|
||||
AND datetime(j.finished_at) < datetime(r.ref_finished_at, ?)
|
||||
ORDER BY j.node_name, j.finished_at
|
||||
""",
|
||||
(JobStatus.COMPLETED, JobStatus.COMPLETED, modifier),
|
||||
)
|
||||
return [Job(**dict(row)) for row in rows]
|
||||
|
||||
@staticmethod
|
||||
def get_latest_completed_per_node() -> dict:
|
||||
"""node_name -> finished_at (MAX) de las restauraciones completadas de cada nodo.
|
||||
|
||||
Sirve como salvaguarda de la retención: la carpeta-fecha de esta referencia (la más
|
||||
reciente de cada nodo) nunca debe tocarse.
|
||||
"""
|
||||
rows = db.fetchall(
|
||||
"""
|
||||
SELECT node_name, MAX(finished_at) AS ref_finished_at
|
||||
FROM jobs
|
||||
WHERE status = ? AND finished_at IS NOT NULL AND node_name IS NOT NULL
|
||||
GROUP BY node_name
|
||||
""",
|
||||
(JobStatus.COMPLETED,),
|
||||
)
|
||||
return {row["node_name"]: row["ref_finished_at"] for row in rows}
|
||||
|
||||
@staticmethod
|
||||
def mark_purged(job_id: str) -> None:
|
||||
"""Marca que la retención ya borró del disco el archivo de este job.
|
||||
|
||||
No borra la fila: conserva el historial, las estadísticas y la vista de la UI; solo
|
||||
evita que la retención vuelva a intentar borrar un archivo que ya no existe.
|
||||
"""
|
||||
now = datetime.utcnow().isoformat()
|
||||
db.execute(
|
||||
"UPDATE jobs SET purged_at = ?, updated_at = ? WHERE job_id = ?",
|
||||
(now, now, job_id),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def exists_by_hash(source_hash: str) -> bool:
|
||||
"""Verifica si existe un job con el hash dado."""
|
||||
"""Verifica si existe un job con el hash dado (cualquier estado)."""
|
||||
row = db.fetchone(
|
||||
"SELECT COUNT(*) as count FROM jobs WHERE source_hash = ?",
|
||||
(source_hash,)
|
||||
)
|
||||
return row["count"] > 0
|
||||
|
||||
@staticmethod
|
||||
def has_blocking_job_by_hash(source_hash: str) -> bool:
|
||||
"""True si hay un job con ese hash EXITOSO o EN CURSO (dedup real).
|
||||
|
||||
Los estados terminales fallidos (failed/failed_restart/cancelled) NO bloquean: así,
|
||||
un fallo transitorio (p.ej. 'database is locked') deja de impedir para siempre el
|
||||
reproceso del archivo si reaparece.
|
||||
"""
|
||||
row = db.fetchone(
|
||||
"SELECT COUNT(*) as count FROM jobs "
|
||||
"WHERE source_hash = ? AND status NOT IN (?, ?, ?)",
|
||||
(source_hash, JobStatus.FAILED, JobStatus.FAILED_RESTART, JobStatus.CANCELLED),
|
||||
)
|
||||
return row["count"] > 0
|
||||
|
||||
@staticmethod
|
||||
def delete_failed_by_hash(source_hash: str) -> int:
|
||||
"""Elimina jobs terminales fallidos/cancelados con ese hash (permite un reintento fresco)."""
|
||||
cursor = db.execute(
|
||||
"DELETE FROM jobs WHERE source_hash = ? AND status IN (?, ?, ?)",
|
||||
(source_hash, JobStatus.FAILED, JobStatus.FAILED_RESTART, JobStatus.CANCELLED),
|
||||
)
|
||||
try:
|
||||
return cursor.rowcount if cursor else 0
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def get_stats() -> dict:
|
||||
|
||||
Reference in New Issue
Block a user