11import contextlib
2+ import dataclasses
3+ import datetime
4+ import re
25import tarfile
36import threading
47import time
58from pathlib import Path
6- from typing import Optional
9+ from typing import Optional , List
710
811from typing_extensions import override
912
1518from prime_backup .utils .run_once import RunOnceFunc
1619
1720
21+ @dataclasses .dataclass (frozen = True )
22+ class DbBackupFile :
23+ path : Path
24+ date : datetime .datetime
25+
26+
1827class CreateDbBackupTask (HeavyTask [Optional [threading .Thread ]]):
1928 __task_sem = threading .Semaphore (1 )
29+ _db_backup_file_regex = re .compile (r'^db_backup_(?P<date>\d{8})_(?P<time>\d{6})\.tar\.xz$' )
2030
2131 @property
2232 @override
@@ -56,6 +66,11 @@ def tar_thread():
5666 ))
5767 except Exception :
5868 self .logger .exception ('db backup: Compress database backup to {} failed' .format (db_backup_file ))
69+ else :
70+ try :
71+ self ._delete_old_db_backup_files (db_backup_root )
72+ except Exception :
73+ self .logger .exception ('db backup: Delete old database backups failed' )
5974 finally :
6075 sem_releaser ()
6176 temp_db_path .unlink (missing_ok = True )
@@ -77,3 +92,44 @@ def tar_thread():
7792 except Exception :
7893 sem_releaser ()
7994 raise
95+
96+ @classmethod
97+ def __parse_db_backup_file (cls , path : Path ) -> Optional [DbBackupFile ]:
98+ if not path .is_file ():
99+ return None
100+ if (match := cls ._db_backup_file_regex .fullmatch (path .name )) is None :
101+ return None
102+
103+ try :
104+ date = datetime .datetime .strptime (match ['date' ] + match ['time' ], '%Y%m%d%H%M%S' )
105+ except ValueError :
106+ return None
107+ return DbBackupFile (path = path , date = date )
108+
109+ @classmethod
110+ def _get_db_backup_files (cls , db_backup_root : Path ) -> List [DbBackupFile ]:
111+ files : List [DbBackupFile ] = []
112+ for path in db_backup_root .iterdir ():
113+ if (backup_file := cls .__parse_db_backup_file (path )) is not None :
114+ files .append (backup_file )
115+ files .sort (key = lambda f : (f .date , f .path .name ), reverse = True )
116+ return files
117+
118+ def _delete_old_db_backup_files (self , db_backup_root : Path ):
119+ max_amount = self .config .database .backup .max_amount
120+ if max_amount <= 0 :
121+ return
122+
123+ db_backup_files = self ._get_db_backup_files (db_backup_root )
124+ files_to_delete = db_backup_files [max_amount :]
125+ if not files_to_delete :
126+ return
127+
128+ self .logger .info ('db backup: Deleting {} old database backup(s), keeping latest {}' .format (len (files_to_delete ), max_amount ))
129+ for backup_file in files_to_delete :
130+ try :
131+ backup_file .path .unlink ()
132+ except Exception :
133+ self .logger .exception ('db backup: Failed to delete old database backup {}' .format (backup_file .path .as_posix ()))
134+ else :
135+ self .logger .info ('db backup: Deleted old database backup {}' .format (backup_file .path .as_posix ()))
0 commit comments