|
@@ -0,0 +1,236 @@
|
|
|
|
+import datetime
|
|
|
|
+import json
|
|
|
|
+import re
|
|
|
|
+import shutil
|
|
|
|
+import tempfile
|
|
|
|
+import time
|
|
|
|
+from zipfile import ZipFile
|
|
|
|
+from pathlib import Path
|
|
|
|
+
|
|
|
|
+from djangotools.cmdline.common.command import Command, Argument
|
|
|
|
+from djangotools.common.date import CurrentDate
|
|
|
|
+from djangotools.config import app_dir
|
|
|
|
+
|
|
|
|
+from djangotools.config import get_settings
|
|
|
|
+JOURS = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"]
|
|
|
|
+MOIS = ["", "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Aout", "Septembre", "Octobre", "Novembre", "Décembre"]
|
|
|
|
+
|
|
|
|
+def period_to_str(period, date):
|
|
|
|
+ if period == "daliy":
|
|
|
|
+ return f"journalière du {date}"
|
|
|
|
+ if period == "weekly":
|
|
|
|
+ return f"hebdomadaire de la semaine {date}"
|
|
|
|
+ if period == "monthly":
|
|
|
|
+ return f"mensuelle du mois de {date}"
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class BackupFile:
|
|
|
|
+
|
|
|
|
+ def __init__(self, dir, quiet):
|
|
|
|
+ self.quiet = quiet
|
|
|
|
+ self.file = Path(dir) / "backup.json"
|
|
|
|
+ self.log_file = Path(dir) / "backup.log"
|
|
|
|
+ self.log_messages = []
|
|
|
|
+ if self.file.is_file():
|
|
|
|
+ self.content = json.loads(self.file.read_text())
|
|
|
|
+ else:
|
|
|
|
+ self.content = {
|
|
|
|
+ "daily" : {
|
|
|
|
+ "last" : None,
|
|
|
|
+ "file" : None,
|
|
|
|
+ "date" : None
|
|
|
|
+ },
|
|
|
|
+ "weekly" : {
|
|
|
|
+ "last" : None,
|
|
|
|
+ "file" : None,
|
|
|
|
+ "date" : None
|
|
|
|
+ },
|
|
|
|
+ "monthly" : {
|
|
|
|
+ "last" : None,
|
|
|
|
+ "file" : None,
|
|
|
|
+ "date" : None
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @staticmethod
|
|
|
|
+ def get_wwek_number(now=None):
|
|
|
|
+ now = now or CurrentDate.now()
|
|
|
|
+ first_day = datetime.datetime(now.year, 1, 1)
|
|
|
|
+ nb = (now-first_day).days + first_day.weekday()
|
|
|
|
+ return nb // 7
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ @classmethod
|
|
|
|
+ def get_date(cls, period, now = None):
|
|
|
|
+ now = now or CurrentDate.now()
|
|
|
|
+ date = None
|
|
|
|
+ if period == "daily":
|
|
|
|
+ date = JOURS[now.weekday()]
|
|
|
|
+ elif period == "weekly":
|
|
|
|
+ date = cls.get_wwek_number()
|
|
|
|
+ elif period == "monthly":
|
|
|
|
+ date = MOIS[now.month]
|
|
|
|
+ return date
|
|
|
|
+
|
|
|
|
+ def get_todo(self):
|
|
|
|
+ periods = ["daily", "weekly", "monthly"]
|
|
|
|
+ ret = []
|
|
|
|
+ for period in periods:
|
|
|
|
+ if self.content[period]["date"] != self.get_date(period):
|
|
|
|
+ ret.append(period)
|
|
|
|
+ return ret
|
|
|
|
+
|
|
|
|
+ def _log(self, type, *args):
|
|
|
|
+ line = None
|
|
|
|
+ if type=="backup":
|
|
|
|
+ line = f"{args[0]} : Sauvegarde {period_to_str(args[1], args[3])} dans {args[2]}"
|
|
|
|
+ elif type == "message":
|
|
|
|
+ line = f"{args[0]} : {args[1]}"
|
|
|
|
+ else:
|
|
|
|
+ raise ValueError(f"Erreur le type de message '{type}' est inconnu")
|
|
|
|
+ self.log_messages.append(line)
|
|
|
|
+ if not self.quiet: print(line)
|
|
|
|
+
|
|
|
|
+ def log(self, message):
|
|
|
|
+ now = CurrentDate.now()
|
|
|
|
+ t = now.strftime("%d/%m/%Y %H:%M:%S")
|
|
|
|
+ self._log("message", t, message)
|
|
|
|
+
|
|
|
|
+ def do_backup(self, period, file):
|
|
|
|
+ if period not in self.content:
|
|
|
|
+ raise ValueError(f"La période de backup '{period}' est inconnu")
|
|
|
|
+ now = CurrentDate.now()
|
|
|
|
+ t = now.strftime("%d/%m/%Y %H:%M:%S")
|
|
|
|
+ date = self.get_date(period)
|
|
|
|
+ self._log("backup", t, period, file, date)
|
|
|
|
+
|
|
|
|
+ self.content[period] = {
|
|
|
|
+ "last" : t,
|
|
|
|
+ "file" : str(file),
|
|
|
|
+ "date" : date
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ def save(self):
|
|
|
|
+ log = "\n".join(self.log_messages)+"\n"
|
|
|
|
+ with open(self.log_file, "a") as fd:
|
|
|
|
+ fd.write(log)
|
|
|
|
+ self.file.write_text(json.dumps(self.content, indent=2))
|
|
|
|
+
|
|
|
|
+class BackupCommand(Command):
|
|
|
|
+ NAME = "backup"
|
|
|
|
+ ALIASES = ["ba"]
|
|
|
|
+ PREFIX = "backup"
|
|
|
|
+ HELP = "Execute une sauvegarde"
|
|
|
|
+
|
|
|
|
+ ARGUMENT = [
|
|
|
|
+ Argument("--daily", "-d", action = "store_true", help="Execute le backup journalier"),
|
|
|
|
+ Argument("--weekly", "-w", action = "store_true", help="Execute le backup hebdomadaire"),
|
|
|
|
+ Argument("--monthly", "-m", action = "store_true", help="Execute le backup mensuel"),
|
|
|
|
+ Argument("--quiet", "-q", action = "store_true", help="Mode silencieux"),
|
|
|
|
+ Argument("--prefix", "-p", help="Prefix des fichier zip de backup"),
|
|
|
|
+
|
|
|
|
+ ]
|
|
|
|
+
|
|
|
|
+ def __init__(self):
|
|
|
|
+ super().__init__()
|
|
|
|
+ self.settings = get_settings()
|
|
|
|
+ self.backup_dir = self.settings.CONFIG.backup_dir
|
|
|
|
+ self._lock_file = self.backup_dir / "lock"
|
|
|
|
+ self.backup_dir_daily = self.backup_dir / "daliy"
|
|
|
|
+ self.backup_dir_weekly = self.backup_dir / "weekly"
|
|
|
|
+ self.backup_dir_monthly = self.backup_dir / "monthly"
|
|
|
|
+
|
|
|
|
+ for dir in [self.backup_dir_daily, self.backup_dir_weekly, self.backup_dir_monthly, self._lock_file.parent]:
|
|
|
|
+ dir.mkdir(exist_ok=True, parents=True)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def get_name(self):
|
|
|
|
+ now = CurrentDate.now()
|
|
|
|
+ return f"{self.PREFIX}-{str(now.year).zfill(4)}_{str(now.month).zfill(2)}_{str(now.day).zfill(2)}.zip"
|
|
|
|
+
|
|
|
|
+ def get_date(self, file):
|
|
|
|
+ file = Path(file)
|
|
|
|
+ for x in re.findall(r"\d{4}_\d{2}_\d{2}.zip$", file.name):
|
|
|
|
+ x = [int(n) for n in x.replace(".zip", "").split("_")]
|
|
|
|
+ return datetime.datetime(*x)
|
|
|
|
+ return None
|
|
|
|
+
|
|
|
|
+ def gen_backup(self, output):
|
|
|
|
+ root = self.settings.DATA_DIR
|
|
|
|
+ queue = [root]
|
|
|
|
+ with ZipFile(output, "w") as zip:
|
|
|
|
+ while queue:
|
|
|
|
+ curr = queue.pop(0)
|
|
|
|
+ for file in curr.iterdir():
|
|
|
|
+ if file.is_dir():
|
|
|
|
+ if file == self.backup_dir: continue
|
|
|
|
+ queue.append(file)
|
|
|
|
+ else:
|
|
|
|
+ zip.write(file, file.relative_to(root.parent))
|
|
|
|
+
|
|
|
|
+ def lock(self):
|
|
|
|
+ while self._lock_file.is_file():
|
|
|
|
+ time.sleep(1)
|
|
|
|
+ self._lock_file.touch()
|
|
|
|
+
|
|
|
|
+ def unlock(self):
|
|
|
|
+ if self._lock_file.is_file():
|
|
|
|
+ self._lock_file.unlink()
|
|
|
|
+
|
|
|
|
+ def __enter__(self):
|
|
|
|
+ self.lock()
|
|
|
|
+
|
|
|
|
+ def __exit__(self, exc_type, exc_val, exc_tb):
|
|
|
|
+ self.unlock()
|
|
|
|
+
|
|
|
|
+ def clean(self):
|
|
|
|
+ now = CurrentDate.now()
|
|
|
|
+ for file in list(self.backup_dir_daily.iterdir()):
|
|
|
|
+ date = self.get_date(file)
|
|
|
|
+ if not date: continue
|
|
|
|
+ if date + datetime.timedelta(days=8) < now:
|
|
|
|
+ file.unlink()
|
|
|
|
+
|
|
|
|
+ for file in list(self.backup_dir_weekly.iterdir()):
|
|
|
|
+ date = self.get_date(file)
|
|
|
|
+ if not date: continue
|
|
|
|
+ if date + datetime.timedelta(days=32) < now:
|
|
|
|
+ file.unlink()
|
|
|
|
+
|
|
|
|
+ for file in list(self.backup_dir_monthly.iterdir()):
|
|
|
|
+ date = self.get_date(file)
|
|
|
|
+ if not date: continue
|
|
|
|
+ if date + datetime.timedelta(days=366) < now:
|
|
|
|
+ file.unlink()
|
|
|
|
+
|
|
|
|
+ def run(self, data):
|
|
|
|
+ with tempfile.TemporaryDirectory() as temp:
|
|
|
|
+ temp = Path(temp) / "data.zip"
|
|
|
|
+ self.clean()
|
|
|
|
+ back = BackupFile(self.backup_dir, data.quiet)
|
|
|
|
+
|
|
|
|
+ with self:
|
|
|
|
+ self.gen_backup(temp)
|
|
|
|
+ if not any(getattr(data, x) for x in ("daily", "weekly", "monthly") ):
|
|
|
|
+ todo = set(back.get_todo())
|
|
|
|
+ data.daily = data.daily or (todo & {"daily"})
|
|
|
|
+ data.weekly = data.weekly or (todo & {"weekly"})
|
|
|
|
+ data.monthly = data.monthly or (todo & {"monthly"})
|
|
|
|
+
|
|
|
|
+ name = self.get_name()
|
|
|
|
+
|
|
|
|
+ if data.daily:
|
|
|
|
+ back.do_backup("daily", self.backup_dir_daily / name)
|
|
|
|
+ shutil.copy(temp, self.backup_dir_daily / name)
|
|
|
|
+ if data.weekly:
|
|
|
|
+ back.do_backup("weekly", self.backup_dir_weekly / name)
|
|
|
|
+ shutil.copy(temp, self.backup_dir_weekly / name)
|
|
|
|
+ if data.monthly:
|
|
|
|
+ back.do_backup("monthly", self.backup_dir_monthly / name)
|
|
|
|
+ shutil.copy(temp, self.backup_dir_monthly / name)
|
|
|
|
+
|
|
|
|
+ back.save()
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|