Kaynağa Gözat

fix: sudo rm sur archives — sudoers + centralisation via sudo_rm()

- sudo_rm() et sudo_rm_archive() dans utils.py (un appel par fichier
  pour coller au pattern sudoers)
- retention.py : os.remove() → sudo_rm() (les archives sont root-owned)
- jobs.py archive_delete et api.py : sudo rm -f two-args → sudo_rm_archive()
- jobs.py tmp cleanup : rm -f → rm -rf (correspond au sudoers existant)
- sudoers : ajout rm -f /home/yunohost.backup/archives/*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cedric Hansen 1 ay önce
ebeveyn
işleme
b56a7358b2

+ 1 - 0
conf/sudoers

@@ -15,6 +15,7 @@ __APP__ ALL=(root) NOPASSWD: /usr/bin/find /home/yunohost.backup/archives *
 __APP__ ALL=(root) NOPASSWD: /usr/bin/rsync *
 __APP__ ALL=(root) NOPASSWD: /usr/bin/tar *
 __APP__ ALL=(root) NOPASSWD: /usr/bin/rm -rf /tmp/backupmanager_*
+__APP__ ALL=(root) NOPASSWD: /usr/bin/rm -f /home/yunohost.backup/archives/*
 __APP__ ALL=(root) NOPASSWD: /usr/bin/mkdir -p *
 __APP__ ALL=(root) NOPASSWD: /usr/bin/chown *
 __APP__ ALL=(root) NOPASSWD: /usr/bin/chmod *

+ 2 - 5
sources/blueprints/api.py

@@ -120,11 +120,8 @@ def api_archives():
 @bp.route("/archives/<name>", methods=["DELETE"])
 def api_archive_delete(name):
     backup_dir = current_app.config["YUNOHOST_BACKUP_DIR"]
-    from jobs.utils import sudo_exists
-    for ext in (".tar", ".info.json"):
-        path = os.path.join(backup_dir, name + ext)
-        if sudo_exists(path):
-            subprocess.run(["sudo", "rm", "-f", path], capture_output=True)
+    from jobs.utils import sudo_rm_archive
+    sudo_rm_archive(name, backup_dir)
     return jsonify({"status": "deleted", "name": name})
 
 

+ 3 - 5
sources/blueprints/jobs.py

@@ -219,7 +219,7 @@ def archive_download(archive_name):
                             break
                         yield chunk
             finally:
-                subprocess.run(["sudo", "rm", "-f", tmp_path], capture_output=True)
+                subprocess.run(["sudo", "rm", "-rf", tmp_path], capture_output=True)
 
         return Response(
             stream_with_context(_stream()),
@@ -234,11 +234,9 @@ def archive_download(archive_name):
 
 @bp.route("/archives/<path:archive_name>/delete", methods=["POST"])
 def archive_delete(archive_name):
-    import os, subprocess
+    from jobs.utils import sudo_rm_archive
     backup_dir = current_app.config["YUNOHOST_BACKUP_DIR"]
-    tar_path = os.path.join(backup_dir, archive_name + ".tar")
-    info_path = os.path.join(backup_dir, archive_name + ".info.json")
-    subprocess.run(["sudo", "rm", "-f", tar_path, info_path], capture_output=True)
+    sudo_rm_archive(archive_name, backup_dir)
     flash(f"Archive « {archive_name} » supprimée.", "success")
     return redirect(url_for("jobs.archives"))
 

+ 11 - 0
sources/jobs/utils.py

@@ -93,6 +93,17 @@ def batch_list_archives(backup_dir):
     return stats
 
 
+def sudo_rm(path):
+    """Supprime un fichier root-owned via sudo rm. Silencieux si absent."""
+    subprocess.run(["sudo", "rm", "-f", path], capture_output=True)
+
+
+def sudo_rm_archive(archive_name, backup_dir):
+    """Supprime .tar et .info.json d'une archive root-owned (deux appels séparés)."""
+    for ext in (".tar", ".info.json"):
+        sudo_rm(os.path.join(backup_dir, archive_name + ext))
+
+
 def sudo_listdir(directory):
     """Liste les fichiers d'un répertoire via sudo find si non accessible directement."""
     try:

+ 3 - 3
sources/retention.py

@@ -13,14 +13,14 @@ def apply_retention(job, new_archive_name, backup_dir):
     else:
         return []
 
+    from jobs.utils import sudo_rm
     deleted = []
     for archive_filename in to_delete:
         base = os.path.splitext(archive_filename)[0]
         for ext in (".tar", ".info.json"):
             full = os.path.join(backup_dir, base + ext)
-            if os.path.exists(full):
-                os.remove(full)
-                deleted.append(base + ext)
+            sudo_rm(full)
+            deleted.append(base + ext)
     return deleted