|
|
@@ -43,7 +43,7 @@ logging.basicConfig(
|
|
|
|
|
|
# --- Extensions --------------------------------------------------------------
|
|
|
|
|
|
-from db import db, Job, Run
|
|
|
+from db import db, Job, Run, Destination
|
|
|
|
|
|
db.init_app(app)
|
|
|
|
|
|
@@ -124,7 +124,8 @@ def index():
|
|
|
def job_new():
|
|
|
if request.method == "POST":
|
|
|
return _save_job(None)
|
|
|
- return render_template("job_form.html", job=None, ynh_apps=_get_ynh_apps())
|
|
|
+ return render_template("job_form.html", job=None, ynh_apps=_get_ynh_apps(),
|
|
|
+ destinations=Destination.query.filter_by(enabled=True).all())
|
|
|
|
|
|
|
|
|
@app.route("/jobs/<int:job_id>/edit", methods=["GET", "POST"])
|
|
|
@@ -132,7 +133,8 @@ def job_edit(job_id):
|
|
|
job = db.get_or_404(Job, job_id)
|
|
|
if request.method == "POST":
|
|
|
return _save_job(job)
|
|
|
- return render_template("job_form.html", job=job, ynh_apps=_get_ynh_apps())
|
|
|
+ return render_template("job_form.html", job=job, ynh_apps=_get_ynh_apps(),
|
|
|
+ destinations=Destination.query.filter_by(enabled=True).all())
|
|
|
|
|
|
|
|
|
@app.route("/jobs/<int:job_id>/delete", methods=["POST"])
|
|
|
@@ -266,6 +268,7 @@ def _save_job(job):
|
|
|
job = Job()
|
|
|
db.session.add(job)
|
|
|
|
|
|
+ dest_id = f.get("destination_id", "").strip()
|
|
|
job.name = name
|
|
|
job.type = job_type
|
|
|
job.config_json = json.dumps(cfg)
|
|
|
@@ -274,6 +277,7 @@ def _save_job(job):
|
|
|
job.retention_value = int(f.get("retention_value", 7))
|
|
|
job.enabled = f.get("enabled") == "1"
|
|
|
job.core_only = cfg.get("core_only", False)
|
|
|
+ job.destination_id = int(dest_id) if dest_id else None
|
|
|
job.updated_at = datetime.utcnow()
|
|
|
|
|
|
db.session.commit()
|
|
|
@@ -286,6 +290,106 @@ def _save_job(job):
|
|
|
flash(f"Job « {job.name} » enregistré.", "success")
|
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
+# --- Destinations ------------------------------------------------------------
|
|
|
+
|
|
|
+@app.route("/destinations")
|
|
|
+def destinations_list():
|
|
|
+ destinations = Destination.query.order_by(Destination.name).all()
|
|
|
+ return render_template("destinations.html", destinations=destinations)
|
|
|
+
|
|
|
+
|
|
|
+@app.route("/destinations/new", methods=["GET", "POST"])
|
|
|
+def destination_new():
|
|
|
+ if request.method == "POST":
|
|
|
+ return _save_destination(None)
|
|
|
+ return render_template("destination_form.html", dest=None)
|
|
|
+
|
|
|
+
|
|
|
+@app.route("/destinations/<int:dest_id>/edit", methods=["GET", "POST"])
|
|
|
+def destination_edit(dest_id):
|
|
|
+ dest = db.get_or_404(Destination, dest_id)
|
|
|
+ if request.method == "POST":
|
|
|
+ return _save_destination(dest)
|
|
|
+ pub_key = _get_pub_key(dest)
|
|
|
+ return render_template("destination_form.html", dest=dest, pub_key=pub_key)
|
|
|
+
|
|
|
+
|
|
|
+@app.route("/destinations/<int:dest_id>/delete", methods=["POST"])
|
|
|
+def destination_delete(dest_id):
|
|
|
+ dest = db.get_or_404(Destination, dest_id)
|
|
|
+ db.session.delete(dest)
|
|
|
+ db.session.commit()
|
|
|
+ flash(f"Destination « {dest.name} » supprimée.", "success")
|
|
|
+ return redirect(url_for("destinations_list"))
|
|
|
+
|
|
|
+
|
|
|
+@app.route("/destinations/<int:dest_id>/test", methods=["POST"])
|
|
|
+def destination_test(dest_id):
|
|
|
+ dest = db.get_or_404(Destination, dest_id)
|
|
|
+ from jobs.transfer import test_connection
|
|
|
+ ok, msg = test_connection(dest, app.config["DATA_DIR"])
|
|
|
+ flash(msg, "success" if ok else "error")
|
|
|
+ return redirect(url_for("destinations_list"))
|
|
|
+
|
|
|
+
|
|
|
+@app.route("/archives/<path:archive_name>/transfer", methods=["POST"])
|
|
|
+def archive_transfer(archive_name):
|
|
|
+ dest_id = request.form.get("destination_id", type=int)
|
|
|
+ dest = db.get_or_404(Destination, dest_id)
|
|
|
+
|
|
|
+ def _do_transfer():
|
|
|
+ with app.app_context():
|
|
|
+ try:
|
|
|
+ from jobs.transfer import transfer_archive
|
|
|
+ transfer_archive(archive_name, dest, app.config["YUNOHOST_BACKUP_DIR"],
|
|
|
+ app.config["DATA_DIR"])
|
|
|
+ app.logger.info(f"Transfert {archive_name} → {dest.remote_str} OK")
|
|
|
+ except Exception as exc:
|
|
|
+ app.logger.error(f"Transfert {archive_name} échoué : {exc}")
|
|
|
+
|
|
|
+ import threading
|
|
|
+ threading.Thread(target=_do_transfer, daemon=True).start()
|
|
|
+ flash(f"Transfert de « {archive_name} » vers {dest.remote_str} démarré.", "success")
|
|
|
+ return redirect(request.referrer or url_for("index"))
|
|
|
+
|
|
|
+
|
|
|
+def _save_destination(dest):
|
|
|
+ f = request.form
|
|
|
+ name = f.get("name", "").strip()
|
|
|
+ host = f.get("host", "").strip()
|
|
|
+ if not name or not host:
|
|
|
+ flash("Nom et hôte sont requis.", "error")
|
|
|
+ return render_template("destination_form.html", dest=dest)
|
|
|
+
|
|
|
+ is_new = dest is None
|
|
|
+ if is_new:
|
|
|
+ dest = Destination()
|
|
|
+ db.session.add(dest)
|
|
|
+
|
|
|
+ dest.name = name
|
|
|
+ dest.host = host
|
|
|
+ dest.port = int(f.get("port", 22) or 22)
|
|
|
+ dest.user = f.get("user", "root").strip() or "root"
|
|
|
+ dest.remote_path = f.get("remote_path", "/home/yunohost.backup/archives").strip()
|
|
|
+ dest.enabled = f.get("enabled") == "1"
|
|
|
+ db.session.flush() # obtenir l'id si nouveau
|
|
|
+
|
|
|
+ # Génération de la clé SSH si absente
|
|
|
+ if not dest.key_name:
|
|
|
+ from jobs.transfer import generate_key
|
|
|
+ dest.key_name = generate_key(dest.name, app.config["DATA_DIR"])
|
|
|
+
|
|
|
+ db.session.commit()
|
|
|
+ flash(f"Destination « {dest.name} » enregistrée.", "success")
|
|
|
+ return redirect(url_for("destination_edit", dest_id=dest.id))
|
|
|
+
|
|
|
+
|
|
|
+def _get_pub_key(dest):
|
|
|
+ if not dest.key_name:
|
|
|
+ return None
|
|
|
+ from jobs.transfer import get_public_key
|
|
|
+ return get_public_key(dest.key_name, app.config["DATA_DIR"])
|
|
|
+
|
|
|
# --- API v1 ------------------------------------------------------------------
|
|
|
|
|
|
@app.route("/api/v1/health")
|