From 4a439a96abb04537152c99d170e84e79ae941ef1 Mon Sep 17 00:00:00 2001 From: xengineering Date: Fri, 23 Sep 2022 20:06:21 +0200 Subject: Refactor folder structure This removes the `install` target. Should be re-introduced later. --- Makefile | 22 ++--------- main.py | 21 +++++++++++ setup.py | 23 ++++++++++++ src/.gitignore | 1 - src/Makefile | 19 ---------- src/debug.py | 18 --------- src/scripts/xbackup | 21 ----------- src/setup.py | 23 ------------ src/xbackup/__init__.py | 0 src/xbackup/backup.py | 98 ------------------------------------------------- src/xbackup/config.py | 25 ------------- src/xbackup/prune.py | 49 ------------------------- src/xbackup/script.py | 94 ----------------------------------------------- src/xbackup/utils.py | 36 ------------------ xbackup/__init__.py | 0 xbackup/backup.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ xbackup/config.py | 25 +++++++++++++ xbackup/prune.py | 49 +++++++++++++++++++++++++ xbackup/script.py | 94 +++++++++++++++++++++++++++++++++++++++++++++++ xbackup/utils.py | 36 ++++++++++++++++++ 20 files changed, 349 insertions(+), 403 deletions(-) create mode 100755 main.py create mode 100644 setup.py delete mode 100644 src/.gitignore delete mode 100644 src/Makefile delete mode 100644 src/debug.py delete mode 100644 src/scripts/xbackup delete mode 100644 src/setup.py delete mode 100644 src/xbackup/__init__.py delete mode 100644 src/xbackup/backup.py delete mode 100644 src/xbackup/config.py delete mode 100644 src/xbackup/prune.py delete mode 100644 src/xbackup/script.py delete mode 100644 src/xbackup/utils.py create mode 100644 xbackup/__init__.py create mode 100644 xbackup/backup.py create mode 100644 xbackup/config.py create mode 100644 xbackup/prune.py create mode 100644 xbackup/script.py create mode 100644 xbackup/utils.py diff --git a/Makefile b/Makefile index 6a4c10e..d9dcf55 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,13 @@ # vim: shiftwidth=4 softtabstop=4 tabstop=4 noexpandtab - -DESTDIR="" # leave empty for the current system or provide a fakeroot here -PREFIX="/usr" - - -.PHONY: all clean install tarball - +.PHONY: all clean tarball all: - make -C src all - + python setup.py build clean: find . -type d -iname '__pycache__' -exec rm -rf {} +; - make -C src clean - - -install: all - install -Dm 644 config/default.json $(DESTDIR)/etc/xbackup/config.json - mkdir -p $(DESTDIR)/var/lib/xbackup/borg - chown -R root:root $(DESTDIR)/var/lib/xbackup - chmod -R 700 $(DESTDIR)/var/lib/xbackup - make -C src install DESTDIR=$(abspath $(DESTDIR)) - + rm -rf build tarball: clean tar --exclude-vcs -cvf xbackup.tar * diff --git a/main.py b/main.py new file mode 100755 index 0000000..569202e --- /dev/null +++ b/main.py @@ -0,0 +1,21 @@ +#!/usr/bin/python3 +# vim: shiftwidth=4 tabstop=4 expandtab + + +"""Main Executable of xbackup + +Have a look at https://cgit.xengineering.eu/xbackup for details. +""" + + +from xbackup import script + + +def main(): + """Main Function of this Script""" + + script.run() + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f83b318 --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +#!/usr/bin/python3 +# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab + + +"""setup.py + +Configuration file for setuptools. +""" + + +from distutils.core import setup + + +setup( + name="xbackup", + version="1.0.0-dev", + description="Convenience wrapper around the Borg backup tool", + author="xengineering", + author_email="me@xengineering.eu", + url="https://xengineering.eu", + packages=["xbackup"], + scripts=["main.py"], +) diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index 378eac2..0000000 --- a/src/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build diff --git a/src/Makefile b/src/Makefile deleted file mode 100644 index 5401e84..0000000 --- a/src/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -# vim: shiftwidth=4 softtabstop=4 tabstop=4 noexpandtab - - -DESTDIR="" # leave empty for the current system or provide a fakeroot here - - -.PHONY: all clean install - - -all: - python3 setup.py build - - -clean: - rm -rf build - - -install: all - python3 setup.py install --root=$(DESTDIR) --optimize=1 --skip-build diff --git a/src/debug.py b/src/debug.py deleted file mode 100644 index 1deaf45..0000000 --- a/src/debug.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/python3 -# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab - - -"""A Debug Script for Development Purposes""" - - -from xbackup import script - - -def main(): - """main function of this script""" - - script.run() - - -if __name__ == "__main__": - main() diff --git a/src/scripts/xbackup b/src/scripts/xbackup deleted file mode 100644 index 569202e..0000000 --- a/src/scripts/xbackup +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/python3 -# vim: shiftwidth=4 tabstop=4 expandtab - - -"""Main Executable of xbackup - -Have a look at https://cgit.xengineering.eu/xbackup for details. -""" - - -from xbackup import script - - -def main(): - """Main Function of this Script""" - - script.run() - - -if __name__ == "__main__": - main() diff --git a/src/setup.py b/src/setup.py deleted file mode 100644 index 0ba6d7a..0000000 --- a/src/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab - - -"""setup.py - -Configuration file for setuptools. -""" - - -from distutils.core import setup - - -setup( - name="xbackup", - version="1.0.0", - description="Convenience wrapper around the Borg backup tool", - author="xengineering", - author_email="me@xengineering.eu", - url="https://xengineering.eu", - packages=["xbackup"], - scripts=["scripts/xbackup"], -) diff --git a/src/xbackup/__init__.py b/src/xbackup/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/xbackup/backup.py b/src/xbackup/backup.py deleted file mode 100644 index 8378977..0000000 --- a/src/xbackup/backup.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/python3 -# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab -# pylint: disable=too-few-public-methods - - -"""backup.py - -This module contains the backup functionality of xbackup. -""" - - -import os - -from xbackup.utils import POSITIVE_ANSWERS,shell - - -def backup(generic_cfg, paths_cfg, scripted): - """perform a file-based full system backup""" - - # generate all necessary paths based on config - paths = Filepaths(generic_cfg, paths_cfg) - print(paths) - - # get confirmation from user if not in scripted execution mode - if not scripted: - answer = input("\nDo you want to continue? [y/N] ") - if answer not in POSITIVE_ANSWERS: - print("\nTerminated.") - return - - # init borg repository (accepting failure on already existing repo) - shell(f"borg init -e none {paths.borg_repo}", panic=False) - - # run backup - run_backup(paths) - - -class Filepaths(): - """A Container Class to hold Filepaths for the Borg Backup Tool""" - - def __init__(self, generic_cfg, paths_cfg): - """The Constructor""" - - # parse backup source (usually / for system backups) - self.backup_root = paths_cfg["backup_root"] - - # parse folder for borg repositories - self.borg_repos_folder = generic_cfg["borg_repos_folder"] - - # path to the borg repository - hostname = os.uname()[1] - self.borg_repo = os.path.join(self.borg_repos_folder, hostname) - - # create blacklist of folders to be excluded from the backup - self.blacklist = [] - for entry in paths_cfg["blacklist"]: - self.blacklist.append(os.path.join(self.backup_root, entry)) - - # avoid matroska-style backups of backups (yes, it is important) - self.blacklist.append(self.borg_repos_folder) - - def __str__(self): - """Converts an Object based on this Class to a String""" - - retval = "" - - retval += "\nbackup_root:\n" + self.backup_root + "\n" - - retval += "\nblacklist:\n" - for item in self.blacklist: - retval += item + "\n" - - retval += "\nborg_repo:\n" + self.borg_repo - - return retval - - -def run_backup(paths): - """Execute the Borg Backup""" - - # base command - command = "borg create -v --stats --compression zstd \\" - - # set repository path - command += \ - f"{paths.borg_repo}::'{{hostname}}-{{user}}-{{now:%Y-%m-%d_%H:%M:%S}}' \\" - - # set backup source - command += f"\n {paths.backup_root} \\" - - # append blacklist elements - for key,value in enumerate(paths.blacklist): - command += f"\n --exclude '{value}'" - if key != len(paths.blacklist) -1: - command += " \\" - - # run command - shell(command) diff --git a/src/xbackup/config.py b/src/xbackup/config.py deleted file mode 100644 index 4c72915..0000000 --- a/src/xbackup/config.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/python3 -# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab - - -"""Module for Configuration Parsing Functionality of xbackup""" - - -import json - - -def get(path): - """read and parse the xbackup configuration file""" - - with open(path, "r", encoding="utf-8") as _file: - content = _file.read() - - return json.loads(content) - -def dump(config, pretty=True): - """returns the given configuration as JSON string""" - - if pretty: - return json.dumps(config, indent=4) - - return json.dumps(config) diff --git a/src/xbackup/prune.py b/src/xbackup/prune.py deleted file mode 100644 index 7e57830..0000000 --- a/src/xbackup/prune.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/python3 -# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab - - -"""prune.py - -This module contains the pruning functionality of xbackup. -""" - - -import os - -from xbackup.utils import POSITIVE_ANSWERS,shell - - -def prune(generic_cfg, prune_cfg, scripted): - """prune backups""" - - # generate backup repository path - hostname = os.uname()[1] - repo = os.path.join(generic_cfg["borg_repos_folder"], hostname) - - # parse prune values - hourly = prune_cfg["keep-hourly"] - daily = prune_cfg["keep-daily"] - weekly = prune_cfg["keep-weekly"] - monthly = prune_cfg["keep-monthly"] - yearly = prune_cfg["keep-yearly"] - - # generate command - cmd = f"borg prune -v {repo}" - cmd += f" -H {hourly} -d {daily} -w {weekly} -m {monthly} -y {yearly}" - - # ask if execution is wanted - if not scripted: - text = f"\nGoing to prune your backups at '{repo}'." - text += "\nThis will delete every backup except:" - text += f"\n- {hourly} hourly backups" - text += f"\n- {daily} daily backups" - text += f"\n- {weekly} weekly backups" - text += f"\n- {monthly} monthly backups" - text += f"\n- {yearly} yearly backups" - text += "\nDo you want to continue? [y/N] " - answer = input(text) - if answer not in POSITIVE_ANSWERS: - return - - # execute pruning - shell(cmd, panic=True) diff --git a/src/xbackup/script.py b/src/xbackup/script.py deleted file mode 100644 index 9d80603..0000000 --- a/src/xbackup/script.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/python3 -# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab - - -"""Module for the Script Functionality of xbackup""" - - -import argparse -import sys - -from xbackup import config -from xbackup import backup -from xbackup import prune - - -def run(): - """runs xbackup script functionality""" - - args = parse_arguments() - cfg = config.get(args.config) - - welcome() - - if args.command == "backup": - backup.backup(cfg["generic"], cfg["paths"], args.scripted) - elif args.command == "prune": - prune.prune(cfg["generic"], cfg["prune"], args.scripted) - else: - print(f"Unknown command '{args.command}'") - sys.exit(1) - - bye() - -def parse_arguments(): - """handles argument parsing with the argparse module""" - - # parser creation - parser = argparse.ArgumentParser( - prog="xbackup", - description= \ - "xbackup CLI Script to make a File-based full System Backup", - epilog= \ - "Project page: https://cgit.xengineering.eu/xbackup", - ) - - # main command argument (positional) - parser.add_argument("command", type=str, - help="the action to be performed", - choices=["backup", "prune"] - ) - - # path to configuration file (optional) - parser.add_argument("-c", "--config", type=str, - metavar="CONFIG", - help="path to the JSON configuration file", - default="/etc/xbackup/config.json", - ) - - # flag for scripted execution (optional) - parser.add_argument("-s", "--script", - dest="scripted", - action="store_true", - help="use this flag to enable a non-interactive mode" - ) - parser.set_defaults(scripted=False) - - # argument parsing - args = parser.parse_args() - return args - - -def welcome(): - """Print Welcome Text""" - - print(r""" -################################################################################ - _ _ - __ _| |__ __ _ ___| | ___ _ _ __ - \ \/ / '_ \ / _` |/ __| |/ / | | | '_ \ - > <| |_) | (_| | (__| <| |_| | |_) | - /_/\_\_.__/ \__,_|\___|_|\_\\__,_| .__/ - |_| - -################################################################################ -""", end="") - - -def bye(): - """Print final Text""" - - print(""" -################################################################################ - -""", end="") diff --git a/src/xbackup/utils.py b/src/xbackup/utils.py deleted file mode 100644 index e1480d1..0000000 --- a/src/xbackup/utils.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/python3 -# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab - - -"""utils.py - -This module contains reusable code for xbackup. -""" - - -import sys -import subprocess - - -POSITIVE_ANSWERS = ["Y", "y", "Yes", "yes", "YES"] - - -def shell(command, panic=True): - """Savely execute a Shell Command - - - set panic=False to continue with execution on non-zero return code - """ - - # print command - print("\nExecuting '" + command + "' ...") - - # command execution - return_code = subprocess.call(command, shell=True) - - # handle non-zero return code - if return_code != 0 and panic: - print(f"Command '{command}'\nfailed with return code {return_code}") - sys.exit(return_code) - - # final message and return - print("... done!") diff --git a/xbackup/__init__.py b/xbackup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xbackup/backup.py b/xbackup/backup.py new file mode 100644 index 0000000..8378977 --- /dev/null +++ b/xbackup/backup.py @@ -0,0 +1,98 @@ +#!/usr/bin/python3 +# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab +# pylint: disable=too-few-public-methods + + +"""backup.py + +This module contains the backup functionality of xbackup. +""" + + +import os + +from xbackup.utils import POSITIVE_ANSWERS,shell + + +def backup(generic_cfg, paths_cfg, scripted): + """perform a file-based full system backup""" + + # generate all necessary paths based on config + paths = Filepaths(generic_cfg, paths_cfg) + print(paths) + + # get confirmation from user if not in scripted execution mode + if not scripted: + answer = input("\nDo you want to continue? [y/N] ") + if answer not in POSITIVE_ANSWERS: + print("\nTerminated.") + return + + # init borg repository (accepting failure on already existing repo) + shell(f"borg init -e none {paths.borg_repo}", panic=False) + + # run backup + run_backup(paths) + + +class Filepaths(): + """A Container Class to hold Filepaths for the Borg Backup Tool""" + + def __init__(self, generic_cfg, paths_cfg): + """The Constructor""" + + # parse backup source (usually / for system backups) + self.backup_root = paths_cfg["backup_root"] + + # parse folder for borg repositories + self.borg_repos_folder = generic_cfg["borg_repos_folder"] + + # path to the borg repository + hostname = os.uname()[1] + self.borg_repo = os.path.join(self.borg_repos_folder, hostname) + + # create blacklist of folders to be excluded from the backup + self.blacklist = [] + for entry in paths_cfg["blacklist"]: + self.blacklist.append(os.path.join(self.backup_root, entry)) + + # avoid matroska-style backups of backups (yes, it is important) + self.blacklist.append(self.borg_repos_folder) + + def __str__(self): + """Converts an Object based on this Class to a String""" + + retval = "" + + retval += "\nbackup_root:\n" + self.backup_root + "\n" + + retval += "\nblacklist:\n" + for item in self.blacklist: + retval += item + "\n" + + retval += "\nborg_repo:\n" + self.borg_repo + + return retval + + +def run_backup(paths): + """Execute the Borg Backup""" + + # base command + command = "borg create -v --stats --compression zstd \\" + + # set repository path + command += \ + f"{paths.borg_repo}::'{{hostname}}-{{user}}-{{now:%Y-%m-%d_%H:%M:%S}}' \\" + + # set backup source + command += f"\n {paths.backup_root} \\" + + # append blacklist elements + for key,value in enumerate(paths.blacklist): + command += f"\n --exclude '{value}'" + if key != len(paths.blacklist) -1: + command += " \\" + + # run command + shell(command) diff --git a/xbackup/config.py b/xbackup/config.py new file mode 100644 index 0000000..4c72915 --- /dev/null +++ b/xbackup/config.py @@ -0,0 +1,25 @@ +#!/usr/bin/python3 +# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab + + +"""Module for Configuration Parsing Functionality of xbackup""" + + +import json + + +def get(path): + """read and parse the xbackup configuration file""" + + with open(path, "r", encoding="utf-8") as _file: + content = _file.read() + + return json.loads(content) + +def dump(config, pretty=True): + """returns the given configuration as JSON string""" + + if pretty: + return json.dumps(config, indent=4) + + return json.dumps(config) diff --git a/xbackup/prune.py b/xbackup/prune.py new file mode 100644 index 0000000..7e57830 --- /dev/null +++ b/xbackup/prune.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 +# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab + + +"""prune.py + +This module contains the pruning functionality of xbackup. +""" + + +import os + +from xbackup.utils import POSITIVE_ANSWERS,shell + + +def prune(generic_cfg, prune_cfg, scripted): + """prune backups""" + + # generate backup repository path + hostname = os.uname()[1] + repo = os.path.join(generic_cfg["borg_repos_folder"], hostname) + + # parse prune values + hourly = prune_cfg["keep-hourly"] + daily = prune_cfg["keep-daily"] + weekly = prune_cfg["keep-weekly"] + monthly = prune_cfg["keep-monthly"] + yearly = prune_cfg["keep-yearly"] + + # generate command + cmd = f"borg prune -v {repo}" + cmd += f" -H {hourly} -d {daily} -w {weekly} -m {monthly} -y {yearly}" + + # ask if execution is wanted + if not scripted: + text = f"\nGoing to prune your backups at '{repo}'." + text += "\nThis will delete every backup except:" + text += f"\n- {hourly} hourly backups" + text += f"\n- {daily} daily backups" + text += f"\n- {weekly} weekly backups" + text += f"\n- {monthly} monthly backups" + text += f"\n- {yearly} yearly backups" + text += "\nDo you want to continue? [y/N] " + answer = input(text) + if answer not in POSITIVE_ANSWERS: + return + + # execute pruning + shell(cmd, panic=True) diff --git a/xbackup/script.py b/xbackup/script.py new file mode 100644 index 0000000..9d80603 --- /dev/null +++ b/xbackup/script.py @@ -0,0 +1,94 @@ +#!/usr/bin/python3 +# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab + + +"""Module for the Script Functionality of xbackup""" + + +import argparse +import sys + +from xbackup import config +from xbackup import backup +from xbackup import prune + + +def run(): + """runs xbackup script functionality""" + + args = parse_arguments() + cfg = config.get(args.config) + + welcome() + + if args.command == "backup": + backup.backup(cfg["generic"], cfg["paths"], args.scripted) + elif args.command == "prune": + prune.prune(cfg["generic"], cfg["prune"], args.scripted) + else: + print(f"Unknown command '{args.command}'") + sys.exit(1) + + bye() + +def parse_arguments(): + """handles argument parsing with the argparse module""" + + # parser creation + parser = argparse.ArgumentParser( + prog="xbackup", + description= \ + "xbackup CLI Script to make a File-based full System Backup", + epilog= \ + "Project page: https://cgit.xengineering.eu/xbackup", + ) + + # main command argument (positional) + parser.add_argument("command", type=str, + help="the action to be performed", + choices=["backup", "prune"] + ) + + # path to configuration file (optional) + parser.add_argument("-c", "--config", type=str, + metavar="CONFIG", + help="path to the JSON configuration file", + default="/etc/xbackup/config.json", + ) + + # flag for scripted execution (optional) + parser.add_argument("-s", "--script", + dest="scripted", + action="store_true", + help="use this flag to enable a non-interactive mode" + ) + parser.set_defaults(scripted=False) + + # argument parsing + args = parser.parse_args() + return args + + +def welcome(): + """Print Welcome Text""" + + print(r""" +################################################################################ + _ _ + __ _| |__ __ _ ___| | ___ _ _ __ + \ \/ / '_ \ / _` |/ __| |/ / | | | '_ \ + > <| |_) | (_| | (__| <| |_| | |_) | + /_/\_\_.__/ \__,_|\___|_|\_\\__,_| .__/ + |_| + +################################################################################ +""", end="") + + +def bye(): + """Print final Text""" + + print(""" +################################################################################ + +""", end="") diff --git a/xbackup/utils.py b/xbackup/utils.py new file mode 100644 index 0000000..e1480d1 --- /dev/null +++ b/xbackup/utils.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 +# vim: shiftwidth=4 softtabstop=4 tabstop=4 expandtab + + +"""utils.py + +This module contains reusable code for xbackup. +""" + + +import sys +import subprocess + + +POSITIVE_ANSWERS = ["Y", "y", "Yes", "yes", "YES"] + + +def shell(command, panic=True): + """Savely execute a Shell Command + + - set panic=False to continue with execution on non-zero return code + """ + + # print command + print("\nExecuting '" + command + "' ...") + + # command execution + return_code = subprocess.call(command, shell=True) + + # handle non-zero return code + if return_code != 0 and panic: + print(f"Command '{command}'\nfailed with return code {return_code}") + sys.exit(return_code) + + # final message and return + print("... done!") -- cgit v1.2.3-70-g09d2