Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Backup handlers

  • Loading branch information...
commit e1926725a2a2489f737cd78cf1e1f53715830e1e 1 parent 923ba01
Dmitry Konishchev authored
7 pyvsb/backup.py
View
@@ -51,12 +51,12 @@
class Backup:
"""Controls backup creation."""
- def __init__(self, config):
+ def __init__(self, config, storage):
# Backup config
self.__config = config
# Backup storage abstraction
- self.__storage = None
+ self.__storage = storage
# Backup name
self.__name = None
@@ -90,8 +90,6 @@ def __init__(self, config):
try:
- self.__storage = Storage(self.__config["backup_root"])
-
self.__group, self.__name, path = self.__storage.create_backup(
self.__config["max_backups"])
@@ -286,6 +284,7 @@ def __deduplicate(self, path, stat_info, fingerprint, file_obj):
# Find files with the same hash <--
+
def __load_all_backup_metadata(self, trust_modify_time):
"""Loads all metadata from previous backups."""
45 pyvsb/backuper.py
View
@@ -13,6 +13,7 @@
from .core import LogicalError
from .backup import Backup
+from .storage import Storage
LOG = logging.getLogger(__name__)
@@ -25,16 +26,47 @@ class Backuper:
"""Controls backup process."""
def __init__(self, config):
+ def get_handler(name):
+ handler = config.get(name)
+
+ if handler is None:
+ def handler(*args):
+ pass
+
+ return handler
+
+ logger = logging.getLogger("pyvsb.handler." + name)
+
+ def wrapper(*args):
+ LOG.info("Executing %s handler...", name)
+
+ try:
+ handler(logger, *args)
+ except Exception:
+ LOG.exception("%s handler crashed.", name)
+ self.__ok = False
+
+ return wrapper
+
# Config
self.__config = config
+ # False if something went wrong during the backup
+ self.__ok = True
+
# Default open() flags
self.__open_flags = os.O_RDONLY | os.O_NOFOLLOW
if hasattr(os, "O_NOATIME"):
self.__open_flags |= os.O_NOATIME
+ # Backup storage abstraction
+ storage = Storage(config["backup_root"],
+ on_group_created = get_handler("on_group_created"),
+ on_group_deleted = get_handler("on_group_deleted"),
+ on_backup_created = get_handler("on_backup_created"))
+
# Holds backup writing logic
- self.__backup = Backup(config)
+ self.__backup = Backup(config, storage)
def __enter__(self):
@@ -49,21 +81,19 @@ def __exit__(self, exc_type, exc_val, exc_tb):
def backup(self):
"""Starts the backup."""
- ok = True
-
try:
for path, params in self.__config["backup_items"].items():
if self.__run_script(params.get("before")):
- ok &= self.__backup_path(path, params.get("filter", []), path)
- ok &= self.__run_script(params.get("after"))
+ self.__ok &= self.__backup_path(path, params.get("filter", []), path)
+ self.__ok &= self.__run_script(params.get("after"))
else:
- ok = False
+ self.__ok = False
self.__backup.commit()
finally:
self.__backup.close()
- return ok
+ return self.__ok
def close(self):
@@ -76,7 +106,6 @@ def __backup_path(self, path, filters, toplevel):
"""Backups the specified path."""
ok = True
-
LOG.info("Backing up '%s'...", path)
try:
11 pyvsb/config.py
View
@@ -5,6 +5,8 @@
import os
import re
+from collections import Callable
+
from .core import Error
@@ -27,6 +29,15 @@ def get_config(path):
_get_param(config_obj, config, "trust_modify_time", bool, default = True)
_get_param(config_obj, config, "preserve_hard_links", bool, default = True)
+ for handler_name in ( "on_group_created", "on_group_deleted", "on_backup_created" ):
+ if hasattr(config_obj, handler_name):
+ handler = getattr(config_obj, handler_name)
+
+ if not isinstance(handler, Callable):
+ raise Error("{} must be a callable object.", handler_name)
+
+ config[handler_name] = handler
+
return config
37 pyvsb/storage.py
View
@@ -32,11 +32,26 @@
class Storage:
"""Backup data storage abstraction."""
- def __init__(self, backup_root):
+ def __init__(
+ self, backup_root, on_group_created = None,
+ on_group_deleted = None, on_backup_created = None
+ ):
# Backup root directory
self.__backup_root = backup_root
+ # Event handlers
+
+ if on_group_created is not None:
+ self.__on_group_created = on_group_created
+
+ if on_group_deleted is not None:
+ self.__on_group_deleted = on_group_deleted
+
+ if on_backup_created is not None:
+ self.__on_backup_created = on_backup_created
+
+
def backup_path(self, group, name, temp = False):
"""Returns a path to the specified backup."""
@@ -84,6 +99,8 @@ def commit_backup(self, group, name):
raise Error("Unable to rename backup data directory '{}' to '{}': {}.",
cur_path, new_path, psys.e(e))
+ self.__on_backup_created(group, name, new_path)
+
@staticmethod
def create(backup_path):
@@ -153,9 +170,13 @@ def rotate_groups(self, max_backup_groups):
for group in groups[max_backup_groups:]:
LOG.info("Removing backup group '%s'...", group)
+
shutil.rmtree(self.group_path(group),
onerror = lambda func, path, excinfo:
LOG.error("Failed to remove '%s': %s.", path, psys.e(excinfo[1])))
+
+ if not os.path.exists(self.group_path(group)):
+ self.__on_group_deleted(group)
except Exception as e:
LOG.error("Failed to rotate backup groups: %s", e)
@@ -175,6 +196,8 @@ def __create_group(self):
raise Error("Unable to create a new backup group '{}': {}.",
group_path, psys.e(e))
+ self.__on_group_created(group)
+
return group
@@ -189,3 +212,15 @@ def __groups(self, check = False, reverse = False):
except EnvironmentError as e:
raise Error("Error while reading backup root directory '{}': {}.",
self.__backup_root, psys.e(e))
+
+
+ def __on_backup_created(self, logger, *args):
+ """An empty backup creation handler."""
+
+
+ def __on_group_created(self, logger, *args):
+ """An empty group creation handler."""
+
+
+ def __on_group_deleted(self, logger, *args):
+ """An empty group deletion handler."""
44 tests/test.py
View
@@ -97,6 +97,50 @@ def test_double(env, max_backups):
assert _hash_tree(env["restore_path"] + env["data_path"]) == source_tree
+@pytest.mark.parametrize("with_raise", ( False, True ))
+def test_on_backup_created_handler(env, with_raise):
+ log = []
+ groups = []
+
+ def on_group_created(logger, group):
+ assert group == _get_groups(env)[-1]
+ log.append("group_created")
+ groups.append(group)
+
+ def on_group_deleted(logger, group):
+ assert group == groups.pop(0)
+ log.append("group_deleted")
+
+ def on_backup_created(logger, group, name, path):
+ assert group == groups[-1]
+ assert name == os.path.basename(path)
+ assert path == _get_backups(env, group = group)[-1]
+ log.append("backup_created")
+
+ if with_raise:
+ raise Exception("Test exception")
+
+ env["config"]["max_backups"] = 1
+ env["config"]["max_backup_groups"] = 1
+
+ env["config"]["on_group_created"] = on_group_created
+ env["config"]["on_group_deleted"] = on_group_deleted
+ env["config"]["on_backup_created"] = on_backup_created
+
+
+ with Backuper(env["config"]) as backuper:
+ assert backuper.backup() == ( not with_raise )
+ assert log == [ "group_created", "backup_created" ]
+
+
+ time.sleep(1)
+ del log[:]
+
+ with Backuper(env["config"]) as backuper:
+ assert backuper.backup() == ( not with_raise )
+ assert log == [ "group_created", "backup_created", "group_deleted" ]
+
+
@pytest.mark.parametrize(( "max_groups", "max_backups" ), (
( 1, 1 ), ( 2, 1 ), ( 2, 3 ),
))
Please sign in to comment.
Something went wrong with that request. Please try again.