diff --git a/PKGBUILD b/PKGBUILD index b8c1932..f8e599e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -3,12 +3,12 @@ #Maintainer: Daniele Fucini # -pkgname=simple-backup +pkgname=simple_backup pkgver=2.0.0.r3.gc61c704 pkgrel=1 pkgdesc='Simple backup script that uses rsync to copy files' arch=('any') -url="https://github.com/Fuxino/simple-backup.git" +url="https://github.com/Fuxino/simple_backup.git" license=('GPL3') makedepends=('git') depends=('python3' diff --git a/README.md b/README.md index ff79de7..5d48a10 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# simple-backup +# simple_backup A simple backup script ## Description -simple-backup is a Python script that allows you to backup your files. +simple_backup is a Python script that allows you to backup your files. It reads from a configuration file the files/directories that must be copied, the destination directory for the backup and a few other options. diff --git a/simple-backup.py b/simple-backup.py deleted file mode 100755 index 776f2a9..0000000 --- a/simple-backup.py +++ /dev/null @@ -1,854 +0,0 @@ -#!/usr/bin/python3 - -# Import libraries -from sys import exit, argv - -import os -from os.path import expanduser, isfile, isdir, islink, exists, abspath -from shutil import move, rmtree - -import subprocess - -from datetime import datetime -from tempfile import mkstemp - - -class Backup: - - def __init__(self, *args, **kwargs): - super(Backup, self).__init__(*args, **kwargs) - - self.log_path = '' - self.logfile = None - self.err_path = '' - self.errfile = None - self.warn_path = '' - self.warnfile = None - self.homedir = '' - self.backup_dev = '' - self.backup_dir = '' - self.last_backup = '' - self.inputs = '' - self.inputs_path = '' - self.exclude_path = '' - self.exclude = '' - self.options = '' - self.keep = -1 - self.n_in = 0 - - # Help function - @staticmethod - def help_function(): - print('simple_backup, version 2.0.0') - print('') - print('Usage: {} [OPTIONS]'.format(argv[0])) - print('') - print('Options:') - print('-h, --help Print this help and exit.') - print('-c, --config CONFIG_FILE Use the specified configuration file') - print(' instead of the default one.') - print(' All other options are ignored.') - print('-i, --input INPUT [INPUT...] Specify a file/dir to include in the backup.') - print('-d, --directory DIR Specify the output directory for the backup.') - print('-e, --exclude PATTERN [PATTERN...] Specify a file/dir/pattern to exclude from') - print(' the backup.') - print('-k, --keep NUMBER Specify the number of old backups to keep.') - print(' Default: keep all.') - print('-s, --checksum Use the checksum rsync option to compare files') - print(' (MUCH slower).') - print('') - print('If no option is given, the program uses the default') - print('configuration file: $HOMEDIR/.simple_backup/config.') - print('') - print('Report bugs to dfucini@gmail.com') - - exit(0) - - # Function to read configuration file - def read_conf(self, config=None): - if config is None: - config = self.homedir + '/.simple_backup/config' - - if not isfile(config): - # If default config file doesn't exist, exit - log_message = str(datetime.now()) + ': Backup failed (see errors.log)' - self.logfile.write(log_message) - self.logfile.write('\n') - print('Backup failed') - err_message = 'Error: Configuration file not found' - print(err_message) - self.errfile.write(err_message) - self.errfile.write('\n') - - self.logfile.close() - self.errfile.close() - self.warnfile.close() - - try: - move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log') - move(self.err_path, self.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(self.homedir)) - - try: - os.remove(self.warn_path) - except Exception: - print('Failed to remove temporary file') - - exit(1) - else: - if not isfile(config): - # If the provided configuration file doesn't exist, exit - log_message = str(datetime.now()) + ': Backup failed (see errors.log)' - self.logfile.write(log_message) - self.logfile.write('\n') - print('Backup failed') - err_message = 'Error: Configuration file not found' - print(err_message) - self.errfile.write(err_message) - self.errfile.write('\n') - - self.logfile.close() - self.errfile.close() - self.warnfile.close() - - try: - move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log') - move(self.err_path, self.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(self.homedir)) - - try: - os.remove(self.warn_path) - except Exception: - print('Failed to remove temporary file') - - exit(1) - - # Create temporary files - inputs_handle, self.inputs_path = mkstemp(prefix='tmp_inputs', text=True) - exclude_handle, self.exclude_path = mkstemp(prefix='tmp_exclude', text=True) - - # Open temporary files - self.inputs = open(self.inputs_path, 'w') - self.exclude = open(self.exclude_path, 'w') - - # Parse the configuration file - with open(config, 'r') as fp: - line = fp.readline() - - while line: - if line[:7] == 'inputs=': - line = line[7:].rstrip() - input_values = line.split(',') - for i in input_values: - if not exists(i): - warn_message = 'Warning: input "' + i + '" not found. Skipping' - print(warn_message) - self.warnfile.write(warn_message) - self.warnfile.write('\n') - else: - self.inputs.write(i) - self.inputs.write('\n') - self.n_in = self.n_in + 1 - elif line[:11] == 'backup_dir=': - line = line[11:].rstrip() - self.backup_dev = line - elif line[:8] == 'exclude=': - line = line[8:].rstrip() - exclude_values = line.split(',') - for i in exclude_values: - self.exclude.write(i) - self.exclude.write('\n') - elif line[:5] == 'keep=': - line = line[5:].rstrip() - self.keep = int(line) - - line = fp.readline() - - fp.close() - self.inputs.close() - self.exclude.close() - - # If the backup directory is not set or doesn't exist, exit - if self.backup_dev == '' or not isdir(self.backup_dev): - log_message = str(datetime.now()) + ': Backup failed (see errors.log)' - self.logfile.write(log_message) - self.logfile.write('\n') - print('Backup failed') - err_message = 'Error: Output folder "' + self.backup_dev + '" not found' - print(err_message) - self.errfile.write(err_message) - self.errfile.write('\n') - - self.logfile.close() - self.errfile.close() - self.warnfile.close() - - try: - move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log') - move(self.err_path, self.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(self.homedir)) - - try: - os.remove(self.warn_path) - os.remove(self.inputs_path) - os.remove(self.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(1) - - self.backup_dir = self.backup_dev + '/simple_backup' - date = str(datetime.now()) - - # Create the backup subdirectory using date - if isdir(self.backup_dir): - # If previous backups exist, save link to the last backup - self.last_backup = self.backup_dir + '/last_backup' - if islink(self.last_backup): - try: - self.last_backup = os.readlink(self.last_backup) - except Exception: - self.last_backup = '' - err_message = 'An error occurred when reading the last_backup link. Continuing anyway' - print(err_message) - self.errfile.write(err_message) - self.errfile.write('\n') - - else: - self.last_backup = '' - - self.backup_dir = self.backup_dir + '/' + date - - try: - os.makedirs(self.backup_dir) - except PermissionError as e: - log_message = str(datetime.now()) + 'Backup failed (see errors.log)' - self.logfile.write(log_message) - self.logfile.write('\n') - print('Backup failed') - print(str(e)) - self.errfile.write(str(e)) - self.errfile.write('\n') - - self.logfile.close() - self.errfile.close() - self.warnfile.close() - - try: - move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log') - move(self.err_path, self.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(self.homedir)) - - try: - os.remove(self.warn_path) - os.remove(self.inputs_path) - os.remove(self.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(1) - except Exception: - log_message = str(datetime.now()) + 'Backup failed (see errors.log)' - self.logfile.write(log_message) - self.logfile.write('\n') - print(log_message) - err_message = 'Failed to create backup directory' - self.errfile.write(err_message) - self.errfile.write('\n') - - self.logfile.close() - self.errfile.close() - self.warnfile.close() - - try: - move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log') - move(self.err_path, self.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(self.homedir)) - - try: - os.remove(self.warn_path) - os.remove(self.inputs_path) - os.remove(self.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(1) - - # Function to parse options - def parse_options(self): - # Create temporary files - inputs_handle, self.inputs_path = mkstemp(prefix='tmp_inputs', text=True) - exclude_handle, self.exclude_path = mkstemp(prefix='tmp_exclude', text=True) - - # Open temporary files - self.inputs = open(self.inputs_path, 'w') - self.exclude = open(self.exclude_path, 'w') - - i = 1 - while i < len(argv): - var = argv[i] - - if var in ['-h', '--help']: - self.help_function() - elif var in ['-i', '--input']: - val = argv[i+1] - while i < len(argv) - 1 and val[0] != '-': - inp = val - - if not exists(inp): - warn_message = 'Warning: input "' + inp + '" not found. Skipping' - print(warn_message) - self.warnfile.write(warn_message) - self.warnfile.write('\n') - else: - self.n_in = self.n_in + 1 - self.inputs.write(inp) - self.inputs.write('\n') - - i = i + 1 - val = argv[i+1] - elif var in ['-d', '--directory']: - self.backup_dev = argv[i+1] - self.backup_dev = abspath(self.backup_dev) - - if not exists(self.backup_dev) or not isdir(self.backup_dev): - log_message = str(datetime.now()) + ': Backup failed (see errors.log)' - self.logfile.write(log_message) - self.logfile.write('\n') - print('Backup failed') - err_message = 'Error: output folder "' + self.backup_dev + '" not found' - print(err_message) - self.errfile.write(err_message) - self.errfile.write('\n') - - self.logfile.close() - self.errfile.close() - self.warnfile.close() - self.inputs.close() - self.exclude.close() - - try: - move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log') - move(self.err_path, self.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(self.homedir)) - - try: - os.remove(self.warn_path) - os.remove(self.inputs_path) - os.remove(self.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(1) - - self.backup_dir = self.backup_dev + '/simple_backup' - date = str(datetime.now()) - - # Create the backup subdirectory using date - if isdir(self.backup_dir): - # If previous backups exist, save link to the last backup - self.last_backup = self.backup_dir + '/last_backup' - if islink(self.last_backup): - try: - self.last_backup = os.readlink(self.last_backup) - except Exception: - self.last_backup = '' - err_message = 'An error occurred when reading the last_backup link. Continuing anyway' - print(err_message) - self.errfile.write(err_message) - self.errfile.write('\n') - - else: - self.last_backup = '' - - self.backup_dir = self.backup_dir + '/' + date - - try: - os.makedirs(self.backup_dir) - except PermissionError as e: - log_message = str(datetime.now()) + 'Backup failed (see errors.log)' - self.logfile.write(log_message) - self.logfile.write('\n') - print('Backup failed') - print(str(e)) - self.errfile.write(str(e)) - self.errfile.write('\n') - - self.logfile.close() - self.errfile.close() - self.warnfile.close() - self.inputs.close() - self.exclude.close() - - try: - move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log') - move(self.err_path, self.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(self.homedir)) - - try: - os.remove(self.warn_path) - os.remove(self.inputs_path) - os.remove(self.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(1) - except Exception: - log_message = str(datetime.now()) + 'Backup failed (see errors.log)' - self.logfile.write(log_message) - self.logfile.write('\n') - print('Backup failed') - err_message = 'Failed to create backup directory' - print(err_message) - self.errfile.write(err_message) - self.errfile.write('\n') - - self.logfile.close() - self.errfile.close() - self.warnfile.close() - self.inputs.close() - self.exclude.close() - - try: - move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log') - move(self.err_path, self.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(self.homedir)) - - try: - os.remove(self.warn_path) - os.remove(self.inputs_path) - os.remove(self.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(1) - - i = i + 1 - elif var in ['-e', '--exclude']: - val = argv[i+1] - while i < len(argv) - 1 and val[0] != '-': - exc = val - self.exclude.write(exc) - self.exclude.write('\n') - - i = i + 1 - val = argv[i+1] - elif var in ['-k', '--keep']: - self.keep = int(argv[i+1]) - - i = i + 1 - elif var in ['-c', '--config']: - self.read_conf(argv[i+1]) - - i = i + 1 - elif var in ['-s', '--checksum']: - self.options = '-arcvh -H -X' - else: - log_message = str(datetime.now()) + ': Backup failed (see errors.log)' - self.logfile.write(log_message) - self.logfile.write('\n') - print('Backup failed') - err_message = 'Error: Option "' + var +\ - '" not recognised. Use "simple-backup -h" to see available options' - print(err_message) - self.errfile.write(err_message) - self.errfile.write('\n') - - self.logfile.close() - self.errfile.close() - self.warnfile.close() - self.inputs.close() - self.exclude.close() - - try: - move(self.log_path, self.homedir + '/.simple_backup/simple_backup.log') - move(self.err_path, self.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(self.homedir)) - - try: - os.remove(self.warn_path) - os.remove(self.inputs_path) - os.remove(self.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(1) - - i = i + 1 - - self.inputs.close() - self.exclude.close() - - def exec_(self): - print('Copying files. This may take a long time...') - - if self.last_backup == '': - rsync = 'rsync ' + self.options + ' --exclude-from=' + self.exclude_path +\ - ' --files-from=' + self.inputs_path + ' / "' + self.backup_dir +\ - '" --ignore-missing-args >> ' + self.log_path + ' 2>> ' + self.err_path - else: - rsync = 'rsync ' + self.options + ' --link-dest="' + self.last_backup + '" --exclude-from=' +\ - self.exclude_path + ' --files-from=' + self.inputs_path + ' / "' + self.backup_dir +\ - '" --ignore-missing-args >> ' + self.log_path + ' 2>> ' + self.err_path - - subprocess.run(rsync, shell=True) - - -def main(): - - backup = Backup() - - # Create temporary log files - log_handle, backup.log_path = mkstemp(prefix='tmp_log', text=True) - err_handle, backup.err_path = mkstemp(prefix='tmp_err', text=True) - warn_handle, backup.warn_path = mkstemp(prefix='tmp_warn', text=True) - - # Open log files - backup.logfile = open(backup.log_path, 'w') - backup.errfile = open(backup.err_path, 'w') - backup.warnfile = open(backup.warn_path, 'w') - - # Set homedir and default options - try: - backup.homedir = '/home/' + os.environ['SUDO_USER'] - except Exception: - backup.homedir = expanduser('~') - - backup.options = '-arvh -H -X' - - # Check number of parameters - if len(argv) == 1: - # If simple backup directory doesn't exist, create it and exit - if not isdir(backup.homedir + '/.simple_backup'): - try: - os.mkdir(backup.homedir + '/.simple_backup') - log_message = 'Created directory "' + backup.homedir + '/.simple_backup".\n' +\ - 'Copy there the sample configuration and edit it\n' +\ - 'to your needs before running the backup,\n' +\ - 'or pass options directly on the command line.' - backup.logfile.write(log_message) - backup.logfile.write('\n') - print(log_message) - - backup.logfile.close() - backup.errfile.close() - backup.warnfile.close() - - try: - move(backup.log_path, backup.homedir + '/.simple_backup/simple_backup.log') - except Exception: - print('Failed to create logs in {}'.format(backup.homedir)) - - try: - os.remove(backup.err_path) - os.remove(backup.warn_path) - except Exception: - print('Failed to remove temporary files') - except Exception: - print('Failed to create .simple_backup directory in {}'.format(backup.homedir)) - - backup.logfile.close() - backup.errfile.close() - backup.warnfile.close() - - try: - os.remove(backup.log_path) - os.remove(backup.err_path) - os.remove(backup.warn_path) - except Exception: - print('Failed to remove temporary files') - - exit(1) - - # Read configuration file - backup.read_conf() - else: - # Parse command line options - backup.parse_options() - - if backup.n_in > 0 and (backup.backup_dir == '' or - not isdir(backup.backup_dir)): - # If the backup directory is not set or doesn't exist, exit - log_message = str(datetime.now()) + ': Backup failed (see errors.log)' - backup.logfile.write(log_message) - backup.logfile.write('\n') - print('Backup failed') - err_message = 'Error: Output folder "' + backup.backup_dev + '" not found' - print(err_message) - backup.errfile.write(err_message) - backup.errfile.write('\n') - - backup.logfile.close() - backup.errfile.close() - backup.warnfile.close() - - try: - move(backup.log_path, backup.homedir + '/.simple_backup/simple_backup.log') - move(backup.err_path, backup.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(backup.homedir)) - - try: - os.remove(backup.warn_path) - os.remove(backup.inputs_path) - os.remove(backup.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(1) - elif backup.n_in == 0 and backup.backup_dir == '': - if not isdir(backup.homedir + '/.simple_backup'): - try: - os.mkdir(backup.homedir + '/.simple_backup') - log_message = 'Created directory "' + backup.homedir + '/.simple_backup".\n' +\ - 'Copy there the sample configuration and edit it\n' +\ - 'to your needs before running the backup,\n' +\ - 'or pass options directly on the command line.' - backup.logfile.write(log_message) - backup.logfile.write('\n') - print(log_message) - - backup.logfile.close() - backup.errfile.close() - backup.warnfile.close() - - try: - move(backup.log_path, backup.homedir + '/.simple_backup/simple_backup.log') - except Exception: - print('Failed to create logs in {}'.format(backup.homedir)) - - try: - os.remove(backup.err_path) - os.remove(backup.warn_path) - os.remove(backup.inputs_path) - os.remove(backup.exclude_path) - except Exception: - print('Failed to remove temporary files') - except Exception: - print('Failed to create .simple_backup directory in {}'.format(backup.homedir)) - - backup.logfile.close() - backup.errfile.close() - backup.warnfile.close() - - try: - os.remove(backup.log_path) - os.remove(backup.err_path) - os.remove(backup.warn_path) - os.remove(backup.inputs_path) - os.remove(backup.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(1) - - try: - os.remove(backup.inputs_path) - os.remove(backup.exclude_path) - except Exception: - print('Failed to remove temporary files') - - backup.read_conf(backup.homedir + '/.simple_backup/config') - - if backup.n_in == 0: - log_message = str(datetime.now()) + ': Backup finished (no files copied)' - backup.logfile.write(log_message) - backup.logfile.write('\n') - print('Backup finished (no files copied') - warn_message = 'Warning: no valid input selected. Nothing to do' - print(warn_message) - backup.warnfile.write(warn_message) - backup.warnfile.write('\n') - - backup.logfile.close() - backup.errfile.close() - backup.warnfile.close() - - try: - move(backup.log_path, backup.homedir + '/.simple_backup/simple_backup.log') - move(backup.warn_path, backup.homedir + '/.simple_backup/warnings.log') - except Exception: - print('Failed to create logs in {}'.format(backup.homedir)) - - try: - os.remove(backup.err_path) - os.remove(backup.inputs_path) - os.remove(backup.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(0) - - log_message = str(datetime.now()) + ': Starting backup' - backup.logfile.write(log_message) - backup.logfile.write('\n') - print('Starting backup...') - - # If specified, keep the last n backups and remove the others. Default: keep all - if backup.keep > -1: - try: - dirs = os.listdir(backup.backup_dev + '/simple_backup') - except Exception: - err_message = 'Failed to access backup directory' - backup.errfile.write(err_message) - backup.errfile.write('\n') - print(err_message) - - backup.logfile.close() - backup.errfile.close() - backup.warnfile.close() - - try: - move(backup.log_path, backup.homedir + '/.simple_backup/simple_backup.log') - move(backup.err_path, backup.homedir + '.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(backup.homedir)) - - try: - os.remove(backup.warn_path) - os.remove(backup.inputs_path) - os.remove(backup.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(1) - if dirs.count('last_backup') > 0: - dirs.remove('last_backup') - n_backup = len(dirs) - 1 - - if n_backup > backup.keep: - log_message = str(datetime.now()) + ': Removing old backups' - backup.logfile.write(log_message) - backup.logfile.write('\n') - print('Removing old backups...') - dirs.sort() - - for i in range(n_backup-backup.keep): - try: - rmtree(backup.backup_dev + '/simple_backup/' + dirs[i]) - log_message = 'Removed backup: ' + dirs[i] - backup.logfile.write(log_message) - backup.logfile.write('\n') - except Exception: - err_message = 'Error while removing backup ' + dirs[i] - backup.errfile.write(err_message) - backup.errfile.write('\n') - print(err_message) - - backup.logfile.close() - backup.errfile.close() - backup.warnfile.close() - - backup.exec_() - - backup.logfile = open(backup.log_path, 'a') - backup.errfile = open(backup.err_path, 'a') - - if islink(backup.backup_dev + '/simple_backup/last_backup'): - try: - os.remove(backup.backup_dev + '/simple_backup/last_backup') - except Exception: - err_message = 'Failed to remove last_backup link' - backup.errfile.write(err_message) - backup.errfile.write('\n') - print(err_message) - - try: - os.symlink(backup.backup_dir, backup.backup_dev + '/simple_backup/last_backup') - except Exception: - err_message = 'Failed to create last_backup link' - backup.errfile.write(err_message) - backup.errfile.write('\n') - print(err_message) - - backup.errfile.close() - - # Update the logs - if os.stat(backup.err_path).st_size > 0: - log_message = str(datetime.now()) + ': Backup finished with errors (see errors.log)' - backup.logfile.write(log_message) - backup.logfile.write('\n') - print('Backup finished with errors') - - try: - move(backup.err_path, backup.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to create logs in {}'.format(backup.homedir)) - - try: - os.remove(backup.warn_path) - except Exception: - print('Failed to remove temporary file') - elif os.stat(backup.warn_path).st_size > 0: - log_message = str(datetime.now()) + ': Backup finished with warnings (see warnings.log)' - backup.logfile.write(log_message) - backup.logfile.write('\n') - print('Backup finished (warnings)') - - try: - move(backup.warn_path, backup.homedir + '/.simple_backup/warnings.log') - except Exception: - print('Failed to create logs in {}'.format(backup.homedir)) - - try: - os.remove(backup.err_path) - except Exception: - print('Failed to remove temporary file') - - if isfile(backup.homedir + '/.simple_backup/errors.log'): - try: - os.remove(backup.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to remove old logs') - else: - log_message = str(datetime.now()) + ': Backup finished' - backup.logfile.write(log_message) - backup.logfile.write('\n') - print('Backup finished') - - try: - os.remove(backup.err_path) - os.remove(backup.warn_path) - except Exception: - print('Failed to remove temporary files') - - if isfile(backup.homedir + '/.simple_backup/errors.log'): - try: - os.remove(backup.homedir + '/.simple_backup/errors.log') - except Exception: - print('Failed to remove old logs') - if isfile(backup.homedir + '/.simple_backup/warnings.log'): - try: - os.remove(backup.homedir + '/.simple_backup/warnings.log') - except Exception: - print('Failed to remove old logs') - - backup.logfile.close() - - # Copy log files in home directory - try: - move(backup.log_path, backup.homedir + '/.simple_backup/simple_backup.log') - except Exception: - print('Failed to create logs in {}'.format(backup.homedir)) - - # Delete temporary files - try: - os.remove(backup.inputs_path) - os.remove(backup.exclude_path) - except Exception: - print('Failed to remove temporary files') - - exit(0) - - -if __name__ == '__main__': - main() diff --git a/simple-backup.config b/simple_backup.config similarity index 62% rename from simple-backup.config rename to simple_backup.config index df7b0ee..cd50e63 100644 --- a/simple-backup.config +++ b/simple_backup.config @@ -8,7 +8,7 @@ inputs=/home/my_home,/etc backup_dir=/media/Backup #Exclude patterns. Use a comma to separate items -exclude=.gvfs,.cache*,[Cc]ache*,.thumbnails*,[Tt]rash*,*.backup*,*~,.dropbox* +exclude=.gvfs,.local/share/gvfs-metadata,.cache,.dbus,.Trash,.local/share/Trash,.macromedia,.adobe,.recently-used,.recently-used.xbel,.thumbnails,*.backup* #Number of snapshots to keep (use -1 to keep all) keep=-1 \ No newline at end of file diff --git a/simple-backup.install b/simple_backup.install similarity index 61% rename from simple-backup.install rename to simple_backup.install index 1d5bcd3..649e655 100644 --- a/simple-backup.install +++ b/simple_backup.install @@ -1,5 +1,6 @@ post_install() { echo "An example configuration file is found in /etc/simple_backup/config." + echo "Copy it to ~/.simple_backup/simple_backup.config and modify it as needed" } post_upgrade() { diff --git a/simple_backup.py b/simple_backup.py new file mode 100755 index 0000000..5c73ab7 --- /dev/null +++ b/simple_backup.py @@ -0,0 +1,232 @@ +#!/usr/bin/python3 + +# Import libraries +from dotenv import load_dotenv +import os +from shutil import rmtree +import argparse +import configparser +import logging +from logging import StreamHandler +from logging.handlers import RotatingFileHandler +import subprocess +from datetime import datetime +from tempfile import mkstemp + + +class MyFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): + pass + + +class Backup: + + def __init__(self, inputs, output, exclude, keep, options): + self.inputs = inputs + self.output = output + self.exclude = exclude + self.options = options + self.keep = keep + self._last_backup = '' + self._output_dir = '' + self._inputs_path = '' + self._exclude_path = '' + + def check_params(self): + if self.inputs is None or len(self.inputs) == 0: + logger.info('No files or directory specified for backup.') + + return False + + if not os.path.isdir(self.output): + logger.critical('Output path for backup does not exist') + + return False + + if self.keep is None: + self.keep = -1 + + return True + + # Function to create the actual backup directory + def create_backup_dir(self): + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + self._output_dir = f'{self.output}/simple_backup/{now}' + + os.makedirs(self._output_dir, exist_ok=True) + + def remove_old_backups(self): + try: + dirs = os.listdir(f'{self.output}/simple_backup') + except Exception: + logger.info('No older backups to remove') + + return + + if dirs.count('last_backup') > 0: + dirs.remove('last_backup') + + n_backup = len(dirs) - 1 + + if n_backup > self.keep: + logger.info('Removing old backups...') + dirs.sort() + + for i in range(n_backup - self.keep): + try: + rmtree(f'{self.output}/simple_backup/{dirs[i]}') + except Exception: + logger.error(f'Error while removing backup {dirs[i]}') + + def find_last_backup(self): + if os.path.islink(f'{self.output}/simple_backup/last_backup'): + try: + self._last_backup = os.readlink(f'{self.output}/simple_backup/last_backup') + except Exception: + logger.warning('No previous backup could be read') + + # Function to read configuration file + def run(self): + logger.info('Starting backup') + + self.create_backup_dir() + self.find_last_backup() + + if os.path.islink(f'{self.output}/simple_backup/last_backup'): + try: + os.remove(f'{self.output}/simple_backup/last_backup') + except Exception: + logger.error('Failed to remove last_backup link') + + inputs_handle, self._inputs_path = mkstemp(prefix='tmp_inputs', text=True) + exclude_handle, self._exclude_path = mkstemp(prefix='tmp_exclude', text=True) + + with open(self._inputs_path, 'w') as fp: + for i in self.inputs: + if not os.path.exists(i): + logger.warning(f'Input {i} not found. Skipping') + else: + fp.write(i) + fp.write('\n') + + with open(self._exclude_path, 'w') as fp: + for e in self.exclude: + fp.write(e) + fp.write('\n') + + logger.info('Copying files. This may take a long time...') + + if self._last_backup == '': + rsync = f'rsync {self.options} --exclude-from={self._exclude_path} ' +\ + f'--files-from={self._inputs_path} / "{self._output_dir}" ' +\ + '--ignore-missing-args' + else: + rsync = f'rsync {self.options} --link-dest="{self._last_backup}" --exclude-from=' +\ + f'{self._exclude_path} --files-from={self._inputs_path} / "{self._output_dir}" ' +\ + '--ignore-missing-args' + + subprocess.run(rsync, shell=True) + + try: + os.symlink(self._output_dir, f'{self.output}/simple_backup/last_backup') + except Exception: + logger.error('Failed to create last_backup link') + + if self.keep != -1: + self.remove_old_backups() + + os.remove(self._inputs_path) + os.remove(self._exclude_path) + + +load_dotenv() +euid = os.geteuid() + +if euid == 0: + user = os.getenv("SUDO_USER") + homedir = os.path.expanduser(f'~{user}') +else: + homedir = os.getenv('HOME') + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(os.path.basename(__file__)) +c_handler = StreamHandler() +#f_handler = RotatingFileHandler(f'{homedir}/.simple_backup/simple_backup.log') +f_handler = RotatingFileHandler(f'./simple_backup.log', maxBytes=1024000, backupCount=5) +c_handler.setLevel(logging.INFO) +f_handler.setLevel(logging.INFO) +c_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s') +f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +c_handler.setFormatter(c_format) +f_handler.setFormatter(f_format) +logger.addHandler(c_handler) +logger.addHandler(f_handler) + + +def _parse_arguments(): + parser = argparse.ArgumentParser(prog='simple_backup', + description='A simple backup script written in Python that uses rsync to copy files', + epilog='Report bugs to dfucinigmailcom', + formatter_class=MyFormatter) + + parser.add_argument('-c', '--config', default=f'{homedir}/.simple_backup/simple_backup.config', + help='Specify location of configuration file') + parser.add_argument('-i', '--input', nargs='+', help='Paths/files to backup') + parser.add_argument('-o', '--output', help='Output directory for the backup') + parser.add_argument('-e', '--exclude', nargs='+', help='Files/directories/patterns to exclude from the backup') + parser.add_argument('-k', '--keep', type=int, help='Number of old backups to keep') + parser.add_argument('-s', '--checksum', action='store_true', + help='Use checksum rsync option to compare files (MUCH SLOWER)') + + args = parser.parse_args() + + return args + + +def _read_config(config_file): + if not os.path.isfile(config_file): + logger.warning(f'Config file {config_file} does not exist') + + return None, None, None, None + + config = configparser.ConfigParser() + config.read(config_file) + + inputs = config.get('default', 'inputs') + inputs = inputs.split(',') + output = config.get('default', 'backup_dir') + exclude = config.get('default', 'exclude') + exclude = exclude.split(',') + keep = config.getint('default', 'keep') + + return inputs, output, exclude, keep + + +def simple_backup(): + args = _parse_arguments() + inputs, output, exclude, keep = _read_config(args.config) + + if args.input is not None: + inputs = args.input + + if args.output is not None: + output = args.output + + if args.exclude is not None: + exclude = args.exclude + + if args.keep is not None: + keep = args.keep + + if args.checksum: + backup_options = '-arcvh -H -X' + else: + backup_options = '-arvh -H -X' + + backup = Backup(inputs, output, exclude, keep, backup_options) + + if backup.check_params(): + backup.run() + + +if __name__ == '__main__': + simple_backup()