145 lines
4.5 KiB
Python
145 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import datetime
|
|
import itertools
|
|
import time
|
|
|
|
import dateutil.tz, dateutil.relativedelta
|
|
|
|
from . import package_logger
|
|
|
|
WEEKDAYS = [object() for _ in range(7)]
|
|
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = WEEKDAYS
|
|
MONTHLY = object()
|
|
WEEKLY = object()
|
|
|
|
WEEKDAY_NUMBERS = dict(zip(WEEKDAYS, itertools.count()))
|
|
|
|
WEEKDAY_RELATIVE_DAY_MAP = [
|
|
dateutil.relativedelta.MO,
|
|
dateutil.relativedelta.TU,
|
|
dateutil.relativedelta.WE,
|
|
dateutil.relativedelta.TH,
|
|
dateutil.relativedelta.FR,
|
|
dateutil.relativedelta.SA,
|
|
dateutil.relativedelta.SU
|
|
]
|
|
|
|
LOCAL_TZ = dateutil.tz.tzlocal()
|
|
|
|
def module_logger():
|
|
return package_logger().getChild("backup")
|
|
|
|
def next_due_run(timespec, since):
|
|
def next_due_run_part(part):
|
|
tgt = None
|
|
if part is MONTHLY:
|
|
rel = (dateutil.relativedelta
|
|
.relativedelta(months=1, day=1, hour=0, minute=0, second=0,
|
|
microsecond=0))
|
|
# Go to midnight on the first of the next month
|
|
tgt = since + rel
|
|
if part is WEEKLY:
|
|
part = MONDAY
|
|
if part in WEEKDAYS:
|
|
# if since is the same weekday as we have selected, "1st <day>"
|
|
# would just be that day, so we need to also advance one day before
|
|
# asking for the "1st <day>".
|
|
relative_cls = WEEKDAY_RELATIVE_DAY_MAP[WEEKDAY_NUMBERS[part]]
|
|
day = relative_cls(1)
|
|
rel = (dateutil.relativedelta
|
|
.relativedelta(weekday=day, days=+1, hour=0, minute=0,
|
|
second=0, microsecond=0))
|
|
tgt = since + rel
|
|
|
|
assert tgt is not None
|
|
return tgt
|
|
return min((next_due_run_part(part) for part in timespec))
|
|
|
|
|
|
class Backup(object):
|
|
@property
|
|
def logger(self):
|
|
return module_logger().getChild("Backup")
|
|
|
|
def __init__(self, name, paths, backup_name, timespec, backends):
|
|
self.name = name
|
|
self.paths = paths
|
|
self.backup_name = backup_name
|
|
self.timespec = timespec
|
|
self.backends = backends
|
|
|
|
def should_run(self, last_run, now):
|
|
due = next_due_run(self.timespec, last_run)
|
|
return due < now
|
|
|
|
def perform(self, now):
|
|
success = True
|
|
for backend in self.backends:
|
|
success = success and backend.perform(self.paths, self.name, now)
|
|
return success
|
|
|
|
def get_all_archives(self, backends=None, backend_to_primed_list_token_map=None):
|
|
if backends is None:
|
|
backends = self.backends
|
|
|
|
for backend in backends:
|
|
if backend not in self.backends:
|
|
raise Exception("Passed a backend we don't own!?")
|
|
|
|
pairs = []
|
|
|
|
for backend in backends:
|
|
token = None
|
|
if backend_to_primed_list_token_map is not None:
|
|
assert backend in backend_to_primed_list_token_map, "Backend {} not in primed token map?".format(backend)
|
|
token = backend_to_primed_list_token_map[backend]
|
|
pairs.append(
|
|
[backend, backend.existing_archives_for_name(self.name, primed_list_token=token)])
|
|
|
|
return pairs
|
|
|
|
def get_backends(self):
|
|
return list(self.backends)
|
|
|
|
|
|
class BackupSet(object):
|
|
@property
|
|
def logger(self):
|
|
return package_logger().getChild("BackupSet")
|
|
|
|
def __init__(self, state, configured_backups, config_mtime, state_mtime,
|
|
now):
|
|
self.configured_backups = configured_backups
|
|
self.config_mtime = config_mtime
|
|
self.state_mtime = state_mtime
|
|
self.state = state
|
|
self.now = now
|
|
|
|
def state_after_backups(self, backups):
|
|
new_state = self.state.copy()
|
|
for backup in backups:
|
|
new_state[backup.name] = time.mktime(self.now.timetuple())
|
|
return new_state
|
|
|
|
def last_run_of_backup(self, backup):
|
|
stamp = self.state.get(backup.name, 0)
|
|
return datetime.datetime.fromtimestamp(stamp).replace(tzinfo=LOCAL_TZ)
|
|
|
|
def backups_due(self):
|
|
backups_to_run = []
|
|
|
|
if self.config_mtime > self.state_mtime:
|
|
self.logger.info("Configuration changed. Should run all backups.")
|
|
return self.configured_backups
|
|
|
|
for backup in self.configured_backups:
|
|
if backup.should_run(self.last_run_of_backup(backup), self.now):
|
|
backups_to_run.append(backup)
|
|
return backups_to_run
|
|
|
|
def all_backups(self):
|
|
return self.configured_backups
|
|
|