|
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
|
|
|
|