|
@@ -114,6 +114,121 @@ def _abort_if_exists(archive_name, backup_dir):
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+# Restore
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+def restore_db_dump(archive_name, backup_dir):
|
|
|
|
|
+ """Restauration d'une base MySQL ou PostgreSQL depuis une archive BackupManager."""
|
|
|
|
|
+ archive_path = os.path.join(backup_dir, archive_name + ".tar")
|
|
|
|
|
+ if not os.path.exists(archive_path):
|
|
|
|
|
+ raise FileNotFoundError(f"Archive introuvable : {archive_path}")
|
|
|
|
|
+
|
|
|
|
|
+ info = _read_backup_info(archive_path)
|
|
|
|
|
+ db_type = info.get("type")
|
|
|
|
|
+ dbname = info.get("database", "")
|
|
|
|
|
+
|
|
|
|
|
+ if not dbname:
|
|
|
|
|
+ raise ValueError("Nom de base de données introuvable dans backup_info.json.")
|
|
|
|
|
+
|
|
|
|
|
+ with tempfile.TemporaryDirectory() as tmpdir:
|
|
|
|
|
+ dump_path = os.path.join(tmpdir, f"{dbname}.sql")
|
|
|
|
|
+
|
|
|
|
|
+ # Extraction du dump depuis l'archive
|
|
|
|
|
+ with tarfile.open(archive_path) as tar:
|
|
|
|
|
+ member = tar.getmember(f"db/{dbname}.sql")
|
|
|
|
|
+ with tar.extractfile(member) as src, open(dump_path, "wb") as dst:
|
|
|
|
|
+ dst.write(src.read())
|
|
|
|
|
+
|
|
|
|
|
+ if db_type == "mysql":
|
|
|
|
|
+ return _restore_mysql(dbname, dump_path)
|
|
|
|
|
+ elif db_type == "postgresql":
|
|
|
|
|
+ return _restore_postgresql(dbname, dump_path)
|
|
|
|
|
+ else:
|
|
|
|
|
+ raise ValueError(f"Type de base inconnu dans l'archive : {db_type}")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _restore_mysql(dbname, dump_path):
|
|
|
|
|
+ log_lines = []
|
|
|
|
|
+
|
|
|
|
|
+ # Suppression + recréation propre de la base
|
|
|
|
|
+ result = subprocess.run(
|
|
|
|
|
+ ["sudo", "mysql", "-e",
|
|
|
|
|
+ f"DROP DATABASE IF EXISTS `{dbname}`; CREATE DATABASE `{dbname}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"],
|
|
|
|
|
+ capture_output=True, text=True, timeout=60,
|
|
|
|
|
+ )
|
|
|
|
|
+ if result.returncode != 0:
|
|
|
|
|
+ raise RuntimeError(f"Impossible de recréer la base MySQL '{dbname}' :\n{result.stderr.strip()}")
|
|
|
|
|
+ log_lines.append(f"Base MySQL '{dbname}' recréée.")
|
|
|
|
|
+
|
|
|
|
|
+ # Restauration du dump
|
|
|
|
|
+ with open(dump_path, "rb") as f:
|
|
|
|
|
+ result = subprocess.run(
|
|
|
|
|
+ ["sudo", "mysql", dbname],
|
|
|
|
|
+ stdin=f,
|
|
|
|
|
+ capture_output=True,
|
|
|
|
|
+ timeout=7200,
|
|
|
|
|
+ )
|
|
|
|
|
+ log = result.stderr.decode("utf-8", errors="replace").strip()
|
|
|
|
|
+ if result.returncode != 0:
|
|
|
|
|
+ raise RuntimeError(f"mysql restore a échoué (code {result.returncode}) :\n{log}")
|
|
|
|
|
+
|
|
|
|
|
+ log_lines.append(f"Dump restauré dans '{dbname}'.")
|
|
|
|
|
+ if log:
|
|
|
|
|
+ log_lines.append(log)
|
|
|
|
|
+ return "\n".join(log_lines)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _restore_postgresql(dbname, dump_path):
|
|
|
|
|
+ log_lines = []
|
|
|
|
|
+
|
|
|
|
|
+ # Terminer les connexions actives puis drop + recreate
|
|
|
|
|
+ subprocess.run(
|
|
|
|
|
+ ["sudo", "-u", "postgres", "psql", "-c",
|
|
|
|
|
+ f"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{dbname}' AND pid <> pg_backend_pid();"],
|
|
|
|
|
+ capture_output=True, timeout=30,
|
|
|
|
|
+ )
|
|
|
|
|
+ subprocess.run(
|
|
|
|
|
+ ["sudo", "-u", "postgres", "dropdb", "--if-exists", dbname],
|
|
|
|
|
+ capture_output=True, timeout=60,
|
|
|
|
|
+ )
|
|
|
|
|
+ result = subprocess.run(
|
|
|
|
|
+ ["sudo", "-u", "postgres", "createdb", dbname],
|
|
|
|
|
+ capture_output=True, text=True, timeout=60,
|
|
|
|
|
+ )
|
|
|
|
|
+ if result.returncode != 0:
|
|
|
|
|
+ raise RuntimeError(f"Impossible de recréer la base PostgreSQL '{dbname}' :\n{result.stderr.strip()}")
|
|
|
|
|
+ log_lines.append(f"Base PostgreSQL '{dbname}' recréée.")
|
|
|
|
|
+
|
|
|
|
|
+ # Restauration du dump
|
|
|
|
|
+ with open(dump_path, "rb") as f:
|
|
|
|
|
+ result = subprocess.run(
|
|
|
|
|
+ ["sudo", "-u", "postgres", "psql", "-d", dbname, "-v", "ON_ERROR_STOP=1"],
|
|
|
|
|
+ stdin=f,
|
|
|
|
|
+ capture_output=True,
|
|
|
|
|
+ timeout=7200,
|
|
|
|
|
+ )
|
|
|
|
|
+ log = result.stderr.decode("utf-8", errors="replace").strip()
|
|
|
|
|
+ if result.returncode != 0:
|
|
|
|
|
+ raise RuntimeError(f"psql restore a échoué (code {result.returncode}) :\n{log}")
|
|
|
|
|
+
|
|
|
|
|
+ log_lines.append(f"Dump restauré dans '{dbname}'.")
|
|
|
|
|
+ if log:
|
|
|
|
|
+ log_lines.append(log)
|
|
|
|
|
+ return "\n".join(log_lines)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _read_backup_info(archive_path):
|
|
|
|
|
+ try:
|
|
|
|
|
+ with tarfile.open(archive_path) as tar:
|
|
|
|
|
+ member = tar.extractfile("backup_info.json")
|
|
|
|
|
+ if member:
|
|
|
|
|
+ return json.loads(member.read())
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+ return {}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _write_tar(tmpdir, dump_path, dbname, archive_name, backup_dir, job, instance, instance_url):
|
|
def _write_tar(tmpdir, dump_path, dbname, archive_name, backup_dir, job, instance, instance_url):
|
|
|
"""Crée le .tar dans backup_dir et le .info.json YunoHost à côté."""
|
|
"""Crée le .tar dans backup_dir et le .info.json YunoHost à côté."""
|
|
|
archive_path = os.path.join(backup_dir, archive_name + ".tar")
|
|
archive_path = os.path.join(backup_dir, archive_name + ".tar")
|