Merge branch 'master' into development

This commit is contained in:
daniele 2023-06-15 22:10:19 +02:00
commit 82b0ea88fa
Signed by: fuxino
GPG Key ID: 981A2B2A3BBF5514
2 changed files with 92 additions and 18 deletions

View File

@ -1,4 +1,4 @@
.TH SIMPLE_BACKUP 1 2023-06-01 SIMPLE_BACKUP 3.2.6 .TH SIMPLE_BACKUP 1 2023-06-15 SIMPLE_BACKUP 3.2.6
.SH NAME .SH NAME
simple_backup \- Backup files and folders using rsync simple_backup \- Backup files and folders using rsync
.SH SYNOPSIS .SH SYNOPSIS
@ -86,8 +86,27 @@ Default behavior is to remove old backups after successfully completing the back
.TP .TP
.B \-\-no\-syslog .B \-\-no\-syslog
Don't use systemd journal for logging Don't use systemd journal for logging
.TP
.B \-\-rsync\-options OPTIONS [OPTION...]
By default, the following rsync options are used:
.RS
.PP
\-a \-r \-c \-v \-h \-H \-X
.PP
Using \-\-rsync\-options it is possible to manually select which options to use. Supported values are the following:
.PP
\-a, \-l, \-p, \-t, \-g, \-o, \-c, \-h, \-D, \-H, \-X
.PP
Options \-r and \-v are used in any case. Not that options must be specified without dash (\-), for example:
.PP
.EX
simple_backup \-\-rsync\-options a l p
.EE
.TP
Check rsync(1) for details about the options.
.RE
.SH CONFIGURATION .SH CONFIGURATION
An example configuration file is provided at \(aq/etc/simple_backup/simple_backup.conf\(aq. An example configuration file is provided at \(aq/usr/share/doc/simple_backup/simple_backup.conf\(aq.
Copy it to the default location ($HOME/.config/simple_backup) and edit it as needed. Copy it to the default location ($HOME/.config/simple_backup) and edit it as needed.
.SH REMOTE BACKUP .SH REMOTE BACKUP
It is possible to choose a directory on a remote server as destination for the backup. The files It is possible to choose a directory on a remote server as destination for the backup. The files
@ -125,6 +144,22 @@ the password multiple
times. This can pose some security risks, see times. This can pose some security risks, see
.B sshpass (1) .B sshpass (1)
for details. For this reason, use SSH key authentication if possible. for details. For this reason, use SSH key authentication if possible.
.SH EXIT STATUS
.TP
.B 0
The backup was completed without errors
.TP
.B 1
No valid inputs selected for backup
.TP
.B 2
Backup failed because output directory for storing the backup does not exist
.TP
.B 3
Permission denied to access the output directory
.TP
.B 4
rsync error (rsync returned a non-zero value)
.SH SEE ALSO .SH SEE ALSO
.BR rsync (1) .BR rsync (1)
.SH AUTHORS .SH AUTHORS

View File

@ -34,7 +34,6 @@ from paramiko import RSAKey, Ed25519Key, ECDSAKey, DSSKey
warnings.filterwarnings('error') warnings.filterwarnings('error')
try: try:
from systemd import journal from systemd import journal
except ImportError: except ImportError:
@ -163,14 +162,14 @@ class Backup:
"""Check if parameters for the backup are valid""" """Check if parameters for the backup are valid"""
if self.inputs is None or len(self.inputs) == 0: if self.inputs is None or len(self.inputs) == 0:
logger.info('No files or directory specified for backup.') logger.info('No existing files or directories specified for backup. Nothing to do')
return False return 1
if self.output is None: if self.output is None:
logger.critical('No output path specified. Use -o argument or specify output path in configuration file') logger.critical('No output path specified. Use -o argument or specify output path in configuration file')
return False return 2
if self.host is not None and self.username is not None: if self.host is not None and self.username is not None:
self._remote = True self._remote = True
@ -188,19 +187,19 @@ class Backup:
if output != 'ok': if output != 'ok':
logger.critical('Output path for backup does not exist') logger.critical('Output path for backup does not exist')
return False return 2
else: else:
if not os.path.isdir(self.output): if not os.path.isdir(self.output):
logger.critical('Output path for backup does not exist') logger.critical('Output path for backup does not exist')
return False return 2
self.output = os.path.abspath(self.output) self.output = os.path.abspath(self.output)
if self.keep is None: if self.keep is None:
self.keep = -1 self.keep = -1
return True return 0
# Function to create the actual backup directory # Function to create the actual backup directory
def define_backup_dir(self): def define_backup_dir(self):
@ -293,6 +292,15 @@ class Backup:
logger.info('No previous backups available') logger.info('No previous backups available')
return return
except PermissionError:
logger.critical('Cannot access the backup directory. Permission denied')
try:
notify('Backup failed (check log for details)')
except NameError:
pass
sys.exit(3)
try: try:
self._last_backup = dirs[-1] self._last_backup = dirs[-1]
@ -414,7 +422,7 @@ class Backup:
self.find_last_backup() self.find_last_backup()
_, self._inputs_path = mkstemp(prefix='tmp_inputs', text=True) _, self._inputs_path = mkstemp(prefix='tmp_inputs', text=True)
_, self._exclude_path = mkstemp(prefix='tmp_exclude', text=True) count = 0
with open(self._inputs_path, 'w', encoding='utf-8') as fp: with open(self._inputs_path, 'w', encoding='utf-8') as fp:
for i in self.inputs: for i in self.inputs:
@ -423,6 +431,19 @@ class Backup:
else: else:
fp.write(i) fp.write(i)
fp.write('\n') fp.write('\n')
count += 1
if count == 0:
logger.info('No existing files or directories specified for backup. Nothing to do')
try:
notify('Backup finished. No files copied')
except NameError:
pass
return 1
_, self._exclude_path = mkstemp(prefix='tmp_exclude', text=True)
with open(self._exclude_path, 'w', encoding='utf-8') as fp: with open(self._exclude_path, 'w', encoding='utf-8') as fp:
if self.exclude is not None: if self.exclude is not None:
@ -506,6 +527,8 @@ class Backup:
_notify('Some errors occurred while performing the backup. Check log for details') _notify('Some errors occurred while performing the backup. Check log for details')
except NameError: except NameError:
pass pass
return 4
else: else:
logger.info('Backup completed') logger.info('Backup completed')
@ -514,6 +537,8 @@ class Backup:
except NameError: except NameError:
pass pass
return 0
def _parse_arguments(): def _parse_arguments():
parser = argparse.ArgumentParser(prog='simple_backup', parser = argparse.ArgumentParser(prog='simple_backup',
@ -536,6 +561,9 @@ def _parse_arguments():
parser.add_argument('--remove-before-backup', action='store_true', parser.add_argument('--remove-before-backup', action='store_true',
help='Remove old backups before executing the backup, instead of after') help='Remove old backups before executing the backup, instead of after')
parser.add_argument('--no-syslog', action='store_true', help='Disable systemd journal logging') parser.add_argument('--no-syslog', action='store_true', help='Disable systemd journal logging')
parser.add_argument('--rsync-options', nargs='+',
choices=['a', 'l', 'p', 't', 'g', 'o', 'c', 'h', 'D', 'H', 'X'],
help='Specify options for rsync')
args = parser.parse_args() args = parser.parse_args()
@ -546,12 +574,15 @@ def _expand_inputs(inputs):
expanded_inputs = [] expanded_inputs = []
for i in inputs: for i in inputs:
if i == '':
continue
i_ex = glob(os.path.expanduser(i.replace('~', f'~{user}'))) i_ex = glob(os.path.expanduser(i.replace('~', f'~{user}')))
if len(i_ex) == 0: if len(i_ex) == 0:
logger.warning('No file or directory matching input %s. Skipping...', i) logger.warning('No file or directory matching input %s. Skipping...', i)
else: else:
expanded_inputs.extend(glob(os.path.expanduser(i.replace('~', f'~{user}')))) expanded_inputs.extend(i_ex)
return expanded_inputs return expanded_inputs
@ -654,23 +685,31 @@ def simple_backup():
if args.keyfile is not None: if args.keyfile is not None:
ssh_keyfile = args.keyfile ssh_keyfile = args.keyfile
backup_options = ['-a', '-r', '-v', '-h', '-H', '-X', '-s', '--ignore-missing-args', '--mkpath'] if args.rsync_options is None:
rsync_options = ['-a', '-r', '-v', '-h', '-H', '-X', '-s', '--ignore-missing-args', '--mkpath']
else:
rsync_options = ['-r', '-v']
for ro in args.rsync_options:
rsync_options.append(f'-{ro}')
if args.checksum: if args.checksum:
backup_options.append('-c') rsync_options.append('-c')
if args.compress: if args.compress:
backup_options.append('-z') rsync_options.append('-z')
backup_options = ' '.join(backup_options) rsync_options = ' '.join(rsync_options)
backup = Backup(inputs, output, exclude, keep, backup_options, host, username, backup = Backup(inputs, output, exclude, keep, rsync_options, host, username,
ssh_keyfile, remove_before=args.remove_before_backup) ssh_keyfile, remove_before=args.remove_before_backup)
if backup.check_params(): return_code = backup.check_params()
if return_code == 0:
return backup.run() return backup.run()
return 1 return return_code
if __name__ == '__main__': if __name__ == '__main__':