backup-tool/app/specs/spec.py

124 lines
3.3 KiB
Python
Raw Normal View History

2021-04-26 17:45:19 +02:00
"""This module contains the base Spec class."""
from pathlib import Path
from typing import Union, Dict
import skeleton
import os
2021-01-15 21:08:56 +01:00
from notifier import Notifier
import inspect
class Spec:
2021-04-26 17:45:19 +02:00
"""Base class for all other spec types."""
2021-01-15 21:08:56 +01:00
_SKEL = {
"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
},
"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-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.
Args:
name: name of the spec
destination: directory where the backups shall reside
limit: max amount of backups
notifier: notifier object
"""
self.name = name
2021-04-26 17:45:19 +02:00
self.destination = Path(destination)
# 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
except FileExistsError:
raise NotADirectoryError(
"{} already exists, but isn't a directory.".format(
self.destination
)
)
self.limit = limit
2021-01-15 21:08:56 +01:00
self.notifier = Notifier(*notify) if notify else None
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])]
)
def remove_backups(self):
2021-04-26 17:45:19 +02:00
"""Remove all backups exceeding the limit."""
files = sorted(
2021-01-15 21:29:23 +01:00
self.destination.glob("*." + self.extension),
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.
"""
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.
"""
raise NotImplementedError()
@classmethod
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)."""
# Combine defaults with skeleton, creating new skeleton
2021-01-15 21:08:56 +01:00
skel = skeleton.merge(cls.skeleton(), defaults)
# Then, combine actual values with new skeleton
2021-01-15 21:08:56 +01:00
obj = skeleton.merge_with_skeleton(obj, skel)
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.
This function should be imnplemented by the subclasses."""
2021-01-15 21:08:56 +01:00
raise NotImplementedError()