diff --git a/bin/backup b/bin/backup index 1eab51d..6e3867c 100644 --- a/bin/backup +++ b/bin/backup @@ -23,15 +23,12 @@ import argparse import logging import os import sys -from contextlib import contextmanager -from subprocess import CalledProcessError import rbackup.config.config_files as config -import rbackup.rsync +import rbackup.script as script from rbackup.struct.repository import Repository # ========== Constants ========== -SCRIPT_UMASK = 0000 LOGFORMAT = "==> %(levelname)s %(message)s" EXTRA_RSYNC_OPTS = { "dry_run": "--dry-run", @@ -40,11 +37,10 @@ EXTRA_RSYNC_OPTS = { "update": "--update", } - # ----- Error Codes ----- -E_INVALID_SNAPSHOT_NAME = 2 E_PERMISSION = 13 + # ========== Logging Setup ========== console_formatter = logging.Formatter(LOGFORMAT) syslog = logging.getLogger("rbackup") @@ -111,47 +107,18 @@ def parse_cmdline_arguments(**kwargs): return parser.parse_args(**kwargs) -@contextmanager -def change_umask(override=None): - """Creates a context manager in which the umask is changed. - This is to ensure that the script's desired umask is not - visible to the user. - - :param override: non-script-default umask value to use - :type override: int - """ - try: - if override is not None: - old_umask = os.umask(override) - else: - old_umask = os.umask(SCRIPT_UMASK) - yield - finally: - os.umask(old_umask) - - # ========== Main Script ========== if __name__ == "__main__": args = parse_cmdline_arguments() - parsed_config = config.parse_configfile() - - repo = None try: + parsed_config = config.parse_configfile() repo = Repository(args.repository) except PermissionError as e: syslog.critical(e) exit(E_PERMISSION) - - rsync_opts = config.load_list_from_option( - parsed_config, - section="main", - option="RsyncOptions", - fallback=rbackup.rsync.DEFAULT_RSYNC_OPTS.copy(), - ) - - if args.extra_rsync_opts is not None: - rsync_opts.extend(args.extra_rsync_opts) + else: + script.do_backup(repo, parsed_config, args) if args.verbose: stdout_handler.setLevel(logging.INFO) @@ -159,32 +126,6 @@ if __name__ == "__main__": if args.debug: stdout_handler.setLevel(logging.DEBUG) - # We want to iterate through the repository and create the --link-dest - # options before creating the new snapshot - link_dests = tuple(f"--link-dest={s.path}" for s in repo) - - curr_snapshot = None - - with change_umask(args.umask), config.merge_include_files() as include_file, config.merge_exclude_files() as exclude_file: - try: - curr_snapshot = repo.create_snapshot(args.name) - rbackup.rsync.rsync( - *rsync_opts, - f"--files-from={include_file}", - f"--exclude-from={exclude_file}", - *link_dests, - "/", - str(curr_snapshot), - ) - except ValueError as e: - syslog.critical(e) - exit(E_INVALID_SNAPSHOT_NAME) - except CalledProcessError as e: - syslog.critical("Backup process failed") - syslog.critical(f"Failing command: {e.cmd}") - syslog.critical(e.stderr) - exit(e.returncode) - if args.run_post_sync: syslog.info("Running sync operation") os.sync() diff --git a/rbackup/script.py b/rbackup/script.py new file mode 100644 index 0000000..ce52073 --- /dev/null +++ b/rbackup/script.py @@ -0,0 +1,71 @@ +""" +.. moduleauthor:: Eric Torres +.. module:: rbackup.script + :synopsis: library for common script operations +""" + +import logging +from subprocess import CalledProcessError + +import rbackup.config.config_files as config +import rbackup.rsync +import rbackup.system as system + +# ========== Constants ========== +# ----- Error Codes ----- +E_INVALID_SNAPSHOT_NAME = 2 +E_PERMISSION = 13 + + +# ========== Logging Setup =========== +syslog = logging.getLogger(__name__) + + +# ========== Functions ========== +def do_backup(repository, parsed_config, args): + """Run a backup operation. + + It is up to the caller to decide on how to instantiate the Repository object. + + :param repository: repository to operate on + :type repository: ``rbackup.struct.repository.Repository`` object + :param parsed_config: + :type parsed_config: ``configparser.ConfigParser`` object + :param args: arguments relevant to the backup operation + :type args: ``argparse.Namespace`` object + """ + rsync_opts = config.load_list_from_option( + parsed_config, + section="main", + option="RsyncOptions", + fallback=rbackup.rsync.DEFAULT_RSYNC_OPTS.copy(), + ) + + if args.extra_rsync_opts is not None: + rsync_opts.extend(args.extra_rsync_opts) + + # We want to iterate through the repository and create the --link-dest + # options before creating the new snapshot + link_dests = tuple(f"--link-dest={s.path}" for s in repository) + + with system.change_umask( + args.umask + ), config.merge_include_files() as include_file, config.merge_exclude_files() as exclude_file: + try: + curr_snapshot = repository.create_snapshot(args.name) + rbackup.rsync.rsync( + *rsync_opts, + f"--files-from={include_file}", + f"--exclude-from={exclude_file}", + *link_dests, + "/", + str(curr_snapshot), + ) + except ValueError as e: + syslog.critical(e) + exit(E_INVALID_SNAPSHOT_NAME) + except CalledProcessError as e: + syslog.critical("Backup process failed") + syslog.critical(f"Failing command: {e.cmd}") + syslog.critical(e.stderr) + exit(e.returncode) diff --git a/rbackup/system.py b/rbackup/system.py new file mode 100644 index 0000000..4fada45 --- /dev/null +++ b/rbackup/system.py @@ -0,0 +1,36 @@ +""" +.. moduleauthor:: Eric Torres +.. module:: rbackup.system + :synopsis: library for interacting with rbackup over the network +""" + +import logging +import os +from contextlib import contextmanager + +# ========== Constants ========== +DEFAULT_UMASK = 0000 + + +# ========== Logging Setup =========== +syslog = logging.getLogger(__name__) + + +# ========== Functions ========== +@contextmanager +def change_umask(override=None): + """Creates a context manager in which the umask is changed. + This is to ensure that the script's desired umask is not + visible to the user. + + :param override: non-script-default umask value to use + :type override: int + """ + try: + if override is not None: + old_umask = os.umask(override) + else: + old_umask = os.umask(DEFAULT_UMASK) + yield + finally: + os.umask(old_umask)