9 Commits
3.2.8 ... 3.5.0

Author SHA1 Message Date
970d3dde1e Add manual selection of rsync options 2023-06-15 21:30:43 +02:00
4d23bde906 Check that inputs from command line exist
Check that the inputs specified on the command line (i.e. with the
option '-i' or '--input') exist and print a warning when they don't.
If no valid inputs are found, exit.
2023-06-15 18:44:22 +02:00
ff70358496 Version bump 2023-06-15 17:38:00 +02:00
6f1e91e2cd Add more return codes 2023-06-15 17:14:17 +02:00
b3fee0d022 Add some meaningful return codes 2023-06-15 16:58:56 +02:00
d63eb8f771 Improve handling of missing inputs 2023-06-15 15:34:00 +02:00
56df958c5b Add expansion of params in config file
Allow using wildcards (i.e. * to match any character and ~ to match the
user's home directory) in inputs and ouput variables in config file
2023-06-04 12:09:30 +02:00
d6d9fbf6e4 Add no-syslog option 2023-06-04 10:16:50 +02:00
ffbf8ece91 Remove last_backup link 2023-06-02 19:38:28 +02:00
5 changed files with 147 additions and 59 deletions

View File

@ -56,9 +56,47 @@ Same as rsync option \(aq\-\-checksum\(aq, use checksums instead of mod\-time an
.B \-\-remove\-before\-backup .B \-\-remove\-before\-backup
Remove old backups (if necessary) before creating the new backup. Useful to free some space before performing the backup. Remove old backups (if necessary) before creating the new backup. Useful to free some space before performing the backup.
Default behavior is to remove old backups after successfully completing the backup. Default behavior is to remove old backups after successfully completing the backup.
.TP
.B \-\-no\-syslog
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. Copy it to the default location An example configuration file is provided at \(aq/etc/simple_backup/simple_backup.conf\(aq. Copy it to the default location
($HOME/.config/simple_backup) and edit it as needed. ($HOME/.config/simple_backup) and edit it as needed.
.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

@ -1,14 +0,0 @@
#Example config file for simple_backup
[default]
#Input directories. Use a comma to separate items
inputs=/home/my_home,/etc
#Output directory
backup_dir=/media/Backup
#Exclude patterns. Use a comma to separate items
exclude=.gvfs,.local/share/gvfs-metadata,.cache,.dbus,.Trash,.local/share/Trash,.macromedia,.adobe,.recently-used,.recently-used.xbel,.thumbnails
#Number of snapshots to keep (use -1 to keep all)
keep=-1

View File

@ -1,3 +1,3 @@
"""Init.""" """Init."""
__version__ = '3.2.8' __version__ = '3.5.0'

View File

@ -0,0 +1,14 @@
# Example config file for simple_backup
[default]
# Files and directories to backup. Multiple items can be separated using a comma (','). It is possible to use wildcards (i.e. '*' to match multiple characters and '~' for the user's home directory).
inputs=/home/my_home,/etc
# Output directory.
backup_dir=/media/Backup
# Files, directories and patterns to exclude from the backup. Multiple items can be separated using a comma.
exclude=*.bak
# Number of old backups (i.e. excluding the one that's being created) to keep (use -1 to keep all)
keep=-1

View File

@ -1,6 +1,7 @@
#!/usr/bin/python3 #!/usr/bin/python3
# Import libraries # Import libraries
import sys
import os import os
from functools import wraps from functools import wraps
from shutil import rmtree from shutil import rmtree
@ -12,6 +13,8 @@ from timeit import default_timer
from subprocess import Popen, PIPE, STDOUT from subprocess import Popen, PIPE, STDOUT
from datetime import datetime from datetime import datetime
from tempfile import mkstemp from tempfile import mkstemp
from glob import glob
from dotenv import load_dotenv from dotenv import load_dotenv
try: try:
@ -32,6 +35,7 @@ if euid == 0:
user = os.getenv('SUDO_USER') user = os.getenv('SUDO_USER')
homedir = os.path.expanduser(f'~{user}') homedir = os.path.expanduser(f'~{user}')
else: else:
user = os.getenv('USER')
homedir = os.getenv('HOME') homedir = os.getenv('HOME')
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@ -91,26 +95,26 @@ class Backup:
def check_params(self): def check_params(self):
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 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 create_backup_dir(self): def create_backup_dir(self):
@ -125,9 +129,6 @@ class Backup:
except FileNotFoundError: except FileNotFoundError:
return return
if dirs.count('last_backup') > 0:
dirs.remove('last_backup')
n_backup = len(dirs) - 1 n_backup = len(dirs) - 1
count = 0 count = 0
@ -150,14 +151,25 @@ class Backup:
logger.info(f'Removed {count} backups') logger.info(f'Removed {count} backups')
def find_last_backup(self): def find_last_backup(self):
if os.path.islink(f'{self.output}/simple_backup/last_backup'): try:
link = os.readlink(f'{self.output}/simple_backup/last_backup') dirs = sorted([f.path for f in os.scandir(f'{self.output}/simple_backup') if f.is_dir(follow_symlinks=False)])
except FileNotFoundError:
logger.info('No previous backups available')
if os.path.isdir(link): return
self._last_backup = link except PermissionError:
else: logger.critical('Cannot access the backup directory. Permission denied')
logger.info('No previous backups available')
else: try:
notify('Backup failed (check log for details)')
except NameError:
pass
sys.exit(3)
try:
self._last_backup = dirs[-1]
except IndexError:
logger.info('No previous backups available') logger.info('No previous backups available')
# Function to read configuration file # Function to read configuration file
@ -170,11 +182,11 @@ class Backup:
except NameError: except NameError:
pass pass
self.create_backup_dir()
self.find_last_backup() self.find_last_backup()
self.create_backup_dir()
_, 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') as fp: with open(self._inputs_path, 'w') as fp:
for i in self.inputs: for i in self.inputs:
@ -183,6 +195,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') as fp: with open(self._exclude_path, 'w') as fp:
if self.exclude is not None: if self.exclude is not None:
@ -215,25 +240,6 @@ class Backup:
logger.info(f'rsync: {output[-3]}') logger.info(f'rsync: {output[-3]}')
logger.info(f'rsync: {output[-2]}') logger.info(f'rsync: {output[-2]}')
if os.path.islink(f'{self.output}/simple_backup/last_backup'):
try:
os.remove(f'{self.output}/simple_backup/last_backup')
except FileNotFoundError:
logger.error('Failed to remove last_backup link. File not found')
self._err_flag = True
except PermissionError:
logger.error('Failed to remove last_backup link. Permission denied')
self._err_flag = True
try:
os.symlink(self._output_dir, f'{self.output}/simple_backup/last_backup', target_is_directory=True)
except FileExistsError:
logger.error('Failed to create last_backup link. Link already exists')
self._err_flag = True
except PermissionError:
logger.error('Failed to create last_backup link. Permission denied')
self._err_flag = True
if self.keep != -1 and not self.remove_before: if self.keep != -1 and not self.remove_before:
self.remove_old_backups() self.remove_old_backups()
@ -249,12 +255,16 @@ class Backup:
notify('Backup finished with errors (check log for details)') notify('Backup finished with errors (check log for details)')
except NameError: except NameError:
pass pass
return 4
else: else:
try: try:
notify('Backup finished') notify('Backup finished')
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',
@ -272,12 +282,33 @@ def _parse_arguments():
help='Use checksum rsync option to compare files (MUCH SLOWER)') help='Use checksum rsync option to compare files (MUCH SLOWER)')
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('--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()
return args return args
def _expand_inputs(inputs):
expanded_inputs = []
for i in inputs:
if i == '':
continue
i_ex = glob(os.path.expanduser(i.replace('~', f'~{user}')))
if len(i_ex) == 0:
logger.warning(f'No file or directory matching input {i}. Skipping...')
else:
expanded_inputs.extend(i_ex)
return expanded_inputs
def _read_config(config_file): def _read_config(config_file):
if not os.path.isfile(config_file): if not os.path.isfile(config_file):
logger.warning(f'Config file {config_file} does not exist') logger.warning(f'Config file {config_file} does not exist')
@ -289,7 +320,10 @@ def _read_config(config_file):
inputs = config.get('default', 'inputs') inputs = config.get('default', 'inputs')
inputs = inputs.split(',') inputs = inputs.split(',')
inputs = _expand_inputs(inputs)
inputs = list(set(inputs))
output = config.get('default', 'backup_dir') output = config.get('default', 'backup_dir')
output = os.path.expanduser(output.replace('~', f'~{user}'))
exclude = config.get('default', 'exclude') exclude = config.get('default', 'exclude')
exclude = exclude.split(',') exclude = exclude.split(',')
keep = config.getint('default', 'keep') keep = config.getint('default', 'keep')
@ -317,6 +351,13 @@ def notify(text):
def simple_backup(): def simple_backup():
args = _parse_arguments() args = _parse_arguments()
if args.no_syslog:
try:
logger.removeHandler(j_handler)
except NameError:
pass
inputs, output, exclude, keep = _read_config(args.config) inputs, output, exclude, keep = _read_config(args.config)
if args.input is not None: if args.input is not None:
@ -331,19 +372,28 @@ def simple_backup():
if args.keep is not None: if args.keep is not None:
keep = args.keep keep = args.keep
if args.checksum: if args.rsync_options is None:
backup_options = '-arcvh -H -X' if args.checksum:
rsync_options = '-arcvh -H -X'
else:
rsync_options = '-arvh -H -X'
else: else:
backup_options = '-arvh -H -X' rsync_options = '-r -v '
backup = Backup(inputs, output, exclude, keep, backup_options, remove_before=args.remove_before_backup) for ro in args.rsync_options:
rsync_options = rsync_options + f'-{ro} '
if backup.check_params(): if '-c ' not in rsync_options and args.checksum:
backup.run() rsync_options = rsync_options + '-c'
return 0 backup = Backup(inputs, output, exclude, keep, rsync_options, remove_before=args.remove_before_backup)
return 1 return_code = backup.check_params()
if return_code == 0:
return backup.run()
return return_code
if __name__ == '__main__': if __name__ == '__main__':