|
|
@@ -2,6 +2,7 @@ import json
|
|
|
import os
|
|
|
import shlex
|
|
|
import subprocess
|
|
|
+import threading
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
|
|
from flask import Blueprint, current_app, flash, redirect, render_template, request, url_for
|
|
|
@@ -135,6 +136,63 @@ def _collect_instance(instance):
|
|
|
return [{"name": a["name"], "size_bytes": a.get("size_bytes", 0)} for a in raw]
|
|
|
|
|
|
|
|
|
+def _fetch_ssh_file(destination, data_dir, remote_filename):
|
|
|
+ remote_path = shlex.quote(f"{destination.remote_path}/{remote_filename}")
|
|
|
+ result = subprocess.run(
|
|
|
+ _ssh_base(destination, data_dir) + [f"cat {remote_path}"],
|
|
|
+ capture_output=True, timeout=3600,
|
|
|
+ )
|
|
|
+ if result.returncode != 0:
|
|
|
+ raise RuntimeError(result.stderr.decode(errors="replace").strip() or "Connexion SSH échouée")
|
|
|
+ return result.stdout
|
|
|
+
|
|
|
+
|
|
|
+def _do_pull(app, loc_type, archive_name, dest_id=None, instance_id=None):
|
|
|
+ """Rapatrie une archive nommée (SSH ou instance) vers le backup_dir local."""
|
|
|
+ with app.app_context():
|
|
|
+ backup_dir = app.config["YUNOHOST_BACKUP_DIR"]
|
|
|
+ data_dir = app.config["DATA_DIR"]
|
|
|
+ try:
|
|
|
+ if loc_type == "ssh":
|
|
|
+ dest = db.session.get(Destination, dest_id)
|
|
|
+ label = dest.host
|
|
|
+ tar_bytes = _fetch_ssh_file(dest, data_dir, archive_name + ".tar")
|
|
|
+ try:
|
|
|
+ info_bytes = _fetch_ssh_file(dest, data_dir, archive_name + ".info.json")
|
|
|
+ except Exception:
|
|
|
+ info_bytes = None
|
|
|
+
|
|
|
+ elif loc_type == "instance":
|
|
|
+ inst = db.session.get(RemoteInstance, instance_id)
|
|
|
+ label = inst.name
|
|
|
+ from federation.client import FederationClient
|
|
|
+ client = FederationClient(inst)
|
|
|
+ tar_bytes = client.download_archive(archive_name)
|
|
|
+ info_bytes = client.download_info_json(archive_name)
|
|
|
+
|
|
|
+ else:
|
|
|
+ return
|
|
|
+
|
|
|
+ tmp_tar = f"/tmp/backupmanager_pull_{archive_name}.tar"
|
|
|
+ with open(tmp_tar, "wb") as f:
|
|
|
+ f.write(tar_bytes)
|
|
|
+ subprocess.run(["sudo", "rsync", tmp_tar,
|
|
|
+ os.path.join(backup_dir, archive_name + ".tar")], check=True)
|
|
|
+ os.unlink(tmp_tar)
|
|
|
+
|
|
|
+ if info_bytes:
|
|
|
+ tmp_info = f"/tmp/backupmanager_pull_{archive_name}.info.json"
|
|
|
+ with open(tmp_info, "wb") as f:
|
|
|
+ f.write(info_bytes)
|
|
|
+ subprocess.run(["sudo", "rsync", tmp_info,
|
|
|
+ os.path.join(backup_dir, archive_name + ".info.json")], check=True)
|
|
|
+ os.unlink(tmp_info)
|
|
|
+
|
|
|
+ app.logger.info(f"Pull {archive_name} ← {label} OK")
|
|
|
+ except Exception as exc:
|
|
|
+ app.logger.error(f"Pull {archive_name} échoué : {exc}")
|
|
|
+
|
|
|
+
|
|
|
# ---------------------------------------------------------------------------
|
|
|
# Routes
|
|
|
# ---------------------------------------------------------------------------
|
|
|
@@ -253,6 +311,19 @@ def overview_delete():
|
|
|
return redirect(url_for("overview.overview"))
|
|
|
|
|
|
|
|
|
+@bp.route("/overview/pull", methods=["POST"])
|
|
|
+def overview_pull():
|
|
|
+ name = request.form["name"]
|
|
|
+ loc_type = request.form["loc_type"]
|
|
|
+ dest_id = request.form.get("dest_id", type=int)
|
|
|
+ instance_id = request.form.get("instance_id", type=int)
|
|
|
+
|
|
|
+ app = current_app._get_current_object()
|
|
|
+ threading.Thread(target=_do_pull, args=(app, loc_type, name, dest_id, instance_id), daemon=True).start()
|
|
|
+ flash(f"Rapatriement de « {name} » démarré en arrière-plan.", "success")
|
|
|
+ return redirect(url_for("overview.overview"))
|
|
|
+
|
|
|
+
|
|
|
@bp.route("/overview/retention", methods=["POST"])
|
|
|
def overview_retention():
|
|
|
job_id = int(request.form["job_id"])
|