feat: implementar Fase 2 del roadmap (Modo Replica Exacta / Mirroring)

This commit is contained in:
Carlos Tello
2026-08-14 21:50:00 -03:00
parent a72cf1480e
commit a1cafff90e
14 changed files with 421 additions and 14 deletions
+53 -1
View File
@@ -167,6 +167,11 @@ class AgentDaemon:
lj.name = sj.get("name", lj.name)
lj.source_path = sj.get("source_path", lj.source_path)
lj.file_patterns = sj.get("file_patterns", lj.file_patterns)
lj.exclude_patterns = sj.get("exclude_patterns", lj.exclude_patterns)
lj.skip_hidden = sj.get("skip_hidden", lj.skip_hidden)
lj.skip_system = sj.get("skip_system", lj.skip_system)
lj.skip_readonly = sj.get("skip_readonly", lj.skip_readonly)
lj.sync_deletions = sj.get("sync_deletions", lj.sync_deletions)
lj.schedule_cron = sj.get("schedule_cron", lj.schedule_cron)
lj.min_stable_seconds = sj.get("min_stable_time_seconds", lj.min_stable_seconds)
@@ -191,6 +196,11 @@ class AgentDaemon:
name=sj["name"],
source_path=sj["source_path"],
file_patterns=sj["file_patterns"],
exclude_patterns=sj.get("exclude_patterns", ""),
skip_hidden=sj.get("skip_hidden", False),
skip_system=sj.get("skip_system", False),
skip_readonly=sj.get("skip_readonly", False),
sync_deletions=sj.get("sync_deletions", False),
schedule_cron=sj["schedule_cron"],
min_stable_seconds=sj["min_stable_time_seconds"],
last_status=sj.get("status", "En espera")
@@ -209,6 +219,11 @@ class AgentDaemon:
"name": lj.name,
"source_path": lj.source_path,
"file_patterns": lj.file_patterns,
"exclude_patterns": lj.exclude_patterns,
"skip_hidden": lj.skip_hidden,
"skip_system": lj.skip_system,
"skip_readonly": lj.skip_readonly,
"sync_deletions": lj.sync_deletions,
"schedule_cron": lj.schedule_cron,
"min_stable_time_seconds": lj.min_stable_seconds
}
@@ -243,11 +258,26 @@ class AgentDaemon:
logger.warning(f"Source path {source_path} for job {job_name} does not exist. Skipping.")
continue
scanner = DirectoryScanner(source_path, file_patterns, min_stable_seconds=min_stable)
scanner = DirectoryScanner(
source_path=source_path,
file_patterns=file_patterns,
exclude_patterns=getattr(job, "exclude_patterns", ""),
skip_hidden=getattr(job, "skip_hidden", False),
skip_system=getattr(job, "skip_system", False),
skip_readonly=getattr(job, "skip_readonly", False),
min_stable_seconds=min_stable
)
files = scanner.scan()
# Track files successfully backed up in this run
active_relative_paths = []
for filepath in files:
try:
rel_p = str(filepath.relative_to(Path(source_path).resolve())).replace("\\", "/")
active_relative_paths.append(rel_p)
except Exception:
pass
if not is_file_stable(filepath, min_stable_seconds=min_stable):
logger.warning(f"File {filepath.name} is currently locked or growing. Skipping.")
continue
@@ -270,6 +300,7 @@ class AgentDaemon:
res = self.uploader.upload_file(
filepath,
job_id=job.job_id,
source_path=source_path,
progress_callback=on_chunk_progress
)
logger.info(f"Successfully backed up {filepath.name}!")
@@ -284,6 +315,27 @@ class AgentDaemon:
if self.on_error:
self.on_error(filepath.name, str(ex))
# Enforce mirror replica mode (purge orphans on server)
if job.sync_deletions and job.job_id is not None:
try:
base_url = self.config.server_url.rstrip("/")
purge_payload = {
"active_relative_paths": active_relative_paths
}
resp = httpx.post(
f"{base_url}/api/jobs/{job.job_id}/purge-orphans",
headers=self._get_headers(),
json=purge_payload,
timeout=30.0
)
if resp.status_code == 200:
purge_res = resp.json()
purged_count = purge_res.get("purged_count", 0)
if purged_count > 0:
logger.info(f"Purged {purged_count} orphan files from server for job '{job_name}'")
except Exception as e:
logger.error(f"Error purging orphan files from server: {e}")
# Update job state in config after checking directory
job.last_backup_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
job.last_status = "Backup Exitoso" if job.last_status != "Error" else "Error"