retention.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import os
  2. import re
  3. from datetime import datetime, timedelta
  4. def apply_retention(job, new_archive_name, backup_dir):
  5. """Applique la politique de rétention après une sauvegarde réussie."""
  6. archives = _list_archives_for_job(job, backup_dir)
  7. if job.retention_mode == "count":
  8. to_delete = _retention_count(archives, job.retention_value)
  9. elif job.retention_mode == "daily":
  10. to_delete = _retention_daily(archives, job.retention_value)
  11. else:
  12. return []
  13. from jobs.utils import sudo_rm
  14. deleted = []
  15. for archive_filename in to_delete:
  16. base = os.path.splitext(archive_filename)[0]
  17. for ext in (".tar", ".info.json"):
  18. full = os.path.join(backup_dir, base + ext)
  19. sudo_rm(full)
  20. deleted.append(base + ext)
  21. return deleted
  22. def _job_archive_prefix(job, instance_name):
  23. """Retourne le préfixe des archives pour ce job (ex: jerry_nextcloud_)."""
  24. if job.type == "ynh_app":
  25. import json
  26. cfg = json.loads(job.config_json or "{}")
  27. return f"{instance_name}_{cfg.get('app_id', '')}_"
  28. elif job.type == "ynh_system":
  29. return f"{instance_name}_system_"
  30. elif job.type in ("mysql", "postgresql"):
  31. import json
  32. cfg = json.loads(job.config_json or "{}")
  33. return f"{instance_name}_{job.type}_{cfg.get('database', '')}_"
  34. elif job.type == "custom_dir":
  35. label = re.sub(r'[^a-z0-9]+', '-', job.name.lower().strip()).strip('-')
  36. return f"{instance_name}_{label}_"
  37. else:
  38. return f"{instance_name}_{job.name.lower().replace(' ', '-')}_"
  39. def _list_archives_for_job(job, backup_dir):
  40. """Liste les archives correspondant à ce job, triées par date (plus ancienne en premier)."""
  41. from flask import current_app
  42. instance = current_app.config["INSTANCE_NAME"]
  43. prefix = _job_archive_prefix(job, instance)
  44. from jobs.utils import sudo_listdir
  45. archives = [
  46. fname for fname in sudo_listdir(backup_dir)
  47. if fname.startswith(prefix) and fname.endswith(".tar")
  48. ]
  49. archives.sort(key=_extract_date)
  50. return archives
  51. def apply_remote_retention(job, client):
  52. """Applique la rétention sur l'instance distante après un push.
  53. Filtre les archives par le même préfixe que le job local et applique
  54. la même politique (count/daily). Ne touche pas aux archives des autres jobs.
  55. """
  56. from flask import current_app
  57. instance = current_app.config["INSTANCE_NAME"]
  58. prefix = _job_archive_prefix(job, instance)
  59. try:
  60. remote_archives = client.get_archives()
  61. except Exception:
  62. return []
  63. matching = sorted(
  64. [a["name"] + ".tar" for a in remote_archives if a["name"].startswith(prefix)],
  65. key=_extract_date,
  66. )
  67. if job.retention_mode == "count":
  68. to_delete = _retention_count(matching, job.retention_value)
  69. elif job.retention_mode == "daily":
  70. to_delete = _retention_daily(matching, job.retention_value)
  71. else:
  72. return []
  73. deleted = []
  74. for archive_filename in to_delete:
  75. base = os.path.splitext(archive_filename)[0]
  76. try:
  77. client.delete_archive(base)
  78. deleted.append(base)
  79. except Exception:
  80. pass
  81. return deleted
  82. def _extract_date(filename):
  83. match = re.search(r'(\d{8})', filename)
  84. if match:
  85. try:
  86. return datetime.strptime(match.group(1), "%Y%m%d")
  87. except ValueError:
  88. pass
  89. return datetime.min
  90. def _retention_count(archives, keep_n):
  91. if len(archives) <= keep_n:
  92. return []
  93. return archives[: len(archives) - keep_n]
  94. def _retention_daily(archives, days):
  95. cutoff = datetime.utcnow() - timedelta(days=days)
  96. to_delete = []
  97. seen_dates = set()
  98. for archive in reversed(archives):
  99. date = _extract_date(archive)
  100. if date < cutoff:
  101. to_delete.append(archive)
  102. continue
  103. date_key = date.date()
  104. if date_key in seen_dates:
  105. to_delete.append(archive)
  106. else:
  107. seen_dates.add(date_key)
  108. return to_delete