rbackup/bin/backup
2019-04-10 18:08:36 -07:00

154 lines
4.0 KiB
Python

#!/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
from subprocess import CalledProcessError
# ========== Constants ==========
LOGFORMAT = "==> %(levelname)s %(message)s"
RSYNC_DEFAULT_OPTS = [
"--acls",
"--archive",
"--backup",
"--hard-links",
"--ignore-missing-args",
"--prune-empty-dirs",
"--suffix=.old",
"--xattrs",
]
EXTRA_RSYNC_OPTS = {
"dry_run": "--dry-run",
"delete": "--delete-after",
"checksum": "--checksum",
"update": "--update",
}
CONFIG_DIR = "/etc/rbackup"
ETC_INCLUDE_FILE = f"{CONFIG_DIR}/etc-include.conf"
HOME_EXCLUDE_FILE = f"{CONFIG_DIR}/home-exclude.conf"
# ----- Error Codes -----
E_FAILED_PROCESS = 1
# ========== 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 parse_cmdline_arguments(*args, **kwargs):
"""Parse command line arguments passed to the script.
All args and kwargs are passed to ArgumentParser.parse_args()
:rtype: argparse.Namespace object
"""
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 = RSYNC_DEFAULT_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("--backup-dir=backup")
try:
DISPATCHER[args.operation](curr_snapshot, prev_snapshot, rsync_opts)
except CalledProcessError as e:
syslog.error("Backup process failed")
syslog.info(f"Failing command: {e.cmd}")
exit(E_FAILED_PROCESS)
snapshot_symlink = repo.path / "current"
try:
snapshot_symlink.unlink()
except FileNotFoundError:
pass
snapshot_symlink.symlink_to(curr_snapshot.path, target_is_directory=True)
if args.run_post_sync:
os.sync()