1
|
1 |
|
from plugin.core.backup.constants import BACKUP_NAME_REGEX, BACKUP_PATH, BACKUP_PERIODS, BACKUP_RETENTION |
|
|
|
|
2
|
1 |
|
from plugin.core.backup.models import BackupGroup, BackupRevision |
3
|
1 |
|
from plugin.core.backup.tasks import ArchiveTask, CompactTask |
4
|
1 |
|
from plugin.core.helpers.variable import try_convert |
5
|
|
|
|
6
|
1 |
|
from datetime import datetime |
7
|
1 |
|
from fnmatch import fnmatch |
8
|
1 |
|
import logging |
9
|
1 |
|
import os |
10
|
|
|
|
11
|
1 |
|
log = logging.getLogger(__name__) |
12
|
|
|
|
13
|
|
|
|
14
|
1 |
|
class BackupMaintenanceManager(object): |
15
|
1 |
|
def __init__(self): |
16
|
|
|
self.now = datetime.now() |
17
|
|
|
|
18
|
1 |
|
def run(self): |
19
|
|
|
# Run maintenance on backup groups |
20
|
|
|
for group in BackupGroup.list(max_depth=1): |
21
|
|
|
self.process_group(group) |
22
|
|
|
|
23
|
|
|
# |
24
|
|
|
# Process methods |
25
|
|
|
# |
26
|
|
|
|
27
|
1 |
|
def process_group(self, group): |
28
|
|
|
log.debug('Running maintenance on group: %r', group) |
29
|
|
|
|
30
|
|
|
# Process policy periods |
31
|
|
|
for period in BACKUP_PERIODS: |
32
|
|
|
self.process_period(period, group) |
33
|
|
|
|
34
|
1 |
|
def process_period(self, period, group): |
35
|
|
|
policy = BACKUP_RETENTION[period] |
36
|
|
|
|
37
|
|
|
# Retrieve options |
38
|
|
|
p_files = policy.get('files') |
39
|
|
|
|
40
|
|
|
if p_files is None: |
41
|
|
|
raise ValueError('Policy "%s" is missing the "files" attribute') |
42
|
|
|
|
43
|
|
|
# Build lists of revisions, grouped by period |
44
|
|
|
revisions_grouped = {} |
45
|
|
|
|
46
|
|
|
for base_path, dirs, files in os.walk(group.path): |
|
|
|
|
47
|
|
|
# Strip UNC prefix from `base_path` |
48
|
|
|
if base_path.startswith('\\\\?\\'): |
49
|
|
|
base_path = base_path[4:] |
50
|
|
|
|
51
|
|
|
# Ensure directory starts with a year |
52
|
|
|
rel_path = os.path.relpath(base_path, group.path) |
53
|
|
|
|
54
|
|
|
try: |
55
|
|
|
year = rel_path[:rel_path.index(os.path.sep)] |
56
|
|
|
except ValueError: |
57
|
|
|
year = rel_path |
58
|
|
|
|
59
|
|
|
if len(year) != 4 or try_convert(year, int) is None: |
60
|
|
|
continue |
61
|
|
|
|
62
|
|
|
# Search for revision metadata files |
63
|
|
|
for name in files: |
64
|
|
|
# Ensure file name matches the policy "files" filter |
65
|
|
|
if not fnmatch(name, p_files): |
66
|
|
|
continue |
67
|
|
|
|
68
|
|
|
# Build path |
69
|
|
|
path = os.path.join(base_path, name) |
70
|
|
|
|
71
|
|
|
# Match revision metadata against regex pattern |
72
|
|
|
if not BACKUP_NAME_REGEX.match(name): |
73
|
|
|
continue |
74
|
|
|
|
75
|
|
|
# Load metadata from file |
76
|
|
|
try: |
77
|
|
|
revision = BackupRevision.load(path) |
78
|
|
|
except Exception as ex: |
|
|
|
|
79
|
|
|
log.warn('Unable to load revision at %r: %s', path, ex, exc_info=True) |
80
|
|
|
continue |
81
|
|
|
|
82
|
|
|
# Retrieve timestamp period |
83
|
|
|
key = self.timestamp_period(period, revision.timestamp) |
84
|
|
|
|
85
|
|
|
if key == self.timestamp_period(period, self.now): |
86
|
|
|
# Backup occurred in the current period |
87
|
|
|
continue |
88
|
|
|
|
89
|
|
|
# Store details in `revisions_grouped` dictionary |
90
|
|
|
if key not in revisions_grouped: |
91
|
|
|
revisions_grouped[key] = [] |
92
|
|
|
|
93
|
|
|
revisions_grouped[key].append(revision) |
94
|
|
|
|
95
|
|
|
# Ensure revisions have been found |
96
|
|
|
if not revisions_grouped: |
97
|
|
|
return |
98
|
|
|
|
99
|
|
|
log.debug('Processing period: %r (group: %r)', period, group) |
100
|
|
|
|
101
|
|
|
# Search for weeks exceeding the policy |
102
|
|
|
for key, revisions in revisions_grouped.items(): |
103
|
|
|
# Compact period |
104
|
|
|
if policy.get('compact'): |
105
|
|
|
CompactTask.run(period, key, revisions, policy['compact']) |
106
|
|
|
|
107
|
|
|
# Archive revisions (if enabled) |
108
|
|
|
if policy.get('archive'): |
109
|
|
|
ArchiveTask.run(group, period, key, revisions, policy['archive']) |
110
|
|
|
|
111
|
|
|
log.debug('Done') |
112
|
|
|
|
113
|
|
|
# |
114
|
|
|
# Helpers |
115
|
|
|
# |
116
|
|
|
|
117
|
1 |
|
@staticmethod |
118
|
|
|
def timestamp_period(period, timestamp): |
119
|
|
|
if period == 'day': |
120
|
|
|
return timestamp.date() |
121
|
|
|
|
122
|
|
|
if period == 'week': |
123
|
|
|
return tuple(timestamp.isocalendar()[:2]) |
124
|
|
|
|
125
|
|
|
if period == 'month': |
126
|
|
|
return timestamp.year, timestamp.month |
127
|
|
|
|
128
|
|
|
if period == 'year': |
129
|
|
|
return timestamp.year, |
130
|
|
|
|
131
|
|
|
raise ValueError('Unknown period: %r' % period) |
132
|
|
|
|