#!/usr/bin/python3 """ Run a backup. Command-Line Arguments ====================== * -c, --use-checksums use rsync's checksum feature to detect file changes * -d, --dry-run make this backup a dry run * -s, --run-post-sync run sync syscall after backup * -v, --verbose increase script verbosity On each run of this script, a new snapshot is made and any unchanged files are hardlinked into this new snapshot. """ import argparse import logging import os import sys from rbackup.hierarchy.repository import Repository from rbackup.rsync import rsync # ========== Constants ========== LOGFORMAT = "==> %(levelname)s %(message)s" RSYNC_DEFAULT_OPTS = [ "--acls", "--archive", "--backup", "--hard-links", "--ignore-missing-args", "--prune-dirs", "--suffix=.old", "--xattrs", ] EXTRA_RSYNC_OPTS = { "dry_run": "--dry-run", "delete": "--delete-after", "checksum": "--checksum", "update": "--update", } ETC_INCLUDE_FILE = "/etc/rbackup/etc-include.conf" # ========== Logging Setup ========== console_formatter = logging.Formatter(LOGFORMAT) syslog = logging.getLogger("rbackup") syslog.setLevel(logging.DEBUG) stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setLevel(logging.INFO) stdout_handler.setFormatter(console_formatter) stdout_handler.addFilter(lambda record: record.levelno <= logging.INFO) stderr_handler = logging.StreamHandler(sys.stderr) stderr_handler.setLevel(logging.WARNING) stderr_handler.setFormatter(console_formatter) syslog.addHandler(stdout_handler) syslog.addHandler(stderr_handler) # ========== Functions ========== def backup_all(s, rsync_opts): raise NotImplementedError def backup_etc(s, rsync_opts): """Create a backup of /etc :param s: snapshot to back up to :type s: Snapshot object :param opts: options to pass to rsync :type opts: list """ local_opts = rsync_opts local_opts.append(f"--files-from={ETC_INCLUDE_FILE}") syslog.debug("Creating directory {s.etc_path}") s.etc_dir.mkdir(parents=True, exist_ok=False) rsync(*local_opts, "/etc/", str(s.etc_path)) def backup_home(): raise NotImplementedError def backup_pkgmanager(): raise NotImplementedError def backup_system(): raise NotImplementedError DISPATCHER = { "all": backup_all, "etc": backup_etc, "home": backup_home, "system": backup_system, } # ========== Main Script ========== if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "-c", "--use-checksums", action="store_const", dest="extra_rsync_opts", const=EXTRA_RSYNC_OPTS["checksum"], help="use rsync's checksumming feature to look for changed files", ) parser.add_argument( "-d", "--dry-run", action="append_const", dest="extra_rsync_opts", const=EXTRA_RSYNC_OPTS["dry_run"], help="pass --dry-run to rsync", ) parser.add_argument( "-s", "--run-post-sync", action="store_true", help="run sync operation after backup is complete", ) parser.add_argument( "-v", "--verbose", action="store_true", help="increase script verbosity", ) parser.add_argument( "operation", choices=["all", "etc", "home", "pkg", "system"], help="the operation to perform", metavar="op", ) parser.add_argument( "repository", help="repository to back up to", metavar="repo" ) args = parser.parse_args() rsync_opts = [] if args.extra_rsync_opts is not None: rsync_opts.extend(args.extra_rsync_opts) args = parser.parse_args() repo = Repository(args.repository) if args.verbose: stdout_handler.setLevel(logging.DEBUG) if repo.empty: prev_snapshot = None else: prev_snapshot = repo.curr_snapshot repo.create_snapshot() curr_snapshot = repo.curr_snapshot rsync_opts.append(f"--link-dest={prev_snapshot}") rsync_opts.append("--backup-dir=backup") DISPATCHER[args.operation](curr_snapshot, rsync_opts) snapshot_symlink = repo.path / "current" try: snapshot_symlink.unlink() except FileNotFoundError: pass snapshot_symlink.symlink_to(curr_snapshot, target_is_directory=True) if args.run_post_sync: os.sync()