2021-04-26 17:45:19 +02:00
|
|
|
"""This module contains the base Spec class."""
|
2021-01-15 17:58:29 +01:00
|
|
|
from typing import Union, Dict
|
|
|
|
import skeleton
|
|
|
|
import os
|
2021-01-15 21:08:56 +01:00
|
|
|
from notifier import Notifier
|
|
|
|
import inspect
|
2021-01-15 17:58:29 +01:00
|
|
|
|
|
|
|
|
|
|
|
class Spec:
|
2021-04-26 17:45:19 +02:00
|
|
|
"""Base class for all other spec types."""
|
2021-01-15 17:58:29 +01:00
|
|
|
|
2021-01-15 21:08:56 +01:00
|
|
|
_SKEL = {
|
2021-01-15 17:58:29 +01:00
|
|
|
"destination": None,
|
|
|
|
"limit": None,
|
2021-01-15 21:08:56 +01:00
|
|
|
"notify": {
|
|
|
|
"title": "Backup Notification",
|
|
|
|
"events": ["backup_sucess"],
|
2021-01-15 21:29:23 +01:00
|
|
|
"endpoint": None,
|
|
|
|
"api_key": "",
|
2021-01-15 21:08:56 +01:00
|
|
|
},
|
2021-01-15 17:58:29 +01:00
|
|
|
"extension": "tar.gz",
|
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
name: str,
|
|
|
|
destination: Union[Path, str],
|
|
|
|
limit: int,
|
|
|
|
extension: str,
|
2021-01-15 21:08:56 +01:00
|
|
|
notify=None,
|
2021-01-15 17:58:29 +01:00
|
|
|
):
|
2021-04-26 17:45:19 +02:00
|
|
|
"""Initialize a new Spec object.
|
|
|
|
|
|
|
|
This initializer usually gets called by a subclass's init instead of
|
|
|
|
directly.
|
|
|
|
|
2021-01-15 17:58:29 +01:00
|
|
|
Args:
|
|
|
|
name: name of the spec
|
|
|
|
destination: directory where the backups shall reside
|
|
|
|
limit: max amount of backups
|
2021-04-26 18:00:56 +02:00
|
|
|
extension: file extension of the backup files
|
|
|
|
notify: notifier object to send IFTT notifications
|
2021-01-15 17:58:29 +01:00
|
|
|
"""
|
|
|
|
self.name = name
|
2021-04-26 17:45:19 +02:00
|
|
|
self.destination = Path(destination)
|
2021-01-15 17:58:29 +01:00
|
|
|
|
2021-01-16 09:36:23 +01:00
|
|
|
# Create destination if non-existent
|
|
|
|
try:
|
|
|
|
self.destination.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
2021-04-26 17:45:19 +02:00
|
|
|
# TODO just make this some checks in advance
|
2021-01-16 09:36:23 +01:00
|
|
|
except FileExistsError:
|
2021-01-15 17:58:29 +01:00
|
|
|
raise NotADirectoryError(
|
2021-01-16 09:36:23 +01:00
|
|
|
"{} already exists, but isn't a directory.".format(
|
2021-01-15 17:58:29 +01:00
|
|
|
self.destination
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
self.limit = limit
|
2021-01-15 21:08:56 +01:00
|
|
|
self.notifier = Notifier(*notify) if notify else None
|
2021-01-15 17:58:29 +01:00
|
|
|
self.extension = extension
|
|
|
|
|
2021-01-15 21:08:56 +01:00
|
|
|
@classmethod
|
2021-04-25 19:26:12 +02:00
|
|
|
def skeleton(cls: "Spec") -> Dict:
|
2021-04-26 17:45:19 +02:00
|
|
|
"""Return the skeleton for the given class.
|
2021-04-25 19:26:12 +02:00
|
|
|
|
|
|
|
It works by inspecting the inheritance tree and merging the skeleton
|
|
|
|
for each of the parents.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
cls: the class to get the skeleton for
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
a dictionary containing the skeleton
|
|
|
|
"""
|
2021-01-15 21:08:56 +01:00
|
|
|
return skeleton.merge(
|
|
|
|
*[val._SKEL for val in reversed(inspect.getmro(cls)[:-1])]
|
|
|
|
)
|
|
|
|
|
2021-01-15 17:58:29 +01:00
|
|
|
def remove_backups(self):
|
2021-04-26 17:45:19 +02:00
|
|
|
"""Remove all backups exceeding the limit."""
|
2021-01-15 17:58:29 +01:00
|
|
|
files = sorted(
|
2021-01-15 21:29:23 +01:00
|
|
|
self.destination.glob("*." + self.extension),
|
2021-01-15 17:58:29 +01:00
|
|
|
key=os.path.getmtime,
|
|
|
|
reverse=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
if len(files) >= self.limit:
|
|
|
|
for path in files[self.limit - 1 :]:
|
|
|
|
path.unlink()
|
|
|
|
|
|
|
|
def backup(self):
|
2021-04-26 17:45:19 +02:00
|
|
|
"""Create a new backup.
|
|
|
|
|
|
|
|
This function should be implemented by the subclasses.
|
|
|
|
"""
|
2021-01-15 17:58:29 +01:00
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
def restore(self):
|
2021-04-26 17:45:19 +02:00
|
|
|
"""Restore a given backup (NOT IMPLEMENTED).
|
|
|
|
|
|
|
|
This function should be implemented by the subclasses.
|
|
|
|
"""
|
2021-01-15 17:58:29 +01:00
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
@classmethod
|
2021-01-16 09:41:26 +01:00
|
|
|
def from_dict(cls, name, obj: Dict, defaults: Dict) -> "Spec":
|
2021-04-26 17:45:19 +02:00
|
|
|
"""Create the class given a dictionary (e.g. from a config)."""
|
2021-01-15 17:58:29 +01:00
|
|
|
# Combine defaults with skeleton, creating new skeleton
|
2021-01-15 21:08:56 +01:00
|
|
|
skel = skeleton.merge(cls.skeleton(), defaults)
|
2021-01-15 17:58:29 +01:00
|
|
|
|
|
|
|
# Then, combine actual values with new skeleton
|
2021-01-15 21:08:56 +01:00
|
|
|
obj = skeleton.merge_with_skeleton(obj, skel)
|
2021-01-15 17:58:29 +01:00
|
|
|
|
|
|
|
return cls(name, **obj)
|
2021-01-15 21:08:56 +01:00
|
|
|
|
|
|
|
def to_dict(self):
|
2021-04-26 17:45:19 +02:00
|
|
|
"""Export the class as a dictionary.
|
|
|
|
|
2021-04-26 18:00:56 +02:00
|
|
|
This function should be imnplemented by the subclasses.
|
|
|
|
"""
|
2021-01-15 21:08:56 +01:00
|
|
|
raise NotImplementedError()
|